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
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.