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.
@@ -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
- `00000000000005_setup_functions_and_triggers.sql` defines:
51
-
52
- - `handle_new_user()`
53
- - `on_auth_user_created` trigger on `auth.users`
54
-
55
- That trigger:
56
-
57
- - creates the first local admin automatically
58
- - creates later users as `USER`
59
- - inserts or updates `profiles`
60
- - copies selected metadata such as `full_name`, avatar URL, and GitHub username
61
-
62
- ### CMS authorization
63
-
64
- The CMS shell in `app/cms/CmsClientLayout.tsx` currently expects:
65
-
66
- - an authenticated user
67
- - a resolved profile role of `ADMIN` or `WRITER`
68
-
69
- Writers and admins can enter the CMS. Admin-only navigation is used for
70
- settings such as payments, shipping, users, and some branding/config surfaces.
71
-
72
- ### No live app middleware file
73
-
74
- There is a generic Supabase middleware helper in `libs/db/src/lib/supabase`,
75
- but there is no live `apps/nextblock/middleware.ts` file in the current app.
76
- Document the callback, layout, and RLS model as the active auth path rather
77
- than assuming middleware-based route protection is in use.
78
-
79
- ## Schema Overview
80
-
81
- ### Core platform tables
82
-
83
- Defined primarily in `00000000000001_setup_cms_core.sql`:
84
-
85
- - `site_settings`
86
- - `profiles`
87
- - `user_addresses`
88
- - `languages`
89
- - `media`
90
- - `translations`
91
- - `logos`
92
-
93
- ### Content tables
94
-
95
- Defined primarily in `00000000000002_setup_content_tables.sql`:
96
-
97
- - `posts`
98
- - `pages`
99
- - `blocks`
100
- - `navigation_items`
101
- - `page_revisions`
102
- - `post_revisions`
103
- - `product_revisions` (added in `00000000000016`, alongside `products.version`)
104
-
105
- ### Commerce tables
106
-
107
- All defined in the baseline schema `00000000000000` (the numbers `00000000000003`/
108
- `00000000000004` in earlier revisions of this doc were pre-re-baseline file names):
109
-
110
- - `products`
111
- - `product_media`
112
- - `product_attributes`
113
- - `product_attribute_terms`
114
- - `product_variants`
115
- - `inventory_items`
116
- - `variant_attribute_mapping`
117
- - `package_activations`
118
- - `freemius_plans`
119
- - `freemius_pricing`
120
- - `orders`
121
- - `order_items`
122
- - `shipping_zones`
123
- - `shipping_zone_locations`
124
- - `shipping_zone_methods`
125
- - `tax_rates`
126
- - `currencies`
127
-
128
- ### Post-baseline tables
129
-
130
- Added after the squashed baseline by later migrations:
131
-
132
- - `categories` and `product_categories` catalog organization
133
- (migration `00000000000019`; translated via `00000000000020`)
134
- - `custom_block_definitions` — data-driven custom block registry
135
- (migration `00000000000023`; see [10-CUSTOM-BLOCKS.md](./10-CUSTOM-BLOCKS.md))
136
- - `ucp_cart_sessions` — persisted cart sessions (migration `00000000000024`)
137
- - a `blocks` JSONB column plus `product_id` link for block-based product
138
- descriptions (migration `00000000000017`)
139
-
140
- ## Row Level Security Patterns
141
-
142
- `00000000000006_setup_rls_and_grants.sql` is the consolidated RLS file.
143
-
144
- The high-level access model is:
145
-
146
- - public read access for languages, media, translations, published content, and
147
- several storefront commerce tables
148
- - authenticated self-service access for user addresses and customer-owned
149
- orders
150
- - `ADMIN` or `WRITER` write access for most CMS authoring tables
151
- - `ADMIN`-only write access for higher-risk configuration surfaces
152
- - `service_role` full access where background jobs or system syncs need it
153
-
154
- Commerce-specific policy highlights include:
155
-
156
- - public read access for products, product media, product attributes, variants,
157
- shipping zones, shipping methods, tax rates, and active currencies
158
- - customer-scoped read access for `orders` and `order_items`
159
- - service-role management access for orders, order items, inventory, taxes, and
160
- currencies
161
-
162
- ## Migration Structure
163
-
164
- ### Current reality
165
-
166
- The folder was **re-baselined in 2026-07**: the previous 45 migrations
167
- (`00000000000000`–`00000000000044`) were squashed into a four-file idempotent
168
- baseline, generated from a fresh-apply `pg_dump` by
169
- `tools/scripts/rebaseline-transform.mjs` and verified byte-identical to the old
170
- tree. The current sequence is:
171
-
172
- - `00000000000000_baseline_schema.sql` enums, functions, tables, sequences
173
- (all `IF NOT EXISTS` / `CREATE OR REPLACE`) plus the re-attached `auth.users`
174
- → `handle_new_user` trigger.
175
- - `00000000000001_baseline_constraints_and_indexes.sql` primary/unique/check
176
- and foreign-key constraints (guarded) plus all indexes.
177
- - `00000000000002_baseline_security_and_grants.sql` RLS enablement, policies
178
- (`DROP IF EXISTS` first), triggers, and grants.
179
- - `00000000000003_baseline_seed.sql` canonical demo content, `ON CONFLICT DO
180
- NOTHING` (no users, no secrets).
181
-
182
- Every file is fully idempotent. Existing databases already have versions
183
- `000`–`003` recorded, so both appliers skip the baseline — it only runs on a
184
- fresh/empty database.
185
-
186
- Seed *data-fix* migrations (the `seed_seo_*` series, `reposition_marketing_*`) must scope
187
- every UPDATE by **content signature and parent** (`WHERE page_id = v_home AND content::text
188
- LIKE '%Blazing-Fast%'`), never by a numeric `blocks.id`. `blocks`, `pages` and `posts` ids
189
- are identity columns, so any install that created or deleted a row since the baseline has
190
- different ids from the one the migration was written against including the sandbox, which
191
- re-creates rows on every reset. `00000000000035` keyed its product-block updates by id and,
192
- on drifted installs, wrote French Commerce Pro copy over the home-page Live Demo promo and
193
- over the first block of the French install guide; `00000000000037` carries the
194
- signature-scoped repair. Signature guards also make a migration idempotent for free: once
195
- the copy is replaced, the guard no longer matches.
196
-
197
- `00000000000004` was the first migration appended after that re-baseline, not the
198
- one still to be written the folder has grown well past it. **To find the next
199
- number, list `libs/db/src/supabase/migrations` and take the one after the highest
200
- file on disk.** Never copy a hardcoded "next is N" out of a doc.
201
-
202
- ### Production migration policy
203
-
204
- NextBlock has live Supabase data. Treat migrations as append-only for any
205
- production or shared database change.
206
-
207
- - Do not edit, recycle, squash, reorder, or delete migration files that may
208
- already be recorded in a shared or production Supabase project.
209
- - Add a new forward-only `.sql` file under
210
- `libs/db/src/supabase/migrations` for each new schema/data change.
211
- - Keep migrations non-destructive by default. Avoid dropping or rewriting data
212
- that may include orders, users, payments, or customer records.
213
- - Run `npm run db:migrate:check` before `npm run db:migrate`. **Read its pending
214
- list** — do not just look for a success line. If you added a migration and the
215
- check reports `Pending: 0`, that file will never run (see below).
216
- - **Supabase matches migration history by version only, never by content.** A file
217
- whose 14-digit version is already recorded remotely is skipped in silence — no
218
- error, no output. That is why the check prints the pending list and warns when a
219
- version is recorded remotely with no local file behind it.
220
- - If an existing database lists old baseline files such as
221
- `00000000000000_baseline_schema.sql` as pending, do not replay them. Use
222
- `npm run db:migrate:repair-history:check`, then
223
- `npm run db:migrate:repair-history --through=00000000000003` (the baseline's
224
- top file creates no tables, so auto-detection otherwise stops at `000`), then
225
- rerun `npm run db:migrate:check`.
226
- - Use `npm run db:migrate:fresh` only for a brand-new empty database.
227
-
228
- #### Why `db:migrate:check` is read-only by construction
229
-
230
- On 2026-08-10 the check applied migration `00000000000017` to the production
231
- project while printing `DRY RUN: migrations will *not* be pushed` and `Dry run
232
- complete. No database changes were applied.` The `--check` path then ran
233
- `supabase link --yes` followed by `supabase db push --dry-run` (Supabase CLI
234
- v2.107); which of the two executed the SQL was never established, and the decisive
235
- probe would have written a row to the production migration history.
236
-
237
- `tools/scripts/push-db-migrations.js` no longer runs either on the check path. It
238
- now runs only `supabase migration list` a pure read — and derives the pending set
239
- by diffing local files against remote history. Consequences worth keeping:
240
-
241
- - The check links nothing. An unlinked repo is told to run `supabase link` itself
242
- rather than having project state written underneath a command called "check".
243
- - The check needs no `SUPABASE_ACCESS_TOKEN`, because only linking did.
244
- - The apply path derives its baseline-replay guard from the same read instead of
245
- regex-scraping `db push --dry-run` output, and returns early when nothing is
246
- pending, so `db push` is never invoked without work to do.
247
- - `parseMigrationList` is unit-tested in `tools/scripts/push-db-migrations.test.ts`.
248
-
249
- If a future CLI upgrade tempts you back toward `db push --dry-run` for previewing:
250
- don't. A command named `check` must not be able to write.
251
-
252
- ### Category map
253
-
254
- | Migration file | Domain | What it covers |
255
- | :-- | :-- | :-- |
256
- | `00000000000000_baseline_schema.sql` | Core, CMS, Commerce | all enums, 40 functions, 49 tables + sequences (idempotent), and the `auth.users` → `handle_new_user` bootstrap trigger |
257
- | `00000000000001_baseline_constraints_and_indexes.sql` | Core, CMS, Commerce | all primary/unique/check + foreign-key constraints (guarded) and every index |
258
- | `00000000000002_baseline_security_and_grants.sql` | Security | RLS enablement on every table, all policies, timestamp/business triggers, grants |
259
- | `00000000000003_baseline_seed.sql` | Seeds | canonical demo content languages, currencies, site settings, translations, media, pages/posts/blocks, navigation, shipping defaults — all `ON CONFLICT DO NOTHING` |
260
-
261
- The pre-2026-07 history (foundation/enums, cms_core, content_tables, catalog,
262
- fulfillment, functions_and_triggers, rls_and_grants, indexes, the seed files, and
263
- later additions like custom block definitions, product blocks, categories, cart
264
- sessions, drafts, privacy/MFA, system alerts, interactions) is all folded into the
265
- four files above; the earlier per-file boundaries survive only as comment headers
266
- inside the generated SQL.
267
-
268
- ### How to read the folder
269
-
270
- Read the migrations in lexical order from `00000000000000` upward.
271
-
272
- That sequence is the cleanest under-the-hood blueprint for:
273
-
274
- - which tables exist
275
- - what triggers and functions are available
276
- - what security rules are enforced
277
- - what default content and configuration are seeded
278
-
279
- If you need to understand whether the platform really supports something, check
280
- the migration file first, then trace the corresponding route or library code.
281
-
282
- ## Important Site Settings in Active Use
283
-
284
- These keys are actively referenced by the current codebase:
285
-
286
- - `enabled_payment_providers`
287
- - `ecommerce_inventory_settings`
288
- - `invoice_settings`
289
- - `footer_copyright`
290
- - `is_admin_created`
291
-
292
- There are many more seeded settings, but these are the most important ones for
293
- understanding current runtime behavior.
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.