Features
Toggle optional surfaces (docs, blog, API reference, waitlist) from one config, chosen at init and flippable anytime.
Not every product wants a public blog, a docs site, or a waitlist. ZeroStarter makes each optional surface a feature flag: one boolean in config that turns the whole surface on or off. The starter ships with every feature on (it demos them all); a fork picks its set at init and can change it at any time.
The flags
The flags live next to the brand in packages/config/src/site.ts, as a separate features export (typed boolean, not as const, so a fork can flip them and the runtime gates stay live):
export const features = {
allowlist: true,
apiDocs: true,
blog: true,
docs: true,
internalDocs: true,
waitlist: true,
}| Flag | Surface | Off means |
|---|---|---|
allowlist | Console > Access > Allowlist, /api/v1/admin/allowlist*, and the console grant it drives | the page and the API 404, the nav drops it, and console access is granted per person only |
apiDocs | /api/docs Scalar reference UI + the /api/openapi.json spec | both 404 and the navbar "API Docs" link is hidden |
blog | /blog and its posts | /blog 404s, the navbar link, /og/blog, sitemap, and llms.txt blog entries all drop |
docs | /docs | /docs 404s, the navbar link, /og/docs, search (/api/search), sitemap, and llms.txt docs entries all drop |
internalDocs | /console/docs (internal docs, member and above) | the console docs render the not-found page and their search (/api/console/search) 404s |
waitlist | /waitlist page and its API, plus Console > Waitlist > Signups and /api/v1/admin/waitlist* | /waitlist, /api/waitlist and the console page 404, the nav drops it, and a fresh fork's home becomes a plain landing page |
"Off" is not a stub: the route returns a real 404, the links and navigation entries disappear, and the surface is dropped from the sitemap, llms.txt, and search, so nothing points at a page that no longer exists.
One soft 404
The console area is force-dynamic (its gate runs per request), and Next commits a 200 status before a page-level notFound() fires mid-stream, so a disabled /console/docs renders the not-found page with an HTTP 200, not a 404. The same is true of any unknown console path, which a catch-all routes into the same boundary on purpose, so a forbidden page and a nonexistent one cannot be told apart. The area is gated and noindex with no content leak, and the console docs search still returns a hard 404. Every other disabled surface returns a real 404.
One nuance: the API routers stay mounted behind a runtime 404 guard so a fork can flip a flag on later without an AppType change. apiDocs gates the Scalar UI and the /api/openapi.json spec together, but the other flags do not rewrite the spec, so with apiDocs on and, say, waitlist off, /api/waitlist still appears in the document and returns 404 at runtime.
The home and the waitlist
A fresh fork's home (web/next/src/app/page.tsx) reads features.waitlist at runtime:
- on it redirects to the
/waitlistcapture page; - off it renders a plain landing page (your product name and tagline).
Flip features.waitlist and the home re-themes on the next request. Nothing is regenerated. (The starter's own marketing home is separate and always renders; the landing branch is for forks.)
Choosing at init
zerostarter init sets the fork's features for you. Non-interactively, pass a flag per surface (its --no- form turns it off); interactively, a checklist appears, pre-checked to the fork defaults:
# flags: any --<flag>/--no-<flag> skips the picker
bunx zerostarter init my-app --no-blog --waitlist
# no flags + a TTY: pick from the checklist (space toggles, enter confirms)
bunx zerostarter init my-appThe fork defaults are docs, blog, internal docs, and the API reference on; the waitlist off (so a fresh fork lands on a plain home). Skipping the picker keeps those defaults. --yes takes them without prompting.
Enable anytime
Toggling a feature never strips code. init only writes the flags, and every surface stays in the tree gated behind a runtime read of features. So turning a feature back on is a one-line config edit, no re-scaffolding:
// packages/config/src/site.ts
export const features = {
// ...
blog: true, // was false; the /blog, its nav link, sitemap, and llms entries all return
}Bigger surfaces stay on
Auth, the dashboard, the console shell, and organizations are foundational (they reach into the database, login, and route protection), so they are not feature-flagged. The waitlist's database table also stays when the flag is off; dropping it would be a migration.
Next
- Content (Blog & Docs): how the docs, blog, and console collections are authored behind these flags.
- SEO & Metadata: the sitemap and OG routes that read the flags.
- Quickstart: the
initrun that sets them.