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

Getting Started

Installation

Install Nextly in a new or existing Next.js project with step-by-step instructions.

There are two ways to install Nextly: scaffold a new project with the CLI, or add Nextly to an existing Next.js app manually. Both paths take a few minutes.

The create-nextly-app CLI scaffolds a complete Nextly project with your choice of database and template. Storage defaults to local disk, so you don't need to configure cloud storage to get started.

pnpm create nextly-app@alpha my-app
npx create-nextly-app@alpha my-app
yarn create nextly-app@alpha my-app
bun create nextly-app@alpha my-app

The CLI will prompt you to:

  1. Project name -- defaults to the directory name. Use . to scaffold into the current folder.
  2. Template -- Blank (empty config) or Blog (a complete blog with posts, authors, categories, and frontend pages). See Templates for what each one ships with.
  3. Schema approach (only for templates that support both) -- code-first (default for Blog) or visual (Visual Schema Builder).
  4. Database -- SQLite (default), PostgreSQL, or MySQL.
  5. Database connection string -- skipped for SQLite (defaults to file:./data/nextly.db); required for PostgreSQL/MySQL.

Once the CLI completes, change into the project and start the dev server:

cd my-app
pnpm dev
cd my-app
npm run dev
cd my-app
yarn dev
cd my-app
bun run dev

Create the super admin

The first time you run the dev server, the database is empty -- there's no admin account yet. Open the setup page to create one:

http://localhost:3000/admin/setup

Enter an email, password, and name. The first user is bootstrapped with a Super Admin role that has every permission. After that, you'll be redirected to the login screen at /admin.

Default storage is local disk

Out of the box, Nextly uses your project's ./public/uploads/ directory as the media store -- no env vars, no cloud account, no credentials. Files you upload through the admin's media library land there and are served by Next.js as static assets. When you're ready to move to S3, Vercel Blob, or Uploadthing, see Media & Storage.

Option B: Add to an Existing Next.js Project

1. Install Packages

Install the runtime, the admin panel and what it expects from your project, and esbuild, which the CLI and the dev server use to load nextly.config.ts:

pnpm add nextly @nextlyhq/admin @nextlyhq/ui @tanstack/react-query lucide-react
pnpm add -D esbuild
npm install nextly @nextlyhq/admin @nextlyhq/ui @tanstack/react-query lucide-react
npm install -D esbuild
yarn add nextly @nextlyhq/admin @nextlyhq/ui @tanstack/react-query lucide-react
yarn add -D esbuild
bun add nextly @nextlyhq/admin @nextlyhq/ui @tanstack/react-query lucide-react
bun add -d esbuild

Then install your database driver:

pnpm add @nextlyhq/adapter-postgres pg
pnpm add @nextlyhq/adapter-mysql mysql2
pnpm add @nextlyhq/adapter-sqlite better-sqlite3

On pnpm 10 or later, allow the native packages to run their build scripts, or better-sqlite3 has no compiled binding and the app fails at boot, and sharp, esbuild and unrs-resolver fall back to slower paths. On pnpm 10.6 or later, add to pnpm-workspace.yaml (create it with packages: [] if you have none):

pnpm-workspace.yaml
allowBuilds:
  better-sqlite3: true
  esbuild: true
  sharp: true
  unrs-resolver: true
onlyBuiltDependencies:
  - better-sqlite3
  - esbuild
  - sharp
  - unrs-resolver

pnpm 11 reads allowBuilds, pnpm 10.6+ reads onlyBuiltDependencies. On pnpm 10.0 to 10.5, put the same list in package.json instead, as "pnpm": { "onlyBuiltDependencies": ["better-sqlite3", "esbuild", "sharp", "unrs-resolver"] }, or upgrade pnpm. npm, yarn and bun need none of this.

Cloud storage is optional -- Nextly's default is local disk under ./public/uploads/. Install a storage adapter only if you need one:

pnpm add @nextlyhq/storage-s3
pnpm add @nextlyhq/storage-vercel-blob
pnpm add @nextlyhq/storage-uploadthing

2. Create nextly.config.ts

Create a nextly.config.ts file in your project root:

nextly.config.ts
import { defineConfig } from "nextly/config";

export default defineConfig({
  collections: [],
  singles: [],

  typescript: {
    outputFile: "./src/types/generated/nextly-types.ts",
  },
});

3. Update next.config.ts

Add serverExternalPackages so Next.js doesn't try to bundle server-only dependencies:

next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  serverExternalPackages: [
    "nextly",
    "@nextlyhq/adapter-drizzle",
    "@nextlyhq/adapter-postgres",
    "@nextlyhq/adapter-mysql",
    "@nextlyhq/adapter-sqlite",
    "drizzle-orm",
    "drizzle-kit",
    "pg",
    "mysql2",
    "better-sqlite3",
    "bcryptjs",
    "sharp",
    "esbuild",
  ],
};

