# Environment Variables

Every environment variable Nextly reads, organized by category with required vs. optional indicators.

Nextly reads environment variables for database connections, authentication, storage, email, and other runtime settings. Most live in the central env schema (`packages/nextly/src/shared/lib/env.ts`); storage adapter env vars are per-adapter and read by the adapter packages directly.

Copy `.env.example` to `.env` and fill in values:

```bash
cp .env.example .env
```

## Canonical `.env.example`

The base template `templates/base/.env.example` ships the minimum required variables:

```bash title=".env.example"
# Nextly Configuration
# Generated by create-nextly-app

# Database Configuration
DB_DIALECT=postgresql
DATABASE_URL=postgresql://user:password@localhost:5432/nextly_dev

# Authentication (REQUIRED)
# Generate with: openssl rand -base64 32
NEXTLY_SECRET=change-me-generate-a-secure-secret

# Application URL
NEXT_PUBLIC_APP_URL=http://localhost:3000

# Development diagnostics (opt-in — see below)
# NEXTLY_DEV_DIAGNOSTICS=1

# Storage (per-adapter — see "Storage" section below)
```

The `create-nextly-app` scaffolder fills in `DB_DIALECT`, `DATABASE_URL`, and an auto-generated 32-byte base64 `NEXTLY_SECRET` for you. For a production deployment, regenerate `NEXTLY_SECRET` with:

```bash
openssl rand -base64 32
```

***

## Database

Source: `packages/nextly/src/shared/lib/env.ts`.

| Variable                     | Required                                                                  | Default      | Description                                                                                                                                                                                                                                    |
| ---------------------------- | ------------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DB_DIALECT`                 | Yes                                                                       | `postgresql` | Database dialect: `postgresql`, `mysql`, or `sqlite`.                                                                                                                                                                                          |
| `DATABASE_URL`               | Yes for PostgreSQL and MySQL; for SQLite, for the CLI's database commands | —            | Full database connection URL; for `sqlite`, a `file:` URL. The running app falls back to `file:./data/nextly.db` on SQLite, but the CLI commands that use the database (`migrate`, `db:sync`) require it on every dialect. Validated as a URL. |
| `DB_POOL_MAX`                | No                                                                        | `20`         | Max connections in the pool. Min: 1.                                                                                                                                                                                                           |
| `DB_POOL_MIN`                | No                                                                        | `2`          | Min connections in the pool. Min: 0.                                                                                                                                                                                                           |
| `DB_POOL_IDLE_TIMEOUT`       | No                                                                        | `30000`      | Idle connection timeout in milliseconds. Min: 1000.                                                                                                                                                                                            |
| `DB_QUERY_TIMEOUT`           | No                                                                        | `15000`      | Query timeout in milliseconds. Min: 1000.                                                                                                                                                                                                      |
| `DB_HEALTHCHECK_INTERVAL_MS` | No                                                                        | `30000`      | Health-check interval in milliseconds. Min: 1000.                                                                                                                                                                                              |
| `DB_SNAKE_CASE`              | No                                                                        | `false`      | Use `snake_case` for database column names instead of `camelCase`.                                                                                                                                                                             |

### Connection-string formats

```bash
# PostgreSQL (recommended for production)
DATABASE_URL=postgresql://user:password@localhost:5432/nextly_dev

# MySQL
DATABASE_URL=mysql://root:root@localhost:3306/nextly_dev

# SQLite (development / single-instance only)
DATABASE_URL=file:./dev.db
```

***

## Runtime

| Variable   | Required | Default       | Description                                                                                        |
| ---------- | -------- | ------------- | -------------------------------------------------------------------------------------------------- |
| `NODE_ENV` | No       | `development` | Runtime environment: `development`, `production`, or `test`. Affects production-only checks below. |

***

## Development diagnostics

| Variable                 | Required | Default | Description                                                                                                                                                                           |
| ------------------------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NEXTLY_DEV_DIAGNOSTICS` | No       | —       | Set to `1` to add a `_devDiagnostics` field to error responses, carrying the error's log context and the message of its underlying cause. Ignored unless `NODE_ENV` is `development`. |

An error response is deliberately generic: it carries a code, a public message
and a request id, and withholds the log context and the underlying cause so a
response cannot disclose driver output, table names or internal paths. That is
right for a deployed app and unhelpful while you are building, where the detail
you need is exactly what was withheld.

With this set, an error in development also carries:

```json
{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "An unexpected error occurred.",
    "requestId": "req_01HQ...",
    "_devDiagnostics": {
      "logContext": { "collectionName": "posts", "entryId": "abc123" },
      "cause": "duplicate key value violates unique constraint posts_slug_key",
      "flattened": [{ "code": "CONFLICT", "logContext": { "field": "slug" } }]
    }
  }
}
```

