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:
- The
configsmodule exports acli.tsfile with thecachesubcommand. yarn generatescans enabled modules and collects everycli.ts.- The generator emits
apps/saasframe/.saasframe/generated/modules.cli.generated.ts. packages/cliloads that generated registry at runtime.yarn saasframe configs cache ...resolves theconfigsmodule 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 type | Helper path | Underlying cache primitive |
|---|---|---|
stats | collectCacheStats() | collectCrudCacheStats() |
purge --segment | executeCachePurge({ kind: 'segment' }) | purgeCrudCacheSegment() |
purge --tag | executeCachePurge({ kind: 'tags' }) | cache.deleteByTags() |
purge --key | executeCachePurge({ kind: 'keys' }) | cache.delete() for each exact key |
purge --id | executeCachePurge({ kind: 'ids' }) | cache.keys('*token*') + cache.delete() |
purge --pattern | executeCachePurge({ kind: 'pattern' }) | cache.keys(pattern) + cache.delete() |
purge --all | executeCachePurge({ kind: 'all' }) | cache.clear() |
structural | runStructuralCachePurge() | 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.--globalruns inside the global scope (tenantId = null).--all-tenantsloads 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, andpurge 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-tenantsfor the full DI-aware path, including custom cache-service overrides
Source files
| File | Responsibility |
|---|---|
packages/core/src/modules/configs/cli.ts | CLI command parsing, scope resolution, command entrypoints |
packages/core/src/modules/configs/lib/cache-cli.ts | Request normalization, preview mode, purge execution |
packages/core/src/modules/configs/api/cache/route.ts | Admin page HTTP API (stats, purgeAll, purgeSegment) |
packages/shared/src/lib/crud/cache-stats.ts | CRUD/widget segment analysis and segment purge |
packages/cache/src/service.ts | Tenant-aware cache wrapper plus bootstrap-free cross-scope maintenance over memory/SQLite/Redis/jsonfile strategies |
packages/cli/src/lib/post-generate-invalidation.ts | Lightweight automatic structural cache purge and generated-artifact refresh |
For operator usage, see saasframe configs cache.