Get started
Configuration
starter.config.ts(Licensees only) decides which parts of the kit are
on, which providers each part bundles, and your plans, roles, permissions and
languages. You edit it whenever you change one of those. Secrets and values
that differ between deploys go in environment variables instead.
How it works
defineStarterConfigreturns the object unchanged. It exists for its types: the literals you write become unions such asPlanId,UserRole,PermissionandFlagIdinshared/types/config.ts(Licensees only), so a misspelled plan or permission fails typecheck. The shape isStarterConfiginshared/types/starter.ts(Licensees only).nuxt.config.tsloads the file at build time.build/assertions.ts(Licensees only) checks it againstwrangler.jsoncandi18n/locales/, and the registries inmodules/check providers, plans and roles. Every failure names the key and the fix.- Only the providers you list are bundled.
build/runtime-config.ts(Licensees only) declares a runtime-config slot for each credential those providers read, andNUXT_*environment variables fill the slots. - When a deployed Worker boots,
server/plugins/env-check.ts(Licensees only) logs one[env]warning naming every variable an enabled part needs and the deploy lacks. It stays quiet underpnpm dev.
Read the config in code
Import the file directly. It works the same in server routes, components and composables:
import starter from '~~/starter.config'
export const sellsYearly = Object.values(starter.payments.plans).some(
(plan) => plan.mode === 'subscription' && 'year' in plan.intervals,
)
export function isTopPlan(plan: PlanId): boolean {
return plan === 'promax'
}
PlanId and the other unions from shared/types/config.ts are auto-imported
on both sides. Because PlanId is 'pro' | 'promax' | 'lifetime', comparing it
with 'proMax' fails typecheck.
Change it
Save the file and pnpm dev restarts, because nuxt.config.ts watches it.
Keep it pure data. Nuxt, Vite, @nuxt/content, Vitest and the seed scripts all
load it, and the seed scripts' loader
(scripts/seed/lib/starter-config.mjs(Licensees only))
refuses any import other than #starter/define. A function or a second import
breaks at least one of them. The #starter/* entry under imports in
package.json exists for that one import, so leave it there.
The build refuses a config that can't work:
build/assertions.ts(Licensees only): a declared locale with no file ini18n/locales/, or a file there that no locale declares; noHYPERDRIVEbinding;cloudflareemail without asend_emailbinding namedEMAIL; the queue on without its producer, consumer and dead letter consumer;workersaiwithout theAIbinding; a storage purpose whose binding or bucket isn't inr2_buckets; a deletion grace period with auth on and email off; notifications with auth off; afirstAccountRolethat doesn't grantusers:manage.modules/tenancy-assert.ts(Licensees only): an org permission that a plan sells, more than twoorgs.levels, anownerRolethat can't be held at the root, invitations with email off, organizations with auth off.modules/payments-registry.ts(Licensees only): the plan rules on Payments, such as a plan id outside/^[a-z][a-z0-9]*$/ortrialDayswith Creem or Lemon Squeezy enabled.
Set environment variables
Nitro fills each runtime-config key from NUXT_ plus the key's path in upper
snake case:
| Runtime config key | Variable |
|---|---|
payments.stripe.secretKey | NUXT_PAYMENTS_STRIPE_SECRET_KEY |
payments.stripe.refs.pro_month | NUXT_PAYMENTS_STRIPE_REFS_PRO_MONTH |
ai.workersai.models.copy | NUXT_AI_WORKERSAI_MODELS_COPY |
public.siteUrl | NUXT_PUBLIC_SITE_URL |
Locally they go in .env. .env.example(Licensees only) lists every
variable the kit reads and where each value comes from. On a deploy, set them
on the Worker. The Worker reads them per request, so changing one needs no
rebuild:
pnpm exec wrangler secret put NUXT_PAYMENTS_STRIPE_SECRET_KEY --name <worker>
A few are read at build time and baked into the bundle: NUXT_SITE_URL,
NUXT_PUBLIC_TURNSTILE_SITE_KEY and NUXT_IMAGE_PROVIDER. For
pnpm deploy:cf those go in .env.production
(Deployment).
Each topic page lists its own variables. These belong to the app as a whole:
| Variable | Read at | What it does |
|---|---|---|
NUXT_SITE_URL | build | The canonical origin, used for SEO tags, the sitemap and every mailed link. |
NUXT_PUBLIC_SITE_URL | runtime | Overrides NUXT_SITE_URL on a deploy without a rebuild. |
NUXT_SESSION_PASSWORD | runtime | Encrypts the session cookie. 32+ characters. The first pnpm dev writes a random one to .env. |
NUXT_SESSION_COOKIE_SECURE | runtime | false drops the cookie's Secure flag, for a production build served over http such as pnpm preview. |
NUXT_CONTACT_ADDRESS | runtime | Where /contact sends messages, and the address it shows after a Turnstile check. Never sent to a page (Contact). |
NUXT_BLOG_AUTHOR_EMAIL | runtime | The account every blog post is bylined with (Blog). |
NUXT_OG_IMAGE_SECRET | runtime | Signs OG image URLs. Unset, each build signs with its own secret. |
NUXT_PUBLIC_TURNSTILE_SITE_KEY | build | Turnstile's site key. Left out of .env, pnpm dev uses Cloudflare's always-pass test keys. |
NUXT_TURNSTILE_SECRET_KEY | runtime | Turnstile's secret key, the pair to the site key. |
NUXT_IMAGE_PROVIDER | build | cloudflare turns on image optimization. Unset, images are served as they are. |
NUXT_DEV_TUNNEL_HOSTS | dev | Comma-separated hosts the dev server accepts besides *.trycloudflare.com. |
NUXT_DEV_REMOTE_AI | dev | true binds Workers AI in pnpm dev, after pnpm exec wrangler login. |
NUXT_DEV_PGLITE_PORT | dev | Pins the port pnpm dev serves PGlite on. Unset, it takes the first free one from 5432 to 5441. |
DATABASE_URL | dev | A Postgres for pnpm dev in place of PGlite (Database). |
Every key
Defaults are the shipped values. The linked page covers each part in full.
activity, security, legal
| Key | Default | What it does |
|---|---|---|
activity.retentionDays | 90 | Days to keep account activity entries: sign-ins, credential changes, admin actions. false keeps everything. |
security.headers | true | Sends HSTS, a CSP and the other security headers. Turn off only when a proxy in front already sets them. |
security.cspExtend | {} | Extra sources per CSP directive, such as { 'img-src': ['https://cdn.example.com'] } (Security headers). |
legal.termsVersion | false | The current terms version. Changing it asks every signed-in user to accept again. false records no acceptance. |
payments
| Key | Default | What it does |
|---|---|---|
payments.enabled | true | Off bundles no provider and lets every paid check pass. |
payments.identity | 'account' | 'guest' also sells one-time plans to buyers without an account. |
payments.modes | ['one-time', 'subscription'] | The purchase shapes you sell. |
payments.providers | ['stripe'] | Any of stripe, polar, lemonsqueezy, creem. The first is the default. |
payments.couponField | true | Whether the hosted checkout shows its own code box. |
payments.pastDueGraceDays | 14 | Days a subscription keeps access after its first failed renewal. false ends access at once. |
payments.gate | 'none' | 'plan' locks an organization's pages until it holds a plan. |
payments.plans | pro, promax, lifetime | The catalogue: mode, currency, displayed prices, trial. |
payments.adapters | none | Declarations for a provider you add in server/payments/adapters/custom/. |
Details, provider setup and the ref variables are on Payments.
access
| Key | Default | What it does |
|---|---|---|
access.permissions | users:read, users:manage, blog:author, insights:read | Every permission a route can check. A permission exists only if it is listed here. |
access.rolePermissions | user, staff, superadmin | The app roles, keyed by role, and the permissions each grants. |
access.defaultRole | 'user' | What an unrecognised users.role value reads as. Make it the least privileged role. |
access.firstAccountRole | 'superadmin' | Granted to the first account that verifies its email while nobody holds users:manage. false turns it off. |
access.planPermissions | insights:read on each paid plan | The permissions each plan sells. free is an account with no plan. |
See Permissions.
orgs
| Key | Default | What it does |
|---|---|---|
orgs.enabled | true | Off registers no organization route or page. The tables still ship. |
orgs.levels | ['organization'] | The ownership levels, root first. One or two. Each id keys its label in the locale files. |
orgs.createAtSignup | true | Gives each new account an organization. false leaves it without one until it creates one or buys. |
orgs.permissions | org:manage, org:members:manage, org:billing, org:scopes:manage | Every permission an org role can hold. |
orgs.roles | owner, admin, member | Each role's permissions and the levels it can be held at. |
orgs.defaultRole | 'member' | The role the invitation form preselects. |
orgs.ownerRole | 'owner' | The role an organization's creator gets, and the one the last-owner guard counts. |
orgs.invitations | { expiryHours: 336, maxPending: 50 } | How long an invitation stays valid and how many can be open per organization. false turns invitations off. |
orgs.seatLimits | free 3, pro 10, promax false, lifetime 10 | Members each plan admits. false is unlimited. The highest limit among the plans held applies. |
See Organizations.
auth
| Key | Default | What it does |
|---|---|---|
auth.enabled | true | Off registers no auth route. |
auth.primary | 'password' | 'password', 'passwordless' for magic links, or 'none' for OAuth only. |
auth.oauth | [] | Any of github, google, microsoft. Each needs NUXT_OAUTH_<PROVIDER>_CLIENT_ID and _CLIENT_SECRET, and Microsoft also NUXT_OAUTH_MICROSOFT_TENANT. |
auth.totp | true | Authenticator-app codes as a second factor. |
auth.passkey | 'login' | 'login' signs in with a passkey alone, 'second-factor' only confirms a session, false turns passkeys off. |
auth.emailVerification | true | A password signup confirms its address before it can buy, invite or add a way to sign in. Ignored otherwise; passwordless always waits for proof. |
See Auth.
email, contact and ai
| Key | Default | What it does |
|---|---|---|
email.enabled | true | Off, magic links and password resets are logged in development and refused in production. |
email.providers | ['cloudflare'] | cloudflare (the send_email binding) and resend, in delivery order. A later provider only gets a failed send. |
contact.enabled | true | Off drops /contact and both its routes, and every support link renders nothing. |
contact.channels | ['form', 'reveal'] | form mails you a message, reveal shows your address after a Turnstile check. At least one; form needs email on. |
ai.enabled | true | Turns on the model-call layer. Nothing in the kit calls a model. |
ai.providers | ['workersai'] | In fallback order. workersai is the AI binding in wrangler.jsonc. |
ai.gateway | 'default' | The AI Gateway every call goes through. Third-party models are refused without one. |
ai.roles | one per modality | Each role's modality and its model per provider. NUXT_AI_<PROVIDER>_MODELS_<ROLE> overrides a model per deploy. |
email.adapters and ai.adapters declare a provider you drop into that part's
adapters/custom/. See Email, Contact and
AI.
i18n and flags
| Key | Default | What it does |
|---|---|---|
i18n.defaultLocale | 'en' | Served at the root. Every other locale gets a /<code> prefix. |
i18n.locales | en, sr | Each has a code, a BCP 47 language, a name written in that language, and its file in i18n/locales/. |
flags.enabled | false | Turns on feature flags. |
flags.providers | ['selfhosted'] | In order. The first provider that knows a flag answers, and the declared default is the last resort. |
flags.driver | 'kv' | Where selfhosted reads its JSON document: kv or r2. |
flags.key | 'flags.json' | The KV key or R2 object key of that document. |
flags.purpose | 'avatars' | The storage purpose whose bucket the r2 driver reads. |
flags.declarations | maintenance, newPricing | Every flag, with its type and default. |
See Languages and Feature flags.
notifications, storage, account, queue
| Key | Default | What it does |
|---|---|---|
notifications.enabled | true | The dashboard bell and its table (Notifications). |
notifications.retentionDays | 30 | Days to keep a notification after it is read. Unread ones stay. false keeps everything. |
storage.purposes | avatars, org-avatars | One entry per kind of upload: R2 binding and bucket, formats, maxBytes, noun, access, cacheControl (Storage). |
account.deletionGraceDays | 30 | Days a deleted account can still be restored. false deletes at once. |
queue.enabled | true | Background jobs on Cloudflare Queues. Off, callers do the work inline (Background jobs). |
monitoring, analytics, marketing, socialProof
| Key | Default | What it does |
|---|---|---|
monitoring.enabled | false | Forwards server faults to error trackers (Observability). |
monitoring.providers | ['sentry'] | sentry and betterstack. Each needs NUXT_MONITORING_<PROVIDER>_DSN. |
analytics.enabled | true | Server-side analytics (Analytics). |
analytics.providers | ['cloudflare'] | cloudflare (Workers Analytics Engine), umami, posthog. Every event goes to each one. |
analytics.ingestPath | '/api/e' | The page-view beacon's route. Words that blockers match, such as track or analytic, fail the build. |
analytics.pageviews | 'ssr+beacon' | 'ssr' counts document requests, 'ssr+beacon' adds client navigations, false counts none. |
analytics.consent | 'implied' | 'explicit' sends nothing until an _ac=1 cookie is set. _ac=0 stops sending under either. |
marketing.enabled | false | Server-side conversion events. With a provider listed, the consent banner ships. |
marketing.providers | [] | Any of meta, tiktok, x, reddit. |
socialProof.enabled | true | Recent-purchase cards on public pages, drawn from real payment_events rows only. |
socialProof.identity | 'first-name+country' | What a card names: none, country, first-name or first-name+country. When a buyer's consent or data for a level is missing, the card falls back down that list. |
socialProof.windowHours | 72 | How far back a purchase counts. |
socialProof.minEvents | 5 | Below this many purchases in the window, nothing shows. |
socialProof.maxPerSession | 4 | Cards one visitor sees per browser session. |
socialProof.intervalRange | [30, 90] | Seconds between cards, drawn from this range. |
socialProof.routes | ['/', '/pricing'] | Path prefixes the cards appear on, without the locale prefix. |
Good to know
- App code imports this file, so it ships in the browser bundle. Never put a secret in it.
- Ids that end up in a variable name must match
/^[a-z][a-z0-9]*$/: plan ids, AI role ids and the file name of a custom adapter. Nitro mapsproMax,pro-maxandpro_maxto the same variable, which is why the config sayspromaxandworkersai. - A slot exists only for a provider that was listed when you built. Setting
NUXT_PAYMENTS_POLAR_SECRET_KEYdoes nothing untilpolaris inpayments.providersand the app is rebuilt. - Values pass through
destr, and an empty line such asNUXT_PAYMENTS_STRIPE_REFS_PRO_MONTH=sets an empty string over the default. Comment out lines you don't use. - A new plan, app role or org level also needs its copy in every file in
i18n/locales/.plan-copy,role-copyandorg-copyintest/unit/fail until it is there.
A link with a lock opens the kit’s private repository, which every licensee can read.