# 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 template
```

The 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](https://nextlyhq.com/docs/templates/blog.md) 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.

```typescript title="nextly.config.ts"
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](https://nextlyhq.com/docs/configuration.md) 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:

```typescript title="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;
```

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](https://nextlyhq.com/docs/configuration/environment.md) 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 imports `RootLayout`, `QueryProvider`, and `ErrorBoundary` from `@nextlyhq/admin`.
* **`layout.tsx`** -- Reads `admin.branding` from `nextly.config.ts` and 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](https://nextlyhq.com/docs/api-reference/rest-api.md).

### `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](https://nextlyhq.com/docs/guides/media-storage.md).

### `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):

```bash
pnpm types:generate
```

If 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:

```ts title="src/lib/post-form.ts"
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](https://nextlyhq.com/docs/guides/production-migrations.md#commit-ui-schemajson)). 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](https://nextlyhq.com/docs/configuration.md) -- Full `nextly.config.ts` reference
* [Fields](https://nextlyhq.com/docs/configuration/fields.md) -- All available field types
* [Admin Panel](https://nextlyhq.com/docs/admin.md) -- Overview of the admin panel features
* [Environment Variables](https://nextlyhq.com/docs/configuration/environment.md) -- All environment variables explained
