How Openship Uses SetupStateStore to Cache and Verify Prerequisites

Openship caches expensive system prerequisite checks in a SetupStateStore so that isReady() and checkFeature() can return instantly on hot paths, refreshing stale data asynchronously after a 24-hour TTL.

The oblien/openship platform avoids running repeated Docker, Git, and edge container checks on every deploy by persisting the results of its initial setup validation. By leveraging the SetupStateStore cache to verify prerequisites, subsequent operations read a cached view instead of executing slow system commands. This article breaks down the caching architecture, TTL behavior, and persistence mechanisms implemented in the adapters package.

Architecture of the SetupStateStore Cache

The prerequisite caching system spans several files inside packages/adapters/src/system/. At the bottom, the system layer executes low-level commands through SystemManager, RuntimeMode, and PrerequisiteRule. In the middle, the setup state layer persists check results through the SetupStateStore interface and its default FileStateStore fallback. The top state model describes per-component health with SetupState and ComponentState.

The core orchestration lives in packages/adapters/src/system/setup.ts, while packages/adapters/src/system/state.ts defines the store contract. Low-level health checks reside in packages/adapters/src/system/checks.ts, and the component catalog is declared in packages/adapters/src/system/components.ts. High-level design notes are also documented in packages/adapters/docs/ARCHITECTURE.md.

How Prerequisites Are Resolved and Verified

Resolving Required Components

Before any caching occurs, resolveRequired() in setup.ts determines which components are necessary for the selected runtime mode. For example, docker mode requires docker, git, and edge, whereas bare mode requires only git and edge. This filtered list prevents the system from checking irrelevant components.

Hot-Path Cache Reads

On the fast path, SystemManager.isReady() reads the stored SetupState from the configured store. If the state is missing or incomplete, it returns false immediately. Similarly, checkFeature() inspects the cached ComponentState entries; when all required components for a requested feature are marked healthy, the method returns without executing a single shell command.

If the cached state is present but a specific component is unhealthy, checkFeature() falls back to a live check of only the needed components rather than re-verifying everything.

Background Re-verification After TTL

Cached entries are considered stale after CACHE_TTL_MS (24 hours). When isReady() encounters a stale record, it invokes kickBackgroundVerify() to refresh the cache asynchronously. This design keeps request latency low because the current call proceeds with the existing cached result while a background task updates the underlying state.

If a component fails during an operation, the manager calls invalidate(). This wipes both the in-memory cache and the persisted store, forcing the next invocation to run a fresh verify() cycle.

Persisting and Invalidating Cached State

Updating State from Live Checks

After a live check completes via checkAll or checkComponents, updateStateFromChecks() in setup.ts merges the new component statuses into the stored SetupState. It updates timestamps including lastVerifiedAt and updatedAt, then writes the state back through the configured SetupStateStore.

FileStateStore Fallback

When no database implementation is supplied, FileStateStore from packages/adapters/src/system/state.ts handles persistence. It writes the JSON representation of SetupState to /etc/openship/setup-state.json, or to a custom path if configured. The class implements the SetupStateStore contract with three methods: get(), set(), and clear().

Manual Invalidation

You can force a full re-check by calling invalidate() on the SystemManager. This clears both the memory cache and the JSON file (or DB record), ensuring the next isReady() or checkFeature() call executes a live verification.

Working with SetupStateStore in Code

The following examples demonstrate how to instantiate the manager, rely on the default file-based cache, and override the store with a custom implementation.

// Example: Creating a SystemManager with the default file‑based store
import { SystemManager } from "./system/setup";
import { localExecutor } from "./types"; // a CommandExecutor that runs locally

const manager = new SystemManager("docker", {
  executor: localExecutor,
  // stateStore omitted → falls back to FileStateStore
});

// Fast‑path readiness check (no network I/O)
const ready = await manager.isReady(); // true/false

// Ensure the "deploy" feature is available, installing missing components if needed
await manager.ensureFeature("deploy", (log) => console.log(log.message));
// Example: Manually clearing the cached state after a Docker daemon crash
await manager.invalidate();   // wipes in‑memory cache and deletes the JSON file
await manager.isReady();      // will now run a fresh check
// Example: Providing a custom DB‑backed store (pseudo‑code)
import { DBSetupStateStore } from "./db/store";

const dbStore = new DBSetupStateStore(/* db client */);
const manager = new SystemManager("bare", {
  executor: remoteExecutor,
  stateStore: dbStore,
});

Summary

  • SetupStateStore is the interface that lets Openship skip expensive prerequisite checks on every request.
  • SystemManager in packages/adapters/src/system/setup.ts orchestrates caching through isReady(), checkFeature(), and updateStateFromChecks().
  • A 24-hour TTL (CACHE_TTL_MS) triggers kickBackgroundVerify() so long-running servers eventually detect external changes without blocking hot paths.
  • FileStateStore in packages/adapters/src/system/state.ts provides a zero-config fallback that writes JSON to /etc/openship/setup-state.json.
  • Calling invalidate() clears both memory and persisted state, forcing a live re-verification on the next access.

Frequently Asked Questions

What happens when the SetupStateStore cache is empty?

When the cache is empty or incomplete, SystemManager.isReady() returns false and any subsequent feature check falls back to live execution against the required components. The results are then persisted through updateStateFromChecks() so future calls hit the fast path.

How does Openship handle stale cache entries without blocking requests?

If the cached state is older than 24 hours, isReady() still returns the cached result but spawns kickBackgroundVerify() to refresh the data asynchronously. This keeps deploy-time and runtime checks fast while ensuring the system eventually reconciles with the real environment.

Can I use a database instead of the default file-based state store?

Yes. The SetupStateStore interface only requires get(), set(), and clear() methods. You can pass any implementation—such as a DBSetupStateStore—into the SystemManager constructor via the stateStore option.

Where is the fallback FileStateStore JSON file located?

By default, FileStateStore writes to /etc/openship/setup-state.json. You can override this path when constructing the store, making it suitable for containers or custom filesystem layouts.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →