# Nextly Config Reference

Complete reference for every option accepted by defineConfig() in nextly.config.ts.

The `nextly.config.ts` file is the central configuration for your Nextly application. The `defineConfig()` function validates your config, applies defaults, and returns a sanitized `SanitizedNextlyConfig` consumed by the rest of the runtime.

> **Source of truth:** the `NextlyConfig` interface in `packages/nextly/src/shared/types/config.ts` defines the shape this page documents. The `defineConfig()` helper in `packages/nextly/src/collections/config/define-config.ts` wires it up.

## Full example

```typescript title="nextly.config.ts"
import { defineConfig } from "nextly";
import { s3Storage } from "@nextlyhq/storage-s3";
import Posts from "./src/collections/posts";
import Media from "./src/collections/media";
import SiteSettings from "./src/singles/site-settings";
import Header from "./src/singles/header";
import Footer from "./src/singles/footer";
import { Seo, Hero } from "./src/field-groups";

export default defineConfig({
  // Content model
  collections: [Posts, Media],
  singles: [SiteSettings, Header, Footer],
  fieldGroups: [Seo, Hero],

  // Built-in user model extensions
  users: {
    fields: [
      // imported from "nextly"
      // text({ name: "company", label: "Company" }),
    ],
  },

  // Output paths
  typescript: {
    outputFile: "./src/types/generated/nextly-types.ts",
    declare: true,
  },
  db: {
    schemasDir: "./src/db/schemas/collections",
    migrationsDir: "./src/db/migrations",
  },

  // Cloud storage (optional — defaults to local disk under ./public/uploads/)
  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!,
      collections: {
        media: true,
      },
    }),
  ],

  // Security
  security: {
    cors: {
      origin: ["https://example.com"],
      credentials: true,
    },
    sanitization: { enabled: true },
  },

  // Rate limiting (enabled by default — opt out with enabled: false)
  rateLimit: {
    enabled: true,
    readLimit: 100,
    writeLimit: 30,
    windowMs: 60_000,
  },

  // Per-API-key rate limit
  apiKeys: {
    rateLimit: { requestsPerHour: 1000, windowMs: 3_600_000 },
  },

  // Auth opt-in flags
  auth: {
    revealRegistrationConflict: false,
  },

  // Email
  email: {
    providerConfig: {
      provider: "resend",
      apiKey: process.env.RESEND_API_KEY!,
    },
    from: "My App <noreply@example.com>",
    baseUrl: "https://example.com",
  },

  // Admin panel branding & plugin overrides
  admin: {
    branding: {
      logoUrl: "/logo.svg",
      logoText: "My App",
      favicon: "/favicon.ico",
      colors: {
        primary: "#6366f1",
        accent: "#f59e0b",
      },
    },
  },
});
```

## Top-level options