`flattened` lists errors that were rebuilt on the way to the boundary, which is
what makes an unexpected failure name itself instead of looking like every
other one.

**Why it is opt-in rather than on by default.** The gate requires two
independent signals: `NODE_ENV` must be `development` **and** this flag must be
`1`. The second exists precisely because the first can be wrong — `nextly` ships
pre-built and stays external to your build, so `NODE_ENV` is a runtime value a
deployment can carry by mistake.

A default that shipped in `.env` would be true in exactly that case, which is
the one the second signal guards against, collapsing two independent signals
back into one. So `create-nextly-app` writes the setting **commented out** with
this explanation: discoverable, and never enabled by a file that travels with
your app.

Uncomment it in your local `.env` — not in a file you deploy — and keep it out
of any environment where `NODE_ENV` might not be what you expect.

***

## Authentication

Nextly uses a custom JWT auth system (access + refresh tokens, sessions, API keys, RBAC). There is **no** OAuth.

| Variable                           | Required                     | Default | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| ---------------------------------- | ---------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `NEXTLY_SECRET`                    | Yes (production: ≥ 32 chars) | —       | Secret used to sign JWTs and other crypto. Production startup throws if it is missing or shorter than 32 characters. Generate with `openssl rand -base64 32`.                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `NEXTLY_SECRET_PREVIOUS`           | No                           | —       | Secrets you have RETIRED, newest first. Kept for READING only — nothing new is ever keyed with one. See "Rotating `NEXTLY_SECRET`" below.                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `NEXTLY_ALLOWED_ORIGINS`           | Self-hosted                  | —       | Comma-separated list of additional origins allowed for CSRF validation (e.g. `https://admin.example.com,https://staging.example.com`). The origin the request arrives at is always allowed, but a self-hosted server (`next start` or the standalone build) takes that from the address it listens on, such as `http://localhost:3000`, not from your public URL. List the origin browsers use (`https://example.com`) whenever it differs from that address — behind a proxy, by IP or hostname, or in a container — or sign-in and plugin-route writes are refused with `CSRF_FAILED`.               |
| `NEXTLY_ERASE_RETIRED_AUTH_TABLES` | No                           | —       | The retired `accounts` and `sessions` tables that are Nextly's, comma-separated (`accounts`, `sessions` or `accounts,sessions`). Deleting a user erases their rows only from a table named here, and only while it has the shape Nextly created. Nextly keeps no record that it created these tables, and the Auth.js adapters' tables share their names and shape, so name a table only when it is Nextly's. Unset, the rows stay, and the startup warning says so. See [Production migrations](https://nextlyhq.com/docs/guides/production-migrations.md). *Needs the release after 0.0.2-alpha.66.* |

### Rotating `NEXTLY_SECRET`

Rotating the secret re-keys everything derived from it, and the values already
written do not move. For most of them that fails loudly and is fine — a webhook
signing secret that no longer decrypts is one an operator re-enters.

The email delivery log is the exception, because it fails **quietly**. Each row
identifies its recipient by an HMAC of their address, so after a rotation an
erasure request computes a digest none of the older rows carry, matches nothing,
and reports success. "No rows matched" is also what a person with no mail looks
like, so nothing distinguishes the two.

List the old secret in `NEXTLY_SECRET_PREVIOUS` and those rows stay reachable —
for lookups and for erasure — without becoming writable:

```bash
NEXTLY_SECRET=the-new-one
NEXTLY_SECRET_PREVIOUS=the-old-one,an-older-one
```

**Two spellings, because one of them cannot express every legal secret.** The
comma-separated form above is the ordinary one. Use the JSON-array form when a
retired secret contains a comma, or when leading or trailing whitespace is part
of the key — splitting or trimming those produces a key that was never used,
which matches nothing and fails in the silent direction:

```bash
# Single quotes are REQUIRED. Without them the shell strips the inner double
# quotes and the value becomes `[old,with,commas,  spaced  ]`, which is not
# JSON — it falls back to comma splitting and derives four keys that were never
# used, so lookups and erasure silently miss the rows this is meant to reach.
NEXTLY_SECRET_PREVIOUS='["old,with,commas","  spaced  "]'
```

The JSON form also names two generations no string can:

* `null` is the **unkeyed** generation — what an install writes when it has no
  `NEXTLY_SECRET` at all, which is only possible outside production. Rows
  written then carry a plain SHA-256 digest that no HMAC reproduces, so
  enabling a secret later strands them unless you name that generation.
* `""` is a secret that really was the empty string, which is used as an HMAC
  key like any other.

```bash
# was unkeyed in development, now has a secret
NEXTLY_SECRET_PREVIOUS='[null]'
```

