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.
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
/gateuntil 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.tsagainst anHttpOnlycookie —curlwithout 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 errorThe 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-shidentity service or the full auth-surface pattern — this gate was deliberately built to be outgrown, not extended.
