Getting Started

Local setup

Get both applications running on your machine, in the order their pieces depend on each other.

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 needWhere the version comes from
Node.jsThe devEngines.runtime field in the root package.json
pnpmThe packageManager field in the root package.json
GitAny recent version
ShellCheckThe SHELLCHECK_VERSION in the Lint shell scripts and workflows step of .github/workflows/deploy.yml
actionlintThe ACTIONLINT_VERSION in the Lint shell scripts and workflows step of .github/workflows/deploy.yml
DockerRuns Postgres and the local mail inbox
An Infisical accountFree 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.

.env.project.schema
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:

FeatureLayerKeys it asks for
Google sign-in@app/layer-authGOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
GitHub sign-in@app/layer-authGITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET
Payments@app/layer-paymentsPOLAR_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.infisical and apps/site/.env.infisical
  • A machine identity's client ID and secret in apps/web/.env.local and apps/site/.env.local
  • The development environment's /local folder filled in for the apps/web project

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:

apps/web/.env.development
GOOGLE_CLIENT_ID=<your Google client ID>
GITHUB_CLIENT_ID=<your GitHub client ID>
apps/site/.env.development
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:

apps/web/.env.local
INFISICAL_CLIENT_ID=<already there>
INFISICAL_CLIENT_SECRET=<already there>
DATABASE_URL=postgres://postgres:postgres@localhost:5432/web

Then create the override files:

apps/web/.env.development.local
NUXT_PUBLIC_APP_BASE_URL=http://localhost:3000
NUXT_PUBLIC_APP_WEBSITE_BASE_URL=http://localhost:3100
CLOUDFLARE_HYPERDRIVE_ID=00000000000000000000000000000000
apps/site/.env.development.local
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.

Delete the two zeroed IDs once your first deploy has filled in the real ones. This file loads after .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

  1. Sign up at http://localhost:3000/auth/sign-up.
  2. Open http://localhost:8025 and read the verification email.
  3. Open http://localhost:3100/docs and 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 seeWhat to fix
Infisical authentication is requiredThe client ID and secret in that application's .env.local
Value is required but is currently emptyThe key it names, in that application's .env.schema or value file
Value must match regex, or an invalid URL, UUID, or emailA REPLACE_ME you haven't replaced or overridden
A deploy skipped with still assigns REPLACE_MEA REPLACE_ME an untyped key accepts. .env.project.schema is where they live
An error naming a single secretThat key is missing from the /local folder in Infisical
ECONNREFUSED on port 5432The Postgres container isn't running
Sign-up works but no email arrivesThe Mailpit container isn't running
Links to /docs and /blog return 404NUXT_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:

Two more if you skipped something above:

Copyright © 2026