An empty entry in the **comma** form is dropped rather than rejected: a trailing
comma is the likeliest way to write the list, and nothing about `older,` says a
second key was meant. Write `'[""]'` when you mean it — with the outer single quotes, for the same
reason.

***

## Application URLs

| Variable              | Required         | Default                     | Description                                                                                                                         |
| --------------------- | ---------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `NEXT_PUBLIC_APP_URL` | Yes (production) | —                           | Public-facing app URL. Used in client-side code and as the fallback for email-link `baseUrl`. Production startup throws if missing. |
| `API_BASE_URL`        | No               | `http://localhost:3000/api` | Base URL for API routes. Validated as a URL.                                                                                        |

***

## Storage (per-adapter)

The default storage backend is **local disk** under `./public/uploads/`. No env vars are needed for default storage.

Cloud adapters are opt-in. The variables below are read **by the adapter packages**, not by the central env schema, so they only need to exist when you actually configure that adapter in `nextly.config.ts`. Nothing else (env-validation, etc.) checks for them.

### S3 / S3-compatible — `@nextlyhq/storage-s3`

Works with AWS S3, Cloudflare R2, MinIO, and DigitalOcean Spaces. The plugin accepts these values directly; the env-var names below are the conventional ones used in our examples (and what your `nextly.config.ts` typically references with `process.env.X`).

| Variable                | Required  | Description                                                                |
| ----------------------- | --------- | -------------------------------------------------------------------------- |
| `S3_BUCKET`             | Yes       | Bucket name.                                                               |
| `AWS_REGION`            | Yes       | AWS region (e.g. `us-east-1`). Use `auto` for Cloudflare R2.               |
| `AWS_ACCESS_KEY_ID`     | Yes       | Access key ID.                                                             |
| `AWS_SECRET_ACCESS_KEY` | Yes       | Secret access key.                                                         |
| `S3_ENDPOINT`           | Sometimes | Custom endpoint URL. Required for R2 and MinIO.                            |
| `S3_PUBLIC_URL`         | No        | Public URL prefix (e.g. `https://pub-xxxx.r2.dev` for R2 or a CDN domain). |
| `S3_FORCE_PATH_STYLE`   | No        | Set `true` for MinIO and other path-style providers.                       |

```typescript title="nextly.config.ts"
import { s3Storage } from "@nextlyhq/storage-s3";

storage: [
  s3Storage({
    bucket: process.env.S3_BUCKET!,
    region: process.env.AWS_REGION!,
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
    endpoint: process.env.S3_ENDPOINT,
    forcePathStyle: process.env.S3_FORCE_PATH_STYLE === "true",
    publicUrl: process.env.S3_PUBLIC_URL,
    collections: { media: true },
  }),
];
```

### Vercel Blob — `@nextlyhq/storage-vercel-blob`

Recommended for Vercel deployments.

| Variable                | Required | Description                                                                                                             |
| ----------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
| `BLOB_READ_WRITE_TOKEN` | Yes      | Vercel Blob token from the Vercel dashboard. The adapter reads this directly when the `token` config option is omitted. |

### UploadThing — `@nextlyhq/storage-uploadthing`

| Variable            | Required | Description                                                                                       |
| ------------------- | -------- | ------------------------------------------------------------------------------------------------- |
| `UPLOADTHING_TOKEN` | Yes      | UploadThing API token. The adapter reads this directly when the `token` config option is omitted. |

***

## Email / SMTP

Source: `packages/nextly/src/shared/lib/env.ts`. Required only if you send emails (password resets, notifications). In production, if **any** SMTP variable is set, **all four** of `SMTP_HOST`, `SMTP_USER`, `SMTP_PASS`, `SMTP_FROM` must be set or the env validator throws.

| Variable    | Required | Default | Description                                              |
| ----------- | -------- | ------- | -------------------------------------------------------- |
| `SMTP_HOST` | No       | —       | SMTP server hostname (e.g. `smtp.gmail.com`).            |
| `SMTP_PORT` | No       | `587`   | SMTP port. 587 for TLS, 465 for SSL. Range: 1–65535.     |
| `SMTP_USER` | No       | —       | SMTP authentication username.                            |
| `SMTP_PASS` | No       | —       | SMTP authentication password.                            |
| `SMTP_FROM` | No       | —       | From address for outgoing emails. Validated as an email. |

