Getting Started
Set up Unstack Pro and get your development environment running
Get up and running fast. Auth is already configured — you just connect the services and set your environment variables.
The package manager is Bun. All commands use bun — do not
substitute npm or pnpm.
Choose Your Edition
Unstack Pro ships in two editions. Pick one with the switcher below — your choice is remembered and follows you across every page in these docs.
Better Auth runs inside Next.js and talks to PostgreSQL through Drizzle ORM.
There is a single environment surface — .env.local — one process to run, and
schema changes are applied as SQL migrations.
Better Auth runs inside Convex. There are two environment surfaces —
.env.local for Next.js, and the Convex deployment environment — and two
processes to run during development.
Setup
Prerequisites
You'll need accounts on the following services (all have free tiers):
- Bun — the package manager and runtime.
- A PostgreSQL database — any provider works. The app connects with
pg(node-postgres) rather than a vendor-specific driver, so Neon and Supabase are convenient recommendations, not requirements; a Postgres you run yourself is equally fine. - Resend — transactional email (verification, OTP, invitations). You'll need an API key and a verified sender.
- Autumn — organization-scoped subscription billing.
- (Optional) Sentry — error monitoring. The app runs fine without it.
- Bun — the package manager and runtime.
- Convex — real-time database + backend. Auth runs inside Convex.
- Resend — transactional email (verification, OTP, invitations). You'll need an API key and a verified sender.
- Autumn — organization-scoped subscription billing.
- (Optional) Sentry — error monitoring. The app runs fine without it.
Get the code
The Postgres-edition template repository is not published yet; the Convex edition is the one distributed today.
git clone https://github.com/unstack-pro/nextjs-convex.git # Purchasing grants access to the private repo
cd nextjs-convexInstall
bun installConfigure environment
Copy the example file and fill in your values. See Environment Variables for the full reference.
cp .env.example .env.local.env.local is the only place configuration lives — there is no second
deployment environment to populate.
DATABASE_URL="postgresql://user:password@host/dbname?sslmode=require"
NEXT_PUBLIC_APP_URL="http://localhost:3000"
NEXT_PUBLIC_APP_NAME="Unstack Pro"
# openssl rand -base64 32
BETTER_AUTH_SECRET="replace-me"
EMAIL_FROM="Unstack Pro <noreply@yourdomain.com>"
RESEND_API_KEY="re_..."
AUTUMN_SECRET_KEY="am_sk_..."
PASSKEY_RP_ID="localhost".env.local holds the Next.js side only. The secrets Better Auth itself reads
live on the Convex deployment and are set in the next step.
# Printed by `bun convex dev` (….convex.cloud) — swap .cloud for .site for the site URL
NEXT_PUBLIC_CONVEX_URL="https://your-deployment.convex.cloud"
NEXT_PUBLIC_CONVEX_SITE_URL="https://your-deployment.convex.site"
NEXT_PUBLIC_APP_URL="http://localhost:3000"
NEXT_PUBLIC_APP_NAME="Unstack Pro"
# Required in BOTH the Next.js env AND the Convex deployment
AUTUMN_SECRET_KEY="am_sk_..."Set up the database
bun run db:migrateThat applies the SQL migrations in drizzle/ to DATABASE_URL, creating the
Better Auth tables and the app tables.
drizzle-kit does not load .env.local, so run bare it sees DATABASE_URL as
undefined. Export the file into the shell first:
set -a && . ./.env.local && set +a && bunx drizzle-kit migratebun convex dev # in a dedicated terminal — keep it runningThe first run logs you into Convex, creates a deployment, and pushes the
schema/functions. It also writes CONVEX_DEPLOYMENT and the Convex URLs into
.env.local. Leave it running throughout development.
With the deployment up, set the deployment-side variables. These do not live in
.env.local — set each with bunx convex env set NAME "value" (or via the Convex
dashboard):
# Set this before generating JWKS in the next step
bunx convex env set BETTER_AUTH_SECRET "$(openssl rand -base64 32)"
bunx convex env set RESEND_API_KEY "re_..."
bunx convex env set EMAIL_FROM "Unstack Pro <noreply@yourdomain.com>"
# Also required in .env.local
bunx convex env set AUTUMN_SECRET_KEY "am_sk_..."
# Same values as .env.local
bunx convex env set PASSKEY_RP_ID "localhost"
bunx convex env set NEXT_PUBLIC_APP_URL "http://localhost:3000"
bunx convex env set NEXT_PUBLIC_APP_NAME "Unstack Pro"EMAIL_FROM must be a Resend-verified sender — every email flow throws if it's
missing.
Generate signing keys
Nothing to generate. Better Auth runs inside Next.js and issues its own sessions,
so there is no key pair and no JWKS variable.
Better Auth signs the Convex JWTs with an RS256 key pair.
convex/auth.config.ts reads the public key set from the JWKS env var, and
Better Auth signs with the stored private key — which is encrypted with
BETTER_AUTH_SECRET, so that must already be set.
With bun convex dev running (so the functions are deployed):
# Prints the JWKS document set
bunx convex run betterAuth/auth:getLatestJwksSet the output as the JWKS env var. On macOS/Linux, one step:
bunx convex env set JWKS "$(bunx convex run betterAuth/auth:getLatestJwks)"On Windows, copy the printed JSON array and paste it into the same command in place of the substitution.
To rotate keys later, run bunx convex run betterAuth/auth:rotateKeys and set the
result as JWKS again — this logs out all users.
Configure Autumn billing
Subscriptions are defined in autumn.config.ts (a free_plan and a seat-metered
pro plan). Push them to your Autumn account and wire up the secret key:
bunx atmn login # authenticate the Autumn CLI
bunx atmn push # create the products from autumn.config.tsThen set AUTUMN_SECRET_KEY in .env.local using the key from the Autumn
dashboard.
Then set AUTUMN_SECRET_KEY in both .env.local and on the Convex
deployment, using the key from the Autumn dashboard — the Next.js billing routes
and the Convex auth hooks each read it.
Passkeys
PASSKEY_RP_ID must be the bare public domain that serves the app — no
scheme, no port. For local development that is localhost. In production it must
exactly match your deployed domain, and NEXT_PUBLIC_APP_URL must be the
matching https:// origin.
Set it in .env.local:
PASSKEY_RP_ID="localhost"Set it on the Convex deployment, where Better Auth reads it:
bunx convex env set PASSKEY_RP_ID "localhost"Run the app
bun dev # Next.js dev server (Turbopack)Open http://localhost:3000. 🎉
bun dev # Next.js dev server (Turbopack)Open http://localhost:3000. Keep bun convex dev running
in its own terminal — the app does not function without it. 🎉
Commands
bun dev # Next dev server (turbopack)
bun run build # tsgo --noEmit && next build --turbopack
bun run db:generate # drizzle-kit generate (SQL migration from the schema)
bun run db:migrate # drizzle-kit migrate (apply to DATABASE_URL)
bun run auth:schema # regenerate lib/db/auth-schema.ts from lib/auth.ts
bun run lint # biome lint .
bun run format # biome format --write .bun dev # Next dev server (turbopack)
bun convex dev # Convex backend — required for the app to function
bun run build # tsgo --noEmit && next build --turbopack
bun run lint # biome lint .
bun run format # biome format --write .Verify Installation
- Visit the homepage: navigate to
http://localhost:3000 - Register an account: create a test account and complete email verification
- Confirm the data landed: check that your user row was created
- Explore features: log in, create an organization, invite a member
For step 3, query the database directly (select * from "user"; against
DATABASE_URL) or open your provider's table browser.
For step 3, open the Convex dashboard and watch the user table fill in.
Next Steps
- 📖 Authentication — understand the auth system
- 🏢 Organizations — multi-tenant features
- 🔧 Environment Variables — complete configuration
- 🗄️ Database Schema — where the schema and migration workflow are covered
- 🏗️ Project Structure — understand the codebase
- 🔄 Template Updates — pull ongoing template fixes
- 🚀 Deployment — get ready for production
Common Issues
The App Can't Reach Its Data
Check that DATABASE_URL is reachable from where you're running the app (most
hosted providers require ?sslmode=require), and that the migrations have
actually been applied:
set -a && . ./.env.local && set +a && bunx drizzle-kit migrateMake sure your Convex dev server is running:
bun convex devEnvironment Variable Not Found
Double-check that:
.env.localexists in the root directory- The dev server was restarted after you edited it
drizzle-kitcommands were givenDATABASE_URLexplicitly — they do not read.env.local
Double-check that:
.env.localexists in the root directory- Convex-side variables are set via
bunx convex env set(or the dashboard); putting them in.env.localdoes not reach the deployment - If Convex throws that a variable is missing, consult
createAuthOptionsinconvex/betterAuth/auth.ts
Authentication Errors
Ensure:
BETTER_AUTH_SECRETis set in.env.localNEXT_PUBLIC_APP_URLmatches your dev URL- Every migration in
drizzle/has been applied
Ensure:
BETTER_AUTH_SECRETandJWKSare set on the Convex deploymentNEXT_PUBLIC_APP_URLmatches your dev URLbun convex devis running
Never commit your .env.local file to version control. It contains sensitive secrets.