Appearance
POC access (nanaselect.iwpi.com)
What/why: the POC review host. Invited reviewers reach the entire app — public surfaces and
/admin— athttps://nanaselect.iwpi.com, and nobody else can.iwpi.comis POC-only: production-proper must live on a.nanawall.comsubdomain so the Iubenda consent gate can operate (see Consent on this host, and privacy governance for the model).
The access model — one app, one AUD
The whole host sits behind a single site-wide Cloudflare Access application covering nanaselect.iwpi.com/*. Access is the outer perimeter; the admin's own D1 role check stays as the inner authorization layer (unchanged from admin access):
request ──► Cloudflare Access (edge) ──► app
one app: nanaselect.iwpi.com/* public pages render for any
allowlisted reviewers only, Access-authenticated reviewer;
everyone else blocked at the /admin ADDITIONALLY requires an
edge (email one-time PIN) admin_users role (401/403 on top)This reuses the admin JWT machinery from issue #17 rather than adding a second one. The Worker verifies the Cf-Access-Jwt-Assertion header (src/lib/server/auth/jwt.ts via resolveAdminAuth in src/lib/server/auth/access.ts) against whatever single AUD is configured — it carries no admin-only assumption, so pointing it at the site-wide app's AUD just works. src/lib/server/auth/access.spec.ts exercises exactly this: an arbitrary AUD tag is accepted when configured and rejected otherwise (bad-audience).
The one rule that follows: CF_ACCESS_AUD must be the site-wide app's AUD tag. If a separate admin-only Access app were layered on /admin* with a different AUD, the guard (which verifies one AUD) would reject the site-wide JWT and /admin would fail closed with 401. One app, one AUD.
Wiring (wrangler.jsonc vars)
| Var | Value |
|---|---|
CF_ACCESS_TEAM_DOMAIN | the Zero Trust team domain, <team>.cloudflareaccess.com |
CF_ACCESS_AUD | the site-wide application's AUD tag (Zero Trust → Access → Applications → nanaselect.iwpi.com → Application Audience) |
These are not secrets. While they are empty the deployed admin fails closed (401 — no identity can be established), which is the intended pre-provisioning state.
Invite or remove a reviewer
Reviewer access is managed in one place: the Access policy on the site-wide application (Zero Trust dashboard → Access → Applications → nanaselect.iwpi.com → Policies).
Invite:
- Add the reviewer to the policy — either their individual email, or an email-domain rule (the policy already allows the
@iwpi.comand@nanawall.comendings; individual emails cover everyone else). - Send them the link
https://nanaselect.iwpi.com. On first visit they enter their email and receive a one-time PIN — no account or password to provision. - Only if they need
/admin: also grant a role in theadmin_usersroster (/admin/users,manage_userscapability — see admin access). Without a roster row they can browse the public site but/adminreturns 403 — that's the layers working, not a bug.
Remove: delete their email from the Access policy. They are blocked at the edge on their next request; any /admin roster row is then inert (Access never lets them reach the Worker), but tidy it up in /admin/users anyway.
OTP delivery gotcha: one-time PINs to
@iwpi.comaddresses have failed to arrive in the past, so the policy also allows the@peabod.comending. If a login test gets no PIN, try a@peabod.comaddress (or check iwpi.com mail delivery) before assuming the policy is broken.
Two ways the hostname is served
Now: local dev over the tunnel
nanaselect.iwpi.com is a proxied CNAME to the nanaselect-local cloudflared tunnel, which forwards to the local SvelteKit dev server (port 6650); its sibling docs.nanaselect.iwpi.com forwards to the docs-site dev server. The Access application gates both. To serve it:
sh
npm run dev -- --port 6650 # the app
cloudflared tunnel run --config ~/.cloudflared/config-nanaselect.ymlAlways pass --config — the default ~/.cloudflared/config.yml points at an orphaned tunnel. Two repo-side facts make the tunnel work, and both bite when missing:
vite.config.tshasserver.allowedHosts: ['.iwpi.com'](Vite otherwise blocks the non-localhostHost; it can't be rewritten at the tunnel because SvelteKit's CSRF check compares Origin↔Host and would 403 admin POSTs). Vite does not hot-reload its own config — restart the dev server after changing it.- The dev server runs with the local-dev admin bypass, so
/adminidentity is the seeded superuser regardless of who logged in at Access. That is fine for the tunnel preview; the deployed Worker (below) is where the Access JWT becomes the identity.
At publish: the deployed Worker
When the POC should stop tracking a laptop's dev server, flip the hostname from the tunnel to the Worker:
- Free the DNS name: delete the
nanaselect.iwpi.comCNAME to<tunnel-id>.cfargotunnel.com(iwpi.com zone). The tunnel can keep running for the docs host; only this record moves. - Uncomment the route in
wrangler.jsonc("routes": [{ "pattern": "nanaselect.iwpi.com", "custom_domain": true }]— staged next toworkers_dev, which stays on as fallback). It is commented out until this moment because a live Custom Domain route fights the tunnel CNAME: deploys fail on the conflicting record — or, forced, silently unhook the tunnel. - Set the vars: fill
CF_ACCESS_TEAM_DOMAIN/CF_ACCESS_AUD(table above) inwrangler.jsonc. - Deploy per deployment.
- Verify (see below). History note: an earlier half-finished Custom Domain on this hostname served the deployed Worker with no Access in front — it was removed on 2026-07-05. Step order matters: the Access application must cover the hostname before the route goes live.
Verify the perimeter
After either serving mode changes, confirm all three layers:
- Anonymous is blocked at the edge:
curl -sI https://nanaselect.iwpi.com→ a302to<team>.cloudflareaccess.com(never200, never app HTML). - A reviewer gets in: log in via OTP → the public site renders;
/selectflows work. - Admin stays layered: a reviewer without an
admin_usersrole gets 403 on/admin; a rostered admin gets in. (On the deployed Worker a badCF_ACCESS_AUDshows up here as a blanket 401 — recheck the AUD tag.)
Consent on this host: fails closed, by design
Behavioral capture (measurement/marketing events, lead capture consent) is OFF on nanaselect.iwpi.com and will stay off:
- The consent source of truth is Iubenda via GTM on nanawall.com; its
_iub_cs-<siteId>cookie is scoped to nanawall.com and never accompanies requests toiwpi.com. IUBENDA_SITE_IDis deliberately empty on this host, so the consent gate is unconfigured and fails closed — no capture, ever.
Reviewers exercising the guided flow will therefore see no analytics/lead side-effects. This is the designed POC posture, not a bug — do not "fix" it by hardcoding consent. Production-proper on a .nanawall.com subdomain is where consent lights up; the model is in privacy governance.