Skip to main content

Cache CLI architecture

The cache CLI is intentionally implemented as a module CLI, not as a hard-coded command in the core CLI binary. That keeps cache operations aligned with Open Saasframe's module architecture: the configs module owns the cache management feature in the UI, the HTTP API, and the CLI.

Discovery through the module generator

The command lives in packages/core/src/modules/configs/cli.ts. It becomes runnable only after the generator scans enabled modules and emits the CLI registry used by yarn saasframe.

Flow:

  1. The configs module exports a cli.ts file with the cache subcommand.
  2. yarn generate scans enabled modules and collects every cli.ts.
  3. The generator emits apps/saasframe/.saasframe/generated/modules.cli.generated.ts.
  4. packages/cli loads that generated registry at runtime.
  5. yarn saasframe configs cache ... resolves the configs module command and dispatches into the cache helper.

This design matters because:

  • the CLI surface stays additive and module-owned
  • apps can disable the owning module and lose the command naturally
  • generator output remains the single source of truth for discovered module commands

For the full generator system, see Generator architecture.

Runtime architecture

The cache command delegates into packages/core/src/modules/configs/lib/cache-cli.ts, which normalizes user input into one of a few request shapes.

Request types

Request typeHelper pathUnderlying cache primitive
statscollectCacheStats()collectCrudCacheStats()
purge --segmentexecuteCachePurge({ kind: 'segment' })purgeCrudCacheSegment()
purge --tagexecuteCachePurge({ kind: 'tags' })cache.deleteByTags()
purge --keyexecuteCachePurge({ kind: 'keys' })cache.delete() for each exact key
purge --idexecuteCachePurge({ kind: 'ids' })cache.keys('*token*') + cache.delete()
purge --patternexecuteCachePurge({ kind: 'pattern' })cache.keys(pattern) + cache.delete()
purge --allexecuteCachePurge({ kind: 'all' })cache.clear()
structuralrunStructuralCachePurge()purge --pattern 'nav:*' + the admin-nav and portal-nav CRUD segments

Scope handling

The helper does not talk to cache storage directly. It always resolves the tenant-aware cache service from DI and applies the selected scope through runWithCacheTenant(...).

Scope rules:

  • --tenant <id> runs inside one tenant scope.
  • --global runs inside the global scope (tenantId = null).
  • --all-tenants loads active tenants from the directory module and executes once for global plus once per tenant.

That is why structural --all-tenants is the safe default for page/module/sidebar changes: navigation cache can exist per user and per tenant, so a single-tenant purge is often insufficient during development.

Structural purge behavior

The structural shortcut is intentionally narrow. It targets nav:* keys and the cached admin/portal navigation API segments because those are the stale entries that typically survive:

  • backend sidebar payloads
  • settings/profile section navigation
  • entity-driven sidebar additions
  • per-user sidebar preference projections

It does not wipe unrelated CRUD caches unless you explicitly ask for --all, --segment, or another broader purge target.

Relationship to the admin cache page

The Backend → Configuration → Cache page and the CLI share the same ownership boundary but not the same surface area.

  • The admin page exposes stats, purge all, and purge segment.
  • The CLI adds tag, key, id, pattern, dry-run, JSON output, and multi-scope iteration.

This split is deliberate:

  • the UI stays safe and task-focused for operators
  • the CLI stays expressive for developers, CI, and troubleshooting

Generate-time structural purge

yarn generate performs a best-effort, bootstrap-free structural invalidation after successful generation when the app enables configs. The automatic path scans the configured stock cache backend's tenant metadata once, removes the structural navigation entries across every stored scope, and refreshes generated artifacts for Turbopack. It does not load the generated CLI registry, initialize application DI, connect to the tenant database, or create request containers.

That post-step is intentionally non-fatal:

  • generation still succeeds if the app does not enable configs
  • generation still succeeds if the configured cache backend is unavailable
  • the generated-artifact refresh is attempted even when cache maintenance fails
  • operators can still run yarn saasframe configs cache structural --all-tenants for the full DI-aware path, including custom cache-service overrides

Source files

FileResponsibility
packages/core/src/modules/configs/cli.tsCLI command parsing, scope resolution, command entrypoints
packages/core/src/modules/configs/lib/cache-cli.tsRequest normalization, preview mode, purge execution
packages/core/src/modules/configs/api/cache/route.tsAdmin page HTTP API (stats, purgeAll, purgeSegment)
packages/shared/src/lib/crud/cache-stats.tsCRUD/widget segment analysis and segment purge
packages/cache/src/service.tsTenant-aware cache wrapper plus bootstrap-free cross-scope maintenance over memory/SQLite/Redis/jsonfile strategies
packages/cli/src/lib/post-generate-invalidation.tsLightweight automatic structural cache purge and generated-artifact refresh

For operator usage, see saasframe configs cache.