create-nextly-app 0.0.2-alpha.7 → 0.0.2-alpha.70
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.
- package/README.md +36 -9
- package/dist/cli.cjs +3047 -32460
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.mjs +3048 -3602
- package/dist/cli.mjs.map +1 -1
- package/dist/create-nextly-5wiGMVI6.mjs +52550 -0
- package/dist/create-nextly-5wiGMVI6.mjs.map +1 -0
- package/dist/create-nextly-BbKGDrEC.cjs +52628 -0
- package/dist/create-nextly-BbKGDrEC.cjs.map +1 -0
- package/dist/index.cjs +3 -24861
- package/dist/index.d.cts +40 -34
- package/dist/index.d.ts +40 -34
- package/dist/index.mjs +1 -6
- package/package.json +28 -19
- package/skills/nextly-authentication/SKILL.md +187 -0
- package/skills/nextly-collections/SKILL.md +215 -0
- package/skills/nextly-deploy/SKILL.md +248 -0
- package/skills/nextly-install/SKILL.md +121 -0
- package/templates/base/.env.example +21 -6
- package/templates/base/AGENTS.md.template +226 -0
- package/templates/base/CLAUDE.md.template +1 -0
- package/templates/base/gitignore +42 -0
- package/templates/base/next.config.ts +11 -17
- package/templates/base/src/app/admin/[[...params]]/page.tsx +3 -0
- package/templates/base/src/app/admin/api/media/[[...path]]/route.ts +26 -0
- package/templates/base/src/app/globals.css +15 -2
- package/templates/base/src/app/layout.tsx +32 -18
- package/templates/base/src/lib/site-url.ts +55 -0
- package/templates/base/src/types/generated/nextly-types.ts +6 -4
- package/templates/base/src/types/generated/plugin-admin-components.generated.ts +6 -0
- package/templates/base/src/types/generated/plugin-admin-imports.generated.ts +32 -0
- package/templates/base/tsconfig.json +8 -1
- package/templates/blank/src/{components → app/(frontend)/components}/ThemeToggle.tsx +55 -24
- package/templates/blank/src/app/(frontend)/page.tsx +2 -2
- package/templates/blank/src/app/globals.css +37 -1
- package/templates/blank/src/app/layout.tsx +28 -18
- package/templates/blank/src/instrumentation.ts +32 -0
- package/templates/plugin/AGENTS.md.template +125 -0
- package/templates/plugin/CLAUDE.md.template +1 -0
- package/templates/plugin/README.md +48 -0
- package/templates/plugin/dev/.env.example +11 -0
- package/templates/plugin/dev/next.config.ts +43 -0
- package/templates/plugin/dev/nextly.config.ts +17 -0
- package/templates/plugin/dev/src/app/admin/[[...params]]/page.tsx +24 -0
- package/templates/plugin/dev/src/app/admin/api/[[...params]]/route.ts +19 -0
- package/templates/plugin/dev/src/app/layout.tsx +13 -0
- package/templates/plugin/dev/src/instrumentation.ts +25 -0
- package/templates/plugin/dev/src/seed-dev-user.ts +51 -0
- package/templates/plugin/dev/src/types/generated/plugin-admin-components.generated.ts +8 -0
- package/templates/plugin/dev/src/types/generated/plugin-admin-imports.generated.ts +37 -0
- package/templates/plugin/dev/tsconfig.json +22 -0
- package/templates/plugin/eslint.config.mjs +19 -0
- package/templates/plugin/gitignore +13 -0
- package/templates/plugin/src/admin/SettingsPage.tsx +57 -0
- package/templates/plugin/src/admin/index.ts +24 -0
- package/templates/plugin/src/collections/example.ts +13 -0
- package/templates/plugin/src/index.ts +6 -0
- package/templates/plugin/src/plugin.test.ts +35 -0
- package/templates/plugin/src/plugin.ts +48 -0
- package/templates/plugin/src/types.ts +17 -0
- package/templates/plugin/template.json +14 -0
- package/templates/plugin/tsconfig.json +18 -0
- package/templates/plugin/tsdown.config.ts +30 -0
- package/templates/plugin/vitest.config.ts +27 -0
- package/dist/chunk-RPGFJSDP.mjs +0 -28835
- package/dist/chunk-RPGFJSDP.mjs.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/templates/blank/src/access/README.md +0 -22
- package/templates/blank/src/collections/README.md +0 -22
- package/templates/blank/src/lib/README.md +0 -15
- package/templates/blank/src/singles/README.md +0 -19
- /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.70
|
|
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.70
|
|
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
|
-
#
|
|
10
|
-
NEXTLY_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
|
+
# emails and preview links point at.
|
|
13
14
|
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
14
15
|
|
|
15
|
-
#
|
|
16
|
-
|
|
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
|