create-nextly-app 0.0.2-alpha.7 → 0.0.2-alpha.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +36 -9
  2. package/dist/cli.cjs +3047 -32460
  3. package/dist/cli.cjs.map +1 -1
  4. package/dist/cli.d.cts +1 -1
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.mjs +3048 -3602
  7. package/dist/cli.mjs.map +1 -1
  8. package/dist/create-nextly-5wiGMVI6.mjs +52550 -0
  9. package/dist/create-nextly-5wiGMVI6.mjs.map +1 -0
  10. package/dist/create-nextly-BbKGDrEC.cjs +52628 -0
  11. package/dist/create-nextly-BbKGDrEC.cjs.map +1 -0
  12. package/dist/index.cjs +3 -24861
  13. package/dist/index.d.cts +40 -34
  14. package/dist/index.d.ts +40 -34
  15. package/dist/index.mjs +1 -6
  16. package/package.json +28 -19
  17. package/skills/nextly-authentication/SKILL.md +187 -0
  18. package/skills/nextly-collections/SKILL.md +215 -0
  19. package/skills/nextly-deploy/SKILL.md +248 -0
  20. package/skills/nextly-install/SKILL.md +121 -0
  21. package/templates/base/.env.example +21 -6
  22. package/templates/base/AGENTS.md.template +226 -0
  23. package/templates/base/CLAUDE.md.template +1 -0
  24. package/templates/base/gitignore +42 -0
  25. package/templates/base/next.config.ts +11 -17
  26. package/templates/base/src/app/admin/[[...params]]/page.tsx +3 -0
  27. package/templates/base/src/app/admin/api/media/[[...path]]/route.ts +26 -0
  28. package/templates/base/src/app/globals.css +15 -2
  29. package/templates/base/src/app/layout.tsx +32 -18
  30. package/templates/base/src/lib/site-url.ts +55 -0
  31. package/templates/base/src/types/generated/nextly-types.ts +6 -4
  32. package/templates/base/src/types/generated/plugin-admin-components.generated.ts +6 -0
  33. package/templates/base/src/types/generated/plugin-admin-imports.generated.ts +32 -0
  34. package/templates/base/tsconfig.json +8 -1
  35. package/templates/blank/src/{components → app/(frontend)/components}/ThemeToggle.tsx +55 -24
  36. package/templates/blank/src/app/(frontend)/page.tsx +2 -2
  37. package/templates/blank/src/app/globals.css +37 -1
  38. package/templates/blank/src/app/layout.tsx +28 -18
  39. package/templates/blank/src/instrumentation.ts +32 -0
  40. package/templates/plugin/AGENTS.md.template +125 -0
  41. package/templates/plugin/CLAUDE.md.template +1 -0
  42. package/templates/plugin/README.md +48 -0
  43. package/templates/plugin/dev/.env.example +11 -0
  44. package/templates/plugin/dev/next.config.ts +43 -0
  45. package/templates/plugin/dev/nextly.config.ts +17 -0
  46. package/templates/plugin/dev/src/app/admin/[[...params]]/page.tsx +24 -0
  47. package/templates/plugin/dev/src/app/admin/api/[[...params]]/route.ts +19 -0
  48. package/templates/plugin/dev/src/app/layout.tsx +13 -0
  49. package/templates/plugin/dev/src/instrumentation.ts +25 -0
  50. package/templates/plugin/dev/src/seed-dev-user.ts +51 -0
  51. package/templates/plugin/dev/src/types/generated/plugin-admin-components.generated.ts +8 -0
  52. package/templates/plugin/dev/src/types/generated/plugin-admin-imports.generated.ts +37 -0
  53. package/templates/plugin/dev/tsconfig.json +22 -0
  54. package/templates/plugin/eslint.config.mjs +19 -0
  55. package/templates/plugin/gitignore +13 -0
  56. package/templates/plugin/src/admin/SettingsPage.tsx +57 -0
  57. package/templates/plugin/src/admin/index.ts +24 -0
  58. package/templates/plugin/src/collections/example.ts +13 -0
  59. package/templates/plugin/src/index.ts +6 -0
  60. package/templates/plugin/src/plugin.test.ts +35 -0
  61. package/templates/plugin/src/plugin.ts +48 -0
  62. package/templates/plugin/src/types.ts +17 -0
  63. package/templates/plugin/template.json +14 -0
  64. package/templates/plugin/tsconfig.json +18 -0
  65. package/templates/plugin/tsdown.config.ts +30 -0
  66. package/templates/plugin/vitest.config.ts +27 -0
  67. package/dist/chunk-RPGFJSDP.mjs +0 -28835
  68. package/dist/chunk-RPGFJSDP.mjs.map +0 -1
  69. package/dist/index.cjs.map +0 -1
  70. package/dist/index.mjs.map +0 -1
  71. package/templates/blank/src/access/README.md +0 -22
  72. package/templates/blank/src/collections/README.md +0 -22
  73. package/templates/blank/src/lib/README.md +0 -15
  74. package/templates/blank/src/singles/README.md +0 -19
  75. /package/templates/blank/src/{components → app/(frontend)/components}/README.md +0 -0
