Lossless Changelog
component

The hub is gated — a passcode or a @reach.edu email now unlocks everything

The hub was public by design, back when the site was mostly a friendly hello. Now that it's carrying real fundraise decks for a specific stakeholder, it needed a door. So it has one — the lightest one that still actually works.

Michael StatonMusic Editoraugmented with Claude Code on Claude Sonnet 5

A whole-site gate, no database required

Why Care?

Reach Education Hub started life as a public "hello" between Reach University and The Lossless Group — nothing sensitive, nothing worth locking. That changed the moment real fundraise decks with real figures went up for a real stakeholder. Time for a front door.

There's already a fully-built shared identity system elsewhere in this stack (OAuth, magic links, Identity/Session tables in astro:db) — the kind of thing calmstorm-decks and chroma-decks run on. That's the wrong tool here: one known person, one email domain, no need for accounts. So instead of standing up a database, this ships the lightest version that's still a real gate rather than a decorative one: a single server-checked cookie.

What's New?

  • The whole site is gated. Every route redirects to /gate until you're in — home page, strategy decks, one-pagers, the lot.

  • Two ways in, one form field. Type the shared passcode, or type any email ending in @reach.edu — either one unlocks it. No account, no magic-link email, no password reset flow. The email path is a phrase match, not identity verification; that trade-off is intentional for a single known stakeholder, not a general-purpose login.

  • The gate is real, not theatrical. The check happens server-side in src/middleware.ts against an HttpOnly cookie — curl without the cookie gets a redirect, not the page. The passcode and allowed domain live in server-only env vars (RUH_GATE_PASSCODE, RUH_ALLOWED_EMAIL_DOMAIN), never shipped to the browser bundle.

  • 90-day cookie. One stakeholder, no reason to make her re-enter it every session.

How it Works

This is the "Option C" pattern from calmstorm-decks' Gate-Sensitive-Information-with-Simple-Code.md exploration — middleware + cookie, no theater — extended with an email-domain check that exploration didn't cover:

request → src/middleware.ts
            ├─ path is /gate or /api/unlock? → let it through (or nothing
            │  could ever unlock)
            ├─ has the ruh_gate=granted cookie? → let it through
            └─ otherwise → redirect to /gate?next=<original path>

/gate (form) → POST /api/unlock
                 ├─ input contains "@"? → check it ends with
                 │  @{RUH_ALLOWED_EMAIL_DOMAIN}
                 └─ otherwise → compare against RUH_GATE_PASSCODE
                      (trimmed, case-insensitive either way)
               match → set HttpOnly cookie, redirect to `next`
               no match → back to /gate with an inline error

The one wrinkle worth naming: four pages (/strategies, /stories, /automations, /one-pager/[deck]) previously opted into export const prerender = true — static output Vercel serves directly from its CDN, which never touches Astro middleware at all. A prerendered page is invisible to a cookie check by construction. All four lost that flag so the gate actually covers them; the rest of the site was already server-rendered per request (needed for the @dididecks/shell integration), so this was the only place prerendering quietly would have punched a hole in the gate.

What's Next

Two follow-ups worth flagging, not blocking:

  • The site is fully gated now, which means link previews (Slack, email, iMessage unfurls) will likely stop rendering for gated pages — the preview bot doesn't have the cookie any more than a scraper would. Same trade-off calmstorm hit with Vercel's built-in deployment protection. Not fixed here; flagging it since it'll surface the next time a link gets shared.

  • If the stakeholder list ever grows past one person (or one domain), this is the natural point to graduate to the shared id-didi-sh identity service or the full auth-surface pattern — this gate was deliberately built to be outgrown, not extended.