Storage, CAS, and persistence
foxctl uses several storage classes with different durability expectations. Production operators and skill developers must distinguish canonical state (preserve and migrate) from rebuildable local projections (regenerate from inputs).
Path roots
Section titled “Path roots”Defaults come from internal/platform/config/config.go:
| Config key | Default | Purpose |
|---|---|---|
storage.root | ~/.foxctl/storage | Root for all database files |
paths.cas | ~/.foxctl/cas | Content-addressable storage |
paths.cache | ~/.foxctl/cache | Transient cache files |
paths.jobs | ~/.foxctl/jobs | Async job tracking |
paths.observability | ~/.foxctl/observability | Event persistence |
Override any path with the corresponding environment variable (e.g. FOXCTL_STORAGE_ROOT, FOXCTL_PATHS_CAS, FOXCTL_OBS_DIR).
Store classes
Section titled “Store classes”The canonical store registry lives in internal/storage/registry.go. Each store has a class that determines its backup and sync policy:
| Class | Examples | Production stance |
|---|---|---|
sync_critical | coordination.db, sessions.db, tasks.db, mailbox.db, agents.db, memory.db, companion.db, contextvar.db | Core continuity and agent/runtime state. Preserve and migrate deliberately. |
sync_useful | knowledge.db, teams.db, trajectory.db | Useful cross-device context. Back up regularly. |
local_only | blackboard.db, cache.db, jobs.db, quotas.db, graph.db, repoindex/<key>.db, embedding_queue.db, summary_queue.db | Rebuildable or device-local state. Regenerate from canonical inputs. |
observability | events.db | Structured event persistence. See Observability for query patterns. |
external | opencode.db | External import surface. |
Database schemas
Section titled “Database schemas”| Database | Key tables |
|---|---|
memory.db | named_memory, embedding_metadata, indexer_state |
contextvar.db | context_variables, context_sequences |
companion.db | companion_turns, companion_events, companion_hard_state_entries, companion_soft_episodes, companion_evidence_snippets, companion_assumptions_ledger |
repoindex/<key>.db | Graph nodes, edges, index metadata per workspace |
Database drivers
Section titled “Database drivers”The storage layer supports per-store database drivers through internal/storage/dbdriver:
| Driver | Use case |
|---|---|
sqlite | Default. Local development, single-node. |
turso | Remote SQLite-compatible. Cloud deployments. |
postgres | Horizontal scaling, shared state. |
Driver selection uses per-store config with fallback:
FOXCTL_<STORE>_DB_DRIVER(store-specific)FOXCTL_DB_DRIVER(global fallback)sqlite(default)
PostgreSQL configuration
Section titled “PostgreSQL configuration”For PostgreSQL-backed stores, set the DSN environment variable. The DSN is loaded via FOXCTL_<STORE>_POSTGRES_DSN with fallback to FOXCTL_POSTGRES_DSN then DATABASE_URL. Connection pooling controls:
FOXCTL_POSTGRES_MAX_CONNS— maximum connections (default: 10)FOXCTL_POSTGRES_MAX_IDLE_CONNS— maximum idle connections (default: 5)
Each logical store runs as a separate PostgreSQL schema (default schema name is the lower-cased store name). Migrations run under advisory lock per schema to prevent concurrent migration conflicts across pods.
See PostgreSQL storage architecture for full details on DSN wiring, pgvector detection, and pooling behavior.
Content-addressable storage (CAS)
Section titled “Content-addressable storage (CAS)”CAS stores large outputs by SHA-256 digest. When a skill produces output exceeding the inline threshold (default 32KB), the payload moves to CAS and the envelope returns a summary plus an artifact digest.
CAS backends
Section titled “CAS backends”| Backend | Config key | Use case |
|---|---|---|
file | FOXCTL_CAS_DRIVER=file | Local filesystem (default) |
sqlite | FOXCTL_CAS_DRIVER=sqlite | Single-file CAS |
turso | FOXCTL_CAS_DRIVER=turso | Remote SQLite-compatible |
s3 | FOXCTL_CAS_DRIVER=s3 | Enterprise object store |
For S3-backed CAS:
FOXCTL_CAS_DRIVER=s3FOXCTL_CAS_S3_BUCKET=my-cas-bucketFOXCTL_CAS_S3_REGION=us-east-1FOXCTL_CAS_S3_ENDPOINT=https://s3.example.com # optional, for MinIOFOXCTL_CAS_S3_PREFIX=foxctl/FOXCTL_CAS_S3_FORCE_PATH_STYLE=trueCAS contract
Section titled “CAS contract”| Rule | Behavior |
|---|---|
| Addressing | SHA-256 digest (sha256:...) |
| Inline threshold | Large outputs move to CAS with summary + artifact pointer |
| Integrity | Reads re-validate digest and fail on mismatch |
| Retention | Use pin/gc controls for lifecycle management |
CAS commands
Section titled “CAS commands”# Store an artifactfoxctl cas put < artifact.json
# Retrieve by digestfoxctl cas get sha256:abc123...
# Pin an artifact to prevent garbage collectionfoxctl cas pin sha256:abc123...
# Garbage collect unpinned artifacts older than 7 daysfoxctl cas gc --older-than=168h --dry-runArtifactized envelope
Section titled “Artifactized envelope”When a skill artifactizes output, the envelope returns a compact summary instead of the full payload:
{ "data": { "summary": "Evidence bundle with 247 records across 3 files", "artifact": "sha256:abc123..." }}See Protocol v1 for the full artifactization rules.
Observability persistence
Section titled “Observability persistence”Events use a hybrid persistence model with five modes:
| Mode | Description | Use case |
|---|---|---|
PersistDefault | NDJSON file | Most events — fast, append-only |
PersistNDJSON | Explicit NDJSON file | Same as default, explicit selection |
PersistSQL | Direct SQLite write | High-value events needing queryability |
PersistHybrid | NDJSON + background SQLite sync | Fast writes + queryability |
PersistNone | No persistence | Events only for sampling/logging |
Storage locations:
| File | Location |
|---|---|
| NDJSON events | $FOXCTL_OBS_DIR/events/foxcular_events.ndjson |
| SQLite database | $FOXCTL_OBS_DIR/events.db |
| Custom NDJSON | $FOXCTL_OBS_DIR/events/<name>.ndjson |
The background syncer runs every 30 seconds with a batch size of 100 events by default. See Observability for query examples.
Backup and database inspection
Section titled “Backup and database inspection”# Create a named backupfoxctl backup create --name nightly
# List existing backupsfoxctl backup list
# Inspect a specific databasefoxctl db --helpStorage invariants
Section titled “Storage invariants”| Invariant | Why it matters |
|---|---|
| Deterministic schema migrations | Keeps CLI/daemon startup predictable |
| Store class boundaries | Prevents accidental syncing of local-only data |
| CAS for large payloads | Keeps envelopes small and replayable |
| Context cancellation through storage calls | Prevents stuck long-running DB operations |
Production boundaries
Section titled “Production boundaries”- Do not treat local repoindex databases as source of truth — they are rebuildable projections.
- Preserve
repo_keyand workspace identity boundaries when moving artifacts. - Keep generated or local-only database files out of git.
- Record whether a store is file, sqlite/libsql/Turso, Postgres, or remote.
- When running in a restricted filesystem sandbox, set writable paths explicitly:
FOXCTL_STORAGE_ROOT=/tmp/foxctl/storage \FOXCTL_PATHS_CAS=/tmp/foxctl/cas \FOXCTL_OBS_DIR=/tmp/foxctl/observability \foxctl run <skill>