@@ -0,0 +1,248 @@
1
+ ---
2
+ name: nextly-deploy
3
+ description: >-
4
+ Use when deploying a Nextly project to production, on Vercel or a Node.js
5
+ server: choosing the production database, creating and committing
6
+ migrations, running them on deploy, the environment variables Nextly
7
+ requires, media storage that survives restarts, and checking the live site.
8
+ Also use when a deployed Nextly app has missing tables, refuses to start,
9
+ or loses uploaded files.
10
+ metadata:
11
+ nextly-version: 0.0.2-alpha.71
12
+ ---
13
+
14
+ # Deploy a Nextly project
15
+
16
+ A Nextly app is a Next.js app, so it deploys wherever Next.js does. What
17
+ Nextly adds: a production database, migrations applied before the new code
18
+ serves traffic, a few environment variables, and media storage that is not
19
+ the local disk on serverless hosts.
20
+
21
+ Checked against `nextly` 0.0.2-alpha.67 with Next.js 16.4 and PostgreSQL 17:
22
+ the steps below were run end to end, from an empty database to a published
23
+ post in a production build. Run `npx nextly --version`; if it prints a newer
24
+ version, prefer <https://nextlyhq.com/docs/guides/deployment> where it
25
+ disagrees with this skill.
26
+
27
+ ## 1. Pick the production database, and develop on the same kind
28
+
29
+ Use PostgreSQL (recommended) or MySQL in production; on Vercel, a managed one
30
+ such as Neon, Supabase or Vercel Postgres. SQLite does not survive on
31
+ serverless hosts.
32
+
33
+ Develop against the same kind of database, and switch **before** creating the
34
+ first migration. `migrate:create` writes SQL in the dialect of the database
35
+ configured when you run it, so migrations made on SQLite will not run on
36
+ PostgreSQL. If SQLite migrations already exist and nothing has been deployed,
37
+ delete `src/db/migrations/` (the `.sql` files and `meta/`) after switching and
38
+ run `migrate:create` again. A live SQLite installation needs its data moved,
39
+ which this skill does not cover.
40
+
41
+ To switch a project from SQLite to PostgreSQL:
42
+
43
+ Nextly's packages are released together, so install each `@nextlyhq/*`
44
+ package at exactly the version of `nextly` the project runs, which `npx nextly
45
+ --version` prints:
46
+
47
+ ```bash
48
+ npm install "@nextlyhq/adapter-postgres@$(npx nextly --version)" pg # pnpm add / yarn add / bun add
49
+ npm ls nextly @nextlyhq/adapter-postgres # or pnpm ls / yarn why; both at the same version
50
+ ```
51
+
52
+ Use the project's own package manager throughout, so its lockfile stays the
53
+ one that is deployed.
54
+
55
+ Then, in `.env`, set `DB_DIALECT=postgresql` and `DATABASE_URL` to a local
56
+ PostgreSQL database, and add `"@nextlyhq/adapter-postgres"` and `"pg"` to
57
+ `serverExternalPackages` in `next.config.ts`. Restart `next dev`.
58
+
59
+ ## 2. Create the migrations and commit them
60
+
61
+ In development, schema changes apply directly. Production only applies
62
+ migration files. Whenever collections change, before deploying:
63
+
64
+ ```bash
65
+ npx nextly migrate:create <what_changed>
66
+ ```
67
+
68
+ It writes `src/db/migrations/<timestamp>_<name>.sql` and a snapshot in
69
+ `src/db/migrations/meta/`. Commit both. Migrations only move forward: to undo
70
+ one, add a new one.
71
+
72
+ ## 3. Run migrations on every deploy
73
+
74
+ In `package.json`:
75
+
76
+ ```json
77
+ {
78
+ "scripts": {
79
+ "build": "nextly migrate && next build"
80
+ }
81
+ }
82
+ ```
83
+
84
+ `nextly migrate` first creates or updates Nextly's own tables (users, roles,
85
+ media and the rest), then applies your pending migrations, against the
86
+ database in `DATABASE_URL`. On an empty database this is all that is needed
87
+ before the first start.
88
+
89
+ With several developers or preview deploys pointing at one database, run
90
+ `npx nextly migrate` as its own CI step before the deploy instead, and keep
91
+ `build` as `next build`.
92
+
93
+ ## 4. Environment variables
94
+
95
+ | Variable | Value |
96
+ | ------------------------ | ----------------------------------------------------------------------------------------------------------- |
97
+ | `DB_DIALECT` | `postgresql` or `mysql` |
98
+ | `DATABASE_URL` | the production database |
99
+ | `NEXTLY_SECRET` | at least 32 characters, new for production: `openssl rand -base64 32` |
100
+ | `NEXT_PUBLIC_APP_URL` | the public URL, such as `https://example.com` |
101
+ | `NEXTLY_ALLOWED_ORIGINS` | only if browsers reach the app on an origin it does not see itself, such as behind a proxy; comma-separated |
102
+
103
+ `NEXT_PUBLIC_APP_URL` is built into the client bundle, so set it before the
104
+ build. Never commit `.env` files.
105
+
106
+ ## 5. Media storage
107
+
108
+ Uploads go to `./public/uploads` by default. That is lost on every deploy or
109
+ cold start on Vercel and other serverless hosts. Use a storage adapter,
110
+ installed at the same version as `nextly` as above:
111
+
112
+ ```ts
113
+ // nextly.config.ts, on Vercel: npm install @nextlyhq/storage-vercel-blob@<nextly version>
114
+ import { defineConfig } from "nextly/config";
115
+ import { vercelBlobStorage } from "@nextlyhq/storage-vercel-blob";
116
+
117
+ export default defineConfig({
118
+ storage: [
119
+ vercelBlobStorage({
120
+ token: process.env.BLOB_READ_WRITE_TOKEN!,
121
+ collections: { media: true },
122
+ }),
123
+ ],
124
+ // collections, typescript, ...
125
+ });
126
+ ```
127
+
128
+ Vercel sets `BLOB_READ_WRITE_TOKEN` when Blob storage is added to the project.
129
+ For AWS S3, use `@nextlyhq/storage-s3` with
130
+ `s3Storage({ bucket, region, accessKeyId, secretAccessKey, collections: { media: true } })`.
131
+ Other S3-compatible providers also need their `endpoint`, and a `publicUrl`
132
+ when files are served from another address. For Cloudflare R2:
133
+
134
+ ```ts
135
+ s3Storage({
136
+ bucket: process.env.R2_BUCKET!,
137
+ region: "auto",
138
+ accessKeyId: process.env.R2_ACCESS_KEY_ID!,
139
+ secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
140
+ endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
141
+ publicUrl: process.env.R2_PUBLIC_URL,
142
+ collections: { media: true },
143
+ });
144
+ ```
145
+
146
+ For other providers, use the settings in the adapter's README. A Node.js
147
+ server with a persistent disk can keep the default.
148
+
149
+ On Vercel, uploads from the admin pass through a Vercel Function, which
150
+ refuses request bodies over 4.5 MB (`413 FUNCTION_PAYLOAD_TOO_LARGE`). With
151
+ this setup, files above that size cannot be uploaded through the admin; a
152
+ Node.js server has no such limit.
153
+
154
+ ## 6. Plan how the first admin is created
155
+
156
+ A new production database has no admin. In production, `/admin/setup` does
157
+ not hand the site to whoever opens it first: it asks for a one-time setup
158
+ token. Pick one of these; both refuse once an admin exists.
159
+
160
+ - **The setup token, after deploying.** Either set your own token before the
161
+ deploy, as `NEXTLY_SETUP_TOKEN` (at least 43 characters with at least 16
162
+ distinct ones; `npx nextly setup-token --generate` prints one), or let
163
+ Nextly generate one: the first production start prints a line beginning
164
+ `[nextly] SETUP TOKEN` to the server's output (on Vercel, the runtime logs).
165
+ Then open `/admin/setup` on the live site and enter it. A generated token
166
+ lasts 24 hours and one setup; `npx nextly setup-token`, run with the
167
+ production environment, replaces a lost one.
168
+ - **The CLI, before or after deploying.** With the production environment
169
+ (`DATABASE_URL`, and every other variable the config reads):
170
+
171
+ ```bash
172
+ NEXTLY_ADMIN_PASSWORD='<your password>' \
173
+ npx nextly users:create-admin --email admin@example.com --name "Site Admin"
174
+ ```
175
+
176
+ Without `NEXTLY_ADMIN_PASSWORD` it prompts for the password at a terminal. The password must be 8 to 128 characters with a lowercase letter, an uppercase letter, a number and a symbol.
177
+ It needs no token, and it creates nothing if an account already uses that
178
+ email.
179
+
180
+ Both need the release after 0.0.2-alpha.68. On an earlier version there is no
181
+ setup token: whoever opens `/admin/setup` first becomes Super Admin, so keep
182
+ the deployment private, behind your host's access protection, until you have
183
+ created the admin there.
184
+
185
+ Never run `next dev` against the production database. Development mode
186
+ applies schema changes directly to whatever database it points at.
187
+
188
+ ## 7. Deploy
189
+
190
+ - **Vercel:** import the repository; Next.js is detected. Add the variables
191
+ above for Production (and Preview, with its own database), then deploy.
192
+ The `build` script migrates the database during the build.
193
+ - **Node.js server:** install from the lockfile (`npm ci`, `pnpm install
194
+ --frozen-lockfile`, `yarn install --immutable` on Yarn 2+, `yarn install --frozen-lockfile` on
195
+ Yarn 1, or `bun install --frozen-lockfile`), run the `build` script, then `next start` under a
196
+ process manager such as systemd, pm2 or Docker. Put a reverse proxy in
197
+ front that terminates TLS and forwards `Host`, `X-Forwarded-Proto` and
198
+ `X-Forwarded-For`.
199
+
200
+ ### Behind a proxy, including Vercel
201
+
202
+ Nextly ignores `X-Forwarded-For` unless told to trust it, and then it cannot
203
+ tell clients apart: every sign-in, registration and password-reset request
204
+ shares one rate-limit bucket, 30 an hour by default, and everyone gets 429
205
+ after that. When every request reaches the app through a proxy that sets
206
+ `X-Forwarded-For` (Vercel does; configure nginx or Caddy to), trust it:
207
+
208
+ ```ts
209
+ // nextly.config.ts
210
+ import { defineConfig } from "nextly/config";
211
+
212
+ export default defineConfig({
213
+ security: { trustProxy: true },
214
+ // collections, typescript, ...
215
+ });
216
+ ```
217
+
218
+ When requests pass through more than one proxy, list the proxies' addresses
219
+ in `TRUSTED_PROXY_IPS` (comma-separated CIDRs), so the client is read from the
220
+ right hop. Do not turn this on for an app clients can reach directly: they
221
+ could then pick their own address.
222
+
223
+ ## 8. Check the live site
224
+
225
+ - `npx nextly migrate:status`, run against the production `DATABASE_URL`,
226
+ shows nothing pending.
227
+ - `/admin/setup` says setup is already complete, and `/admin` signs in with
228
+ the admin created in step 6.
229
+ - Publish an entry and open a page that lists it: it appears.
230
+ - Upload an image in the admin, redeploy, and check it still loads.
231
+ - Do not use `/api/health` as an uptime probe on serverless hosts. It reports
232
+ 503 until Nextly has started in that same process, which a fresh instance
233
+ that only receives probes never does. Probe a page that reads content
234
+ instead.
235
+
236
+ ## If something fails
237
+
238
+ - **Tables missing, or `relation "dc_..." does not exist`:** the migrations
239
+ were not committed, or `nextly migrate` did not run against this database.
240
+ - **The admin and its API answer 500 in production:** check the server log.
241
+ `NEXTLY_SECRET of at least 32 characters is required` means the secret is
242
+ missing or too short, and `NEXT_PUBLIC_APP_URL is required` that the URL is
243
+ not set.
244
+ - **Sign-in fails with `CSRF_FAILED` behind a proxy:** add the public origin
245
+ to `NEXTLY_ALLOWED_ORIGINS`.
246
+ - **`next build` fails on a page reading content:** in an app with
247
+ `cacheComponents: true`, wrap the read in `cachedFind` (the
248
+ `nextly-collections` skill) or render it inside `<Suspense>`.
@@ -0,0 +1,121 @@
1
+ ---
2
+ name: nextly-install
3
+ description: >-
4
+ Use when adding Nextly, the open-source CMS that runs inside a Next.js app,
5
+ to an existing Next.js App Router project: installing its packages,
6
+ creating nextly.config.ts, mounting the admin panel at /admin, choosing a
7
+ database, and creating the first admin account. Also use when an existing
8
+ Nextly install shows no collections, fails to load nextly.config.ts, or its
9
+ admin panel misbehaves after setup.
10
+ metadata:
11
+ nextly-version: 0.0.2-alpha.71
12
+ ---
13
+
14
+ # Install Nextly into an existing Next.js app
15
+
16
+ Nextly runs inside the app: same server, same database, no separate service.
17
+ The project generator detects an existing Next.js app and installs Nextly into
18
+ it, including the build approvals, `esbuild` and the routes the admin needs.
19
+
20
+ Checked against `nextly` 0.0.2-alpha.67 with Next.js 16.4. Run
21
+ `npx nextly --version` after installing; if it prints a newer version, prefer
22
+ <https://nextlyhq.com/docs> where it disagrees with this skill.
23
+
24
+ ## Before you start
25
+
26
+ - Next.js 16.4.1 or later, or 15.5.28 or later on the 15 line, with the App
27
+ Router (`app/` or `src/app/`). Earlier versions carry the October 2026
28
+ security advisories these releases fix.
29
+ - Node.js `^22.12.0 || >=24.0.0`. Node 20 and Node 23 are not supported.
30
+ - A clean git working tree, so every change below is visible in one diff.
31
+
32
+ ## 1. Run the generator in the project root
33
+
34
+ ```bash
35
+ npx create-nextly-app@alpha # npm
36
+ pnpm dlx create-nextly-app@alpha # pnpm
37
+ yarn dlx create-nextly-app@alpha # yarn 2 and later
38
+ yarn create nextly-app # yarn 1 (Classic): no tag; installs `latest`, the current release
39
+ bunx create-nextly-app@alpha # bun
40
+ ```
41
+
42
+ It finds `next` in `package.json`, asks "Install Nextly in this project?", then
43
+ asks which database and, for PostgreSQL or MySQL, its connection string. `-y`
44
+ skips the questions and uses SQLite at `./data/nextly.db`. `--database
45
+ postgresql` or `--database mysql` picks another; with `-y` as well, `.env` gets
46
+ a placeholder `DATABASE_URL` to replace before starting.
47
+
48
+ It installs with the app's own package manager (inside a monorepo, the
49
+ workspace's), adding the packages to the app's own `package.json`. On pnpm 10
50
+ and later it approves the native build scripts Nextly needs (`better-sqlite3`,
51
+ `esbuild`, `sharp`) in the setting that pnpm version reads: `pnpm-workspace.yaml`
52
+ from pnpm 10.6, `package.json` before it. In a monorepo the approvals go at the
53
+ workspace root.
54
+
55
+ Develop on the database you will run in production. Migrations are written in
56
+ the dialect of the database you develop against (the `nextly-deploy` skill),
57
+ and SQLite is not suitable for serverless hosting.
58
+
59
+ What it changes. It edits `package.json`, the Next config and pnpm's settings
60
+ in place, and adds to `.env` and `.env.example` only the keys they lack. A file
61
+ it would create that the app already has (`nextly.config.ts`,
62
+ `instrumentation.ts`, the admin's files and the routes) is kept as it is and
63
+ reported as skipped, with what Nextly needs in it. Read its report, then review
64
+ the rest in `git diff`:
65
+
66
+ | File | What |
67
+ | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
68
+ | `package.json` | `nextly`, `@nextlyhq/admin`, `@nextlyhq/ui`, the database adapter and driver, `@tanstack/react-query`, `lucide-react`; `esbuild` as a dev dependency |
69
+ | `nextly.config.ts` | the config the CMS boots from, with empty `collections` and `singles` |
70
+ | `next.config.ts` | Nextly, the adapter and the driver added to `serverExternalPackages` |
71
+ | `src/instrumentation.ts` | starts Nextly when the server starts |
72
+ | `src/app/admin/[[...params]]/` | the admin panel page and layout |
73
+ | `src/app/admin/api/[[...params]]/route.ts` | the admin API |
74
+ | `src/app/admin/api/media/[[...path]]/route.ts` | the admin's media route, where it uploads and edits media (signed-in only) |
75
+ | `src/app/api/media/[[...path]]/route.ts` | public, read-only media routes |
76
+ | `src/app/api/health/route.ts` | a health check |
77
+ | `src/types/generated/` | where generated types go |
78
+ | `.env`, `.env.example` | `DB_DIALECT`, `DATABASE_URL`, `NEXTLY_SECRET`, `NEXT_PUBLIC_APP_URL`, for the keys missing |
79
+
80
+ Without a `src/` folder the same files go under `app/`, and `instrumentation.ts`
81
+ sits at the project root.
82
+
83
+ If it reports that it left the Next config alone (a config it cannot change
84
+ safely), add what it prints. The list it needs holds `nextly`,
85
+ `@nextlyhq/adapter-drizzle`, `drizzle-orm`, `drizzle-kit`, `bcryptjs`,
86
+ `sharp`, `esbuild`, and your adapter and driver: `@nextlyhq/adapter-sqlite`
87
+ and `better-sqlite3`, `@nextlyhq/adapter-postgres` and `pg`, or
88
+ `@nextlyhq/adapter-mysql` and `mysql2`.
89
+
90
+ ## 2. Start it and create the first admin
91
+
92
+ ```bash
93
+ npm run dev
94
+ ```
95
+
96
+ Open `http://localhost:3000/admin/setup` and create the first account; it gets
97
+ the Super Admin role. Nextly creates its own tables when the server starts,
98
+ from `instrumentation.ts`; without that hook, on the first request. Then sign in
99
+ at `/admin`. Keep to one address for the admin: a form posted from an origin
100
+ other than the one serving the request is refused, unless that origin is listed
101
+ in `NEXTLY_ALLOWED_ORIGINS`.
102
+
103
+ ## Check it worked
104
+
105
+ - `/admin` shows the dashboard after signing in.
106
+ - `npx nextly --version` prints the installed version.
107
+ - The dev server log has no `esbuild` error, and `/api/health` answers.
108
+
109
+ ## Doing it by hand
110
+
111
+ If the generator cannot be used, the manual steps are at
112
+ <https://nextlyhq.com/docs/getting-started/installation> (option B). They
113
+ include the pnpm build approvals, `esbuild` and the admin's media route, which
114
+ the generator otherwise adds for you.
115
+
116
+ ## Next
117
+
118
+ - Define content: the `nextly-collections` skill.
119
+ - Who can sign in and what they can do: the `nextly-authentication` skill.
120
+ - Going live: the `nextly-deploy` skill.
121
+ - Every docs page, for agents: <https://nextlyhq.com/llms.txt>.
@@ -5,12 +5,27 @@
5
5
  DB_DIALECT={{databaseDialect}}
6
6
  DATABASE_URL={{databaseUrl}}
7
7
 
8
- # Authentication (REQUIRED)
9
- # Generate with: openssl rand -base64 32
10
- NEXTLY_SECRET=change-me-generate-a-secure-secret
8
+ # Authentication (REQUIRED): a random secret of at least 32 characters, for
9
+ # example from `openssl rand -base64 32`. The real one goes in .env, never here.
10
+ NEXTLY_SECRET=
11
11
 
12
- # Application URL
12
+ # Where the Nextly app itself lives: the admin, the API, and the origin its
13
+ # emails and preview links point at.
13
14
  NEXT_PUBLIC_APP_URL=http://localhost:3000
14
15
 
15
- # Storage Configuration
16
- {{storageEnvVars}}
16
+ # Where READERS find the public site. Used for canonical links, Open Graph
17
+ # tags, the sitemap and RSS. Set it when the public site is served from a
18
+ # different origin than the app above; otherwise the app's origin is used.
19
+ # NEXT_PUBLIC_SITE_URL=https://example.com
20
+
21
+ # Development diagnostics (opt-in)
22
+ # Uncomment to add a _devDiagnostics field to error responses, carrying the
23
+ # error's log context and the underlying cause, so a failure names itself while
24
+ # you build instead of only in the server log.
25
+ #
26
+ # Left commented on purpose. It is the SECOND of two independent signals — the
27
+ # first is NODE_ENV — and the second exists precisely because NODE_ENV is a
28
+ # runtime value a deployment can carry by mistake. A default that ships in this
29
+ # file would be true in exactly that case, which is the one it guards against.
30
+ # NEXTLY_DEV_DIAGNOSTICS=1
31
+
@@ -0,0 +1,226 @@
1
+ <!-- nextly:managed:start -->
2
+ <!--
3
+ Everything between these markers is generated by create-nextly-app and may be
4
+ replaced when the project is upgraded. Write your own notes OUTSIDE the block:
5
+ anything above or below it is yours and is preserved.
6
+ -->
7
+
8
+ # Agent guide for this Nextly project
9
+
10
+ A Next.js app with [Nextly](https://nextlyhq.com) for content. The CMS runs
11
+ inside this app rather than beside it: same server, same database, no separate
12
+ service to start.
13
+
14
+ ## Where things are
15
+
16
+ | path | what it is |
17
+ | ---------------------- | ------------------------------------------------------------------ |
18
+ | `nextly.config.ts` | the config the whole app boots from — collections, database, admin |
19
+ | `src/collections/` | code-first collection definitions, when this project uses them |
20
+ | `src/app/(frontend)/` | the public site |
21
+ | `src/app/admin/` | the admin panel, served at `/admin` |
22
+ | `migrations/` | SQL migrations, in this project's dialect |
23
+ | `src/types/generated/` | generated types — never edit by hand |
24
+
25
+ ## Commands
26
+
27
+ ```bash
28
+ {{runCommand}} dev # start Next.js; schema changes apply in-process
29
+ {{runCommand}} build # migrate, then build
30
+ {{runCommand}} db:setup # first run: create the schema
31
+ {{runCommand}} db:migrate # apply pending migrations
32
+ {{runCommand}} db:migrate:status # what has and has not been applied
33
+ {{runCommand}} db:migrate:fresh # drop everything and re-run (destructive)
34
+ {{runCommand}} types:generate # regenerate types after a schema change
35
+ ```
36
+
37
+ This project uses **{{databaseDialect}}**.
38
+
39
+ ## Reading content
40
+
41
+ Content is read through the Direct API, on the server:
42
+
43
+ ```ts
44
+ import { getNextly } from "nextly";
45
+ import nextlyConfig from "@nextly-config";
46
+
47
+ const nextly = await getNextly({ config: nextlyConfig });
48
+ const posts = await nextly.find({ collection: "posts", limit: 10 });
49
+ ```
50
+
51
+ **`find()` returns loosely-typed documents until types are generated.** Without
52
+ `{{runCommand}} types:generate`, `posts.items` is `Record<string, unknown>[]` and a
53
+ field access is `unknown`. Generating types makes the slug and the return type
54
+ infer, so `posts.items` becomes `Post[]`.
55
+
56
+ Until then, narrow with a runtime check rather than an assertion — a document
57
+ missing a field yields `undefined`, and asserting the type only moves the
58
+ failure to where the value is used:
59
+
60
+ ```ts
61
+ const slug = typeof doc.slug === "string" ? doc.slug : undefined;
62
+ ```
63
+
64
+ ## Things that surprise people
65
+
66
+ - **Users are not a normal collection.** Read them through `nextly.users`, not
67
+ `find({ collection: "users" })`. The users namespace supports pagination and
68
+ `search`; it does not support a `where` filter.
69
+ - **A migration you generate is one file, in THIS project's dialect.**
70
+ `{{execCommand}} nextly migrate:create <name>` (no package.json script exists for it) writes a
71
+ single `migrations/<timestamp>_<slug>.sql` containing **{{databaseDialect}}**
72
+ SQL — no `.mysql.sql` or `.sqlite.sql` beside it. Edit that file.
73
+ - **A suffixed set means the migration was shipped, not generated.** A template
74
+ that supports every dialect ships `0001_x.sql` (Postgres) alongside
75
+ `0001_x.mysql.sql` and `0001_x.sqlite.sql`, and the CLI runs the variant for
76
+ the configured dialect. Those are prebuilt. Adding companion variants to a
77
+ migration you generated is not part of the normal workflow, and nothing will
78
+ run them unless this project is itself meant to support several dialects.
79
+ - **The admin needs a first user.** Visit `/admin` after `{{runCommand}} dev` and
80
+ complete the setup screen.
81
+ - **Cache tags come from `nextly/runtime`.** Wrap PUBLIC reads in `cachedFind`
82
+ and tag them with `nextlyTags(collection)` so a write invalidates the right
83
+ pages.
84
+
85
+ **`keyParts` is a security boundary, not a performance detail.** Two callers
86
+ whose `keyParts` match share one cache entry. So if the read applies any
87
+ per-caller rule — a code-defined `access.read` that reads the caller's
88
+ roles or permissions, an API key's narrowed scope — the caller's identity
89
+ has to be IN the key, or one user's rows get served to another:
90
+
91
+ **A shared cache entry is only safe if the read is public for EVERY reader.**
92
+ Two independent things can make it not so, and each needs its own answer:
93
+
94
+ - **drafts** — `find()` defaults to `overrideAccess: true`, a trusted server
95
+ read, so a slug filter alone can return an unpublished row. `status:
96
+ "published"` is enforced even on a trusted read and fixes this one.
97
+ - **access rules** — a code-defined `access.read` on the collection that
98
+ decides on the caller (their roles, their permissions, whether they are
99
+ signed in at all), or an API key's narrowed scope. `status` does NOT
100
+ evaluate these; it is a draft/published lifecycle filter and nothing more.
101
+ A published row that only some readers may see is still returned by the
102
+ query above.
103
+
104
+ - **field-level read rules** — a field can carry its own `access.read` even
105
+ when the collection declares none. The shared branch below reads with the
106
+ Direct API's default `overrideAccess: true`, which bypasses field rules
107
+ too, so the cached row holds every field and the entry serves fields the
108
+ schema meant to redact. A collection with ANY field-level read rule takes
109
+ the per-caller branch — or selects only fields known to be public.
110
+
111
+ So the decision is: **does this collection declare a read rule, on the
112
+ collection or on any of its fields?**
113
+
114
+ ```ts
115
+ import { cachedFind, nextlyTags } from "nextly/runtime";
116
+
117
+ // NO read rule, on the collection OR its fields — the result is identical for
118
+ // every reader, so one shared entry is correct and gets the full ISR benefit.
119
+ // `find()` always returns the list envelope `{ items, meta }`, so a detail route takes the
120
+ // first item — and caches THAT, not the envelope around it.
121
+ cachedFind(
122
+ async () =>
123
+ (
124
+ await nextly.find({
125
+ collection: "posts",
126
+ where: { slug: { equals: slug } },
127
+ status: "published",
128
+ limit: 1,
129
+ })
130
+ ).items[0] ?? null,
131
+ { tags: nextlyTags("posts"), keyParts: ["posts", slug] }
132
+ );
133
+
134
+ // A READ RULE — evaluate it as the caller, and key by EVERY dimension the
135
+ // rule reads, not just who the caller is.
136
+ //
137
+ // Both role fields are optional and Nextly reads BOTH: the many-to-many
138
+ // `roles` unioned with the singular `role`. Deduped and sorted, so one role
139
+ // set spelled two ways is one cache entry rather than two.
140
+ const roleKey = (u: { role?: string; roles?: string[] }) =>
141
+ [...new Set([...(u.roles ?? []), u.role ?? []].flat())].sort().join(",");
142
+
143
+ cachedFind(
144
+ () => nextly.find({ collection: "orders", user, overrideAccess: false }),
145
+ {
146
+ tags: nextlyTags("orders"),
147
+ keyParts: ["orders", "list", user.id, roleKey(user)],
148
+ }
149
+ );
150
+ ```
151
+
152
+ **`user.id` alone is not the authorization context.** Roles change, custom
153
+ claims change, an API key's scope is narrower than its owner's. A key built
154
+ from identity alone survives all of those, so the same person can fill the
155
+ entry while privileged and read it back after being downgraded — the tags bust
156
+ on CONTENT changes, and a permission change is not one. Include whatever the
157
+ rules actually read: the role set, the claim, the key's scope.
158
+
159
+ If you are unsure whether a collection or any of its fields declares a read
160
+ rule, do not cache it. A missing cache is slow; a shared one leaks.
161
+
162
+ ## Which Nextly this is, and where its docs are
163
+
164
+ An agent working here will otherwise answer from whatever it remembers of some
165
+ other version. Ask the project instead:
166
+
167
+ ```bash
168
+ {{execCommand}} nextly --version # the running core version
169
+ ```
170
+
171
+ That is the same version the plugin resolver validates a plugin's `nextly`
172
+ range against, so a compatibility answer and a reported version cannot
173
+ disagree. The dependency range this project pins is in `package.json` under
174
+ `nextly`; the resolved version is what the command above prints.
175
+
176
+ **The authoritative documentation is <https://nextlyhq.com/docs>.** Two files
177
+ are published for machine readers and are the ones to fetch:
178
+
179
+ | file | what it is |
180
+ | ---- | ---------- |
181
+ | <https://nextlyhq.com/llms.txt> | the index — every documentation page, with a one-line summary |
182
+ | <https://nextlyhq.com/llms-full.txt> | the same documentation as one file |
183
+
184
+ Both are generated from the pages the site publishes, so they cannot describe a
185
+ page that does not exist. Neither is version-stamped: they describe the CURRENT
186
+ release, so check the version above before trusting a detail against an older
187
+ pin.
188
+
189
+ ## Skills for Nextly tasks
190
+
191
+ If this project has `.agents/skills/`, it holds skills for common Nextly
192
+ tasks, and `.claude/skills/` holds the same files for Claude Code. Load the
193
+ one that matches a task before starting it. They were written for the Nextly
194
+ release this project was created with.
195
+
196
+ Each skill names the Nextly release it was checked against in its
197
+ frontmatter, as `metadata.nextly-version`. The command below installs skills
198
+ written for `{{skillsVersion}}`. Restore them as they were released only when
199
+ `.agents/skills/nextly-install/SKILL.md` exists, its `metadata.nextly-version`
200
+ is exactly `{{skillsVersion}}`, and `{{execCommand}} nextly --version` prints
201
+ exactly `{{skillsVersion}}`:
202
+
203
+ ```bash
204
+ npx skills add 'nextlyhq/nextly#v{{skillsTag}}' --skill '*' --agent claude-code codex cursor --copy -y
205
+ ```
206
+
207
+ It rewrites both folders, so edits made to a skill there are replaced.
208
+ `{{skillsTag}}` is the release of `create-nextly-app` that made this project,
209
+ whose tag publishes these skills. Releases up to 0.0.2-alpha.66 publish no
210
+ skills for building apps: installing from one of those tags brings the skills
211
+ Nextly's own contributors use, so never put an older version in its place.
212
+
213
+ Otherwise, with no skills or with skills for a different release than this
214
+ project runs, do not run the command above: it would install skills written
215
+ for another release. See <https://nextlyhq.com/docs/guides/coding-agents> for
216
+ skills that match.
217
+
218
+ ## Conventions worth keeping
219
+
220
+ - Server components read content directly; do not build an API route to fetch
221
+ your own content.
222
+ - Regenerate types in the same change as the schema edit that needs them.
223
+ - Migrations are append-only once shared. Add a new one rather than editing an
224
+ applied file.
225
+
226
+ <!-- nextly:managed:end -->
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,42 @@
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+ !.env.example
36
+
37
+ # vercel
38
+ .vercel
39
+
40
+ # typescript
41
+ *.tsbuildinfo
42
+ next-env.d.ts