Skip to main content

Portal

@cona/portal — the only CONA surface an external customer touches. 38 files, deployed to Vercel, reading through @cona/core only. Small app, but it carries the platform’s only non-Auth0 authentication scheme, so the access flow deserves precision.

Access flow

Access is two-factor: possession of the share link, plus knowledge of the customer’s postal code. Neither alone is sufficient. Participants: Page = /portal/[accessToken], S = portal-session.ts, L = rate-limit.ts, Core = @cona/core/domains/portal. The signed cookie is bound to the access token: verifyPortalSession rejects a cookie whose accessToken does not match the URL (app/lib/portal-session.ts:42). A session stolen from one share link cannot open another.

Routes

Two entry paths: the generic share link, and a Shopify-specific path where a customer arrives from their Shopify account to fetch an order document.

Rate limiting

Three independent limiters (app/lib/rate-limit.ts:11-27), all using atomic Redis helpers from @cona/core/redis: Keying the PLZ limiter per access token rather than per IP is the right choice — a distributed guessing attack against one link is still throttled.

Dependencies

Portal reaches the database only through @cona/core — no direct Prisma, no @cona/database import at all. It is the cleanest of the three Next.js apps in that respect.

Notes

Portal has no proxy.ts or middleware.ts, and sets no security headers in code. Its entire header surface is three entries in apps/portal/vercel.json: X-Content-Type-Options, X-Frame-Options, Referrer-Policy.Compared to webapp it lacks CSP, Strict-Transport-Security, and Permissions-Policy. Webapp generates a per-request CSP nonce in proxy.ts:22; portal has no equivalent.This is a hardening gap on the one publicly reachable surface. It is a headers issue, not an authentication one — the access flow above is sound.
No Sentry, no PostHog. Portal reports to Axiom and OpenTelemetry only. See Observability. Portal tokens are minted upstream. ensurePortalTokensForDocumentsActivity runs as step 5.6 of the shared ingestion pipeline, so documents arrive portal-ready. See Temporal Orchestration.