| Option        | Type                 | Default                                  | Description                                                                                                               |
| ------------- | -------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `collections` | `CollectionConfig[]` | `[]`                                     | Content types with multiple entries. See [Collections](https://nextlyhq.com/docs/configuration/collections.md).           |
| `singles`     | `SingleConfig[]`     | `[]`                                     | Single-document content (site settings, header, etc.). See [Singles](https://nextlyhq.com/docs/configuration/singles.md). |
| `fieldGroups` | `FieldGroupConfig[]` | `[]`                                     | Reusable field groups. See [Field Groups](https://nextlyhq.com/docs/configuration/field-groups.md).                       |
| `users`       | `UserConfig`         | `undefined`                              | Extend the built-in user model with custom fields.                                                                        |
| `typescript`  | `TypeScriptConfig`   | See below                                | Generated types path and module augmentation.                                                                             |
| `db`          | `DatabaseConfig`     | See below                                | Where Drizzle schema and migration files are written.                                                                     |
| `rateLimit`   | `RateLimitingConfig` | Enabled (100 read / 30 write per minute) | Global API rate limiting.                                                                                                 |
| `apiKeys`     | `ApiKeysConfig`      | 1000 req/hour, 1-hour window             | Per-API-key rate limit (applies when `Authorization: Bearer nx_live_...` is used).                                        |
| `auth`        | `AuthConfig`         | `{ revealRegistrationConflict: false }`  | Auth-related opt-in flags.                                                                                                |
| `storage`     | `StoragePlugin[]`    | `[]` (local disk used)                   | Cloud storage plugins. Default storage is local disk under `./public/uploads/`.                                           |
| `plugins`     | `PluginDefinition[]` | `[]`                                     | Plugins extending Nextly with collections, hooks, sidebar items, etc.                                                     |
| `email`       | `EmailConfig`        | `undefined`                              | Email provider for password resets and notifications.                                                                     |
| `security`    | `SecurityConfig`     | Secure defaults                          | CORS, security headers, upload restrictions, sanitization, auth-rate-limit, body-size limits.                             |
| `admin`       | `AdminConfig`        | `undefined`                              | Admin panel branding and plugin sidebar overrides.                                                                        |

The next sections walk through each option.

***

### `collections`

```ts
collections?: CollectionConfig[];
```

Array of collection configurations created with `defineCollection()`. Each collection becomes a database table, an admin panel section, and a set of REST endpoints under `/admin/api/collections/[slug]/entries`.

```typescript
import Posts from "./src/collections/posts";
import Media from "./src/collections/media";

export default defineConfig({
  collections: [Posts, Media],
});
```

See [Collections](https://nextlyhq.com/docs/configuration/collections.md) for the full `CollectionConfig` shape.

***

### `singles`

```ts
singles?: SingleConfig[];
```

Array of single-document configurations created with `defineSingle()`. A single is an auto-created, non-deletable document (site settings, header, footer, homepage). Slugs must be unique across collections, singles, and field groups.

```typescript
import SiteSettings from "./src/singles/site-settings";

export default defineConfig({
  singles: [SiteSettings],
});
```

See [Singles](https://nextlyhq.com/docs/configuration/singles.md) for the full `SingleConfig` shape.

***

### `fieldGroups`

```ts
fieldGroups?: FieldGroupConfig[];
```

Array of reusable field structures created with `defineFieldGroup()`. Field groups are embedded inside collections, singles, or other field groups via the `component` field type. Slugs must be unique across collections, singles, and field groups.

```typescript
import { Seo, Hero } from "./src/field-groups";

export default defineConfig({
  fieldGroups: [Seo, Hero],
});
```

See [Field Groups](https://nextlyhq.com/docs/configuration/field-groups.md).

***

### `users`

```ts
users?: UserConfig;
```

Extend the built-in user model with custom fields. Custom fields are stored in a separate `user_ext` table with proper typed columns. Only scalar field types are accepted: `text`, `textarea`, `number`, `email`, `select`, `radio`, `checkbox`, `date`.

```typescript
import { text, select, option } from "nextly";

export default defineConfig({
  users: {
    fields: [
      text({ name: "company", label: "Company" }),
      select({
        name: "department",
        options: [option("Engineering"), option("Sales"), option("Marketing")],
      }),
    ],
    admin: {
      // Show these custom columns in the user list
      listFields: ["company", "department"],
      // Override the form-section group label (default: "Additional Information")
      group: "Profile",
    },
  },
});
```

***

### `typescript`

```ts
typescript?: { outputFile?: string; declare?: boolean };
```

Controls TypeScript type generation.

| Property     | Type      | Default                                   | Description                                                        |
| ------------ | --------- | ----------------------------------------- | ------------------------------------------------------------------ |
| `outputFile` | `string`  | `"./src/types/generated/nextly-types.ts"` | Path the generator writes types to.                                |
| `declare`    | `boolean` | `true`                                    | Whether to add `declare module` blocks for runtime type inference. |

Earlier versions wrote the default file as `payload-types.ts`. If your config doesn't set `outputFile` and that file is still there, the generator writes `nextly-types.ts` and turns `payload-types.ts` into a one-line re-export of it, so existing imports keep compiling. It tells you when it does. Change imports of `payload-types` to `nextly-types`, then delete the old file: a later release stops keeping it. A `payload-types.ts` that Nextly didn't generate is never changed. To keep the old name, set `outputFile` to it.

***

### `db`

```ts
db?: { schemasDir?: string; migrationsDir?: string };
```

Where Drizzle schema files and migration files are written.

| Property        | Type     | Default                          | Description                          |
| --------------- | -------- | -------------------------------- | ------------------------------------ |
| `schemasDir`    | `string` | `"./src/db/schemas/collections"` | Per-collection Drizzle schema files. |
| `migrationsDir` | `string` | `"./src/db/migrations"`          | Generated migration files.           |

See [Environment variables](https://nextlyhq.com/docs/configuration/environment.md) for the runtime database connection settings.

***

### `rateLimit`

```ts
rateLimit?: RateLimitingConfig;
```

Global API rate limiting. **Enabled by default** with 100 read / 30 write requests per minute. Opt out with `rateLimit: { enabled: false }`.

| Property       | Type                                                          | Default              | Description                                                           |
| -------------- | ------------------------------------------------------------- | -------------------- | --------------------------------------------------------------------- |
| `enabled`      | `boolean`                                                     | `true`               | Toggle rate limiting.                                                 |
| `readLimit`    | `number`                                                      | `100`                | Max GET requests per window.                                          |
| `writeLimit`   | `number`                                                      | `30`                 | Max POST/PATCH/PUT/DELETE per window.                                 |
| `windowMs`     | `number`                                                      | `60_000`             | Window in milliseconds.                                               |
| `store`        | `RateLimitStore`                                              | In-memory            | Pluggable store; use a Redis store across multi-instance deployments. |
| `keyGenerator` | `(request: Request) => string`                                | Client IP            | Custom rate-limit key.                                                |
| `skip`         | `(request: Request) => boolean \| Promise<boolean>`           | Skips `/admin/api/*` | Skip rate limiting for matching requests.                             |
| `collections`  | `Record<string, { readLimit?: number; writeLimit?: number }>` | None                 | Per-collection overrides.                                             |

```typescript
rateLimit: {
  enabled: true,
  readLimit: 100,
  writeLimit: 30,
  collections: {
    media: { readLimit: 50, writeLimit: 10 },
    logs: { readLimit: 200 },
  },
}
```

The default `skip` exempts every request under `/admin/api/`, so the admin's own navigation, which fires several requests per page, never trips the per-IP limit. A scaffolded app serves its whole REST API there, though, so by default these limits apply to none of its requests. To apply them, pass `skip: () => false`; the admin's requests then count against the same budget, so size `readLimit` for it.

***

### `apiKeys`

```ts
apiKeys?: { rateLimit?: { requestsPerHour?: number; windowMs?: number } };
```

Per-key rate limit applied when a request authenticates with an API key (`Authorization: Bearer nx_live_...`). Session-cookie requests use the global `rateLimit` block instead, subject to its `skip`, which by default exempts everything under `/admin/api/`. Omitting this block falls back to built-in defaults.

| Property                    | Type     | Default     | Description                                                      |
| --------------------------- | -------- | ----------- | ---------------------------------------------------------------- |
| `rateLimit.requestsPerHour` | `number` | `1000`      | Maximum requests per sliding window. Must be a positive integer. |
| `rateLimit.windowMs`        | `number` | `3_600_000` | Sliding window duration in milliseconds.                         |

`defineConfig()` throws at startup if either value is non-positive.

***

### `auth`

```ts
auth?: { revealRegistrationConflict?: boolean };
```

Auth-related opt-in flags. Today this block exposes a single flag.

| Property                     | Type      | Default | Description                                                                                                                                                                                                                                                                                                  |
| ---------------------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `revealRegistrationConflict` | `boolean` | `false` | When `false` (default), `/auth/register` returns the same "we've sent a confirmation link" response whether or not the email exists, to prevent account enumeration. Set to `true` only if your threat model genuinely doesn't care about email enumeration (e.g. closed admin tool with controlled signup). |

Authentication is custom: email + password, JWT access/refresh tokens, sessions, API keys, RBAC. There is no OAuth.

***

### `storage`

```ts
storage?: StoragePlugin[];
```

Cloud storage plugins. **The default is local disk** — Nextly writes uploads to `./public/uploads/` if `storage` is empty or omitted.

Available adapters:

* `@nextlyhq/storage-s3` — AWS S3, Cloudflare R2, MinIO, DigitalOcean Spaces
* `@nextlyhq/storage-vercel-blob` — Vercel Blob Storage
* `@nextlyhq/storage-uploadthing` — UploadThing

Install whichever adapter you need:



**pnpm**

```bash
pnpm add @nextlyhq/storage-s3
```

**npm**

```bash
npm install @nextlyhq/storage-s3
```

**yarn**

```bash
yarn add @nextlyhq/storage-s3
```

**bun**

```bash
bun add @nextlyhq/storage-s3
```

```typescript
import { s3Storage } from "@nextlyhq/storage-s3";

export default defineConfig({
  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!,
      collections: {
        media: true,
        "private-docs": {
          prefix: "private/",
          signedDownloads: true,
          clientUploads: true,
        },
      },
    }),
  ],
});
```

See [Environment variables — Storage](https://nextlyhq.com/docs/configuration/environment.md#storage-per-adapter) for adapter-specific env vars.

***

### `plugins`

```ts
plugins?: PluginDefinition[];
```

Plugins extend Nextly with extra collections, hooks, sidebar items, and admin views.

```typescript
import { formBuilder } from "@nextlyhq/plugin-form-builder";

const formBuilderPlugin = formBuilder();

export default defineConfig({
  plugins: [formBuilderPlugin.plugin],
  collections: [Posts],
});
```

***

### `email`

```ts
email?: EmailConfig;
```

Email provider for password resets, email verification, and other transactional emails. The provider configured here is the **code-first fallback**; database-managed providers configured via the admin Settings UI take precedence at runtime.

| Property            | Type                                               | Required                               | Description                                                                    |
| ------------------- | -------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------ |
| `providerConfig`    | `SmtpConfig \| ResendConfig \| SendLayerConfig`    | Yes                                    | Provider details.                                                              |
| `from`              | `string`                                           | Yes                                    | Default From address (e.g., `"App <noreply@example.com>"`).                    |
| `baseUrl`           | `string`                                           | No                                     | Used for password reset and verify links; falls back to `NEXT_PUBLIC_APP_URL`. |
| `resetPasswordPath` | `string`                                           | No (default `"/admin/reset-password"`) | Reset-password page path appended to `baseUrl`.                                |
| `verifyEmailPath`   | `string`                                           | No (default `"/admin/verify-email"`)   | Email verification page path appended to `baseUrl`.                            |
| `templates`         | `{ welcome?, passwordReset?, emailVerification? }` | No                                     | Override the default HTML templates.                                           |

```typescript
email: {
  providerConfig: {
    provider: "resend",
    apiKey: process.env.RESEND_API_KEY!,
  },
  from: "My App <noreply@example.com>",
  baseUrl: "https://example.com",
}
```

***

### `security`

```ts
security?: SecurityConfig;
```

Security configuration. All sub-sections are optional — secure defaults are applied by middleware.

| Property            | Type                                                                                        | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ------------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `headers`           | `SecurityHeadersConfig`                                                                     | CSP, X-Content-Type-Options, X-Frame-Options, HSTS, Referrer-Policy, Permissions-Policy.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `cors`              | `CorsConfig`                                                                                | Cross-Origin Resource Sharing. Default: same-origin only.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `uploads`           | `UploadSecurityConfigInput`                                                                 | MIME-type allowlist and SVG serving behavior.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `sanitization`      | `SanitizationConfigInput`                                                                   | HTML stripping for plain-text fields, CSS validation in rich text, URL protocol validation.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `limits`            | `{ json?, multipart?, fileSize?, fileCount?, fieldCount?, fieldSize? }`                     | Body and multipart size caps. Defaults: `json` 1mb / `multipart` 50mb / `fileSize` 10mb / `fileCount` 10 / `fieldCount` 50 / `fieldSize` 100kb. Numeric values accept `"1mb"`-style suffixes.                                                                                                                                                                                                                                                                                                                                                                                                            |
| `authRateLimit`     | `{ requestsPerHour?: number; windowMs?: number }`                                           | Per-IP rate limit shared across `/auth/login`, `/auth/register`, `/auth/forgot-password`, and `/auth/reset-password`. Default: 30 req/hour. Set `requestsPerHour: 0` to disable (test/dev only).                                                                                                                                                                                                                                                                                                                                                                                                         |
| `trustProxy`        | `boolean`                                                                                   | Trust `X-Forwarded-For` (filtered through the `TRUSTED_PROXY_IPS` env-var CIDR list) for client-IP resolution. Default: `false`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `breachedPasswords` | `{ source: "pwned-passwords" \| { url: string } \| { range(prefix) }; timeoutMs?: number }` | `@experimental` Where a plugin's password check asks whether a password appeared in a breach, when the plugin asks for that check. Only a 20-bit prefix of the password's SHA-1 leaves the process. A `url` must be `https`: any other address is sent nothing, the check answers "unchecked", and boot warns. Default source: `"pwned-passwords"`; `timeoutMs` is 1 to 60000 ms, default 2000 (another value is named at boot and replaced by the default). See [Security helpers](https://nextlyhq.com/docs/plugins/security-helpers.md#breached-passwords). *Needs the release after 0.0.2-alpha.66.* |

```typescript
security: {
  cors: {
    origin: ["https://example.com", "https://app.example.com"],
    credentials: true,
  },
  headers: {
    contentSecurityPolicy: "default-src 'self'",
  },
  uploads: {
    additionalMimeTypes: ["application/xml"],
  },
  sanitization: {
    enabled: true,
    stripHtmlFromText: true,
  },
  limits: {
    multipart: "20mb",
    fileSize: "5mb",
  },
  authRateLimit: {
    requestsPerHour: 30,
  },
  trustProxy: true,
}
```

***

### `admin`

```ts
admin?: AdminConfig;
```

Admin panel branding and per-plugin sidebar overrides.

#### `admin.branding`

| Property         | Type                   | Default                                 | Description                                                                                                            |
| ---------------- | ---------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `logoUrl`        | `string`               | None                                    | Logo image URL (replaces text logo when set).                                                                          |
| `logoUrlLight`   | `string`               | None                                    | Light-mode logo URL (used when `logoUrl` is not set).                                                                  |
| `logoUrlDark`    | `string`               | None                                    | Dark-mode logo URL (used when `logoUrl` is not set).                                                                   |
| `logoText`       | `string`               | `"Nextly"`                              | Sidebar text label and `alt` text when `logoUrl` is set.                                                               |
| `favicon`        | `string`               | None                                    | Custom favicon URL.                                                                                                    |
| `colors.primary` | `string` (6-digit hex) | None                                    | Primary brand color (e.g. `"#6366f1"`). Foreground colors auto-calculated for WCAG AA contrast.                        |
| `colors.accent`  | `string` (6-digit hex) | None                                    | Accent brand color (e.g. `"#f59e0b"`).                                                                                 |
| `showBuilder`    | `boolean`              | `process.env.NODE_ENV !== "production"` | Show or hide the Visual Schema Builder navigation in the admin. Defaults to visible in dev/test, hidden in production. |

#### `admin.pluginOverrides`

Override any plugin's sidebar placement and appearance without modifying the plugin's source.

```typescript
import { defineConfig, AdminPlacement } from "nextly";

export default defineConfig({
  admin: {
    pluginOverrides: {
      "form-builder": {
        placement: AdminPlacement.SETTINGS, // "collections" | "singles" | "users" | "settings" | "plugins" | "standalone"
        order: 80,
        after: "settings",
        appearance: { icon: "FileText" },
      },
    },
  },
});
```

| Property     | Type                                                                                         | Description                                                                                                                        |
| ------------ | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `placement`  | `AdminPlacement`                                                                             | Sidebar section the plugin appears in. Values: `"collections"`, `"singles"`, `"users"`, `"settings"`, `"plugins"`, `"standalone"`. |
| `order`      | `number`                                                                                     | Sort order within the section.                                                                                                     |
| `after`      | `"dashboard" \| "collections" \| "singles" \| "media" \| "plugins" \| "users" \| "settings"` | Position anchor for `STANDALONE` plugins (which built-in section to appear after).                                                 |
| `appearance` | `Partial<PluginAdminAppearance>`                                                             | Shallow-merged onto the plugin's own appearance (icon, label, etc.).                                                               |

***

## Validation

`defineConfig()` performs these checks at startup:

* **Duplicate slugs.** Collection, single, and field group slugs must be unique across all three.
* **Cross-type conflicts.** A single cannot share a slug with a collection or field group.
* **Field group nesting.** Circular field group references and excessive nesting (max depth 3) are rejected.
* **User config.** Only the allowed scalar field types are accepted.
* **API-key bounds.** `apiKeys.rateLimit.requestsPerHour` and `apiKeys.rateLimit.windowMs` must be positive.

Validation failures throw a descriptive error at startup so misconfigurations surface immediately.

## Next steps

* [Collections](https://nextlyhq.com/docs/configuration/collections.md) — define content types with multiple entries
* [Singles](https://nextlyhq.com/docs/configuration/singles.md) — define single-document content
* [Field Groups](https://nextlyhq.com/docs/configuration/field-groups.md) — reusable field groups
* [Fields](https://nextlyhq.com/docs/configuration/fields.md) — every available field type
* [Environment variables](https://nextlyhq.com/docs/configuration/environment.md) — every env var explained
