Skip to content

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).

Defaults come from internal/platform/config/config.go:

Config keyDefaultPurpose
storage.root~/.foxctl/storageRoot for all database files
paths.cas~/.foxctl/casContent-addressable storage
paths.cache~/.foxctl/cacheTransient cache files
paths.jobs~/.foxctl/jobsAsync job tracking
paths.observability~/.foxctl/observabilityEvent persistence

Override any path with the corresponding environment variable (e.g. FOXCTL_STORAGE_ROOT, FOXCTL_PATHS_CAS, FOXCTL_OBS_DIR).

The canonical store registry lives in internal/storage/registry.go. Each store has a class that determines its backup and sync policy:

ClassExamplesProduction stance
sync_criticalcoordination.db, sessions.db, tasks.db, mailbox.db, agents.db, memory.db, companion.db, contextvar.dbCore continuity and agent/runtime state. Preserve and migrate deliberately.
sync_usefulknowledge.db, teams.db, trajectory.dbUseful cross-device context. Back up regularly.
local_onlyblackboard.db, cache.db, jobs.db, quotas.db, graph.db, repoindex/<key>.db, embedding_queue.db, summary_queue.dbRebuildable or device-local state. Regenerate from canonical inputs.
observabilityevents.dbStructured event persistence. See Observability for query patterns.
externalopencode.dbExternal import surface.
DatabaseKey tables
memory.dbnamed_memory, embedding_metadata, indexer_state
contextvar.dbcontext_variables, context_sequences
companion.dbcompanion_turns, companion_events, companion_hard_state_entries, companion_soft_episodes, companion_evidence_snippets, companion_assumptions_ledger
repoindex/<key>.dbGraph nodes, edges, index metadata per workspace

The storage layer supports per-store database drivers through internal/storage/dbdriver:

DriverUse case
sqliteDefault. Local development, single-node.
tursoRemote SQLite-compatible. Cloud deployments.
postgresHorizontal scaling, shared state.

Driver selection uses per-store config with fallback:

  1. FOXCTL_<STORE>_DB_DRIVER (store-specific)
  2. FOXCTL_DB_DRIVER (global fallback)
  3. sqlite (default)

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.

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.

BackendConfig keyUse case
fileFOXCTL_CAS_DRIVER=fileLocal filesystem (default)
sqliteFOXCTL_CAS_DRIVER=sqliteSingle-file CAS
tursoFOXCTL_CAS_DRIVER=tursoRemote SQLite-compatible
s3FOXCTL_CAS_DRIVER=s3Enterprise object store

For S3-backed CAS:

Terminal window
FOXCTL_CAS_DRIVER=s3
FOXCTL_CAS_S3_BUCKET=my-cas-bucket
FOXCTL_CAS_S3_REGION=us-east-1
FOXCTL_CAS_S3_ENDPOINT=https://s3.example.com # optional, for MinIO
FOXCTL_CAS_S3_PREFIX=foxctl/
FOXCTL_CAS_S3_FORCE_PATH_STYLE=true
RuleBehavior
AddressingSHA-256 digest (sha256:...)
Inline thresholdLarge outputs move to CAS with summary + artifact pointer
IntegrityReads re-validate digest and fail on mismatch
RetentionUse pin/gc controls for lifecycle management
Terminal window
# Store an artifact
foxctl cas put < artifact.json
# Retrieve by digest
foxctl cas get sha256:abc123...
# Pin an artifact to prevent garbage collection
foxctl cas pin sha256:abc123...
# Garbage collect unpinned artifacts older than 7 days
foxctl cas gc --older-than=168h --dry-run

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.

Events use a hybrid persistence model with five modes:

ModeDescriptionUse case
PersistDefaultNDJSON fileMost events — fast, append-only
PersistNDJSONExplicit NDJSON fileSame as default, explicit selection
PersistSQLDirect SQLite writeHigh-value events needing queryability
PersistHybridNDJSON + background SQLite syncFast writes + queryability
PersistNoneNo persistenceEvents only for sampling/logging

Storage locations:

FileLocation
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.

Terminal window
# Create a named backup
foxctl backup create --name nightly
# List existing backups
foxctl backup list
# Inspect a specific database
foxctl db --help
InvariantWhy it matters
Deterministic schema migrationsKeeps CLI/daemon startup predictable
Store class boundariesPrevents accidental syncing of local-only data
CAS for large payloadsKeeps envelopes small and replayable
Context cancellation through storage callsPrevents stuck long-running DB operations
  • Do not treat local repoindex databases as source of truth — they are rebuildable projections.
  • Preserve repo_key and 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:
Terminal window
FOXCTL_STORAGE_ROOT=/tmp/foxctl/storage \
FOXCTL_PATHS_CAS=/tmp/foxctl/cas \
FOXCTL_OBS_DIR=/tmp/foxctl/observability \
foxctl run <skill>