Skip to content
Sponsor

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.

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.

profile.ts
const entry = await cache.getOrSetEntry({
key: "profile",
request: { userId },
factory: async () => loadProfile(userId),
})
entry.durable // false — principal-derived values never reach shared L2

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.

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.

public-feed.ts
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 data

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

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.

tenant-settings.ts
await cache.getOrSet({
key: "settings",
scope: { tenant: tenantId },
factory: async () => loadTenantSettings(tenantId),
})

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.