Configuration
Control branding, routes, and every flag-gated feature from the one shared @repo/config object.
Most configuration lives in @repo/config (packages/config/src/index.ts) - one typed config constant the backend and every frontend import, so a value changed once propagates everywhere. UI menus are the exception: they're per-app files so each frontend owns its own navigation and banners.
Where configuration lives
| Surface | Location | Controls |
|---|---|---|
| Central config | packages/config/src/index.ts (config) | Feature flags, identity/branding, routes, vendor providers |
| Pricing | packages/config/src/pricing.ts (pricingConfig) | Plans, prices, credits, products - see payments |
| Locales | packages/config/src/i18n.ts (i18nConfig) | Supported languages - see i18n |
| Cache & limits | packages/config/src/cache.ts (cacheConfig) | TTLs, rate-limit windows - see caching |
| Section tabs | packages/config/src/section-tabs.ts (sectionTabsConfig) | /admin, /settings sub-tabs - see dashboard |
| Roles & tenancy | packages/config/src/{roles,tenancy}.ts | Role names, org limits - see organizations |
| Navbar / Sidebar / User menu | apps/web-next/config/{navbar,sidebar,user-menu}.ts | This frontend's menus - see navigation |
| Banners | apps/web-next/config/banner.ts (bannerConfig) | Announcement bars - see banners |
| Secrets & env | root .env | API keys, connection strings - see environment variables |
The rest of this page covers the central config object; follow the links above for each surface's detail.
How flags gate features
Toggleable features are a discriminated union - { enabled: false } | { enabled: true; ... } - so reading config.X.enabled narrows the type and exposes provider fields only when on.
import { config } from "@repo/config";
if (config.payment.enabled) {
// config.payment.provider is now available ("stripe" | "polar")
}A disabled feature must never render or execute. Always check config.X.enabled before rendering UI or running logic - guard backend handlers, hide nav items, and skip client effects when it is false.
Feature flag reference
Each feature page documents the same three things you should reason about: the flag, what hides when off, and the shipped default.
| Flag | Default | Hidden when off |
|---|---|---|
storage.enabled | true (s3) | File uploads - see storage |
payment.enabled | true (stripe) | Pricing, checkout, billing - see payments |
sms.enabled | true (twilio) | Phone 2FA, SMS sends - see sms |
newsletter.enabled | true (listmonk) | Newsletter signup - see email |
apiKeys.enabled | true | API key generation - see api |
apiDocs | true | Scalar API reference at /api/docs - see api |
notifications.enabled | true | Notification bell - see notifications |
adminNotifications.enabled | true | Admin business-event alerts - see notifications |
captcha.enabled | true (turnstile) | Bot protection on auth forms - see authentication |
blog.enabled | true | Blog routes - see blog |
contentApi.enabled | true | Content API - see blog |
docs.enabled | false (on with generatesaas init --docs) | "Docs" link in marketing nav - see authoring docs |
desktop.enabled | false (on with generatesaas init --desktop) | Desktop app + device sign-in - see desktop app |
ai.enabled | false (a buyer opts in + adds keys) | AI product: chat, scheduled runs, capabilities - see AI |
companion.enabled | false (requires ai) | Companion daemon pairing + the Companions settings tab - see companion |
mcpServer.enabled | false (no ai required) | Outward MCP server URL + the MCP settings tab - see external agent (MCP) |
observability.sentry.enabled | false (also needs a dsn) | Sentry error reporting on the API, browser, and desktop app; off, the SDK is never loaded - see error reporting |
tenancy.multiTenant | true | Organizations, teams - see organizations |
waitlist | false | Normal signup (waitlist mode off) - see waitlist |
cookieBanner | "auto" | Cookie consent banner; false removes all consent gating - see cookie consent |
consentPolicy | "gdpr-and-unknown" | Which visitors must consent before consent-required analytics run ("gdpr-only", "everyone", "never") - see cookie consent |
Two flags are bare booleans, not the { enabled } union - read config.apiDocs and config.waitlist directly (no .enabled). config.performanceMonitor.enabled toggles the Hono request logger; fullstack projects generate with it false (Next.js logs requests natively, so the mounted API would double-log) and separate-backend projects with it true - see api. config.observability.sentry is nested one level deeper than the rest and needs both enabled: true and a non-empty dsn before anything is reported; it lives in its own env-free packages/config/src/observability.mjs so the desktop app can read it - see error reporting.
Some capabilities are presence-gated, not boolean: config.analytics, config.support, and config.affiliate activate per provider key you add - see integrations.
The desktop app's AI orchestration gates on config.desktop.agents.enabled, which a desktop project derives from your AI option (not a separate switch). The app always ships that AI source, so flipping the flag on turns the screens back on - but the bundled companion daemon is not runtime-toggleable: a project generated without AI never received apps/companion. Leaving the flag off costs nothing (the staging step skips itself and the app packages fine); it is turning it on that requires re-running the CLI with AI on first - see desktop AI.
Identity, branding, and SEO
Always-present fields drive titles, structured data, and the logo.
| Key | Type | Description |
|---|---|---|
siteName / fullSiteName | string | Short and full app names |
domain | string | Primary domain, no protocol |
baseUrl | string | process.env.BASE_URL ?? "https://generatesaas.com" |
logo | { main, square } | Logo image paths |
indexable | boolean (default true) | Gates search indexing + sitemap; set false for staging |
business | { name, address, registrationNumber } | Legal pages, invoices |
phone | { number, formatted } | tel: links |
seo | SeoConfig | Organization structured-data fields |
social | object | Footer profile links - absent keys render nothing |
CORS origins
config.origins lists the origins allowed to call the backend.
- It reads
process.env.TRUSTED_ORIGINS(comma-separated, trimmed), falling back to local dev ports. - It is passed verbatim to both Hono CORS and Better Auth
trustedOriginswith nobaseUrlinjection. - Every origin you serve from - including production
baseUrl- must be inTRUSTED_ORIGINS. See environment variables.
Shipping the desktop app? It needs no entry in TRUSTED_ORIGINS: its main process makes every backend call - dev and packaged alike - with no Origin header, so the backend treats it as a native client and never CORS-checks it. TRUSTED_ORIGINS governs your web frontends.
Routes - never hardcode paths
config.routes.* centralizes shared in-app paths (home, auth, loginRedirect, dashboard, pricing, onboarding, settings, chat, schedules, settingsModels, settingsIntegrations, settingsCompanions, settingsMcp, notifications, admin) so a path change is one edit.
import { config } from "@repo/config";
redirect(config.routes.loginRedirect); // "/dashboard" - survives a path changeRule: use config.routes.* for any redirect or shared link - never hardcode "/dashboard". Framework-idiomatic route construction (locale prefixing, navigation helpers) lives in data fetching.
AI product - config.ai
config.ai configures the AI product (chat, scheduled runs, capabilities); the AI page covers what it ships. The fields below set who pays for inference and which models are pickable.
| Field | Type | Description |
|---|---|---|
enabled | boolean | Master switch. When false the backend /ai/* API endpoints are inert (400) and no AI UI renders. Ships false; a buyer opts in and adds provider keys. |
builtin | boolean | Built-in AI: your app offers models under its OWN name, billed from user credits. Powered by OpenRouter at live per-token pricing times your shared markup - the only setup is OPENROUTER_API_KEY. Users see your product as the provider, never "OpenRouter". |
builtinModels | string[]? | Optionally limit which models the built-in AI offers, as plain OpenRouter model ids. Omit for the full catalog. |
byok | boolean | Let users connect their own provider API keys; runs on their key never use credits. |
defaults | { model?, effort?, fallbackModel?, fallbackEffort? } | What every user starts with - set these and built-in AI works with zero user setup (chat, schedules, and the picker are pre-configured; users only need credits). Model values are plain OpenRouter model ids; a user's own choice always wins. |
userSchedules | boolean (default true) | Whether end-users may author their own scheduled runs. When false, the create form is hidden, the create route returns 403, and the create_schedule capability is withheld; listing, running, editing, and deleting existing schedules are unaffected. Mirrors the desktop app's desktop.agents.userSchedules. |
webSearch | boolean (default on) | Provider-native web search, on automatically for every provider whose SDK supports it. Always on for user-keyed runs; set false to opt out on built-in runs (provider search fees are billed outside token usage and are not metered by credits). |
One AI sidebar flag is derived, not a config key: aiByok (AI on and byok on - the Models surface) is computed in sidebar-flags.ts, so it follows config.ai automatically and takes no separate setting.
Barrel exports
@repo/config also re-exports named values that are NOT keys on config - import them directly.
| Export | Purpose |
|---|---|
pricingConfig, mainCurrency | Plans, credits, products - see payments |
i18nConfig, Locale | Supported locales - see i18n |
cacheConfig | TTLs + rate-limit windows - see caching |
getBillingScope(), getOrganizationLimit() | Tenancy helpers - see organizations |
getFooterColumns(), SOCIAL_PROVIDERS_META | Footer + social provider metadata |
Frequently asked questions
How do I turn a feature off?
Set its flag to { enabled: false } (or false for waitlist) in packages/config/src/index.ts. Every consumer reads the flag, so UI, routes, and jobs stand down automatically.
Where do the defaults live?
In config itself - the shipped values in packages/config/src/index.ts are the defaults. Edit them in place; there is no hidden layer.
Why are provider fields sometimes missing in TypeScript?
Toggleable configs are a discriminated union. Provider fields only exist after you narrow with config.X.enabled === true.
Where do secrets and API keys go?
Not here. config holds non-secret behavior; keys and credentials live in environment variables.