You're reading docs for Nextly Alpha. APIs may change between releases.

Configuration

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

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/components";

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

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

  // Output paths
  typescript: {
    outputFile: "./src/types/generated/payload-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!,
      credentials: {
        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

OptionTypeDefaultDescription
collectionsCollectionConfig[][]Content types with multiple entries. See Collections.
singlesSingleConfig[][]Single-document content (site settings, header, etc.). See Singles.
componentsComponentConfig[][]Reusable field groups. See Components.
usersUserConfigundefinedExtend the built-in user model with custom fields.
typescriptTypeScriptConfigSee belowGenerated types path and module augmentation.
dbDatabaseConfigSee belowWhere Drizzle schema and migration files are written.
rateLimitRateLimitingConfigEnabled (100 read / 30 write per minute)Global API rate limiting.
apiKeysApiKeysConfig1000 req/hour, 1-hour windowPer-API-key rate limit (applies when Authorization: Bearer sk_... is used).
authAuthConfig{ revealRegistrationConflict: false }Auth-related opt-in flags.
storageStoragePlugin[][] (local disk used)Cloud storage plugins. Default storage is local disk under ./public/uploads/.
pluginsPluginDefinition[][]Plugins extending Nextly with collections, hooks, sidebar items, etc.
emailEmailConfigundefinedEmail provider for password resets and notifications.
securitySecurityConfigSecure defaultsCORS, security headers, upload restrictions, sanitization, auth-rate-limit, body-size limits.
adminAdminConfigundefinedAdmin panel branding and plugin sidebar overrides.

The next sections walk through each option.


collections

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 /api/[slug].

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

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

See Collections for the full CollectionConfig shape.


singles

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

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

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

See Singles for the full SingleConfig shape.


components

components?: ComponentConfig[];

Array of reusable field-group templates created with defineComponent(). Components are embedded inside collections, singles, or other components via the component field type. Slugs must be unique across collections, singles, and components.

import { Seo, Hero } from "./src/components";

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

See Components.


users

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.

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

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

Controls TypeScript type generation.

PropertyTypeDefaultDescription
outputFilestring"./src/types/generated/payload-types.ts"Path the generator writes types to.
declarebooleantrueWhether to add declare module blocks for runtime type inference.

db

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

Where Drizzle schema files and migration files are written.

PropertyTypeDefaultDescription
schemasDirstring"./src/db/schemas/collections"Per-collection Drizzle schema files.
migrationsDirstring"./src/db/migrations"Generated migration files.

See Environment variables for the runtime database connection settings.


rateLimit

rateLimit?: RateLimitingConfig;

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

PropertyTypeDefaultDescription
enabledbooleantrueToggle rate limiting.
readLimitnumber100Max GET requests per window.
writeLimitnumber30Max POST/PATCH/PUT/DELETE per window.
windowMsnumber60_000Window in milliseconds.
storeRateLimitStoreIn-memoryPluggable store; use a Redis store across multi-instance deployments.
keyGenerator(request: Request) => stringClient IPCustom rate-limit key.
skip(request: Request) => boolean | Promise<boolean>NoneSkip rate limiting for matching requests.
collectionsRecord<string, { readLimit?: number; writeLimit?: number }>NonePer-collection overrides.
rateLimit: {
  enabled: true,
  readLimit: 100,
  writeLimit: 30,
  collections: {
    media: { readLimit: 50, writeLimit: 10 },
    logs: { readLimit: 200 },
  },
}

apiKeys

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

Per-key rate limit applied when a request authenticates with an API key (Authorization: Bearer sk_...). Session-cookie requests use the global rateLimit block instead. Omitting this block falls back to built-in defaults.

PropertyTypeDefaultDescription
rateLimit.requestsPerHournumber1000Maximum requests per sliding window. Must be a positive integer.
rateLimit.windowMsnumber3_600_000Sliding window duration in milliseconds.

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


auth

auth?: { revealRegistrationConflict?: boolean };

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

PropertyTypeDefaultDescription
revealRegistrationConflictbooleanfalseWhen 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

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 add @nextlyhq/storage-s3
npm install @nextlyhq/storage-s3
yarn add @nextlyhq/storage-s3
bun add @nextlyhq/storage-s3
import { s3Storage } from "@nextlyhq/storage-s3";

export default defineConfig({
  storage: [
    s3Storage({
      bucket: process.env.S3_BUCKET!,
      region: process.env.AWS_REGION!,
      credentials: {
        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 for adapter-specific env vars.


plugins

plugins?: PluginDefinition[];

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

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

const formBuilderPlugin = formBuilder();

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

email

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.

PropertyTypeRequiredDescription
providerConfigSmtpConfig | ResendConfig | SendLayerConfigYesProvider details.
fromstringYesDefault From address (e.g., "App <noreply@example.com>").
baseUrlstringNoUsed for password reset and verify links; falls back to NEXT_PUBLIC_APP_URL.
resetPasswordPathstringNo (default "/admin/reset-password")Reset-password page path appended to baseUrl.
verifyEmailPathstringNo (default "/admin/verify-email")Email verification page path appended to baseUrl.
templates{ welcome?, passwordReset?, emailVerification? }NoOverride the default HTML templates.
email: {
  providerConfig: {
    provider: "resend",
    apiKey: process.env.RESEND_API_KEY!,
  },
  from: "My App <noreply@example.com>",
  baseUrl: "https://example.com",
}

security

security?: SecurityConfig;

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

PropertyTypeDescription
headersSecurityHeadersConfigCSP, X-Content-Type-Options, X-Frame-Options, HSTS, Referrer-Policy, Permissions-Policy.
corsCorsConfigCross-Origin Resource Sharing. Default: same-origin only.
uploadsUploadSecurityConfigInputMIME-type allowlist and SVG serving behavior.
sanitizationSanitizationConfigInputHTML 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).
trustProxybooleanTrust X-Forwarded-For (filtered through the TRUSTED_PROXY_IPS env-var CIDR list) for client-IP resolution. Default: false.
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

admin?: AdminConfig;

Admin panel branding and per-plugin sidebar overrides.

admin.branding

PropertyTypeDefaultDescription
logoUrlstringNoneLogo image URL (replaces text logo when set).
logoUrlLightstringNoneLight-mode logo URL (used when logoUrl is not set).
logoUrlDarkstringNoneDark-mode logo URL (used when logoUrl is not set).
logoTextstring"Nextly"Sidebar text label and alt text when logoUrl is set.
faviconstringNoneCustom favicon URL.
colors.primarystring (6-digit hex)NonePrimary brand color (e.g. "#6366f1"). Foreground colors auto-calculated for WCAG AA contrast.
colors.accentstring (6-digit hex)NoneAccent brand color (e.g. "#f59e0b").
showBuilderbooleanprocess.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.

import { AdminPlacement } from "nextly";

admin: {
  pluginOverrides: {
    "form-builder": {
      placement: AdminPlacement.SETTINGS, // "collections" | "singles" | "users" | "settings" | "plugins" | "standalone"
      order: 80,
      after: "settings",
      appearance: { icon: "FileText" },
    },
  },
}
PropertyTypeDescription
placementAdminPlacementSidebar section the plugin appears in. Values: "collections", "singles", "users", "settings", "plugins", "standalone".
ordernumberSort order within the section.
after"dashboard" | "collections" | "singles" | "media" | "plugins" | "users" | "settings"Position anchor for STANDALONE plugins (which built-in section to appear after).
appearancePartial<PluginAdminAppearance>Shallow-merged onto the plugin's own appearance (icon, label, etc.).

Validation

defineConfig() performs these checks at startup:

  • Duplicate slugs — collection, single, and component slugs must be unique across all three.
  • Cross-type conflicts — a single cannot share a slug with a collection or component.
  • Component nesting — circular component references and excessive nesting (max depth 3) are rejected.
  • User config — only the allowed scalar field types are accepted.
  • API-key boundsapiKeys.rateLimit.requestsPerHour and apiKeys.rateLimit.windowMs must be positive.

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

Next steps