For local development, the repo ships a Mailpit profile (`docker compose --profile with-mailpit up -d mailpit`) on host ports 1025 (SMTP) and 8025 (web UI) — see [Email guide](https://nextlyhq.com/docs/guides/email.md) for details.

***

## Security

These env vars are read by middleware and security helpers. None are part of the central env schema; they are read directly by the modules that use them.

| Variable            | Required | Default | Description                                                                                                                                                                                                                             | Read by                                              |
| ------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `TRUSTED_PROXY_IPS` | No       | empty   | Comma-separated CIDR list of trusted reverse-proxy IPs. Combined with `security.trustProxy: true`, controls which `X-Forwarded-For` values are honoured for client-IP resolution (rate limiting, refresh-token binding, audit logging). | `packages/nextly/src/utils/get-trusted-client-ip.ts` |

***

## Performance — permission cache

Nextly's RBAC layer ships a hybrid (in-memory LRU + database) permission cache. These env vars tune it; none are part of the central env schema — they are read directly by the permission service.

| Variable                       | Required | Default | Description                                                                                                                                                                                                                                                                                         | Read by                                           |
| ------------------------------ | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| `PERMISSION_CACHE_ENABLED`     | No       | `true`  | Set to `false` or `0` to disable the hybrid permission cache.                                                                                                                                                                                                                                       | `packages/nextly/src/services/lib/permissions.ts` |
| `PERMISSION_CACHE_TTL_SECONDS` | No       | `300`   | Time-to-live for database cache entries (seconds). This tier is shared between instances and the signal that retires it is held in memory, so until that signal is stored beside the rows this value is also the longest a revoked grant can survive on an instance that did not handle the change. | `packages/nextly/src/services/lib/permissions.ts` |
| `PERMISSION_CACHE_MEMORY_SIZE` | No       | `10000` | In-memory LRU cache size (number of entries).                                                                                                                                                                                                                                                       | `packages/nextly/src/services/lib/permissions.ts` |

The database permission cache benefits from periodic cleanup. A typical daily cron:

```bash
0 2 * * * curl -X POST http://localhost:3000/admin/api/auth/cache/cleanup
```

***

## Debug

| Variable      | Required | Default | Description                                                | Read by                                                                 |
| ------------- | -------- | ------- | ---------------------------------------------------------- | ----------------------------------------------------------------------- |
| `DEBUG_RBAC`  | No       | —       | Set `1` to log detailed RBAC permission decisions.         | `packages/nextly/src/services/lib/permissions.ts`                       |
| `DEBUG_CACHE` | No       | —       | Set `1` to log permission-cache hit/miss/eviction details. | `packages/nextly/src/domains/auth/services/permission-cache-service.ts` |

***

## Docker development (optional)

Used by the bundled `docker-compose.yml` for local Postgres / Redis / Adminer / Drizzle Studio / Mailpit setup. Not needed when connecting to an existing database.

| Variable              | Default      | Description                                  |
| --------------------- | ------------ | -------------------------------------------- |
| `DB_NAME`             | `nextly_dev` | PostgreSQL database name.                    |
| `DB_USER`             | `postgres`   | PostgreSQL user.                             |
| `DB_PASSWORD`         | —            | PostgreSQL password. Override in production. |
| `DB_PORT`             | `5432`       | PostgreSQL host port.                        |
| `ADMINER_PORT`        | `8080`       | Adminer (database browser) UI port.          |
| `REDIS_PORT`          | `6379`       | Redis cache port.                            |
| `DRIZZLE_STUDIO_PORT` | `4983`       | Drizzle Studio port for the database GUI.    |

***

## Example `.env` files

### Minimal production

```bash
# Runtime
NODE_ENV=production

# Database
DB_DIALECT=postgresql
DATABASE_URL=postgresql://user:password@db.example.com:5432/nextly_prod

# Auth
NEXTLY_SECRET=your-generated-secret-at-least-32-characters-long
NEXT_PUBLIC_APP_URL=https://your-domain.com

# Storage (Vercel Blob example)
BLOB_READ_WRITE_TOKEN=vercel_blob_rw_xxxxxxxxxxxx
```

### Development with S3-compatible storage (MinIO)

```bash
# Database
DB_DIALECT=postgresql
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/nextly_dev

# Auth
NEXTLY_SECRET=dev-secret-minimum-32-characters-long-replace-in-production
NEXT_PUBLIC_APP_URL=http://localhost:3000

# Storage (MinIO)
S3_BUCKET=nextly-dev
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=minioadmin
AWS_SECRET_ACCESS_KEY=minioadmin
S3_ENDPOINT=http://localhost:9000
S3_FORCE_PATH_STYLE=true
```

## Next steps

* [Nextly config](https://nextlyhq.com/docs/configuration/nextly-config.md) — configure storage and security from `nextly.config.ts`
* [Database](https://nextlyhq.com/docs/database.md) — choose and configure PostgreSQL, MySQL, or SQLite
* [Authentication guide](https://nextlyhq.com/docs/guides/authentication.md) — auth-related env vars in depth
* [Deployment guide](https://nextlyhq.com/docs/guides/deployment.md) — production environment-variable checklist
