A visitor without a session can reach a small set of pages. This page covers how each is served.

proxy.ts

Next.js 16 renamed middleware to proxy. apps/saas/proxy.ts exports proxy(request) and a config.matcher. There is no middleware.ts.
It runs in exactly two cases and does two jobs.

Job 1: reject malformed server action ids

Any request with a next-action header is matched. If the header value is not 40 or 42 lowercase hex characters, the proxy answers 400 invalid server action and Next.js never sees the request.

Job 2: show the landing on the bare domain

For a request to / with no cookie whose name contains session_token: The URL stays /. A signed in user passes through and reaches (account)/page.tsx, which redirects to their studio. The proxy does not guard any other route. Deep links such as /login or /{slug}/clients are untouched. The (authenticated) layout is what redirects signed out visitors to /login.
The cookie check is only a routing hint. A stale or forged cookie shows the app shell’s redirect to /login. It grants nothing.

The marketing app

apps/marketing is a separate, very small Next.js app:
  • Dependencies are only next, react and react-dom. It does not use @repo/ui, Tailwind tokens, next-intl or the theme. It is one Hebrew, right to left page (html lang="he" dir="rtl") with its own fonts and CSS.
  • next.config.ts sets output: "export" and assetPrefix: "/mkt".

Why assetPrefix

The landing is served from the app’s own domain, so its chunks must not collide with the app’s /_next files. With the prefix, the exported HTML requests /mkt/_next/.... Images are referenced under /mkt-assets/....

How it reaches production

The build copies the export into the app’s public folder. Both the Dockerfile and the dev.yml workflow do this after building: The proxy’s production rewrite then serves public/mkt/index.html.

In development

pnpm dev runs both apps. The proxy rewrites / to the marketing dev server, and next.config.ts adds two development only rewrites so its assets resolve through port 3000: /mkt/:path* and /mkt-assets/:path*.
  • The pricing section sends the visitor to /start or /start?tier=....
  • The login link goes to /login.
  • The waitlist form posts to /api/waitlist, which forwards to the core API. Because the landing is on the app’s origin, no CORS is involved.
The theme check ignores apps/marketing. Its fixed palette is the design.

/start: self serve signup

app/(start)/start/ is a full screen wizard for a new studio owner. It has its own fonts and palette, loaded from Google Fonts in page.tsx, and is exempt from the theme check. Copy is Hebrew and written inline, not through next-intl. The layout mounts only SessionProvider. The visitor starts anonymous and becomes signed in partway through, so the page must work in both states. Wizard answers are autosaved to localStorage. A Google sign in redirects away and back with ?signup=1, which reopens the signup popup with nothing lost. The account creation sequence is described in Authentication. After the studio exists, the wizard calls these actions, all through performApi with the new slug: ?tier= carries the plan the visitor picked on the landing through to the in app plan picker. The page sets robots: { index: false }.

/sign/[token]: document signing

A trainee opens this from a message. No account, no session. See Route handlers for the relays and Feature areas for the files.
  • Metadata sets robots to no index and referrer: "same-origin", so the token in the path is never sent to another origin.
  • The layout forces a dark background and loads Heebo for body text.
  • SignPage.test.tsx, SignaturePad.test.tsx, sign-model.test.ts and signature-ink.test.ts cover it.

Auth pages

/login, /signup, /verify, /forgot-password and /reset-password render inside AuthWrapper: logo linking to config.marketingUrl (or /), LocaleSwitch, ColorModeToggle, a centered card and Footer. They follow the dashboard theme and translations.

Static pages

Both are plain server components with their content inline. app/robots.ts allows everything. Private pages opt out individually through their metadata.

Reserved paths

These top level paths are real routes or static folders, so a studio slug must never equal one of them: login, signup, verify, forgot-password, reset-password, start, sign, privacy, support, settings, admin, onboarding, new-organization, choose-plan, checkout-return, set-password, organization-invitation, api, mkt, mkt-assets, trainee-preview, select-size-preview. forbiddenOrganizationSlugs in packages/auth/config.ts lists only six of them (new-organization, admin, settings, ai-demo, organization-invitation, chatbot). Slugs are generated by the backend, so check its rules before assuming the rest are protected. Static routes win over the dynamic [organizationSlug] segment in Next.js, so a studio that did get such a slug would be unreachable. The route would not break.