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.
Job 1: reject malformed server action ids
Any request with anext-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,reactandreact-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.tssetsoutput: "export"andassetPrefix: "/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’spublic 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*.
Links out of the landing
- The pricing section sends the visitor to
/startor/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.
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
robotsto no index andreferrer: "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.tsandsignature-ink.test.tscover 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.