frontend/apps/saas/app/api. Most of the app uses server actions, so each handler here exists for a specific reason: the browser needs a streamed body, a cancellable GET, a public endpoint or a transparent proxy.
Summary
The catch-all proxy
app/api/[[...rest]]/route.ts forwards anything not matched by a more specific handler to the core API with proxyToCoreApi. This is how /api/auth/* (better-auth) and /api/rpc/* (oRPC) work from the browser. The details are in Talking to the core API.
Chunked media upload relay
app/api/content/media-upload/[action]/route.ts accepts POST for four actions: start, part, complete and abort. Any other action returns 404.
The media bucket is shared with other products and its CORS policy cannot be changed, so the browser cannot upload to storage directly. Instead it slices the file and posts each part to this route, and the API streams the part into an S3 multipart upload.
Limits and checks:
- The studio comes from
?slug=throughresolveStudioContext. No context returns401. This is what stops the route being an open relay. - JSON bodies are capped at 128 KiB (
MAX_JSON_BYTES). - A part is capped at 16 MiB (
MAX_PART_BYTES). The client sends 8 MiB parts. An empty part returns400, an oversized one413. - The
partslist incompleteis validated entry by entry:partNumbermust be an integer of at least 1 andetaga non empty string. - A
CoreApiErroris relayed as its status andcodeonly. Other failures return502.
part action forwards the body as raw bytes. Parsing or base64 encoding it would corrupt the part.
PDF relays for a studio
modules/shared/lib/studio-pdf-relay.ts exports relayStudioPdf(request, id, pathOf, disposition?). The media CDN answers without CORS headers, so pdf.js cannot read a stored PDF by URL, and a link with download to another origin only opens a tab. Streaming through the web origin solves both.
idmust match/^[A-Za-z0-9_-]{1,64}$/, otherwise404.?slug=selects the studio. No context returns401.- The upstream body is streamed back with
content-type: application/pdf,cache-control: private, no-storeandx-content-type-options: nosniff. - A refusal is relayed by status and code only.
Public signing relays
Trainees open a signing link from a message, usually on a phone and signed out. The token in the path is the only credential.modules/sign/sign-upstream.ts holds the shared code.
isSignToken(token)checks the token againstSIGN_TOKEN_PATTERN(16 to 64 characters of letters, digits,_and-) before anything is sent upstream. A bad token returns404.signUpstream(token, init)makes a signed call to/v1/public/sign/:tokenand forwards the visitor’s address and browser asx-client-ipanduser-agent.clientIpOf(headers)preferscf-connecting-ip, thenx-real-ip, then the last element ofx-forwarded-for. The leftmostx-forwarded-forentry is client supplied and is never trusted.- Responses carry
cache-control: no-storeandx-robots-tag: noindex.
The page itself,
(sign)/sign/[token]/page.tsx, loads the first view on the server with loadSigning(token), which is wrapped in React cache() so generateMetadata and the page body share one call.
Public passthroughs
These have no session and forward to public core API endpoints with a plainfetch.
/api/onboarding/*
Used by SignupPopup.tsx in the /start wizard to verify a phone number and an email before an account exists. Each handler:
- Returns
503whenCORE_API_URLis not set. - Rejects bodies over 2,048 bytes with
413. - Forwards the caller’s address as
x-client-ip(same header order as the signing relay), so the upstream per IP throttle does not see every visitor as the web server. - Returns the upstream status and body, or
502on a network error.
/api/waitlist
The marketing landing is served on the app’s origin, so its waitlist form posts here and the handler forwards to /v1/public/waitlist-leads. No CORS is involved. Bodies over 4,096 bytes return 413, invalid JSON returns 400.
Work board reads
The task panel on the work board loads a lot of trainee data as the coach moves between tasks. These reads are GET route handlers instead of server actions, for two reasons stated inworkboard/panel-fetch.ts:
- Next.js runs a page’s server actions one at a time. A queued board read was holding up the next task’s context, and even “mark done”.
- A
fetchtakes anAbortSignal, so a stale read is cancelled when the selection moves on.
workboard/panel-route.ts holds the shared server side:
401 when that fails), runs the loader, and returns JSON with cache-control: no-store. A CoreApiError becomes its status and code, anything else 502. Upstream error text never crosses over.
The loaders are the same functions the server actions export, so access rules are identical.
On the browser side,
panelReadUrl(slug, segments, query) builds the URL with every segment encoded, and the request functions throw a PanelReadError carrying the status.
Writing a new handler
Prefer a server action. Add a route handler only when one of these applies:- The response is a stream or a file.
- The request body is binary and large.
- The read must run in parallel with other reads, or be cancellable.
- The caller has no session (public link, marketing form).
- Resolve the studio from the session with
resolveStudioContext(slug)and return401when it isnull. - Validate ids and sizes before calling upstream.
- Relay only the status and the error
code. Do not forward upstream error text. - Set
cache-control: no-storeon anything private.