Nuxt Better Auth template
Source · Demo · Database-free Nuxt template
Nuxt 4, TypeScript, Tailwind/shadcn-vue, Better Auth, PostgreSQL/Drizzle, and Brevo email, with separate staging and production deployments through DockIY.
Local development
Install Node.js 24+, pnpm 11+, Docker Compose, and the DockIY CLI. Encrypted files also need SOPS and your own SOPS identity. Create a project with dockiy app init nuxt-betterauth my-app, or clone the template.
cp .env.example .env
openssl rand -base64 32
# Set NUXT_BETTER_AUTH_SECRET to the generated value and fill in NUXT_BREVO_*.
docker compose up -d
pnpm install --frozen-lockfile
pnpm db:migrate
pnpm devNuxt and Drizzle load .env. Keep NUXT_BETTER_AUTH_URL=http://localhost:3000 locally. Email signup requires verification; configure the Brevo API key, verified sender email, and sender name for signup and password reset.
Encrypted local values
Configure your own .sops.yaml, then use sops edit .enc.local.env to create an encrypted file with the same keys. Wrap commands manually:
sops exec-env .enc.local.env 'docker compose up -d'
sops exec-env .enc.local.env 'pnpm db:migrate'
sops exec-env .enc.local.env 'pnpm dev'Or replace selected package.json scripts, for example:
"dev": "sops exec-env .enc.local.env 'nuxt dev'",
"db:migrate": "sops exec-env .enc.local.env 'drizzle-kit migrate'"Use the underlying command inside a script to avoid recursion. SOPS injects variables without writing plaintext; restart Nuxt after changing them. Keep plaintext env files/private keys uncommitted and replace inherited encrypted files.
Staging and production
Start the DockIY base stack and configure your VPS connection. In dockiy.yml, set the app name and the staging/production host values. Keep each environment's secrets_file pointing at its own encrypted dotenv.
sops edit .enc.staging.env
sops edit .enc.production.envUse .env.example as the key list, with separate credentials and auth secrets:
| Setting | Staging example | Production example |
|---|---|---|
POSTGRES_DB / POSTGRES_USER | my_app_staging | my_app |
POSTGRES_PASSWORD | Separate database password | Separate database password |
POSTGRES_DOCKER_PORT | 5434 | 5435 |
NUXT_DATABASE_URL | postgresql://my_app_staging:PASSWORD@db:5432/my_app_staging | postgresql://my_app:PASSWORD@db:5432/my_app |
NUXT_BETTER_AUTH_SECRET | Fresh openssl rand -base64 32 output | Fresh openssl rand -base64 32 output |
NUXT_BETTER_AUTH_URL | https://staging.example.com | https://example.com |
NUXT_BREVO_* | API key and verified sender | API key and verified sender |
NUXT_GOOGLE_CLIENT_ID / NUXT_GOOGLE_CLIENT_SECRET | Optional Google OAuth credentials | Optional Google OAuth credentials |
In staging and production, the port in NUXT_DATABASE_URL is always 5432: it is PostgreSQL's internal container port. POSTGRES_DOCKER_PORT exposes a separate port on the VPS host so you can connect from your computer through an SSH tunnel.
The auth origin must match the environment host; it determines email links and OAuth callbacks. DockIY decrypts the selected file into deploy.env and passes its values to the app. New server settings need a runtimeConfig entry in nuxt.config.ts and a NUXT_* variable; only browser-visible values go under public.
Commit the application, migrations, and encrypted configuration, then deploy:
dockiy app deploy staging
dockiy app deploy production --version v1.0.0The migration image runs before the app starts. See deployment commands for status and rollback. Rollback restores the app, not the database schema.
Database
Keep NUXT_DATABASE_URL credentials consistent with POSTGRES_*. URL-encode special characters in URI credentials (@ → %40); keep POSTGRES_PASSWORD unencoded. Changing env values does not change credentials in an existing DB volume.
| Connection from | Address |
|---|---|
| Local Nuxt / Drizzle | localhost:5432, or your POSTGRES_DOCKER_PORT |
| App, migration, or pgAdmin container | db:5432 |
| Desktop client via SSH tunnel | Your tunnel's local port |
The local example URL is postgresql://dockiy-nuxt-betterauth:secret-password@localhost:5432/dockiy-nuxt-betterauth. Changing POSTGRES_DOCKER_PORT changes only the host port; update the local URL too. Choose distinct unused ports for deployed environments; they bind to VPS loopback. To reach staging from a desktop client:
ssh -N -L 15432:127.0.0.1:5434 USER@VPS
# Connect to localhost:15432 with staging credentials.Local pgAdmin is at http://localhost:82 (admin@m.com / admin). Register host db, port 5432, with the local POSTGRES_* credentials.
Define/export tables in server/db/schema/; import getDb from #server/db for queries. After schema edits, run pnpm db:generate, review/commit the SQL and metadata, then pnpm db:migrate locally. Reserve pnpm db:push for disposable DBs.
Authentication
Email/password and Google auth are preconfigured.
- Client session: import
authClientfrom@/lib/auth-clientand read the session withawait authClient.useSession(useFetch). Session usage. - Gate pages:
definePageMeta({ middleware: "auth" }). Protect pages. - Protect APIs separately: call
const user = await requireUser(event), then check resource ownership.
Google auth setup
- Select/create a project in Google Cloud Console. Under Google Auth Platform, complete Branding and Audience; choose External for users outside your organization and add test users while testing. Consent setup.
- Open Clients → Create client → Web application. Add the exact callback URLs below under Authorized redirect URIs. Google OAuth setup.
- Copy the client ID/secret to
NUXT_GOOGLE_CLIENT_IDandNUXT_GOOGLE_CLIENT_SECRETin each environment's dotenv file. SetNUXT_BETTER_AUTH_URLto the corresponding origin, then restart locally or redeploy. Better Auth Google guide.
| Environment | Authorized redirect URI |
|---|---|
| Local | http://localhost:3000/api/auth/callback/google |
| Staging | https://staging.example.com/api/auth/callback/google |
| Production | https://example.com/api/auth/callback/google |
Use your actual domains. The callback URI must match exactly, including scheme and path; a mismatch causes redirect_uri_mismatch. Before public launch, review Audience → Publishing status and Google's requested verification steps.
