TrademarkTrademark
Ctrl k
Search docs...
Composition

Surfaces and slices

The two units an app is assembled from, and the invariants they hold.
An app is assembled from three kinds of thing: one shell, some surfaces, and the slices those surfaces require. The shell is always on. It is the root layout, the providers, the design tokens, the icon set, the SEO helpers, the OG image routes and the error contract. It is deliberately small, and it must never import from a surface or a slice. A slice is a capability with dependencies, environment variables, providers and files. auth is the one shipped today: the Supabase client layer under src/lib/sb, the sign-in routes, the profile API and the rate-limiting proxy. A slice degrades gracefully at runtime — missing credentials disable it rather than crashing — but graceful degradation is the fallback, not the mechanism. The real mechanism is assembly-time exclusion: a project scaffolded without auth contains none of those files at all. A surface is a user-facing feature area that declares the slices it needs. A surface with requires: [] must work in a project that contains no slice code whatsoever. landing, docs, store and agency all meet that bar. The store surface is the clearest illustration of what that constraint costs and buys. It is a complete storefront — collections, search, product galleries, variants with per-option pricing and availability, a currency selector and a working cart — and it needs no credentials for any of it, because the catalog is read from the filesystem and the cart lives in the browser. What it does not have is a commerce provider. Taking money means credentials, and credentials would break the requires: [] bar. So the surface stops at one function: checkout in (store)/components/StoreProvider.tsx, which reports that nothing is wired rather than pretending to charge anyone. A commerce slice implements that function and swaps the illustrative currency rates for the provider's own prices. No component above it changes shape — which is the whole point of a headless storefront. This repo is the union of every surface and has to build as a single app, so two surfaces can never own the same route. landing owns /; agency — a studio site that would naturally live at the root — is served from /studio instead. Surfaces in that position declare a rootRedirect. In a composition where nothing serves /, the scaffolder writes the redirect into the generated next.config.mjs, and prints which surface it took it from: The first surface named on the command line that declares one wins. These hold for every valid combination, and the scaffolder is what enforces them:
  • The generated project passes next build.
  • A project without a slice contains none of that slice's files, dependencies, environment variables or providers.
  • Providers are wired conditionally — UserProvider exists only when the auth slice does.
  • The generated .env.example lists only the chosen slices' variables.
  • Every route has exactly one owning surface, so the repo — which contains all of them — builds as a single app.
When a slice is left out, the shell still has the lines that referenced it. Each slice carries absentTransforms: exact-match edits applied to the generated project to remove those references. If an anchor no longer matches, the scaffold fails rather than producing a half-wired app — drift in the shell is reported, not absorbed. Surfaces carry absentTransforms too, for the same reason: leaving out the landing surface removes its exports from the content barrel.
Last updated August 27, 2026
Edit this page on GitHub