Configuration
Control branding, routes, and every flag-gated feature from the one shared @repo/config object, plus the per-app files that own navigation and banners.
Most configuration lives in @repo/config (packages/config/src/index.ts) - one typed config constant the backend and the frontend both import, so a value changed once propagates everywhere. It is pure static data: no process.env, so a packaged desktop app and a sandboxed renderer can import the same module.
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 and /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/config/{navbar,sidebar,user-menu}.ts | This frontend's menus - see navigation |
| Banners | apps/web/config/banner.ts (bannerConfig) | Announcement bars - see banners |
| Secrets & env | root .env | API keys, connection strings - see environment variables |
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. 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
| Flag | Default | Hidden when off |
|---|---|---|
storage.enabled | true (s3) | File uploads - see storage |
payment.enabled | your init answer | Pricing, checkout, billing - see payments |
sms.enabled | false | Phone 2FA, SMS sends - see sms |
newsletter.enabled | false | 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 | false (turnstile when on) | Bot protection on auth forms - see authentication |
content.sections | blog, alternatives, compare | Content routes - see content |
contentApi.enabled | false | Content API - see authoring content |
docs.enabled | your --docs answer | "Docs" nav link - see authoring docs |
desktop.enabled | your --desktop answer | Desktop app + device sign-in - see desktop app |
ai.enabled | your init answer | Chat, automated runs, capabilities - see AI |
runner.enabled | your init answer (needs ai) | Daemon pairing + the Runners settings tab - see runner |
mcpServer.enabled | false (no ai needed) | Outward MCP server URL + the MCP settings tab - see external agent (MCP) |
revenueSharing.enabled | your init answer | Nothing. It only decides whether the daily license heartbeat self-reports aggregate revenue - see licensing & heartbeat |
observability.sentry.dsn / observability.posthog.publicKey | "" | Error reporting everywhere. The credential is the switch: empty loads no SDK at all - see error reporting |
tenancy.multiTenant | your init answer | Organizations, teams - see organizations |
projects.enabled | your init answer | Project switcher + the Projects settings tab - see projects |
waitlist | false | Waitlist mode; off means signups create real accounts - see waitlist |
cookieBanner | "auto" | Cookie consent banner; false removes all consent gating - see cookie consent |
consentPolicy | "gdpr-and-unknown" | Who must consent before gated analytics run. Also "gdpr-only", "everyone", "never" - see cookie consent |
Shapes that break the { enabled } pattern:
config.apiDocsandconfig.waitlistare bare booleans - read them directly, with no.enabled.config.performanceMonitor.enabledtoggles the Hono request logger. Fullstack projects generate itfalse(Next.js logs requests natively, so the mounted API would double-log); separate-backend projects generate ittrue- see api.config.observabilityhas noenabledflag and no provider selector: a provider runs when its own credential is non-empty, and both may run at once - see error reporting.config.analytics,config.support,config.affiliateare presence-gated - each provider activates when you add its sub-config - see integrations.config.emailhas no off switch - you only pick itsprovider- see email.
The desktop app's AI orchestration gates on config.desktop.agents.enabled, which a desktop project derives from your AI option rather than a separate switch. Every desktop project ships the agent runtime the app forks, so flipping the flag on turns those screens back on with no regeneration. See desktop AI.
Identity, branding, and SEO
| Key | Type | Description |
|---|---|---|
siteName / fullSiteName | string | Short and full app names |
domain | string | Primary domain, no protocol |
baseUrl | string | The project's own origin. Server code reads the BASE_URL env when a deployment sets one, else this literal |
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 |
builtWith | { founderName? }? | Footer attribution line. Set founderName for "Built by {name} using GenerateSaaS"; leave it empty for "Built with GenerateSaaS"; delete the block to remove the line |
Rebranding walks these fields one by one in customization.
CORS origins
config.origins lists the origins allowed to call the backend, as both the Hono CORS allowlist and Better Auth trustedOrigins.
- It is static data, so it holds your development origins and any origin you want committed.
- The backend unions it with
TRUSTED_ORIGINS(comma-separated env), which is where a per-deployment production URL belongs. baseUrlis never auto-trusted. Every origin you serve from must appear in one of the two - 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, automations, settingsModels, settingsIntegrations, settingsRunners, settingsMcp, settingsBilling, notifications, admin, adminOrganizations, adminBillingLogs) so a path change is one edit.
import { redirect } from "next/navigation";
import { config } from "@repo/config";
redirect(config.routes.loginRedirect); // "/dashboard" - survives a path changeUse config.routes.* for any redirect or shared link. Framework-idiomatic route construction (locale prefixing, navigation helpers) lives in data fetching.
AI product - config.ai
config.ai sets who pays for inference and which models are pickable. The AI page covers what it ships.
| Field | Type | Description |
|---|---|---|
enabled | boolean | Master switch. When false the /ai/* endpoints are inert (400) and no AI UI renders. |
builtin | boolean | Your app offers models under its OWN name, billed from user credits. Runs through OpenRouter at live per-token pricing times your markup; the only setup is OPENROUTER_API_KEY. |
builtinModels | string[]? | Limit which models built-in AI offers, as plain OpenRouter model ids. Omit for the full catalog. |
byok | boolean | Let users connect their own provider API keys; MODEL calls on their key never use credits. Web-tool calls still do — they spend your vendor keys, not theirs. |
defaults | { model?, effort?, fallbackModel?, fallbackEffort? } | What every user starts on, so built-in AI needs zero user setup. Their own choice always wins. |
titles | { builtin?, byok?, cli? } | The cheap model that names each conversation, per lane - your decision, never a user setting. See AI. |
terminals | { cli? } | The model a desktop terminal session STARTS on, per coding CLI ({ model }). Empty by default: each CLI picks its own default, on the user's own subscription. |
userAutomations | boolean (default true) | Whether end-users may author automated runs. false hides the create form, 403s the create route, and withholds the create_automation capability; existing automations still list, run, edit, and delete. |
webSearch | boolean (default on) | Master switch over BOTH search lanes. Users opt in with the composer toggle — off by default, remembered per browser. false hides the toggle and refuses search on every lane, BYOK included (searching sends prompts to a third-party provider, so the switch is about data egress, not cost). See web search and extract. |
webTools | { searchEngine?, extractEngine?, searchCostUsd?, extractCostUsd? } | Tunes the app's OWN web tools: which engine serves each capability when several are configured ("firecrawl" by default, "parallel" for the cheaper vendor, "tinyfish" for the free one - chosen per capability, so the two may differ), and what one call costs you (billed to credits with your markup, same price whichever engine ran). Leave the costs unset and each engine charges its own vendor rate - 0.011 Firecrawl search, 0.005 Parallel search, 0 TinyFish search, 0.0032 Firecrawl page, 0.001 Parallel page, 0 TinyFish page. The tools themselves are switched on by FIRECRAWL_API_KEY / PARALLEL_API_KEY / TINYFISH_API_KEY, not here. |
chats | { cap } | Stored chat threads per user; the server evicts the oldest beyond the cap. |
runLimits | { perUser, perPayer } | Concurrent metered runs per user, and the per-balance fallback for plans that set no maxConcurrentRuns of their own. BYOK and runner runs never count. See concurrency caps. |
Two AI sidebar flags are derived, not config keys: aiByok (AI on and byok on) and aiModels (the Models page). Both are computed in sidebar-flags.ts and follow config.ai automatically - see navigation.
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 |
getOrganizationLimit() | Tenancy helper - see organizations |
getFooterColumns(), SOCIAL_PROVIDERS_META | Footer + social provider metadata |
Quick Start
Go from your license key to a running app - create the project, fill in .env, start the dev stack, then build your SaaS with an AI agent.
Environment Variables
The required keys, the origin and rate-limit knobs, how every tool finds the root .env, and the generated Zod schema that validates it at boot.