# How Openship Uses SetupStateStore to Cache and Verify Prerequisites

> Learn how Openship uses SetupStateStore to cache and verify prerequisites for instant isReady and checkFeature calls. Stale data refreshes asynchronously after a 24-hour TTL.

- Repository: [oblien/openship](https://github.com/oblien/openship)
- Tags: internals
- Published: 2026-08-19

---

**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`](https://github.com/oblien/openship/blob/main/packages/adapters/src/system/setup.ts), while [`packages/adapters/src/system/state.ts`](https://github.com/oblien/openship/blob/main/packages/adapters/src/system/state.ts) defines the store contract. Low-level health checks reside in [`packages/adapters/src/system/checks.ts`](https://github.com/oblien/openship/blob/main/packages/adapters/src/system/checks.ts), and the component catalog is declared in [`packages/adapters/src/system/components.ts`](https://github.com/oblien/openship/blob/main/packages/adapters/src/system/components.ts). High-level design notes are also documented in [`packages/adapters/docs/ARCHITECTURE.md`](https://github.com/oblien/openship/blob/main/packages/adapters/docs/ARCHITECTURE.md).

## How Prerequisites Are Resolved and Verified

### Resolving Required Components

Before any caching occurs, `resolveRequired()` in [`setup.ts`](https://github.com/oblien/openship/blob/main/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`](https://github.com/oblien/openship/blob/main/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`](https://github.com/oblien/openship/blob/main/packages/adapters/src/system/state.ts) handles persistence. It writes the JSON representation of `SetupState` to [`/etc/openship/setup-state.json`](https://github.com/oblien/openship/blob/main//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.

```ts
// 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));

```

```ts
// 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

```

```ts
// 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`](https://github.com/oblien/openship/blob/main/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`](https://github.com/oblien/openship/blob/main/packages/adapters/src/system/state.ts) provides a zero-config fallback that writes JSON to [`/etc/openship/setup-state.json`](https://github.com/oblien/openship/blob/main//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`](https://github.com/oblien/openship/blob/main//etc/openship/setup-state.json). You can override this path when constructing the store, making it suitable for containers or custom filesystem layouts.