Project Structure
Understand how a Nextly project is organized, where key files live, and what each directory does.
A Nextly project follows the standard Next.js App Router structure with a few additions: a nextly.config.ts at the root, admin routes under src/app/admin/, and API routes for the backend. This page explains where everything lives and what each part does.
Directory Overview
Here is the structure of a project scaffolded with create-nextly-app (Blank template, with src/ directory):
my-nextly-app/
├── src/
│ ├── app/
│ │ ├── (frontend)/
│ │ │ ├── components/ # Components for the public site
│ │ │ └── page.tsx # Home page
│ │ ├── admin/
│ │ │ ├── [[...params]]/
│ │ │ │ ├── page.tsx # Admin panel UI (client component)
│ │ │ │ └── layout.tsx # Admin layout — injects branding CSS
│ │ │ └── api/
│ │ │ ├── [[...params]]/
│ │ │ │ └── route.ts # Admin API catch-all handler
│ │ │ └── media/
│ │ │ └── [[...path]]/
│ │ │ └── route.ts # The admin media library's API (permission-checked)
│ │ ├── api/
│ │ │ ├── health/
│ │ │ │ └── route.ts # Health check endpoint
│ │ │ └── media/
│ │ │ └── [[...path]]/
│ │ │ └── route.ts # Public media reads (no writes)
│ │ ├── layout.tsx # Root layout
│ │ ├── globals.css # Global styles
│ │ └── favicon.ico
│ ├── lib/
│ │ └── site-url.ts # The public site's origin, for links and metadata
│ └── types/
│ └── generated/
│ └── nextly-types.ts # Generated TypeScript types
├── data/ # The SQLite database, when you chose SQLite
├── public/ # Static files; uploads land in public/uploads/
├── nextly.config.ts # Nextly configuration
├── next.config.ts # Next.js configuration
├── AGENTS.md # Instructions for coding agents (CLAUDE.md points to it)
├── CLAUDE.md
├── eslint.config.mjs
├── postcss.config.mjs
├── pnpm-workspace.yaml # Lets pnpm build native dependencies
├── tsconfig.json
├── package.json
├── .env # Environment variables (gitignored)
└── .env.example # Environment templateThe Blog template adds a few more folders -- src/collections/, src/singles/, src/components/, src/access/, src/actions/, src/lib/, plus a (frontend) route group for the public-facing blog pages. See Templates → Blog for the blog-specific layout.
Key Files
nextly.config.ts
The central configuration file. This is where you define code-first collections, singles, plugins, storage adapters, email configuration, and admin branding. It lives at the project root.
import { defineConfig } from "nextly/config";
export default defineConfig({
collections: [/* your collections */],
singles: [/* your singles */],
plugins: [/* your plugins */],
storage: [/* storage adapters */],
email: { /* email provider config */ },
admin: {
branding: {
logoText: "My App",
colors: { primary: "#387c26" },
},
},
typescript: {
outputFile: "./src/types/generated/nextly-types.ts",
},
});See Configuration for the full reference.
next.config.ts
Standard Next.js configuration. The main Nextly-specific addition is serverExternalPackages, which prevents Next.js from bundling server-only dependencies like database drivers:
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;A scaffolded project lists only the adapter and driver for the database you chose. When adding Nextly by hand, list the ones you use.
.env
Environment variables for database connection and authentication. The CLI generates a complete .env with secure defaults; the essentials are:
| Variable | Required | Purpose |
|---|---|---|
DB_DIALECT | Yes | Database type (postgresql, mysql, sqlite) |
DATABASE_URL | Yes for PostgreSQL and MySQL; for SQLite, for the CLI's database commands | Database connection string. For SQLite, a file URL such as file:./data/nextly.db; the CLI's database commands need it on every dialect |
NEXTLY_SECRET | Yes (production) | Secret for JWT signing and session encryption. Min 32 chars; the CLI auto-generates a base64 value |
NEXT_PUBLIC_APP_URL | Yes (production) | Public URL of your app, used by metadata and the admin |
Storage env vars are only needed when you opt in to a cloud adapter -- the default is local disk under public/uploads/. See Environment Variables for the full list.
Key Directories
src/app/admin/
The admin panel lives here as a standard Next.js route. The catch-all [[...params]] pattern lets Nextly handle all admin panel routing internally.
page.tsx-- Renders the admin panel UI. This is a client component that importsRootLayout,QueryProvider, andErrorBoundaryfrom@nextlyhq/admin.layout.tsx-- Readsadmin.brandingfromnextly.config.tsand injects the generated CSS so logos and colors apply on every admin page.api/[[...params]]/route.ts-- The admin API. Handles all CRUD operations for collections, singles, users, roles and permissions.api/media/[[...path]]/route.ts-- The media library's API. Every operation is checked against the caller's media permissions, except fetching the raw bytes of a publicly servable file such as a web font, which a browser does without a session.
src/app/api/
Public routes: a health check and media reads. A new project has no /api catch-all, so Nextly's endpoints are served under /admin/api by the admin route above. To serve the REST API under /api as well, add the catch-all described in REST API.
public/uploads/
Default media store when no storage adapter is configured in nextly.config.ts. Files uploaded through the admin's media library land here and are served directly by Next.js. To switch to S3, Vercel Blob, or Uploadthing, see Media & Storage.
src/types/generated/
Generated TypeScript types for your content schema. The output path is configured in nextly.config.ts under typescript.outputFile. Saving nextly.config.ts in development syncs the database but does not rewrite this file: regenerate it after each code-first schema change, in the same change. generate:types reads only nextly.config.ts, so it does not cover collections built in the Visual Schema Builder until they are moved into code (below):
pnpm types:generateIf your app keeps its code at the root rather than under src/, the generated files follow it: types go to types/generated/ and schemas to db/schemas/collections/zod/, with no src/ folder created. That holds while typescript.outputFile and db.schemasDir are left at their defaults; a path you set is used as written.
Generated Zod schemas
generate:types also writes a Zod schema for each collection, in zod/ under db.schemasDir (src/db/schemas/collections/zod/ by default). They import Zod from nextly/zod, a re-export of the Zod Nextly itself uses, so they compile whether or not your app depends on zod, with npm, pnpm, Yarn (Plug'n'Play too) or Bun.
To combine them with your own Zod code, import z from nextly/zod as well, so both use one Zod instance:
import { z } from "nextly/zod";
import { PostsCreateInputSchema } from "../db/schemas/collections/zod";
export const PostFormSchema = PostsCreateInputSchema.extend({
acceptTerms: z.literal(true),
});Schema files (Visual Schema Builder)
Collections you create through the Visual Schema Builder are stored in the database, and no TypeScript file is generated for them. In development the admin also writes every change to ui-schema.json (the file db.uiSchemaFile names), which nextly migrate:create reads: commit it with the migration (see Production migrations). To move a collection into code, run nextly db:sync --promote <slug>. Code-first collections live in nextly.config.ts (or wherever you import them from); the two approaches coexist freely.
Code-First vs Visual Schema Builder: Where Content Is Defined
| Approach | Where collections are defined | Schema location |
|---|---|---|
| Code-First | nextly.config.ts (or files you import from it) | Auto-synced on save in development; in production, a migration you create with nextly migrate:create and apply with nextly migrate |
| Visual Schema Builder | Admin panel at /admin/builder/collections | Stored in the database, and written to ui-schema.json in development for migrate:create |
Both feed the same core runtime for the supported field set: database tables, admin UI, REST endpoints, and Direct API access. Two differences remain. A collection made in the Visual Schema Builder cannot carry access functions, field rules or hooks written as functions, because a function cannot be stored in the database (it can carry Nextly's pre-built hooks, such as auto-slug, which run on every write); and nextly generate:types writes types only for what your config defines. You can mix them -- define some collections in config and create others through the Visual Schema Builder.
Next Steps
- Configuration -- Full
nextly.config.tsreference - Fields -- All available field types
- Admin Panel -- Overview of the admin panel features
- Environment Variables -- All environment variables explained