Lossless Changelog
fleet

Confidential Access v2 — Signed-Link Sessions + Telemetry for Client-Content Workspaces

Resolved the long-standing problem that bit us hardest on calmstorm-decks last week: a busy primary stakeholder couldn't get into the gated site, the gate had no server-side telemetry to debug it, and there was no way to attribute downstream sharing. The new pattern — exploration + spec at the cross-cutting level, working implementation in calmstorm-decks — replaces client-side localStorage gates with a DB-backed session model on Astro DB / Turso. Tier 1 is a signed pre-authed link (WhatsApp-deliverable, identity-bound) for direct stakeholders; Tier 3 is a two-role universal passcode (admin / viewer) with a downstream-sharing attribution hack via passcode suffix. Every verify attempt writes an AuthEvent row; every gated page view writes a PageView row; an /admin/activity dashboard surfaces both in real time. Now the lineage of any client interaction is recoverable in a 30-second DB query instead of a multi-hour forensic exercise.

Michael StatonMusic Editoraugmented with Claude Code on Claude Opus 4.7 (1M context)

Confidential Access v2 — A Pattern for Client-Content Workspaces

For three days of work that started as "Lucanus can't get into the deck site," we ended up rebuilding the confidential-access primitive that every site in the family will use when it gates content for a specific counterparty. This is the cross-cutting roll-up.

The problem we kept hitting

Most sites in astro-knots that gate content do so for a reason — fundraise decks, client review surfaces, design-system catalogs that should stay internal, in-progress portfolio sections, sensitive memos. Until this week, the pattern of choice was the [[Confidential-Content-Access-Control-Blueprint]] (context-v/blueprints/): server-validated passcode + httpOnly cookie + middleware route protection. That blueprint was a real improvement over the localStorage gates that preceded it.

But three things had bitten us across multiple sites:

  1. The busy primary stakeholder couldn't get in. This recurred at fullstack-vc earlier (where the OAuth-and-DB-backed system technically worked but UX collapsed downstream — see [[Auth-Identity-System-Worked-but-UX-Failed-Silent-Bounces]]), and at calmstorm-decks last week (where the client-side localStorage gate failed silently in the stakeholder's browser).

  2. We had no telemetry. When auth failed, there was no record to debug. The fullstack-vc team-of-17 lockout became a forensic exercise pulling Turso rows one at a time, and the calmstorm-decks lockout was un-investigable.

  3. Downstream sharing had no attribution. When a stakeholder shared the access code with their LPs / colleagues, those visits were anonymous. We had no way to see "Anthos came in twice and spent 14 minutes on the deck."

All three are symptoms of the same root: the access primitive treated gating as binary (in/out) instead of as a session lifecycle that should leave evidence.

What we built

The pattern, designed cross-cuttingly and implemented end-to-end on calmstorm-decks, has these properties:

A three-tier auth stack (build only the tiers you need)

TierMechanismWho it servesDelivery
1 — DoorPre-authed signed-token link (HMAC-SHA256, identity-bound, expires)Direct named stakeholdersAny 1:1 channel — WhatsApp, Signal, iMessage, email
2 — Self-servicePer-site Google OAuth + domain allowlist + named-guest allowlistReturning stakeholders, partners at the firmStandard OAuth flow
3 — FallbackUniversal passcode, two roles (admin / viewer)Devices without email; downstream sharing the stakeholder chooses to doHand over the string

For most current sites Tier 1 + Tier 3 is enough. Tier 2 lands when the site generalizes beyond one client. The tiers compose — a site can opt in to any subset.

Identity-first data model

Before minting a link, you create an Identity row (slug, label, full_name, email, org, linkedin_url, notes). The mint flow references it. When the link is redeemed, the resulting Session is already bound to a real Identity — no post-hoc reconciliation needed. This structurally avoids the duplicate-row mess that bit fullstack-vc when users alternated providers with different emails.

The schema:

Identity      — pre-seeded by us
MintedToken   — signed-link issuance ledger
Session       — live session (tier, role, shared_label)
AuthEvent     — every verify/redeem attempt, success or failure
PageView      — middleware-driven, every gated request
Action        — interaction events from a vanilla-JS tracker

Lives in Astro DB (libSQL) — same client API for local file DB during dev and Turso in production. Schema is typed TypeScript in db/config.ts, synced to Turso via astro db push --remote.

