create-nextly-app 0.0.2-alpha.65 → 0.0.2-alpha.67

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 (33) hide show
  1. package/README.md +29 -8
  2. package/dist/{chunk-I2U7KBH6.mjs → chunk-XCYSH46I.mjs} +36805 -10917
  3. package/dist/chunk-XCYSH46I.mjs.map +1 -0
  4. package/dist/cli.cjs +27693 -1892
  5. package/dist/cli.cjs.map +1 -1
  6. package/dist/cli.mjs +17 -106
  7. package/dist/cli.mjs.map +1 -1
  8. package/dist/index.cjs +36591 -10708
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +6 -0
  11. package/dist/index.d.ts +6 -0
  12. package/dist/index.mjs +1 -1
  13. package/package.json +18 -7
  14. package/skills/nextly-authentication/SKILL.md +177 -0
  15. package/skills/nextly-collections/SKILL.md +215 -0
  16. package/skills/nextly-deploy/SKILL.md +246 -0
  17. package/skills/nextly-install/SKILL.md +122 -0
  18. package/templates/base/.env.example +3 -5
  19. package/templates/base/AGENTS.md.template +83 -15
  20. package/templates/base/src/types/generated/nextly-types.ts +6 -4
  21. package/templates/blank/src/app/(frontend)/components/ThemeToggle.tsx +55 -24
  22. package/templates/blank/src/instrumentation.ts +27 -0
  23. package/templates/plugin/AGENTS.md.template +33 -4
  24. package/templates/plugin/README.md +4 -0
  25. package/templates/plugin/dev/.env.example +5 -2
  26. package/templates/plugin/dev/src/app/admin/[[...params]]/page.tsx +14 -2
  27. package/templates/plugin/dev/src/instrumentation.ts +20 -0
  28. package/templates/plugin/dev/src/seed-dev-user.ts +51 -0
  29. package/templates/plugin/gitignore +5 -2
  30. package/templates/plugin/src/plugin.test.ts +9 -2
  31. package/templates/plugin/vitest.config.ts +19 -0
  32. package/dist/chunk-I2U7KBH6.mjs.map +0 -1
  33. package/templates/plugin/dev/instrumentation.ts +0 -10
