Skip to content

POC access (nanaselect.iwpi.com)

What/why: the POC review host. Invited reviewers reach the entire app — public surfaces and /admin — at https://nanaselect.iwpi.com, and nobody else can. iwpi.com is POC-only: production-proper must live on a .nanawall.com subdomain 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)

VarValue
CF_ACCESS_TEAM_DOMAINthe Zero Trust team domain, <team>.cloudflareaccess.com
CF_ACCESS_AUDthe 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:

  1. Add the reviewer to the policy — either their individual email, or an email-domain rule (the policy already allows the @iwpi.com and @nanawall.com endings; individual emails cover everyone else).
  2. 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.
  3. Only if they need /admin: also grant a role in the admin_users roster (/admin/users, manage_users capability — see admin access). Without a roster row they can browse the public site but /admin returns 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.com addresses have failed to arrive in the past, so the policy also allows the @peabod.com ending. If a login test gets no PIN, try a @peabod.com address (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.yml

Always 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.ts has server.allowedHosts: ['.iwpi.com'] (Vite otherwise blocks the non-localhost Host; 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 /admin identity 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:

  1. Free the DNS name: delete the nanaselect.iwpi.com CNAME to <tunnel-id>.cfargotunnel.com (iwpi.com zone). The tunnel can keep running for the docs host; only this record moves.
  2. Uncomment the route in wrangler.jsonc ("routes": [{ "pattern": "nanaselect.iwpi.com", "custom_domain": true }] — staged next to workers_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.
  3. Set the vars: fill CF_ACCESS_TEAM_DOMAIN / CF_ACCESS_AUD (table above) in wrangler.jsonc.
  4. Deploy per deployment.
  5. 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:

  1. Anonymous is blocked at the edge: curl -sI https://nanaselect.iwpi.com → a 302 to <team>.cloudflareaccess.com (never 200, never app HTML).
  2. A reviewer gets in: log in via OTP → the public site renders; /select flows work.
  3. Admin stays layered: a reviewer without an admin_users role gets 403 on /admin; a rostered admin gets in. (On the deployed Worker a bad CF_ACCESS_AUD shows up here as a blanket 401 — recheck the AUD tag.)

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 to iwpi.com.
  • IUBENDA_SITE_ID is 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.