Launch NowDocs

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 .env

How 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.

lib/env.ts
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.ts is loaded.

Skipping validation

Set SKIP_ENV_VALIDATION to any non-empty value to turn validation off:

SKIP_ENV_VALIDATION=1 pnpm build

This 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

VariableRequiredScopeDescription
NEXT_PUBLIC_APP_URLYesClientPublic base URL of the app.
NEXT_PUBLIC_SUPPORT_EMAILNoClientShows 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

VariableRequiredScopeDescription
DATABASE_URLYesServerPostgreSQL 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.

.env
DATABASE_URL=postgresql://user:password@localhost:5432/pulse

Authentication

VariableRequiredScopeDescription
BETTER_AUTH_SECRETYesServerSecret used to sign sessions. At least 32 characters.
BETTER_AUTH_URLYesServerBase URL of the Better Auth server.
GITHUB_CLIENT_IDYesServerGitHub OAuth app client ID.
GITHUB_CLIENT_SECRETYesServerGitHub OAuth app client secret.
GOOGLE_CLIENT_IDYesServerGoogle OAuth client ID.
GOOGLE_CLIENT_SECRETYesServerGoogle OAuth client secret.

Generate BETTER_AUTH_SECRET with:

openssl rand -base64 32

BETTER_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:

ProviderWhere to create itCallback URL
GitHubGitHub Developer settingshttp://localhost:3000/api/auth/callback/github
GoogleGoogle Cloud consolehttp://localhost:3000/api/auth/callback/google

Replace http://localhost:3000 with your domain in production.

Email

VariableRequiredScopeDescription
RESEND_API_KEYYesServerResend API key.
EMAIL_FROMYesServerDefault sender, for example Pulse <hello@example.com>.
RESEND_AUDIENCE_IDNoServerResend audience for the newsletter. The newsletter is off when unset.
APP_CONTACT_EMAILNoServerGeneral and privacy contact shown in the legal documents.
APP_HELP_EMAILNoServerSupport address for billing and refund requests, and in security emails.
APP_NOREPLY_EMAILNoServerSender for notification and security emails. Falls back to EMAIL_FROM.
APP_ONBOARDING_EMAILNoServerDeclared 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

VariableRequiredScopeDescription
AWS_ENDPOINT_URL_S3YesServerEndpoint of your S3-compatible storage.
AWS_ACCESS_KEY_IDYesServerAccess key ID.
AWS_SECRET_ACCESS_KEYYesServerSecret access key.
AWS_REGIONYesServerRegion, 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

VariableRequiredScopeDescription
STRIPE_SECRET_KEYYesServerStripe secret key (sk_test_... or sk_live_...).
STRIPE_WEBHOOK_SECRETYesServerSigning secret of the webhook endpoint (whsec_...).
STRIPE_PRO_PRICE_IDNoServerPrice ID of the Pro plan.
STRIPE_ULTRA_PRICE_IDNoServerPrice ID of the Ultra plan.
  • Copy STRIPE_SECRET_KEY from Stripe's API keys page.
  • Locally, stripe listen --forward-to localhost:3000/api/webhook/stripe prints the webhook secret. In production, copy it from the webhook endpoint you create in the Stripe dashboard.
  • pnpm stripe:products prints 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

VariableRequiredScopeDescription
DEMO_EMAILNoServerEmail of the shared demo account.
DEMO_PASSWORDNoServerPassword 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

VariableDescription
SKIP_ENV_VALIDATIONTurns off lib/env.ts validation when set to a non-empty value.

Full example

.env.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-password

Adding a variable

To add your own variable:

  1. Add it to server (or client, with the NEXT_PUBLIC_ prefix) in lib/env.ts, with a Zod schema.
  2. Map it in runtimeEnv.
  3. Add it to .env.example with a comment, so other developers know about it.
  4. Read it with env.MY_VARIABLE from @/lib/env.
lib/env.ts
server: {
  // ...
  MY_API_KEY: z.string().min(1),
},
runtimeEnv: {
  // ...
  MY_API_KEY: process.env.MY_API_KEY,
},