Local setup
By the end of this page you have two applications running on your machine: the public website on port 3100, and the signed-in product on port 3000. The product talks to a Postgres database in Docker, and the email it sends lands in a local inbox instead of a real one. Nothing here deploys anything.
This is stage 1 of three. First deploy puts development,
preview and staging on Cloudflare, and
Going to production attaches your domain once staging
looks right.
Work through the steps in order. Each one depends on the one before it, and the errors you get from skipping ahead name a missing value rather than the step you skipped.
Set aside an hour the first time. Most of it is creating accounts, not writing code.
Prerequisites
| You need | Where the version comes from |
|---|---|
| Node.js | The devEngines.runtime field in the root package.json |
| pnpm | The packageManager field in the root package.json |
| Git | Any recent version |
| ShellCheck | The SHELLCHECK_VERSION in the Lint shell scripts and workflows step of .github/workflows/deploy.yml |
| actionlint | The ACTIONLINT_VERSION in the Lint shell scripts and workflows step of .github/workflows/deploy.yml |
| Docker | Runs Postgres and the local mail inbox |
| An Infisical account | Free plan. Holds your credentials |
Social sign-in and payments also need a Google Cloud project, a GitHub OAuth app, and a Polar sandbox account. You can skip any of the three today and add it later — the step below on which features you're wiring up says what to put in the meantime.
Get the code
Create your own repository from the NuxtStart template, then clone it:
git clone https://github.com/<your-account>/<your-repo>.git
cd <your-repo>
Everything on this page and in First deploy happens on
development. Your clone starts on development, NuxtStart's default branch. development is
the branch that gives you a live URL.
A first push is safe. .env.project.schema and the value files still assign REPLACE_ME, and the
deploy gate skips any Runtime Context they hold one in, so the push builds and ships nothing. It
turns itself on once you have named your project below and your first deploy has replaced those
values. See When a deploy is skipped.
Optional: add NuxtStart as a second remote, so you can pull updates into your project later.
git remote add nuxtstart https://github.com/NuxtStart/NuxtStart.git
Install dependencies
pnpm install
This also prepares both applications, which generates the types your editor needs.
Name your project
Your copy ships with three placeholders in .env.project.schema, at the root of the repository.
They're required, and nothing rejects the placeholder at build time, so the deploy gate is what
holds them — and because this file is read by both applications, a placeholder holds every
Runtime Context of both back rather than one.
PROJECT_SLUG=acme
PROJECT_DISPLAY_NAME=Acme
PROJECT_ZONE=acme.com
PROJECT_SLUG is the lowercase name inside every Worker, bucket, and database you'll create.
PROJECT_DISPLAY_NAME appears in page titles, transactional email, and social images.
PROJECT_ZONE is the DNS zone you own; your sending domains hang off it.
There's a fourth item, PROJECT_DOMAIN, and you can leave it alone for now. It defaults to your
zone, and only production reads it. Project identity is the full
picture — what derives from these, and what stays yours to write.
Decide which features you're wiring up today
Email and password sign-in, the database, and transactional email need no provider account. Social sign-in and payments do, and each one is a layer you can leave unconfigured for now:
| Feature | Layer | Keys it asks for |
|---|---|---|
| Google sign-in | @app/layer-auth | GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET |
| GitHub sign-in | @app/layer-auth | GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET |
| Payments | @app/layer-payments | POLAR_ACCESS_TOKEN, NUXT_WEBHOOK_POLAR_SECRET_KEY |
Every key is required, so put REPLACE_ME in the ones you're skipping. The two later steps say which
file or folder each key goes in. apps/web starts either way, and only the feature that reads the
key fails when you first use it — the Google button, or a checkout.
A REPLACE_ME in a committed value file also holds back that context's deploy, which is what you
want while a credential is still missing. Add the real values whenever you're ready with
Set up sign-in and payments.
Set up your secret store
Both applications read their credentials from Infisical, and neither starts until it can. Follow Set up your secret store, then come back here.
You're ready to continue when you have:
- Your own project ID in
apps/web/.env.infisicalandapps/site/.env.infisical - A machine identity's client ID and secret in
apps/web/.env.localandapps/site/.env.local - The
developmentenvironment's/localfolder filled in for theapps/webproject
Start Postgres and a mail inbox
Postgres stores the product's data. Mailpit catches every email the product sends, so nothing leaves your machine and you can read the messages in a browser.
docker run -d --name web-postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres:17
docker run -d --name web-mailpit -p 1025:1025 -p 8025:8025 axllent/mailpit
Create the database:
docker exec web-postgres createdb -U postgres web
Leave both containers running while you work. The inbox is at http://localhost:8025.
Any Postgres you can reach works here. Only the connection string in the next step changes.
Fill in the values that are yours
Your copy ships with placeholders in the values that belonged to NuxtStart. These are the ones you can choose now, so put them in the committed development files:
GOOGLE_CLIENT_ID=<your Google client ID>
GITHUB_CLIENT_ID=<your GitHub client ID>
NUXT_PUBLIC_CONTACT_EMAIL=hello@example.com
That's a short list because the Worker name, the bucket, the database, and the sending domain aren't in these files at all. They're built from the three values you set above, so renaming your project renames all of them. Project identity lists which names derive and which stay yours.
Your sending domain is one of the derived ones, <slug>-<app>-local.<zone> while you work locally.
Mailpit catches that mail instead of delivering it, so nothing has to be onboarded yet. The
First deploy guide covers onboarding the deployed ones for real
delivery.
Skipping Google or GitHub sign-in? Put REPLACE_ME in that provider's client ID.
Leave every remaining REPLACE_ME alone: the ids come from your first deploy, and the deployed
origins are yours to write at the step that pushes development. A deploy is skipped for any context
whose file still holds one.
Create your two local environment files
Local values live in two gitignored files per application. .env.local holds keys no committed file
sets. .env.development.local overrides keys that .env.development already sets.
Environment values explains the split.
Add the database connection string to the file you already created for your Infisical credentials:
INFISICAL_CLIENT_ID=<already there>
INFISICAL_CLIENT_SECRET=<already there>
DATABASE_URL=postgres://postgres:postgres@localhost:5432/web
Then create the override files:
NUXT_PUBLIC_APP_BASE_URL=http://localhost:3000
NUXT_PUBLIC_APP_WEBSITE_BASE_URL=http://localhost:3100
CLOUDFLARE_HYPERDRIVE_ID=00000000000000000000000000000000
NUXT_PUBLIC_APP_BASE_URL=http://localhost:3100
CLOUDFLARE_D1_DATABASE_ID=00000000-0000-0000-0000-000000000000
Each file holds two kinds of line, and they have different lifespans.
The two localhost URLs are permanent. .env.development names your deployed origins once
First deploy has you write them, and local reads that file,
so without these overrides your local run reports the deployment's URL as its own. Keep them for as
long as you develop locally.
The two zeroed IDs are temporary. A local run opens neither connection, but varlock checks every
value that is present against its declared shape, and the REPLACE_ME your copy ships fails that
check. The stand-in gets you past it.
.env.development, so a stand-in you leave behind silently overrides the real value on your machine
forever. First deploy says so again at the step that produces
them.Put the stand-ins here rather than in the committed .env.development. A deploy has to fail on a
missing ID rather than succeed against a wrong one, and a gitignored file never reaches a deploy.
Generate your first migration
Your copy ships the database schema and no migration history. Migration publication explains why.
cd apps/web
pnpm db:generate
Commit what it writes. You own that history from here. The dev server applies pending migrations when it starts, so there's no separate apply step locally.
Run both applications
Two terminals, one command each. There's no root dev command, because it would have to choose one
of the two applications for you.
pnpm dev:site # http://localhost:3100
pnpm dev:web # http://localhost:3000
Check that it worked
- Sign up at
http://localhost:3000/auth/sign-up. - Open
http://localhost:8025and read the verification email. - Open
http://localhost:3100/docsand confirm the website serves this page.
Troubleshooting
Read the name in the error first. It names the value that's missing, which is rarely the thing you were working on when it broke.
| What you see | What to fix |
|---|---|
Infisical authentication is required | The client ID and secret in that application's .env.local |
Value is required but is currently empty | The key it names, in that application's .env.schema or value file |
Value must match regex, or an invalid URL, UUID, or email | A REPLACE_ME you haven't replaced or overridden |
A deploy skipped with still assigns REPLACE_ME | A REPLACE_ME an untyped key accepts. .env.project.schema is where they live |
| An error naming a single secret | That key is missing from the /local folder in Infisical |
ECONNREFUSED on port 5432 | The Postgres container isn't running |
| Sign-up works but no email arrives | The Mailpit container isn't running |
Links to /docs and /blog return 404 | NUXT_PUBLIC_APP_WEBSITE_BASE_URL in apps/web/.env.development.local |
Next steps
Both applications run, so the rest of this section is about making the project yours:
- Technical details — what each package owns, and the commands you'll use most
- Customization — colors, logo, name, fonts, and page head
- Creating a page — add a page, and decide which package owns it
- First deploy — stage 2:
development,previewandstagingon Cloudflare - Going to production — stage 3: your domain and real users, once staging looks right
- Getting updates — pull NuxtStart's later changes into your project
Two more if you skipped something above:
- Set up sign-in and payments — add Google, GitHub, or
Polar if you left a
REPLACE_MEbehind - Environment values — why the files are arranged this way
