create-nextblock 0.17.0 → 0.17.2
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/package.json +1 -1
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +4 -2
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +8203 -7038
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +1 -1
- package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +1 -1
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +1 -1
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +402 -293
- package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +15 -8
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +5 -5
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +1 -1
- package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +4 -1
- package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +12 -9
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +8 -0
- package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +2 -2
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +154 -163
- package/templates/nextblock-template/lib/config/email-settings.ts +323 -323
- package/templates/nextblock-template/lib/seo/redirect-store.ts +3 -2
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +18 -203
- package/templates/nextblock-template/package.json +1 -1
|
@@ -1,293 +1,402 @@
|
|
|
1
|
-
# 04 Database and Auth
|
|
2
|
-
|
|
3
|
-
## Source of Truth
|
|
4
|
-
|
|
5
|
-
The database and auth implementation is spread across:
|
|
6
|
-
|
|
7
|
-
- `libs/db/src/lib/supabase/*`
|
|
8
|
-
- `libs/db/src/lib/package-validation.ts`
|
|
9
|
-
- `libs/db/src/supabase/config.toml`
|
|
10
|
-
- `libs/db/src/supabase/migrations/*`
|
|
11
|
-
- `apps/nextblock/app/auth/callback/route.ts`
|
|
12
|
-
- `apps/nextblock/app/cms/*`
|
|
13
|
-
|
|
14
|
-
When documentation and a migration disagree, the migration folder is the final
|
|
15
|
-
authority for schema, triggers, grants, and policies.
|
|
16
|
-
|
|
17
|
-
## Supabase Client Surfaces
|
|
18
|
-
|
|
19
|
-
`libs/db/src/server.ts` currently exports:
|
|
20
|
-
|
|
21
|
-
- `createClient()`: request-scoped server client using auth cookies
|
|
22
|
-
- `getProfileWithRoleServerSide()`
|
|
23
|
-
- `getActiveLanguagesServerSide()`
|
|
24
|
-
- `getServiceRoleSupabaseClient()`
|
|
25
|
-
- `getSsgSupabaseClient()`
|
|
26
|
-
- package activation helpers such as `verifyPackageOnline()`
|
|
27
|
-
|
|
28
|
-
Practical usage in the app is split by trust level:
|
|
29
|
-
|
|
30
|
-
- normal server routes and components use `createClient()`
|
|
31
|
-
- public static-ish reads often use `getSsgSupabaseClient()`
|
|
32
|
-
- admin or system workflows use `getServiceRoleSupabaseClient()`
|
|
33
|
-
|
|
34
|
-
## Auth Flow
|
|
35
|
-
|
|
36
|
-
### Session exchange
|
|
37
|
-
|
|
38
|
-
`app/auth/callback/route.ts` handles Supabase auth callback exchanges:
|
|
39
|
-
|
|
40
|
-
1. read the `code` query parameter
|
|
41
|
-
2. exchange it for a session with `supabase.auth.exchangeCodeForSession()`
|
|
42
|
-
3. load the user's profile and role
|
|
43
|
-
4. redirect through `resolvePostAuthRedirect()`
|
|
44
|
-
|
|
45
|
-
### Profile creation
|
|
46
|
-
|
|
47
|
-
The first-user and profile bootstrap logic lives in the database, not in React
|
|
48
|
-
code.
|
|
49
|
-
|
|
50
|
-
`
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
- `
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
- creates
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
- `
|
|
87
|
-
- `
|
|
88
|
-
- `
|
|
89
|
-
- `
|
|
90
|
-
- `
|
|
91
|
-
- `
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
- `
|
|
99
|
-
- `
|
|
100
|
-
- `
|
|
101
|
-
- `
|
|
102
|
-
- `
|
|
103
|
-
- `
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
- `
|
|
112
|
-
- `
|
|
113
|
-
- `
|
|
114
|
-
- `
|
|
115
|
-
- `
|
|
116
|
-
- `
|
|
117
|
-
- `
|
|
118
|
-
- `
|
|
119
|
-
- `
|
|
120
|
-
- `
|
|
121
|
-
- `
|
|
122
|
-
- `
|
|
123
|
-
- `
|
|
124
|
-
- `
|
|
125
|
-
- `
|
|
126
|
-
- `
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
- `
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
`
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
-
|
|
151
|
-
|
|
152
|
-
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
-
|
|
157
|
-
|
|
158
|
-
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
`
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
- The
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
-
|
|
275
|
-
|
|
276
|
-
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
1
|
+
# 04 Database and Auth
|
|
2
|
+
|
|
3
|
+
## Source of Truth
|
|
4
|
+
|
|
5
|
+
The database and auth implementation is spread across:
|
|
6
|
+
|
|
7
|
+
- `libs/db/src/lib/supabase/*`
|
|
8
|
+
- `libs/db/src/lib/package-validation.ts`
|
|
9
|
+
- `libs/db/src/supabase/config.toml`
|
|
10
|
+
- `libs/db/src/supabase/migrations/*`
|
|
11
|
+
- `apps/nextblock/app/auth/callback/route.ts`
|
|
12
|
+
- `apps/nextblock/app/cms/*`
|
|
13
|
+
|
|
14
|
+
When documentation and a migration disagree, the migration folder is the final
|
|
15
|
+
authority for schema, triggers, grants, and policies.
|
|
16
|
+
|
|
17
|
+
## Supabase Client Surfaces
|
|
18
|
+
|
|
19
|
+
`libs/db/src/server.ts` currently exports:
|
|
20
|
+
|
|
21
|
+
- `createClient()`: request-scoped server client using auth cookies
|
|
22
|
+
- `getProfileWithRoleServerSide()`
|
|
23
|
+
- `getActiveLanguagesServerSide()`
|
|
24
|
+
- `getServiceRoleSupabaseClient()`
|
|
25
|
+
- `getSsgSupabaseClient()`
|
|
26
|
+
- package activation helpers such as `verifyPackageOnline()`
|
|
27
|
+
|
|
28
|
+
Practical usage in the app is split by trust level:
|
|
29
|
+
|
|
30
|
+
- normal server routes and components use `createClient()`
|
|
31
|
+
- public static-ish reads often use `getSsgSupabaseClient()`
|
|
32
|
+
- admin or system workflows use `getServiceRoleSupabaseClient()`
|
|
33
|
+
|
|
34
|
+
## Auth Flow
|
|
35
|
+
|
|
36
|
+
### Session exchange
|
|
37
|
+
|
|
38
|
+
`app/auth/callback/route.ts` handles Supabase auth callback exchanges:
|
|
39
|
+
|
|
40
|
+
1. read the `code` query parameter
|
|
41
|
+
2. exchange it for a session with `supabase.auth.exchangeCodeForSession()`
|
|
42
|
+
3. load the user's profile and role
|
|
43
|
+
4. redirect through `resolvePostAuthRedirect()`
|
|
44
|
+
|
|
45
|
+
### Profile creation
|
|
46
|
+
|
|
47
|
+
The first-user and profile bootstrap logic lives in the database, not in React
|
|
48
|
+
code.
|
|
49
|
+
|
|
50
|
+
`02001_baseline_schema.sql` (functions) and `02003_baseline_security_and_grants.sql`
|
|
51
|
+
(the trigger) define:
|
|
52
|
+
|
|
53
|
+
- `handle_new_user()`
|
|
54
|
+
- `on_auth_user_created` trigger on `auth.users`
|
|
55
|
+
|
|
56
|
+
That trigger:
|
|
57
|
+
|
|
58
|
+
- creates the first local admin automatically
|
|
59
|
+
- creates later users as `USER`
|
|
60
|
+
- inserts or updates `profiles`
|
|
61
|
+
- copies selected metadata such as `full_name`, avatar URL, and GitHub username
|
|
62
|
+
|
|
63
|
+
### CMS authorization
|
|
64
|
+
|
|
65
|
+
The CMS shell in `app/cms/CmsClientLayout.tsx` currently expects:
|
|
66
|
+
|
|
67
|
+
- an authenticated user
|
|
68
|
+
- a resolved profile role of `ADMIN` or `WRITER`
|
|
69
|
+
|
|
70
|
+
Writers and admins can enter the CMS. Admin-only navigation is used for
|
|
71
|
+
settings such as payments, shipping, users, and some branding/config surfaces.
|
|
72
|
+
|
|
73
|
+
### No live app middleware file
|
|
74
|
+
|
|
75
|
+
There is a generic Supabase middleware helper in `libs/db/src/lib/supabase`,
|
|
76
|
+
but there is no live `apps/nextblock/middleware.ts` file in the current app.
|
|
77
|
+
Document the callback, layout, and RLS model as the active auth path rather
|
|
78
|
+
than assuming middleware-based route protection is in use.
|
|
79
|
+
|
|
80
|
+
## Schema Overview
|
|
81
|
+
|
|
82
|
+
### Core platform tables
|
|
83
|
+
|
|
84
|
+
Defined in `02001_baseline_schema.sql`:
|
|
85
|
+
|
|
86
|
+
- `site_settings`
|
|
87
|
+
- `profiles`
|
|
88
|
+
- `user_addresses`
|
|
89
|
+
- `languages`
|
|
90
|
+
- `media`
|
|
91
|
+
- `translations`
|
|
92
|
+
- `logos`
|
|
93
|
+
|
|
94
|
+
### Content tables
|
|
95
|
+
|
|
96
|
+
Defined in `02001_baseline_schema.sql`:
|
|
97
|
+
|
|
98
|
+
- `posts`
|
|
99
|
+
- `pages`
|
|
100
|
+
- `blocks`
|
|
101
|
+
- `navigation_items`
|
|
102
|
+
- `page_revisions`
|
|
103
|
+
- `post_revisions`
|
|
104
|
+
- `product_revisions` (alongside `products.version`)
|
|
105
|
+
|
|
106
|
+
### Commerce tables
|
|
107
|
+
|
|
108
|
+
All defined in the baseline schema `02001_baseline_schema.sql` (every table is — the
|
|
109
|
+
per-migration numbers quoted in earlier revisions of this doc were retired by the squashes):
|
|
110
|
+
|
|
111
|
+
- `products`
|
|
112
|
+
- `product_media`
|
|
113
|
+
- `product_attributes`
|
|
114
|
+
- `product_attribute_terms`
|
|
115
|
+
- `product_variants`
|
|
116
|
+
- `inventory_items`
|
|
117
|
+
- `variant_attribute_mapping`
|
|
118
|
+
- `package_activations`
|
|
119
|
+
- `freemius_plans`
|
|
120
|
+
- `freemius_pricing`
|
|
121
|
+
- `orders`
|
|
122
|
+
- `order_items`
|
|
123
|
+
- `shipping_zones`
|
|
124
|
+
- `shipping_zone_locations`
|
|
125
|
+
- `shipping_zone_methods`
|
|
126
|
+
- `tax_rates`
|
|
127
|
+
- `currencies`
|
|
128
|
+
|
|
129
|
+
### Tables that arrived after the original schema
|
|
130
|
+
|
|
131
|
+
All folded into `02001_baseline_schema.sql` by the squashes; listed here because they are
|
|
132
|
+
easy to miss when reading the schema as one blob (their origin migrations live only in git
|
|
133
|
+
history now):
|
|
134
|
+
|
|
135
|
+
- `categories` and `product_categories` — catalog organization
|
|
136
|
+
- `custom_block_definitions` — data-driven custom block registry
|
|
137
|
+
(see [10-CUSTOM-BLOCKS.md](./10-CUSTOM-BLOCKS.md))
|
|
138
|
+
- `ucp_cart_sessions` — persisted cart sessions
|
|
139
|
+
- a `blocks` JSONB column plus `product_id` link for block-based product descriptions
|
|
140
|
+
- `site_themes`, `site_scripts` + `site_script_revisions`, `product_revisions`,
|
|
141
|
+
`mcp_access_tokens`, `product_inquiries`, `message_threads` + `thread_messages`,
|
|
142
|
+
`cms_redirects`, `system_alerts` — generation-1 additions (2026-07 → 2026-09)
|
|
143
|
+
|
|
144
|
+
## Row Level Security Patterns
|
|
145
|
+
|
|
146
|
+
`02003_baseline_security_and_grants.sql` is the consolidated RLS file.
|
|
147
|
+
|
|
148
|
+
The high-level access model is:
|
|
149
|
+
|
|
150
|
+
- public read access for languages, media, translations, published content, and
|
|
151
|
+
several storefront commerce tables
|
|
152
|
+
- authenticated self-service access for user addresses and customer-owned
|
|
153
|
+
orders
|
|
154
|
+
- `ADMIN` or `WRITER` write access for most CMS authoring tables
|
|
155
|
+
- `ADMIN`-only write access for higher-risk configuration surfaces
|
|
156
|
+
- `service_role` full access where background jobs or system syncs need it
|
|
157
|
+
|
|
158
|
+
Commerce-specific policy highlights include:
|
|
159
|
+
|
|
160
|
+
- public read access for products, product media, product attributes, variants,
|
|
161
|
+
shipping zones, shipping methods, tax rates, and active currencies
|
|
162
|
+
- customer-scoped read access for `orders` and `order_items`
|
|
163
|
+
- service-role management access for orders, order items, inventory, taxes, and
|
|
164
|
+
currencies
|
|
165
|
+
|
|
166
|
+
## Migration Structure
|
|
167
|
+
|
|
168
|
+
### Current reality: squash generations (`GGNNN`)
|
|
169
|
+
|
|
170
|
+
The folder holds exactly **one squash generation**. File names are `GGNNN_name.sql`:
|
|
171
|
+
`GG` is the generation (two digits, `02` and up), `NNN` the sequence inside it (three
|
|
172
|
+
digits, contiguous from `000`), `name` lowercase snake case. The first five slots of every
|
|
173
|
+
generation are fixed:
|
|
174
|
+
|
|
175
|
+
| File | What it is |
|
|
176
|
+
| :-- | :-- |
|
|
177
|
+
| `02000_catchup_gen1.sql` | generation 1's forward migrations, replayed **once and version-aware** on databases that sit behind; runs first so the baseline below is a no-op afterwards (details below) |
|
|
178
|
+
| `02001_baseline_schema.sql` | enums, functions, tables, sequences, defaults (`IF NOT EXISTS` / `CREATE OR REPLACE`) plus the re-attached `auth.users` → `handle_new_user` trigger |
|
|
179
|
+
| `02002_baseline_constraints_and_indexes.sql` | primary/unique/check + foreign-key constraints (catalog-guarded) and every index |
|
|
180
|
+
| `02003_baseline_security_and_grants.sql` | RLS enablement, policies (`DROP … IF EXISTS` first), triggers, grants |
|
|
181
|
+
| `02004_baseline_seed.sql` | canonical demo content (no users, no secrets), `ON CONFLICT DO NOTHING`; runs **only on an empty database** and then records the generation it was born at in `site_settings.migration_baseline_generation` |
|
|
182
|
+
| `02005_…` onward | ordinary forward migrations, appended one at a time |
|
|
183
|
+
|
|
184
|
+
Generation 2 was built on 2026-09-10 from generation 1 — the retired 14-digit files
|
|
185
|
+
`00000000000000`–`00000000000042`, themselves the 2026-07 squash of the original 45 — by
|
|
186
|
+
`tools/scripts/rebaseline-transform.mjs` from a fresh-apply `pg_dump`, and verified against
|
|
187
|
+
that fresh apply (schema byte-identical; data identical except the generation marker). The
|
|
188
|
+
retired files live only in git history.
|
|
189
|
+
|
|
190
|
+
**The next number is the highest sequence on disk + 1, in the same generation.**
|
|
191
|
+
`npm run db:migrate:check` prints it and refuses to run on a file that does not match the
|
|
192
|
+
scheme (`tools/scripts/lib/migration-naming.js`; the same lint runs in both generators and
|
|
193
|
+
in `tools/scripts/migration-naming.test.ts` against the real folder). Never copy a "next is
|
|
194
|
+
N" out of a doc or a memory, and never use timestamps.
|
|
195
|
+
|
|
196
|
+
Why the scheme looks like this:
|
|
197
|
+
|
|
198
|
+
- **Digits only.** The Supabase CLI silently skips any file that is not `<digits>_name.sql`
|
|
199
|
+
(verified on CLI 2.107: `squash2_000_x.sql` is skipped with a warning; `02000_x.sql` is
|
|
200
|
+
accepted). The CLI is still on the production path (`db:migrate`, history repair).
|
|
201
|
+
- **Fixed width.** Every applier — the CLI's pending walk, Postgres' `ORDER BY` on
|
|
202
|
+
`supabase_migrations.schema_migrations`, this repo's own appliers — compares versions as
|
|
203
|
+
plain strings, so `020` and `0200` would interleave.
|
|
204
|
+
- **Second digit never 0.** Every database created before the generation-2 squash still
|
|
205
|
+
carries the legacy `000000000000xx` versions in its history; `0G…` with `G ≥ 1` sorts
|
|
206
|
+
after all of them. Timestamps (`2026…`) would still sort after every generation below 20,
|
|
207
|
+
but the lint rejects them so nobody has to reason about that.
|
|
208
|
+
|
|
209
|
+
### How a squash crosses live databases
|
|
210
|
+
|
|
211
|
+
A new generation gets **new versions**, so every one of its files is pending on every
|
|
212
|
+
existing database (production, the sandbox, every downstream install). That is by design,
|
|
213
|
+
and it is safe because of three properties:
|
|
214
|
+
|
|
215
|
+
- **The catch-up replays only what is missing.** `02000_catchup_gen1.sql` is one
|
|
216
|
+
`DO` block; each retired file is embedded as a dollar-quoted string and executed only if
|
|
217
|
+
its version is not recorded in `supabase_migrations.schema_migrations` (or, for Docker
|
|
218
|
+
installs, its file stem in `public._nextblock_docker_migrations`). A verbatim replay would
|
|
219
|
+
be wrong: a migration whose guard is "insert unless X exists" fires again once a later
|
|
220
|
+
migration removed X (generation 1's home promo), and copy-fix chains re-apply on rewritten
|
|
221
|
+
content — both were observed on a replay over a fully migrated database during the build.
|
|
222
|
+
The whole block is skipped on an empty database (no schema yet — the baseline follows) and
|
|
223
|
+
on a database whose `migration_baseline_generation` is already ≥ 2; it sets that marker
|
|
224
|
+
when it finishes. It runs first because the baseline DDL is idempotent only against the
|
|
225
|
+
final schema: `CREATE TABLE IF NOT EXISTS` skips an old-shape table and the next comment
|
|
226
|
+
or index on a newer column fails (observed on a database stopped at generation-1 `020`).
|
|
227
|
+
- **The baseline DDL is idempotent** against the final schema, so on a database the
|
|
228
|
+
catch-up has just brought to the end of generation 1 it changes nothing.
|
|
229
|
+
- **The seed is guarded.** It runs only when `languages` and `site_settings` are both
|
|
230
|
+
empty. Its explicit-id `INSERT`s would otherwise re-create demo rows an operator deleted.
|
|
231
|
+
|
|
232
|
+
What each kind of database needs:
|
|
233
|
+
|
|
234
|
+
- **Production (was at the end of generation 1):** record the squash, run nothing —
|
|
235
|
+
`npm run db:migrate:repair-history:check -- --reconcile-squash` prints the plan, the same
|
|
236
|
+
command without `:check` reverts the retired versions and marks `02000`–`02004` applied.
|
|
237
|
+
`supabase db push` refuses to run while retired versions remain in the remote history, so
|
|
238
|
+
this comes first; `db:migrate:check` says so.
|
|
239
|
+
- **A database that sat behind generation 1:** cross with the lenient applier first —
|
|
240
|
+
`npm run update -- --db-only` — which tolerates retired history rows (the catch-up reads
|
|
241
|
+
them to decide what to replay) and records what it applies; then reconcile as above.
|
|
242
|
+
`--reconcile-squash` detects this case and refuses to revert too early.
|
|
243
|
+
- **The sandbox:** its reset payload wipes `public`, replays the folder from empty
|
|
244
|
+
(seed runs, marker set, catch-up skipped) and re-records the generation's versions.
|
|
245
|
+
- **Downstream installs (Vercel, `npm create nextblock`, Docker):** nothing to do. The
|
|
246
|
+
`/setup` wizard, the build hook, `npm run update` and the Docker runner all apply pending
|
|
247
|
+
files in order and cross the squash automatically.
|
|
248
|
+
- **A database whose history was wiped:** `db:migrate` refuses to apply the baseline when
|
|
249
|
+
the remote history is completely empty, because the catch-up would then replay everything.
|
|
250
|
+
Repair the history first (`npm run db:migrate:repair-history`), or use
|
|
251
|
+
`db:migrate:fresh` if the database really is new.
|
|
252
|
+
|
|
253
|
+
### Production migration policy
|
|
254
|
+
|
|
255
|
+
NextBlock has live Supabase data. Treat migrations as append-only for any
|
|
256
|
+
production or shared database change.
|
|
257
|
+
|
|
258
|
+
- Do not edit, recycle, squash, reorder, or delete migration files that may
|
|
259
|
+
already be recorded in a shared or production Supabase project.
|
|
260
|
+
- Add a new forward-only `.sql` file under
|
|
261
|
+
`libs/db/src/supabase/migrations` for each new schema/data change.
|
|
262
|
+
- Keep migrations non-destructive by default. Avoid dropping or rewriting data
|
|
263
|
+
that may include orders, users, payments, or customer records.
|
|
264
|
+
- Run `npm run db:migrate:check` before `npm run db:migrate`. **Read its pending
|
|
265
|
+
list** — do not just look for a success line. If you added a migration and the
|
|
266
|
+
check reports `Pending: 0`, that file will never run (see below).
|
|
267
|
+
- **Supabase matches migration history by version only, never by content.** A file
|
|
268
|
+
whose 14-digit version is already recorded remotely is skipped in silence — no
|
|
269
|
+
error, no output. That is why the check prints the pending list and warns when a
|
|
270
|
+
version is recorded remotely with no local file behind it.
|
|
271
|
+
- If an existing database whose history was wiped lists the baseline files
|
|
272
|
+
(`02001_baseline_schema.sql` …) as pending, do not replay them blindly. Use
|
|
273
|
+
`npm run db:migrate:repair-history:check`, then `npm run db:migrate:repair-history`
|
|
274
|
+
(it auto-detects the applied high-water mark from the tables that exist; override with
|
|
275
|
+
`--through=<version>`), then rerun `npm run db:migrate:check`.
|
|
276
|
+
- If the check shows retired 14-digit versions "recorded remotely with no local file" next to
|
|
277
|
+
a pending `02000`–`02004`, the database has not crossed the squash yet — see "How a
|
|
278
|
+
squash crosses live databases" above.
|
|
279
|
+
- Use `npm run db:migrate:fresh` only for a brand-new empty database.
|
|
280
|
+
|
|
281
|
+
#### Why `db:migrate:check` is read-only by construction
|
|
282
|
+
|
|
283
|
+
On 2026-08-10 the check applied migration `00000000000017` to the production
|
|
284
|
+
project while printing `DRY RUN: migrations will *not* be pushed` and `Dry run
|
|
285
|
+
complete. No database changes were applied.` The `--check` path then ran
|
|
286
|
+
`supabase link --yes` followed by `supabase db push --dry-run` (Supabase CLI
|
|
287
|
+
v2.107); which of the two executed the SQL was never established, and the decisive
|
|
288
|
+
probe would have written a row to the production migration history.
|
|
289
|
+
|
|
290
|
+
`tools/scripts/push-db-migrations.js` no longer runs either on the check path. It
|
|
291
|
+
now runs only `supabase migration list` — a pure read — and derives the pending set
|
|
292
|
+
by diffing local files against remote history. Consequences worth keeping:
|
|
293
|
+
|
|
294
|
+
- The check links nothing. An unlinked repo is told to run `supabase link` itself
|
|
295
|
+
rather than having project state written underneath a command called "check".
|
|
296
|
+
- The check needs no `SUPABASE_ACCESS_TOKEN`, because only linking did.
|
|
297
|
+
- The apply path derives its baseline-replay guard from the same read instead of
|
|
298
|
+
regex-scraping `db push --dry-run` output, and returns early when nothing is
|
|
299
|
+
pending, so `db push` is never invoked without work to do.
|
|
300
|
+
- `parseMigrationList` is unit-tested in `tools/scripts/push-db-migrations.test.ts`.
|
|
301
|
+
|
|
302
|
+
If a future CLI upgrade tempts you back toward `db push --dry-run` for previewing:
|
|
303
|
+
don't. A command named `check` must not be able to write.
|
|
304
|
+
|
|
305
|
+
### How to read the folder
|
|
306
|
+
|
|
307
|
+
Read `02001_baseline_schema.sql`, then `02002` and `02003`, then the seed — in that
|
|
308
|
+
order they are the cleanest under-the-hood blueprint for:
|
|
309
|
+
|
|
310
|
+
- which tables exist
|
|
311
|
+
- what triggers and functions are available
|
|
312
|
+
- what security rules are enforced
|
|
313
|
+
- what default content and configuration are seeded
|
|
314
|
+
|
|
315
|
+
Skip `02000_catchup_gen1.sql` unless you are debugging an upgrade: it is generation 1's
|
|
316
|
+
history, kept only so databases that sat behind can converge. Everything from `02005`
|
|
317
|
+
upward is an ordinary forward migration and reads as a changelog.
|
|
318
|
+
|
|
319
|
+
If you need to understand whether the platform really supports something, check the
|
|
320
|
+
migration file first, then trace the corresponding route or library code.
|
|
321
|
+
|
|
322
|
+
### Squashing migrations (re-baseline runbook)
|
|
323
|
+
|
|
324
|
+
Do this rarely — a squash retires every forward migration written since the last one, and
|
|
325
|
+
every live database has to cross it. Downstream installs exist, so **a squash always ships a
|
|
326
|
+
catch-up**. Generation `G` replaces generation `G-1`; the steps below produced generation 2
|
|
327
|
+
and are what the next squash repeats with `G = 3`.
|
|
328
|
+
|
|
329
|
+
1. **Preconditions.** Production and the sandbox are at the last version of the current
|
|
330
|
+
generation and `npm run db:migrate:check` is clean. Docker Desktop is running.
|
|
331
|
+
`psql` and `pg_dump` 17 are on `PATH`. Nothing in this runbook touches a shared
|
|
332
|
+
database.
|
|
333
|
+
2. **Fresh-apply the current generation** to a throwaway database. The repo's compose file
|
|
334
|
+
is the easiest source of a real Supabase-shaped Postgres with `auth.users`:
|
|
335
|
+
`docker compose -p nbsquash --env-file <scratch>/.env up -d db auth` with an env file
|
|
336
|
+
holding `POSTGRES_PASSWORD`, `JWT_SECRET` (≥ 32 chars), `POSTGRES_PORT_EXTERNAL`
|
|
337
|
+
(pick a port outside `netsh interface ipv4 show excludedportrange protocol=tcp`; 54329
|
|
338
|
+
is Hyper-V-reserved on this machine, 15432 worked) and dummy values for the other
|
|
339
|
+
interpolated variables. Wait until `select to_regclass('auth.users')` is non-null, then
|
|
340
|
+
apply every file in order with `psql -v ON_ERROR_STOP=1 -1 -f`, recording each version
|
|
341
|
+
in `supabase_migrations.schema_migrations` exactly like the real appliers do — the
|
|
342
|
+
catch-up reads that table, so the harness must fill it. **Apply LF-normalized copies**
|
|
343
|
+
(`tr -d '
|
|
344
|
+
CRLF (git checkouts) and LF (tool-written files), and a multi-line `replace()` pattern
|
|
345
|
+
only matches content seeded with the same line endings — building generation 2 from the
|
|
346
|
+
raw tree silently lost migration 042's copy change. The git-canonical form is LF.
|
|
347
|
+
3. **Dump.** Schema: `pg_dump -n public -s --no-owner --no-tablespaces --no-security-labels
|
|
348
|
+
--no-publications --no-subscriptions -T public._nextblock_docker_migrations > schema.sql`.
|
|
349
|
+
Data: `pg_dump -n public -a --column-inserts --on-conflict-do-nothing --no-owner
|
|
350
|
+
--exclude-table-data=public.profiles -T public._nextblock_docker_migrations > data.sql`.
|
|
351
|
+
4. **Transform.** `node tools/scripts/rebaseline-transform.mjs <dumpDir> <outDir>
|
|
352
|
+
--generation G --catchup-from libs/db/src/supabase/migrations --catchup-after <the
|
|
353
|
+
current generation's seed version, e.g. 02004>`. It classifies every statement, adds the
|
|
354
|
+
idempotency guards, wraps the seed in its empty-database guard, builds the version-aware
|
|
355
|
+
catch-up, normalizes install-state rows (`is_admin_created` → `false`,
|
|
356
|
+
`system_configuration` → `{}`), drops `profiles` data and the Docker tracking table,
|
|
357
|
+
and strips every carriage return (see the comments in the script for why each of these
|
|
358
|
+
exists — every one closes a defect found while building generation 2). It prints object
|
|
359
|
+
counts and flags anything it could not classify.
|
|
360
|
+
5. **Validate — all of these, every time.** Recreate the throwaway stack between runs
|
|
361
|
+
(`docker compose -p nbsquash … down -v`). Apply LF files everywhere and compare
|
|
362
|
+
`pg_dump` output with comments dropped, carriage returns stripped, apply-time values
|
|
363
|
+
masked (SQL and JSON timestamps, `form_key`s and `form_endpoints` keys, `site_themes`
|
|
364
|
+
ids — all generated at seed time), and `INSERT`s sorted. `tools/scripts/rebaseline-harness.sh`
|
|
365
|
+
does all of this:
|
|
366
|
+
- fresh new generation **==** fresh old generation (schema identical; data identical
|
|
367
|
+
except the `migration_baseline_generation` row);
|
|
368
|
+
- re-applying all five files on that database changes nothing;
|
|
369
|
+
- old generation applied part-way (e.g. through its 20th file, versions recorded) then
|
|
370
|
+
the new generation **==** fresh old generation;
|
|
371
|
+
- old generation applied fully then the new generation **==** fresh old generation;
|
|
372
|
+
- the same part-way case with versions recorded only in
|
|
373
|
+
`public._nextblock_docker_migrations` (Docker installs) **==** fresh old generation;
|
|
374
|
+
- negative seed test: delete a `site_settings` row on a populated database, re-run the
|
|
375
|
+
seed file, the row stays deleted.
|
|
376
|
+
6. **Swap the folder.** Delete every file of the old generation, copy the five new ones in,
|
|
377
|
+
run `npx vitest run tools/scripts` (the naming test now enforces the new generation).
|
|
378
|
+
7. **Regenerate the artifacts:** `npm run generate:migrations-bundle && npm run
|
|
379
|
+
generate:sandbox`. A squash never changes the schema, so `npm run db:types` must
|
|
380
|
+
produce no diff.
|
|
381
|
+
8. **Update the docs:** this section (generation number, date, the table above),
|
|
382
|
+
`CLAUDE.md`, `libs/db/CLAUDE.md`, `AGENTS.md`, `docs/05`, the migration table in the
|
|
383
|
+
technical specification, and any code comment that cites a retired file name.
|
|
384
|
+
9. **Ship:** republish `@nextblock-cms/db` (minor bump — standalone installs get the
|
|
385
|
+
folder from the package), `npm run sync:create-nextblock`, commit. Publish the package
|
|
386
|
+
before pushing the template, or `npm create nextblock` pins a version that does not exist.
|
|
387
|
+
10. **Cross the live databases** as described in "How a squash crosses live databases":
|
|
388
|
+
production via `--reconcile-squash`, the sandbox via its reset.
|
|
389
|
+
11. **The first new migration is `G005`.** Never reuse a retired number.
|
|
390
|
+
|
|
391
|
+
## Important Site Settings in Active Use
|
|
392
|
+
|
|
393
|
+
These keys are actively referenced by the current codebase:
|
|
394
|
+
|
|
395
|
+
- `enabled_payment_providers`
|
|
396
|
+
- `ecommerce_inventory_settings`
|
|
397
|
+
- `invoice_settings`
|
|
398
|
+
- `footer_copyright`
|
|
399
|
+
- `is_admin_created`
|
|
400
|
+
|
|
401
|
+
There are many more seeded settings, but these are the most important ones for
|
|
402
|
+
understanding current runtime behavior.
|