Scopes and privacy
Keep identity-bearing values local while allowing deliberate public and tenant sharing.
In Astilba Cache, scope answers a storage question: may this value leave the current isolate and enter a shared tier?
L1 is local to one process or worker; L2 is shared or durable. See Core concepts for the complete storage vocabulary.
Follow the resolution rules
Section titled “Follow the resolution rules”| Inputs | Resolved storage class | Current behavior |
|---|---|---|
scope: “public” |
pub |
Eligible for shared L2, subject to the development request guard below. |
scope: { tenant } |
Hashed ten:<h> |
Eligible for shared L2; the raw tenant identifier is not stored in the scope segment. |
| No declared scope and a visible principal | Hashed usr:<h> |
L1-only and durable: false. |
| No declared scope and no visible principal | pub |
Eligible for shared L2. |
The kernel currently derives a principal from primitive request.userId or request.tenant values, in that order. Other request fields do not affect automatic scope resolution.
const entry = await cache.getOrSetEntry({ key: "profile", request: { userId }, factory: async () => loadProfile(userId),})
entry.durable // false — principal-derived values never reach shared L2Retain private values with L1
Section titled “Retain private values with L1”A principal-derived fill still needs L2 to run in the current kernel, but the result deliberately skips the L2 write. Configure an L1 Store if you want the private value retained for a later call on the same isolate. Without L1, the current call succeeds with durable: false and the next call fills again.
Treat public as a claim
Section titled “Treat public as a claim”With dev: true, Cache wraps ctx.request for an explicitly public factory. Reading any request property demotes that fill to L1-only storage. Merely attaching a request does not demote it; the factory must read through the guarded context.
const entry = await cache.getOrSetEntry({ key: "feed", request: { userId }, scope: "public", factory: async (ctx) => loadFeed(ctx.request?.userId),})
entry.durable // false in dev: the public factory read request dataThis guard is a development aid, not closure analysis. It cannot see identity captured outside ctx.request, and production mode does not install the demoting Proxy. You remain responsible for making every explicit public or tenant cache key cover the data it can expose.
Make tenant sharing deliberate
Section titled “Make tenant sharing deliberate”A request containing only tenant still has visible identity, so an undeclared scope becomes principal-derived and L1-only. Declare a tenant scope when values are intentionally shared inside one tenant.
await cache.getOrSet({ key: "settings", scope: { tenant: tenantId }, factory: async () => loadTenantSettings(tenantId),})Telemetry follows the same posture
Section titled “Telemetry follows the same posture”A plain telemetry sink receives events as emitted and may contain raw identifiers. When telemetry.hosted is true and a project salt is supplied, the kernel HMAC-pseudonymizes every string field except the structural event type. A hosted configuration without a salt suppresses events rather than forwarding raw strings.
Related
Section titled “Related”- Runtime architecture shows how L1 and L2 fit into a configured cache.
- Reading and filling explains durability metadata and tier selection.
- Invalidating data covers the limits of key and scope-qualified selectors.