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
| Path | What 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:
| Directory | A .ts file there becomes | Scanned by |
|---|---|---|
server/jobs/ | A background job, with the filename as kind | modules/jobs-registry.ts(Licensees only) |
server/listeners/ | An event listener | modules/listeners-registry.ts(Licensees only) |
server/{ai,email,flags,monitoring,payments}/adapters/custom/ | A provider, with the filename as its id | build/custom-adapters.ts(Licensees only) |
server/signals/analytics/custom/, server/signals/marketing/custom/ | An analytics or marketing provider | build/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/
| Directory | Runner | Environment |
|---|---|---|
test/unit | vitest, project unit | Node |
test/nuxt | vitest, project nuxt | A built Nuxt app |
test/e2e | Playwright | A 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
.tsfile inserver/jobs/andserver/listeners/is registered as a job or a listener. Keep helpers out of both. - An export added to
server/utils/orshared/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/andtest/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.