The downstream-sharing attribution hack

Tier 3 viewer passcode supports an optional suffix: <passcode>-<label>. If VIEWER_PASSCODE=calmbalm, a stakeholder can share calmbalm-northstar with an LP. The server splits on the first dash, validates the left side as the viewer passcode, stores northstar on Session.shared_label, and upserts a synthetic Identity row (shared:northstar) that all subsequent PageView and Action rows attribute to. Stakeholder shares with zero friction; we get LP-level attribution. No new feature needed for them to use it; it Just Works.

Telemetry from day one

AuthEvent writes a row on every verify attempt (success, bad_passcode, config_missing, token_invalid, token_expired, token_exhausted, token_revoked). PageView writes a row from middleware on every gated request. Both carry identity_id + shared_label when present. The /admin/activity dashboard renders both as filterable tables, newest first. The "did the client ever try the link" question is now a 30-second DB query.

Friendly error states (no more raw 400s)

Every failure path on the gate page renders a specific, human message. The old "OAuth state mismatch — please try logging in again" raw 400 page from fullstack-vc Bug 1 is replaced by a redirect-with-error-code system. The verify endpoint never returns a bare error response; it always sends the user back to the gate with a ?error=<code> that maps to a copy-edited message.

Why this matters beyond calmstorm-decks

The motion of "client-focused content development workspace" recurs across the astro-knots family:

  • fullstack-vc has a Stacks / participants surface that gates writes

  • mpstaton-site has draft / context-v rendering that may want gating for unfinished posts

  • hypernova-site has a confidential portfolio view

  • dark-matter has a confidential portfolio + memo flow

  • future client sites built from the [[Build-a-Fundraise-Deck-Workspace]] blueprint will all want this same shape

Each of those projects has either (a) shipped a one-off gating implementation or (b) deferred the gating story. The v2 pattern lets us collapse those one-offs into one shared model: pull db/config.ts, the src/lib/auth/ helpers, the middleware, and the three CLIs into the site, and you have admin-grade session management + telemetry in an afternoon. Once a second site adopts it, the pattern gets promoted from "lived in calmstorm-decks" to a @lossless-group/lfm-style published package (likely @lossless-group/access or similar). Pre-publish, the copy-and-adapt motion documented in [[CLAUDE.md]] applies.

One lesson worth promoting to a blueprint

In Astro v6 server-side code, read env vars from both import.meta.env and process.env.

Vercel runtime injects env vars into process.env. Astro dev (Vite) loads .env into import.meta.env. Code that reads only one of those works in exactly one environment. Code that reads both — import.meta.env first, process.env fallback — works in both, identically, with no special-casing. We learned this the hard way today: the auth code worked on Vercel but failed silently in pnpm dev because the env reader was process.env-only.

This is the dual-source pattern that belongs in the Maintain-Confidential-Access-with-Persistent-Sessions blueprint when we write it (forthcoming), and probably in any general Astro env-handling guidance in context-v/blueprints/.

Where the artifacts live

  • Exploration (parent-level reasoning, ready for blueprint promotion): context-v/explorations/Rethinking-Confidential-Access-with-Persistent-Sessions-and-Auth-Telemetry.md

  • Working implementation: sites/calmstorm-decks/ (at v0.0.7.0 as of this commit). Three site-level changelogs walk through it: 2026-05-10_07, 2026-05-10_08, 2026-05-11_01.

  • Site spec: sites/calmstorm-decks/context-v/specs/Confidential-Access-v2-Persistent-Sessions-and-Telemetry.md

  • Predecessor (still valid, narrower scope): context-v/blueprints/Confidential-Content-Access-Control-Blueprint.md

What's next at the family level

  1. Promote the exploration to a blueprint. context-v/blueprints/Maintain-Confidential-Access-with-Persistent-Sessions-and-Auth-Telemetry.md. Cross-references calmstorm-decks as the reference implementation.

  2. Add the dual-source env-reader pattern to the styles/architecture or env-handling blueprint so future agents stop re-discovering it.

  3. Adopt on a second site. Dark-matter or hypernova are the natural candidates — both already use the v1 confidential-access blueprint. Migration is mostly schema-push + middleware swap + drop the localStorage gate.

  4. Package extraction once two sites are running it cleanly. Probably @lossless-group/access.