Routes
Every App Router route by area with its auth status, how a published site is reached, and the Convex HTTP endpoints public sites call.
Every route in the Next.js 16 App Router app, with its auth status. Routing
middleware is proxy.ts — Next.js 16 names the file
proxy, not middleware. Auth is Clerk; the protected matcher covers
/dashboard(.*), /onboarding(.*), and /preview(.*).
Status values: working · partial · static-only · dev-only.
Marketing / public (no auth)
| URL | Purpose | Auth | File | Status |
|---|---|---|---|---|
/ | Marketing landing; redirects signed-in users to /dashboard | public | app/page.tsx | working |
/create | Accountless AI generator (prompt → preview) | public | app/create/page.tsx | working |
/pricing | Plan cards | public | app/pricing/page.tsx | working |
/whats-new | User-facing changelog | public | app/whats-new/page.tsx | working |
/privacy-policy | Bilingual privacy policy | public | app/privacy-policy/page.tsx | working |
/terms | Bilingual terms | public | app/terms/page.tsx | working |
/download | Native app downloads | public | app/download/page.tsx | working |
/blog | Marketing blog index (file/MDX, lib/blog) | public | app/blog/page.tsx | working |
/blog/[slug] | Blog post | public | app/blog/[slug]/page.tsx | working |
/blog/tag/[slug], /blog/category/[slug] | Filtered blog lists | public | app/blog/tag|category/[slug]/page.tsx | working |
/blog/feed.xml | RSS/Atom feed | public | app/blog/feed.xml/route.ts | working |
/landing-lab, /landing-lab/* | Design-system concept demos | public | app/landing-lab/** | static-only |
The marketing
/blogis a separate, file-based blog. In-site news articles arepagesrows (pageType: "post") that render under a published site's/news— not here. See Data model.
Auth
| URL | Purpose | Auth | File | Status |
|---|---|---|---|---|
/sign-in/[[...sign-in]] | Clerk sign-in | public | app/sign-in/[[...sign-in]]/page.tsx | working |
/sign-up/[[...sign-up]] | Clerk sign-up | public | app/sign-up/[[...sign-up]]/page.tsx | working |
/sso-callback | OAuth / social callback | public | app/sso-callback/page.tsx | working |
/desktop/signin | Desktop (Tauri) → system-browser OAuth launcher | public | app/desktop/signin/page.tsx | working |
/desktop/handoff | Mints a single-use Clerk ticket → sitebuilder:// deep link | protected | app/desktop/handoff/route.ts | working |
App / dashboard (Clerk-gated)
| URL | Purpose | Auth | File | Status |
|---|---|---|---|---|
/dashboard | Resolver → last company or onboarding | gated | app/dashboard/page.tsx | working |
/dashboard/billing | Subscription + add-ons (Stripe checkout target) | gated | app/dashboard/billing/page.tsx | working (live Stripe gated) |
/dashboard/images | Workspace AI Image Studio (shared across sites) | gated | app/dashboard/images/page.tsx | working |
/onboarding | New-company setup wizard | gated | app/onboarding/page.tsx | working |
/dashboard/websites/[websiteId] | Company home (overview, modules, insights, settings modal) | gated | app/dashboard/websites/[websiteId]/page.tsx | working |
…/[websiteId]/editor | Full visual editor | gated | …/editor/page.tsx | working |
…/[websiteId]/utseende | Appearance / theme | gated | …/utseende/page.tsx | working |
…/[websiteId]/settings | Legacy → redirects to company-home modal | gated | …/settings/page.tsx | working (redirect) |
…/[websiteId]/settings/domain | Custom-domain connect + DNS | gated | …/settings/domain/page.tsx | working (live gated) |
…/[websiteId]/payments | Payments / Stripe Connect dashboard | gated | …/payments/page.tsx | working |
…/[websiteId]/payments/offers | Offers / quotes management | gated | …/payments/offers/page.tsx | working |
…/[websiteId]/services | Services / booking config (Phase S) | gated | …/services/page.tsx | working |
…/[websiteId]/inbox | Leads + bookings inbox | gated | …/inbox/page.tsx | working |
…/[websiteId]/invoices | Invoice management | gated | …/invoices/page.tsx | working |
app/dashboard/layout.tsx adds the Convex Authenticated / Unauthenticated
gate, the account-settings modal, and the image-job watcher. The path segment
under /dashboard/websites/<…> is the company's public slug (id or slug both
resolve via websites.resolveRoute, which canonical-redirects bare ids to the
slug form).
Preview
| URL | Purpose | Auth | File | Status |
|---|---|---|---|---|
/preview/[websiteId]/[[...path]] | Owner's live draft preview | gated | app/preview/[websiteId]/[[...path]]/page.tsx | working (reads draft tables) |
/p/[token]/[[...path]] | Shareable always-live preview (public or password) | token-gated | app/p/[token]/[[...path]]/page.tsx | working |
Both preview surfaces read draft tables through the same SectionRenderer as
production — they never read snapshots. See
Publishing.
Published public site
| URL | Purpose | Auth | File | Status |
|---|---|---|---|---|
/site/[slug]/[[...path]] | Published site (snapshot only) | public | app/site/[slug]/[[...path]]/page.tsx | working (SSR from getPublishedSnapshot) |
/site/[slug]/<locale>/... | Secondary-language pages (/sv, /en) | public | same file | working |
/site/[slug]/sitemap.xml | Per-site sitemap (hreflang, respects noindex) | public | app/site/[slug]/sitemap.xml/route.ts | working |
/site/[slug]/og | On-demand OG share image (next/og, rendered from the snapshot) | public | app/site/[slug]/og/route.tsx | working |
How a published site is reached (proxy.ts)
The middleware resolves an incoming request host to a site slug in three ways:
- Subdomain —
<slug>.<NEXT_PUBLIC_SITES_DOMAIN>rewrites to/site/<slug>. The slug is the subdomain label, so no Convex lookup is needed. Dormant untilNEXT_PUBLIC_SITES_DOMAINis set (sitesSubdomainLabel). - Custom domain — any host that is not the app host and not inside the
platform's own sites zone is resolved through the Convex
/host-slug?host=…endpoint (answer cached ~60s), then rewritten to/site/<slug>. The rewrite setsx-sajt-host: custom(so the site emits root-relative links) andx-sajt-lang. Only domains withstatus === "active"/ LIVE resolve (convex/domains.tsgetSlugByHost). - Path — a direct
/site/<slug>/...request, language-tagged via the/site-langendpoint for the root layout's<html lang>.
Canonical precedence is custom domain > subdomain > path. The <html lang> is
the explicit /en | /sv segment when present, else the looked-up primary
language, else sv.
Customer-facing token pages (no account)
| URL | Purpose | Auth | File | Status |
|---|---|---|---|---|
/invite/[token] | Accept a team invite (sign-in if email differs) | token / public | app/invite/[token]/page.tsx | working |
/invoice/[token] | Customer invoice view + pay link | token-gated | app/invoice/[token]/page.tsx | working |
/offert/[token] | Customer offer (accept / decline) | token-gated | app/offert/[token]/page.tsx | working |
API routes (Next.js route handlers)
| URL | Method | Purpose | File |
|---|---|---|---|
/api/stripe/billing-webhook | POST | Subscription events → api.billing.handleStripeEvent | app/api/stripe/billing-webhook/route.ts |
/api/stripe/connect-webhook | POST | Connect payments → api.stripeConnect.handleConnectEvent | app/api/stripe/connect-webhook/route.ts |
/api/stripe/domain-webhook | POST | Domain-purchase checkout → api.domainsBuy.completeDomainPurchaseFromWebhook | app/api/stripe/domain-webhook/route.ts |
/api/export/[websiteId]/inbox | GET | CSV (leads + bookings) zip export (owner-gated) | app/api/export/[websiteId]/inbox/route.ts |
/api/export/[websiteId]/static | GET | Static HTML site export (owner-gated) | app/api/export/[websiteId]/static/route.ts |
/.well-known/apple-app-site-association | GET | iOS Universal Links config | app/.well-known/apple-app-site-association/route.ts |
Stripe webhooks accept the raw body + signature and forward to a Convex action
that verifies the signature server-side. Export routes call Clerk auth() +
Convex requireWebsiteAccess in the handler.
Convex HTTP endpoints (convex/http.ts)
A separate Convex HTTP router serves the public, no-auth, CORS-open endpoints that published sites and the proxy call. The site is resolved by slug server-side, and writes go only into runtime tables.
| Path | Method | Purpose |
|---|---|---|
/track | POST | Day-bucketed analytics beacon → internal.analytics.record |
/lead | POST | Lead-form capture → internal.analytics.recordLead |
/booking-slots | POST | Read taken slots for a date → internal.bookings.takenSlots |
/booking | POST | Create a native booking → internal.bookings.recordBooking |
/host-slug | GET | Host → site slug (custom-domain routing) → api.domains.getSlugByHost |
/site-lang | GET | Slug → primary language → api.publicSiteMeta.getPrimaryLang |
The write endpoints (/lead, /booking) are rate-limited transactionally
(keyed by slug / IP) and re-validate against the published config — the client is
never trusted. See Auth & permissions.
Synthesis note: the OG share image is a Next.js route handler (
app/site/[slug]/og/route.tsx), not a Convex HTTP endpoint. The two routers are distinct:/api/*and/ogare Next.js handlers; the table above is the Convex router.
Top-level / sitewide
| URL | Purpose | File |
|---|---|---|
/robots.txt | Disallows /dashboard/, /onboarding, /preview/, /api/ | app/robots.ts |
/sitemap.xml | App-level sitemap | app/sitemap.ts |
Native-shell routing notes
- Desktop (
Sajt-DesktopUA): the homepage bounces to/dashboard; protected routes are gated; OAuth runs via the system browser →/desktop/handoff. - iOS (
Sajt-iOSUA): marketing bounces to/sign-in?redirect_url=/dashboard; protected routes are gated; OAuth via the system browser +sitebuilder://deep link.
Dev-only routes
/deveditor and /devpreview are mock-data harnesses that call notFound()
when NODE_ENV === "production". /landing-lab calls notFound() when
VERCEL_ENV === "production" (so it is visible on preview deployments but never
on the production domain). See Deployment.