@@ -0,0 +1,246 @@
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.67
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. Create the first admin before the site is public
155
+
156
+ A new production database has no admin. Until it has one, whoever opens
157
+ `/admin/setup` first becomes Super Admin, so create that account before anyone
158
+ else can reach the site. Setup then closes for good: a later attempt is
159
+ refused with `SETUP_COMPLETE`.
160
+
161
+ The simplest way is a production build on your own machine, pointed at the
162
+ production database:
163
+
164
+ ```bash
165
+ export DATABASE_URL="<the production database>"
166
+ npm run build # runs nextly migrate against it, then next build
167
+ npm start # next start, on http://localhost:3000
168
+ ```
169
+
170
+ Export every other variable the config reads as well. With Vercel Blob,
171
+ `BLOB_READ_WRITE_TOKEN` is needed to load `nextly.config.ts` at all, and the
172
+ build stops without it. With S3, the bucket and region are needed to load
173
+ it; the keys can come from the AWS credential chain instead (an IAM role, a
174
+ profile), so a build that succeeds says nothing about them. Where a value
175
+ exists only in the host's settings and cannot be brought to your machine, use
176
+ the protected-host path below instead.
177
+
178
+ Open `http://localhost:3000/admin/setup`, create the account, stop the
179
+ server, then deploy. If you cannot reach the production database from your
180
+ machine, keep the deployment private until setup is done, behind your host's
181
+ access protection.
182
+
183
+ Never run `next dev` against the production database. Development mode
184
+ applies schema changes directly to whatever database it points at.
185
+
186
+ ## 7. Deploy
187
+
188
+ - **Vercel:** import the repository; Next.js is detected. Add the variables
189
+ above for Production (and Preview, with its own database), then deploy.
190
+ The `build` script migrates the database during the build.
191
+ - **Node.js server:** install from the lockfile (`npm ci`, `pnpm install
192
+ --frozen-lockfile`, `yarn install --immutable` on Yarn 2+, `yarn install --frozen-lockfile` on
193
+ Yarn 1, or `bun install --frozen-lockfile`), run the `build` script, then `next start` under a
194
+ process manager such as systemd, pm2 or Docker. Put a reverse proxy in
195
+ front that terminates TLS and forwards `Host`, `X-Forwarded-Proto` and
196
+ `X-Forwarded-For`.
197
+
198
+ ### Behind a proxy, including Vercel
199
+
200
+ Nextly ignores `X-Forwarded-For` unless told to trust it, and then it cannot
201
+ tell clients apart: every sign-in, registration and password-reset request
202
+ shares one rate-limit bucket, 30 an hour by default, and everyone gets 429
203
+ after that. When every request reaches the app through a proxy that sets
204
+ `X-Forwarded-For` (Vercel does; configure nginx or Caddy to), trust it:
205
+
206
+ ```ts
207
+ // nextly.config.ts
208
+ import { defineConfig } from "nextly/config";
209
+
210
+ export default defineConfig({
211
+ security: { trustProxy: true },
212
+ // collections, typescript, ...
213
+ });
214
+ ```
215
+
216
+ When requests pass through more than one proxy, list the proxies' addresses
217
+ in `TRUSTED_PROXY_IPS` (comma-separated CIDRs), so the client is read from the
218
+ right hop. Do not turn this on for an app clients can reach directly: they
219
+ could then pick their own address.
220
+
221
+ ## 8. Check the live site
222
+
223
+ - `npx nextly migrate:status`, run against the production `DATABASE_URL`,
224
+ shows nothing pending.
225
+ - `/admin/setup` says setup is already complete, and `/admin` signs in with
226
+ the account created before the deploy.
227
+ - Publish an entry and open a page that lists it: it appears.
228
+ - Upload an image in the admin, redeploy, and check it still loads.
229
+ - Do not use `/api/health` as an uptime probe on serverless hosts. It reports
230
+ 503 until Nextly has started in that same process, which a fresh instance
231
+ that only receives probes never does. Probe a page that reads content
232
+ instead.
233
+
234
+ ## If something fails
235
+
236
+ - **Tables missing, or `relation "dc_..." does not exist`:** the migrations
237
+ were not committed, or `nextly migrate` did not run against this database.
238
+ - **The admin and its API answer 500 in production:** check the server log.
239
+ `NEXTLY_SECRET of at least 32 characters is required` means the secret is
240
+ missing or too short, and `NEXT_PUBLIC_APP_URL is required` that the URL is
241
+ not set.
242
+ - **Sign-in fails with `CSRF_FAILED` behind a proxy:** add the public origin
243
+ to `NEXTLY_ALLOWED_ORIGINS`.
244
+ - **`next build` fails on a page reading content:** in an app with
245
+ `cacheComponents: true`, wrap the read in `cachedFind` (the
246
+ `nextly-collections` skill) or render it inside `<Suspense>`.
@@ -0,0 +1,122 @@
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.67
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 14, 15 or 16 with the App Router (`app/` or `src/app/`). On
27
+ Next.js 14 the generator leaves the Next config alone and prints the
28
+ `experimental` block to add instead (`serverComponentsExternalPackages` and
29
+ `instrumentationHook`).
30
+ - Node.js `^20.19.0 || ^22.12.0 || >=24.0.0`. Node 23 is not supported.
31
+ - A clean git working tree, so every change below is visible in one diff.
32
+
33
+ ## 1. Run the generator in the project root
34
+
35
+ ```bash
36
+ npx create-nextly-app@alpha # npm
37
+ pnpm dlx create-nextly-app@alpha # pnpm
38
+ yarn dlx create-nextly-app@alpha # yarn 2 and later
39
+ yarn create nextly-app # yarn 1 (Classic): no tag; installs `latest`, the current release
40
+ bunx create-nextly-app@alpha # bun
41
+ ```
42
+
43
+ It finds `next` in `package.json`, asks "Install Nextly in this project?", then
44
+ asks which database and, for PostgreSQL or MySQL, its connection string. `-y`
45
+ skips the questions and uses SQLite at `./data/nextly.db`. `--database
46
+ postgresql` or `--database mysql` picks another; with `-y` as well, `.env` gets
47
+ a placeholder `DATABASE_URL` to replace before starting.
48
+
49
+ It installs with the app's own package manager (inside a monorepo, the
50
+ workspace's), adding the packages to the app's own `package.json`. On pnpm 10
51
+ and later it approves the native build scripts Nextly needs (`better-sqlite3`,
52
+ `esbuild`, `sharp`) in the setting that pnpm version reads: `pnpm-workspace.yaml`
53
+ from pnpm 10.6, `package.json` before it. In a monorepo the approvals go at the
54
+ workspace root.
55
+
56
+ Develop on the database you will run in production. Migrations are written in
57
+ the dialect of the database you develop against (the `nextly-deploy` skill),
58
+ and SQLite is not suitable for serverless hosting.
59
+
60
+ What it changes. It edits `package.json`, the Next config and pnpm's settings
61
+ in place, and adds to `.env` and `.env.example` only the keys they lack. A file
62
+ it would create that the app already has (`nextly.config.ts`,
63
+ `instrumentation.ts`, the admin's files and the routes) is kept as it is and
64
+ reported as skipped, with what Nextly needs in it. Read its report, then review
65
+ the rest in `git diff`:
66
+
67
+ | File | What |
68
+ | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
69
+ | `package.json` | `nextly`, `@nextlyhq/admin`, `@nextlyhq/ui`, the database adapter and driver, `@tanstack/react-query`, `lucide-react`; `esbuild` as a dev dependency |
70
+ | `nextly.config.ts` | the config the CMS boots from, with empty `collections` and `singles` |
71
+ | `next.config.ts` | Nextly, the adapter and the driver added to `serverExternalPackages` |
72
+ | `src/instrumentation.ts` | starts Nextly when the server starts |
73
+ | `src/app/admin/[[...params]]/` | the admin panel page and layout |
74
+ | `src/app/admin/api/[[...params]]/route.ts` | the admin API |
75
+ | `src/app/admin/api/media/[[...path]]/route.ts` | the admin's media route, where it uploads and edits media (signed-in only) |
76
+ | `src/app/api/media/[[...path]]/route.ts` | public, read-only media routes |
77
+ | `src/app/api/health/route.ts` | a health check |
78
+ | `src/types/generated/` | where generated types go |
79
+ | `.env`, `.env.example` | `DB_DIALECT`, `DATABASE_URL`, `NEXTLY_SECRET`, `NEXT_PUBLIC_APP_URL`, for the keys missing |
80
+
81
+ Without a `src/` folder the same files go under `app/`, and `instrumentation.ts`
82
+ sits at the project root.
83
+
84
+ If it reports that it left the Next config alone (a config it cannot change
85
+ safely, or Next.js 14), add what it prints. The list it needs holds `nextly`,
86
+ `@nextlyhq/adapter-drizzle`, `drizzle-orm`, `drizzle-kit`, `bcryptjs`,
87
+ `sharp`, `esbuild`, and your adapter and driver: `@nextlyhq/adapter-sqlite`
88
+ and `better-sqlite3`, `@nextlyhq/adapter-postgres` and `pg`, or
89
+ `@nextlyhq/adapter-mysql` and `mysql2`.
90
+
91
+ ## 2. Start it and create the first admin
92
+
93
+ ```bash
94
+ npm run dev
95
+ ```
96
+
97
+ Open `http://localhost:3000/admin/setup` and create the first account; it gets
98
+ the Super Admin role. Nextly creates its own tables when the server starts,
99
+ from `instrumentation.ts`; without that hook, on the first request. Then sign in
100
+ at `/admin`. Keep to one address for the admin: a form posted from an origin
101
+ other than the one serving the request is refused, unless that origin is listed
102
+ in `NEXTLY_ALLOWED_ORIGINS`.
103
+
104
+ ## Check it worked
105
+
106
+ - `/admin` shows the dashboard after signing in.
107
+ - `npx nextly --version` prints the installed version.
108
+ - The dev server log has no `esbuild` error, and `/api/health` answers.
109
+
110
+ ## Doing it by hand
111
+
112
+ If the generator cannot be used, the manual steps are at
113
+ <https://nextlyhq.com/docs/getting-started/installation> (option B). They
114
+ include the pnpm build approvals, `esbuild` and the admin's media route, which
115
+ the generator otherwise adds for you.
116
+
117
+ ## Next
118
+
119
+ - Define content: the `nextly-collections` skill.
120
+ - Who can sign in and what they can do: the `nextly-authentication` skill.
121
+ - Going live: the `nextly-deploy` skill.
122
+ - Every docs page, for agents: <https://nextlyhq.com/llms.txt>.
@@ -5,9 +5,9 @@
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
12
  # Where the Nextly app itself lives: the admin, the API, and the origin its
13
13
  # emails and preview links point at.
@@ -29,5 +29,3 @@ NEXT_PUBLIC_APP_URL=http://localhost:3000
29
29
  # file would be true in exactly that case, which is the one it guards against.
30
30
  # NEXTLY_DEV_DIAGNOSTICS=1
31
31
 
32
- # Storage Configuration
33
- {{storageEnvVars}}
@@ -84,9 +84,9 @@ const slug = typeof doc.slug === "string" ? doc.slug : undefined;
84
84
 
85
85
  **`keyParts` is a security boundary, not a performance detail.** Two callers
86
86
  whose `keyParts` match share one cache entry. So if the read applies any
87
- per-caller rule — owner-only scoping, role-based visibility, an API key's
88
- narrowed scope — the caller's identity has to be IN the key, or one user's
89
- rows get served to another:
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
90
 
91
91
  **A shared cache entry is only safe if the read is public for EVERY reader.**
92
92
  Two independent things can make it not so, and each needs its own answer:
@@ -94,16 +94,28 @@ const slug = typeof doc.slug === "string" ? doc.slug : undefined;
94
94
  - **drafts** — `find()` defaults to `overrideAccess: true`, a trusted server
95
95
  read, so a slug filter alone can return an unpublished row. `status:
96
96
  "published"` is enforced even on a trusted read and fixes this one.
97
- - **per-row access rules** — owner-only scoping, role-based visibility, an API
98
- key's narrowed scope. `status` does NOT evaluate these; it is a
99
- draft/published lifecycle filter and nothing more. A published row that only
100
- its owner may read is still returned by the query above.
101
-
102
- So the decision is: **does this collection carry read-access rules?**
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?**
103
113
 
104
114
  ```ts
105
- // NO access rules — the result is identical for every reader, so one shared
106
- // entry is correct and gets the full ISR benefit.
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.
107
119
  // `find()` always returns the list envelope `{ items, meta }`, so a detail route takes the
108
120
  // first item — and caches THAT, not the envelope around it.
109
121
  cachedFind(
@@ -119,8 +131,8 @@ const slug = typeof doc.slug === "string" ? doc.slug : undefined;
119
131
  { tags: nextlyTags("posts"), keyParts: ["posts", slug] }
120
132
  );
121
133
 
122
- // ACCESS RULES — evaluate them as the caller, and key by EVERY dimension the
123
- // rules read, not just who the caller is.
134
+ // A READ RULE — evaluate it as the caller, and key by EVERY dimension the
135
+ // rule reads, not just who the caller is.
124
136
  //
125
137
  // Both role fields are optional and Nextly reads BOTH: the many-to-many
126
138
  // `roles` unioned with the singular `role`. Deduped and sorted, so one role
@@ -144,8 +156,64 @@ const slug = typeof doc.slug === "string" ? doc.slug : undefined;
144
156
  on CONTENT changes, and a permission change is not one. Include whatever the
145
157
  rules actually read: the role set, the claim, the key's scope.
146
158
 
147
- If you are unsure whether a collection has read rules, do not cache it. A
148
- missing cache is slow; a shared one leaks.
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.
149
217
 
150
218
  ## Conventions worth keeping
151
219
 
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * Nextly Generated Types
3
3
  *
4
- * This file will be auto-generated when you run:
5
- * npx nextly dev
6
- * npx nextly generate:types
4
+ * This file is generated by `nextly generate:types`, which this project runs
5
+ * as its `types:generate` script. Run it after every change to the schema in
6
+ * nextly.config.ts. It reads only that file, so collections built in the
7
+ * Schema Builder get types once they are moved into code
8
+ * (`nextly db:sync --promote <slug>`).
7
9
  *
8
10
  * Do not edit this file manually - it will be overwritten.
9
11
  */
10
12
 
11
- // Types will be generated here after running nextly dev
13
+ // Types are generated here by `nextly generate:types`.
12
14
  export {};
@@ -1,36 +1,67 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useState } from "react";
3
+ import { useEffect, useSyncExternalStore } from "react";
4
4
 
5
5
  type Theme = "light" | "dark" | "system";
6
6
 
7
- export function ThemeToggle() {
8
- const [theme, setTheme] = useState<Theme>("system");
9
-
10
- useEffect(() => {
11
- const savedTheme = localStorage.getItem("theme") as Theme | null;
12
- if (savedTheme) {
13
- setTheme(savedTheme);
14
- applyTheme(savedTheme);
15
- } else {
16
- applyTheme("system");
17
- }
18
- }, []);
7
+ const STORAGE_KEY = "theme";
8
+ const THEMES: readonly Theme[] = ["light", "dark", "system"];
9
+ const DARK_QUERY = "(prefers-color-scheme: dark)";
19
10
 
20
- const applyTheme = (newTheme: Theme) => {
21
- const isDark =
22
- newTheme === "dark" ||
23
- (newTheme === "system" &&
24
- window.matchMedia("(prefers-color-scheme: dark)").matches);
11
+ /**
12
+ * Everything showing the theme in this tab. The browser's `storage` event
13
+ * only reaches other tabs, so a change made here is announced here by hand.
14
+ */
15
+ const listeners = new Set<() => void>();
25
16
 
26
- document.documentElement.classList.toggle("dark", isDark);
17
+ function subscribe(listener: () => void) {
18
+ listeners.add(listener);
19
+ window.addEventListener("storage", listener);
20
+ return () => {
21
+ listeners.delete(listener);
22
+ window.removeEventListener("storage", listener);
27
23
  };
24
+ }
28
25
 
29
- const handleThemeChange = (newTheme: Theme) => {
30
- setTheme(newTheme);
31
- localStorage.setItem("theme", newTheme);
32
- applyTheme(newTheme);
33
- };
26
+ /** The saved choice, or "system" when there is none or it is not one of the three. */
27
+ function readTheme(): Theme {
28
+ const saved = localStorage.getItem(STORAGE_KEY);
29
+ return THEMES.find(theme => theme === saved) ?? "system";
30
+ }
31
+
32
+ function saveTheme(theme: Theme) {
33
+ localStorage.setItem(STORAGE_KEY, theme);
34
+ for (const listener of listeners) listener();
35
+ }
36
+
37
+ function applyTheme(theme: Theme) {
38
+ const isDark =
39
+ theme === "dark" ||
40
+ (theme === "system" && window.matchMedia(DARK_QUERY).matches);
41
+ document.documentElement.classList.toggle("dark", isDark);
42
+ }
43
+
44
+ export function ThemeToggle() {
45
+ // Read from storage rather than copied into state by an effect. The server
46
+ // renders "system"; after hydration React swaps in the saved choice.
47
+ const theme = useSyncExternalStore<Theme>(
48
+ subscribe,
49
+ readTheme,
50
+ () => "system"
51
+ );
52
+
53
+ // Keep the page's colours in step with the choice, and with the system's
54
+ // setting for as long as "system" is the choice.
55
+ useEffect(() => {
56
+ applyTheme(theme);
57
+ if (theme !== "system") return;
58
+ const query = window.matchMedia(DARK_QUERY);
59
+ const follow = () => applyTheme("system");
60
+ query.addEventListener("change", follow);
61
+ return () => query.removeEventListener("change", follow);
62
+ }, [theme]);
63
+
64
+ const handleThemeChange = (newTheme: Theme) => saveTheme(newTheme);
34
65
 
35
66
  return (
36
67
  <div className="fixed top-6 right-6 flex items-center border border-foreground/10 bg-background p-1 z-50">
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Starts Nextly when the server starts, before it serves a request.
3
+ *
4
+ * Next.js runs `register()` once per server instance and finishes it before
5
+ * the instance handles traffic, in every serverless function as well as the
6
+ * admin's. Without it, Nextly starts on the first request that happens to
7
+ * need it, so a route that cannot start it alone, such as `/api/health`,
8
+ * answers as if the server could not serve until some other request has.
9
+ *
10
+ * It also registers Nextly's Next.js cache adapter here, so a Direct API write
11
+ * from any function flushes the cache tags it touches.
12
+ *
13
+ * Guarded to the Node.js runtime and imported dynamically inside the guard:
14
+ * Nextly needs Node-only packages that the Edge runtime cannot bundle. Next.js
15
+ * skips `register()` during `next build`, so this never opens a database
16
+ * connection at build time.
17
+ */
18
+ export async function register(): Promise<void> {
19
+ if (process.env.NEXT_RUNTIME !== "nodejs") return;
20
+
21
+ const { registerNextCacheRevalidator } = await import("nextly/runtime");
22
+ registerNextCacheRevalidator();
23
+
24
+ const { createRegister } = await import("nextly");
25
+ const { default: config } = await import("../nextly.config");
26
+ await createRegister(config)();
27
+ }