Control cache sharing
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 isolate; L2 is shared or durable. The Workers factory supplies a bounded memory() L1 automatically. See Cache fundamentals 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 derives a principal from primitive request.userId or request.tenant values, in that order. Other request fields do not affect automatic scope resolution. The React Router adapter carries the application-derived request object through currentRequest(); the application remains responsible for authenticating it.
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. createWorkersCache() includes a bounded memory L1 for this reason.
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 all inputs that can change the returned value.
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. Built-in delivery also swallows a sink that throws or rejects; configure onSinkError to observe that failure separately.
The memory() Store can emit private_evicted when an LRU entry with usr: scope is removed under entry or byte pressure. Its payload carries only a count and byte size—never a key, scope hash, or tag. createWorkersCache() does not expose this internal L1 telemetry option; raw composition is required to enable it.
Apply scope to rendered responses
Section titled “Apply scope to rendered responses”Value scope also gates React Router response caching. The middleware records scope evidence for every served Cache entry and successful fill:
- only readable
pubdependencies remain eligible forCache-Tagemission; - a tenant or principal dependency forces
Cache-Control: privatefor the whole response; - missing or malformed stored scope also fails closed to private;
l3: falsemay suppress a tag, but it never suppresses the entry’s scope evidence.
A bare RenderCollector.dependsOn() is different: it is not backed by a managed entry, so it has no scope claim and cannot poison the response by itself. See Cache HTTP responses for the complete header algorithm.
Related
Section titled “Related”- Runtime architecture shows how L1 and L2 fit into a configured cache.
- React Router shows how a server adapter carries authenticated identity into the request frame.
- Read and cache values explains durability metadata and tier selection.
- Invalidate cached data covers the limits of key and scope-qualified selectors.
- Cache HTTP responses explains how value scope controls shared-response eligibility.
- Inspect cache behavior covers scope evidence in
explain()and telemetry.