export default nextConfig;

Drop adapters and drivers your project doesn't actually use.

4. Set Up Admin Routes

Create the admin panel page and layout:

src/app/admin/[[...params]]/page.tsx
"use client";

import "@nextlyhq/admin/style.css";
import { RootLayout, QueryProvider, ErrorBoundary } from "@nextlyhq/admin";

export default function AdminPage() {
  return (
    <ErrorBoundary
      onError={(error, errorInfo) => {
        console.error("Admin error:", error, errorInfo);
      }}
    >
      <QueryProvider>
        <RootLayout />
      </QueryProvider>
    </ErrorBoundary>
  );
}
src/app/admin/[[...params]]/layout.tsx
import { getBrandingCss } from "nextly/config";

import config from "../../../../nextly.config";

const brandingCss = getBrandingCss(config.admin?.branding);

export default function AdminLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <>
      {brandingCss && (
        <style dangerouslySetInnerHTML={{ __html: brandingCss }} />
      )}
      {children}
    </>
  );
}

Create the admin API catch-all route:

src/app/admin/api/[[...params]]/route.ts
import { createDynamicHandlers } from "nextly/runtime";

import nextlyConfig from "../../../../../nextly.config";

const handlers = createDynamicHandlers({ config: nextlyConfig });

export const GET = handlers.GET;
export const POST = handlers.POST;
export const PUT = handlers.PUT;
export const PATCH = handlers.PATCH;
export const DELETE = handlers.DELETE;
export const OPTIONS = handlers.OPTIONS;

The admin's media library talks to its own route, which the catch-all above does not serve. Without it, uploads fail:

src/app/admin/api/media/[[...path]]/route.ts
import { createMediaHandlers } from "nextly/api/media-handlers";

import nextlyConfig from "../../../../../../nextly.config";

// Checked against the caller's media permissions, except the raw bytes of a
// publicly servable file (a web font), which a browser fetches without a session.
const handlers = createMediaHandlers({ config: nextlyConfig, requireAuth: true });

export const GET = handlers.GET;
export const POST = handlers.POST;
export const PATCH = handlers.PATCH;
export const DELETE = handlers.DELETE;

Optionally, add a health check for your load balancer or uptime monitor:

src/app/api/health/route.ts
export { GET, HEAD } from "nextly/api/health";

Then start Nextly when the server starts. Next.js runs register() once per server instance and finishes it before the instance serves a request, so the health route above, and any code that reads through Nextly, finds it already running. Without it, Nextly starts on the first request that carries its config, and the health route answers 503 until then. Put the file beside app/: src/instrumentation.ts, or instrumentation.ts in the project root when there is no src/ (then import ./nextly.config). If the app already has one, add these lines to its register():

src/instrumentation.ts
export async function register(): Promise<void> {
  // Nextly needs Node.js; the Edge runtime cannot load it.
  if (process.env.NEXT_RUNTIME !== "nodejs") return;

  const { registerNextCacheRevalidator } = await import("nextly/runtime");
  registerNextCacheRevalidator();

  const { createRegister } = await import("nextly");
  const { default: config } = await import("../nextly.config");
  await createRegister(config)();
}

Next.js skips register() during next build, so this opens no database connection at build time. On Next.js 14 the hook runs only when next.config sets instrumentationHook: true in its experimental object (add it beside any keys already there), and if the config sets pageExtensions, name the file with one of them (instrumentation.page.ts for page.ts).

5. Set Up Environment Variables

Create a .env file with your database and auth configuration:

.env
# Database
DB_DIALECT=postgresql
DATABASE_URL=postgresql://user:password@localhost:5432/nextly

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

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

That's the minimum. For storage, email, and other optional settings see Environment Variables.

6. Run Migrations

Initialize the database schema. Nextly seeds default RBAC roles and permissions on first run:

pnpm nextly migrate
npx nextly migrate
yarn nextly migrate
bunx nextly migrate

In development, Nextly auto-syncs schema changes when you save nextly.config.ts, so you usually only need nextly migrate for initial setup and production deploys. See Production migrations for the production flow.

7. Verify

Start the development server:

pnpm dev
npm run dev
yarn dev
bun run dev

Open http://localhost:3000/admin/setup to create the first super admin account, then log in at http://localhost:3000/admin.

Requirements

RequirementVersion
Node.js^20.19.0 || ^22.12.0 || >=24.0.0 (not Node 23)
Next.js16+ (App Router required)
React19+
TypeScript5+
DatabasePostgreSQL (recommended for production), MySQL, or SQLite (local demo only)

Next Steps