Environment variables
Every environment variable Launch Now reads, whether it is required, and where to get its value.
Launch Now reads its configuration from environment variables. Locally, they live in a .env file
at the root of the project; in production, you set them in your hosting provider (see
Deploying).
Start from the example file:
cp .env.example .envHow validation works
Variables are declared and validated in lib/env.ts with
@t3-oss/env-nextjs and Zod. The app imports env from @/lib/env instead of
reading process.env directly, so every value is typed and checked.
import { createEnv } from "@t3-oss/env-nextjs"
import { z } from "zod"
export const env = createEnv({
server: {
DATABASE_URL: z.url(),
BETTER_AUTH_SECRET: z.string().min(32),
// ...
},
client: {
NEXT_PUBLIC_APP_URL: z.url(),
},
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
// ...
},
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
})- Server variables are only available in server code. Importing one in a client component throws an error.
- Client variables start with
NEXT_PUBLIC_and are inlined into the browser bundle at build time. - If a required variable is missing or malformed, the app fails with a list of the invalid
variables as soon as
lib/env.tsis loaded.
Skipping validation
Set SKIP_ENV_VALIDATION to any non-empty value to turn validation off:
SKIP_ENV_VALIDATION=1 pnpm buildThis is meant for builds that have no secrets, such as the build job in
.github/workflows/ci.yml. Do not set it in production: missing variables would then fail later, at
runtime, with less helpful errors.
Good to know: A few files read process.env directly instead of lib/env.ts, so they keep
working in scripts and client components: lib/config/site-config.ts, lib/config/legal.ts,
lib/auth-client.ts, drizzle/db.ts, drizzle.config.ts and the scripts in scripts/.
App
| Variable | Required | Scope | Description |
|---|---|---|---|
NEXT_PUBLIC_APP_URL | Yes | Client | Public base URL of the app. |
NEXT_PUBLIC_SUPPORT_EMAIL | No | Client | Shows a "Support" mailto link in the app sidebar. |
NEXT_PUBLIC_APP_URL is used for metadata, the sitemap, Open Graph images and links, as the Better
Auth client base URL, and as the trusted origin for auth requests. Locally, use
http://localhost:3000. In production, use your domain, without a trailing slash.
NEXT_PUBLIC_SUPPORT_EMAIL is not declared in lib/env.ts. It is read by
lib/config/site-config.ts as siteConfig.supportEmail; when it is empty, the sidebar link is
hidden.
Database
| Variable | Required | Scope | Description |
|---|---|---|---|
DATABASE_URL | Yes | Server | PostgreSQL connection string. |
Any Postgres works. With Neon, copy the connection string from your project's
dashboard. It is used by the app, by drizzle-kit (drizzle.config.ts), by pnpm db:migrate and
by the seed scripts.
DATABASE_URL=postgresql://user:password@localhost:5432/pulseAuthentication
| Variable | Required | Scope | Description |
|---|---|---|---|
BETTER_AUTH_SECRET | Yes | Server | Secret used to sign sessions. At least 32 characters. |
BETTER_AUTH_URL | Yes | Server | Base URL of the Better Auth server. |
GITHUB_CLIENT_ID | Yes | Server | GitHub OAuth app client ID. |
GITHUB_CLIENT_SECRET | Yes | Server | GitHub OAuth app client secret. |
GOOGLE_CLIENT_ID | Yes | Server | Google OAuth client ID. |
GOOGLE_CLIENT_SECRET | Yes | Server | Google OAuth client secret. |
Generate BETTER_AUTH_SECRET with:
openssl rand -base64 32BETTER_AUTH_URL is the same as NEXT_PUBLIC_APP_URL in most setups. Besides configuring Better
Auth, it builds the links in emails: invitation links, newsletter confirmation links and
notification emails.
OAuth apps
Create one OAuth app per environment and register the Better Auth callback URL:
| Provider | Where to create it | Callback URL |
|---|---|---|
| GitHub | GitHub Developer settings | http://localhost:3000/api/auth/callback/github |
| Google Cloud console | http://localhost:3000/api/auth/callback/google |
Replace http://localhost:3000 with your domain in production.
| Variable | Required | Scope | Description |
|---|---|---|---|
RESEND_API_KEY | Yes | Server | Resend API key. |
EMAIL_FROM | Yes | Server | Default sender, for example Pulse <hello@example.com>. |
RESEND_AUDIENCE_ID | No | Server | Resend audience for the newsletter. The newsletter is off when unset. |
APP_CONTACT_EMAIL | No | Server | General and privacy contact shown in the legal documents. |
APP_HELP_EMAIL | No | Server | Support address for billing and refund requests, and in security emails. |
APP_NOREPLY_EMAIL | No | Server | Sender for notification and security emails. Falls back to EMAIL_FROM. |
APP_ONBOARDING_EMAIL | No | Server | Declared for an onboarding sender; not read by any code yet. |
Get an API key from the Resend dashboard. The sender addresses must belong to a domain you verified in Resend.
RESEND_AUDIENCE_ID is not in .env.example. Add it when you want to collect newsletter
subscribers; see Newsletter.
When APP_CONTACT_EMAIL or APP_HELP_EMAIL is missing, the legal config shows a TODO placeholder
instead, which pnpm legal:check reports. See Legal.
File storage
| Variable | Required | Scope | Description |
|---|---|---|---|
AWS_ENDPOINT_URL_S3 | Yes | Server | Endpoint of your S3-compatible storage. |
AWS_ACCESS_KEY_ID | Yes | Server | Access key ID. |
AWS_SECRET_ACCESS_KEY | Yes | Server | Secret access key. |
AWS_REGION | Yes | Server | Region, for example auto for Cloudflare R2. |
The storage client in lib/storage.ts uses path-style URLs and a bucket named assets, which it
tries to create on first upload. Any S3-compatible provider works. See
File storage.
Stripe
| Variable | Required | Scope | Description |
|---|---|---|---|
STRIPE_SECRET_KEY | Yes | Server | Stripe secret key (sk_test_... or sk_live_...). |
STRIPE_WEBHOOK_SECRET | Yes | Server | Signing secret of the webhook endpoint (whsec_...). |
STRIPE_PRO_PRICE_ID | No | Server | Price ID of the Pro plan. |
STRIPE_ULTRA_PRICE_ID | No | Server | Price ID of the Ultra plan. |
- Copy
STRIPE_SECRET_KEYfrom Stripe's API keys page. - Locally,
stripe listen --forward-to localhost:3000/api/webhook/stripeprints the webhook secret. In production, copy it from the webhook endpoint you create in the Stripe dashboard. pnpm stripe:productsprints both price IDs. They are optional for validation, but a plan without a price ID is skipped when the Stripe plugin lists plans, so users cannot subscribe to it.
See Billing.
Demo account
| Variable | Required | Scope | Description |
|---|---|---|---|
DEMO_EMAIL | No | Server | Email of the shared demo account. |
DEMO_PASSWORD | No | Server | Password of the demo account. At least 8 characters. |
When both are set, pnpm db:seed:demo can create the account, the sign-in page shows a demo sign-in
button, and destructive actions are refused for that account. Leave them empty in a real product.
pnpm reset-project tells you to remove them.
Other variables
| Variable | Description |
|---|---|
SKIP_ENV_VALIDATION | Turns off lib/env.ts validation when set to a non-empty value. |
Full example
# --- App ---------------------------------------------------------------------
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_SUPPORT_EMAIL=help@launch-now.dev
# --- Database (Postgres / Neon) ---------------------------------------------
DATABASE_URL=postgresql://user:password@localhost:5432/pulse
# --- Better Auth -------------------------------------------------------------
BETTER_AUTH_SECRET=replace-with-a-32-character-random-secret
BETTER_AUTH_URL=http://localhost:3000
# --- Email (Resend) ----------------------------------------------------------
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxx
EMAIL_FROM="Pulse <hello@example.com>"
APP_CONTACT_EMAIL=contact@example.com
APP_HELP_EMAIL=help@example.com
APP_NOREPLY_EMAIL=noreply@example.com
APP_ONBOARDING_EMAIL=onboarding@example.com
# --- OAuth providers ---------------------------------------------------------
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-google-client-secret
# --- S3-compatible storage (avatars, uploads) --------------------------------
AWS_ENDPOINT_URL_S3=https://s3.example.com
AWS_ACCESS_KEY_ID=your-access-key-id
AWS_SECRET_ACCESS_KEY=your-secret-access-key
AWS_REGION=auto
# --- Stripe ------------------------------------------------------------------
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx
STRIPE_PRO_PRICE_ID=price_xxxxxxxxxxxxxxxx
STRIPE_ULTRA_PRICE_ID=price_xxxxxxxxxxxxxxxx
# --- Demo account (optional) -------------------------------------------------
DEMO_EMAIL=demo@example.com
DEMO_PASSWORD=demo-passwordAdding a variable
To add your own variable:
- Add it to
server(orclient, with theNEXT_PUBLIC_prefix) inlib/env.ts, with a Zod schema. - Map it in
runtimeEnv. - Add it to
.env.examplewith a comment, so other developers know about it. - Read it with
env.MY_VARIABLEfrom@/lib/env.
server: {
// ...
MY_API_KEY: z.string().min(1),
},
runtimeEnv: {
// ...
MY_API_KEY: process.env.MY_API_KEY,
},