GoTrue v2.186 only loads MAILER_TEMPLATES_* over HTTP(S) (local file/file://
paths are rejected), so serve the token_hash confirmation template from the
app's public dir for GoTrue to fetch and cache.
- Add /auth/confirm GET route handler that verifies the signup token via
verifyOtp({ type, token_hash }) and falls back to exchangeCodeForSession
when a PKCE code is present. Whitelists to same-origin paths.
- Signup: pass emailRedirectTo=<APP_URL>/auth/confirm, show a
'check your email' confirmation state with a resend action (cooldown),
and handle the already-registered case.
- Login: detect email_not_confirmed and offer a resend-confirmation action;
surface verification_failed errors from the confirm route.
The upstream admin gateway deterministically truncates the first per-row
getUserById that immediately follows a request's auth check, yielding an
empty body the per-call retry cannot clear — so a couple of tunnel rows
showed owner_email '—' on every load regardless of retry or concurrency.
Replace the per-row getUserById fan-out with a single bounded listUsers
scan (same transient retry) that builds an id->email map and stops early
once every owner on the page is resolved. A single listUsers read does not
hit the truncation pattern, eliminating the dashes. Remove the now-unused
mapWithConcurrency helper and OWNER_EMAIL_CONCURRENCY constant.
The upstream admin gateway tolerates only a small concurrent burst; at
concurrency 4 the tail of the per-row getUserById fan-out was truncated
into empty bodies on every list load (deterministic '—'), and the retries
piled into the same throttle window. Measured 0 failures at concurrency 2
(and 1) versus consistent truncations at 4, so pin enrichment to 2.
A large concurrent burst of getUserById calls during the tunnels-list
email enrichment self-inflicts an upstream throttle (truncated/empty
bodies) that even the per-call retry can't fully escape, intermittently
rendering owner_email as '—'. Add mapWithConcurrency and resolve owner
emails at most a few at a time so each lookup stays inside the throttle
allowance; retry + null fallback preserved.
The defense-in-depth admin guard in middleware short-circuits before the
route handlers' jsonNoStore runs, so its 401/403 JSON denials (and auth
redirects) were served without Cache-Control: no-store. Stamp no-store in
withCookies so every admin deny/redirect response is non-cacheable,
completing Finding #4 for the middleware-originated admin responses.
image/container/network/project names now come from env (WEB_IMAGE,
WEB_CONTAINER, WEB_NETWORK, WEB_PROJECT) with prod values as defaults. Dev
supplies its values via the gitignored .env.production. Makes dev->main merge
safe (prod resolves to web/web:1.0.0/edge by default).
- image web-dev:1.0.0, container web-dev, network dev_edge
- NEXT_PUBLIC_* build args resolved from gitignored .env.production
(api-dev/app-dev URLs) so the dev bundle targets the dev API.
- ignore .env.production (dev secrets) in this branch.