Skip to content

Get started

Project structure

The kit's code is split by where it runs: app/ in the browser, server/ on the Worker, shared/ in both. Check this page before you add a file under server/: in some directories a new file becomes a route, a job or a provider, and in others it does nothing until you import it.

Top level

PathWhat lives there
app/(Licensees only)The Vue app, Nuxt 4's srcDir: pages, components, layouts, composables, Pinia stores
server/(Licensees only)Everything that runs on the Worker
shared/(Licensees only)Types, zod schemas and pure helpers that both the app and the server import
modules/(Licensees only)Nuxt modules that run at build time and generate the registries
build/(Licensees only)Plain functions nuxt.config.ts calls while it loads
emails/(Licensees only)Mail templates, written as Vue components (Email)
content/Blog posts and legal pages, one folder per locale (Blog)
i18n/locales/One JSON catalogue per locale (Languages)
public/Static files, served as they are
test/(Licensees only)The unit, nuxt and e2e suites and their shared helpers (Testing)
scripts/(Licensees only)Node scripts for the database, the seed, the blog pipeline and template updates. Never bundled
starter.config.ts(Licensees only)Which parts are on, and with which providers (Configuration)
wrangler.jsonc(Licensees only)Cloudflare bindings, queues and crons (Deployment)

Which of these are yours and which the kit updates is on Updating from the kit.

server/

Nitro scans six directories. A file in api/ or routes/ is a route, middleware/ runs on every request, plugins/ runs once when the Worker starts, and tasks/ holds the tasks the cron runs (Background jobs). Every named export in utils/ becomes a global in server code.

Middleware runs in filename order, so the name you give a new file decides where it sits in the chain. test/unit/middleware-order.test.ts(Licensees only) pins the order the kit relies on.

The kit's own build code scans a few more:

DirectoryA .ts file there becomesScanned by
server/jobs/A background job, with the filename as kindmodules/jobs-registry.ts(Licensees only)
server/listeners/An event listenermodules/listeners-registry.ts(Licensees only)
server/{ai,email,flags,monitoring,payments}/adapters/custom/A provider, with the filename as its idbuild/custom-adapters.ts(Licensees only)
server/signals/analytics/custom/, server/signals/marketing/custom/An analytics or marketing providerbuild/custom-adapters.ts(Licensees only)

The adapter custom/ directories don't ship; create one when you add a provider. The registries read these directories when Nuxt starts, so restart pnpm dev after you add, rename or delete a file in them.

Everything else under server/ is plain code that nothing scans: ai/, auth/, cache/, contact/, database/, durable/, email/, events/, flags/, monitoring/, notifications/, payments/, signals/, storage/, tenancy/ and types/. The two handlers in contact/routes/ are registered by name, one per entry in contact.channels, from modules/contact-routes.ts(Licensees only) (Contact), so a file you add there is not a route. Import the rest through the #server/* alias:

import { defineListener } from '#server/events/types'
import { requireOrgScope } from '#server/tenancy/scope'

Nuxt refuses an import from server/ in app code, so nothing here can reach the browser bundle by accident. Code that both sides need goes in shared/.

modules/ and build/

Both run at build time. A file in modules/ is a Nuxt module: Nuxt registers every file directly under modules/ and every modules/*/index.ts. The kit's modules generate the registries (#jobs-registry, #payments-registry and the rest), add routes, and fail the build when the config doesn't add up. modules/support/ has no index.ts, so the helpers there are shared by the modules and never registered.

build/ holds plain functions that nuxt.config.ts calls while it is being evaluated, before Nuxt or its aliases exist, so it imports them by relative path. That covers reading wrangler.jsonc, the binding checks in build/assertions.ts(Licensees only), the runtime-config slots in build/runtime-config.ts(Licensees only) and the locale list. Nothing in build/ is bundled. If your code needs Nuxt, write a module; if nuxt.config.ts needs a value from it, put it in build/.

app/ and shared/

app/ follows Nuxt's defaults: components/, composables/ and utils/ are auto-imported in the app, and pages/ is the router. app/types/ holds the types of the kit's own components and queries.

In shared/, the exports of shared/utils/ are auto-imported on both sides, and so are the types in shared/types/, such as PlanId and Permission. Import anything else there through #shared/*:

import { planRefSlots } from '#shared/payments/ref-slots'

test/

DirectoryRunnerEnvironment
test/unitvitest, project unitNode
test/nuxtvitest, project nuxtA built Nuxt app
test/e2ePlaywrightA running nuxt dev

test/helpers is shared by the two vitest projects. pnpm test --run runs both; pnpm test:e2e runs Playwright.

Good to know

  • Every .ts file in server/jobs/ and server/listeners/ is registered as a job or a listener. Keep helpers out of both.
  • An export added to server/utils/ or shared/utils/ becomes a global, and can collide with an existing auto-import. Put a helper you don't want global somewhere else and import it.
  • Vitest collects only files directly inside test/unit/ and test/nuxt/ (test/unit/*.test.ts). A test in a subfolder never runs.
  • Code in build/ runs before Nuxt's aliases exist. Import by relative path there, as its files do with ../starter.config, never through ~~/ or #shared/.

A link with a lock opens the kit’s private repository, which every licensee can read.