Skip to content

Public share surface (tokenized read-only links)

Expert users share read-only financial views via time-limited tokens. Visitors open /teilen?token=sh_… without an account. The pattern combines:

  • Event-sourced token entity (hash in DB, plain show-once)
  • Anonymous query + IP rate-limit
  • Client gate before AuthGate (or createPublicSurface on apex)
  • GDPR export/forget hooks on token metadata

Mirror user-data-rights/download-by-token:

  • tokenHash (SHA-256 hex, unique)
  • expiresAt, revokedAt
  • targetPayload (json snapshot — domain-specific)

Register create, revoke, list, and share-by-token on your app feature.

Check tierHasFeature(tier, TIER_FEATURE.publicShare) before minting. CashColt maps this to Expert.

export const shareByTokenQuery = defineQueryHandler({
name: "share-by-token",
schema: z.object({ token: z.string().min(1) }),
access: { roles: ["anonymous", "Member", "User", "TenantAdmin"] },
rateLimit: { per: "ip+handler", limit: 30, windowSeconds: 60 },
handler: async (query, ctx) => {
const row = await fetchOne(ctx.db.raw, table, {
tokenHash: await hashShareToken(query.payload.token),
});
if (!row || row.revokedAt != null || row.expiresAt <= now) {
throw new NotFoundError("share-token");
}
return buildPublicDto(row);
},
});

Use the same NotFoundError for miss, expired, and revoked — no existence oracle.

await runProdApp({
features: appFeatures,
anonymousAccess: { defaultTenantId: APEX_TENANT_ID },
});

Multi-tenant public hosts: use tenantResolver from subdomain/header (see Anonymous access).

CashColt mounts publicShareClient before the session gate:

// client.tsx — simplified
const gates = [
publicCalculatorClient,
publicShareClient, // /teilen → PublicSharePage
sessionAuthClient,
];

PublicShareGate checks pathname === "/teilen" and renders PublicSharePage, which calls money-horse:query:share-by-token.

Alternative: createPublicSurface for a standalone apex bundle (see apex-surface-auth).

All layouts consume the same ShareByTokenResult DTO. The router picks the React component from presentation.layoutTemplate:

TemplateCharacter
classicBranding header, KPI cards, schedule table — default
heroLarge label + intro, emphasis on headline numbers
editorialMagazine-style typography, narrative intro block
dashboardDense KPI grid, portfolio aggregate for folder packages
immersiveFull-bleed splash + chapter sections (ShareImmersiveSplash, lazy-loaded)

Themes (cashcolt, midnight, forest, graphite, ocean) map to CSS variables via shareThemeCssVars.

  • Public footer — read-only notice, no tracking (see ShareGdprNotice)
  • Art. 17EXT_USER_DATA hook revokes tokens on user forget
  • Art. 20 — export token metadata (without tokenHash / plain token)
LayerWhat to assert
UnitToken entropy, hash stability, plain ≠ entity id
Integrationcreate → query 200; revoke/expired/invalid → 404
E2EExpert dialog → /teilen anonymous, schedule visible

Recipe: public-share-token.

  • managed-pages — branding in public DTO
  • folders — folder-package snapshots
  • CashColt plan: money-horse/docs/plans/public-share-surface.md