create-nextblock 0.16.1 → 0.16.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.
Files changed (258) hide show
  1. package/docker-template/.dockerignore +1 -1
  2. package/libs/db/tsconfig.lib.json +3 -3
  3. package/libs/editor/tsconfig.lib.json +3 -3
  4. package/libs/ui/tsconfig.lib.json +3 -3
  5. package/libs/utils/tsconfig.json +3 -3
  6. package/package.json +50 -50
  7. package/project.json +19 -19
  8. package/templates/nextblock-template/.dockerignore +1 -1
  9. package/templates/nextblock-template/.swcrc +30 -30
  10. package/templates/nextblock-template/AGENTS.md +9 -9
  11. package/templates/nextblock-template/CLAUDE.md +1 -1
  12. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
  13. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
  14. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
  15. package/templates/nextblock-template/app/ToasterProvider.tsx +26 -26
  16. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
  17. package/templates/nextblock-template/app/actions/contactSellerActions.test.ts +280 -280
  18. package/templates/nextblock-template/app/actions/contactSellerActions.ts +222 -222
  19. package/templates/nextblock-template/app/actions/email-retry.test.ts +62 -62
  20. package/templates/nextblock-template/app/actions/email.ts +241 -241
  21. package/templates/nextblock-template/app/actions/formActions.ts +245 -245
  22. package/templates/nextblock-template/app/actions/interactions.ts +489 -489
  23. package/templates/nextblock-template/app/actions/threadActions.ts +166 -166
  24. package/templates/nextblock-template/app/actions/visibilityActions.ts +210 -210
  25. package/templates/nextblock-template/app/api/ai/seo/alt-text/route.ts +221 -221
  26. package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -186
  27. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
  28. package/templates/nextblock-template/app/api/checkout/route.ts +162 -162
  29. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +3092 -3092
  30. package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
  31. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  32. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  33. package/templates/nextblock-template/app/api/view/route.ts +114 -114
  34. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
  35. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
  36. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  37. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  38. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
  39. package/templates/nextblock-template/app/checkout/page.tsx +57 -57
  40. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -558
  41. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
  42. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  43. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -304
  44. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +406 -406
  45. package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
  46. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  47. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  48. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +254 -254
  49. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  50. package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
  51. package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
  52. package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
  53. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  54. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  55. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  56. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  57. package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
  58. package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
  59. package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
  60. package/templates/nextblock-template/app/cms/layout.tsx +101 -101
  61. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  62. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  63. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  64. package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -661
  65. package/templates/nextblock-template/app/cms/messages/actions.ts +404 -404
  66. package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -333
  67. package/templates/nextblock-template/app/cms/messages/page.tsx +87 -87
  68. package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -37
  69. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  70. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +649 -649
  71. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +618 -618
  72. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
  73. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  74. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  75. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  76. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  77. package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
  78. package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
  79. package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
  80. package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
  81. package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
  82. package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
  83. package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
  84. package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
  85. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  86. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  87. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
  88. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  89. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  90. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  91. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
  92. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
  93. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
  94. package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
  95. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  96. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  97. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  98. package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -514
  99. package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -529
  100. package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -57
  101. package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -448
  102. package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -93
  103. package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -46
  104. package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -47
  105. package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
  106. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  107. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  108. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  109. package/templates/nextblock-template/app/layout.tsx +671 -671
  110. package/templates/nextblock-template/app/lib/seo.ts +319 -319
  111. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  112. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  113. package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
  114. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  115. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  116. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  117. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  118. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  119. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  120. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  121. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  122. package/templates/nextblock-template/app/providers.tsx +96 -96
  123. package/templates/nextblock-template/app/robots.ts +123 -123
  124. package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -164
  125. package/templates/nextblock-template/app/thread/[token]/route.ts +57 -57
  126. package/templates/nextblock-template/app/thread/layout.tsx +15 -15
  127. package/templates/nextblock-template/app/thread/page.tsx +98 -98
  128. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
  129. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
  130. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
  131. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
  132. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
  133. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
  134. package/templates/nextblock-template/components/BlockRenderer.tsx +312 -312
  135. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  136. package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
  137. package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -188
  138. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  139. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
  140. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
  141. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  142. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  143. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  144. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
  145. package/templates/nextblock-template/components/Header.tsx +38 -38
  146. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  147. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  148. package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -378
  149. package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -426
  150. package/templates/nextblock-template/components/SiteScripts.tsx +56 -56
  151. package/templates/nextblock-template/components/StaffReplies.tsx +102 -102
  152. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  153. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  154. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
  155. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
  156. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  157. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
  158. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
  159. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -18
  160. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -20
  161. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -25
  162. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -385
  163. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
  164. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -157
  165. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -34
  166. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -612
  167. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
  168. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
  169. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
  170. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  171. package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -32
  172. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  173. package/templates/nextblock-template/components/form-message.tsx +32 -32
  174. package/templates/nextblock-template/components/seo/GenerateMetaButton.tsx +137 -137
  175. package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +244 -244
  176. package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -749
  177. package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -195
  178. package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -144
  179. package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -243
  180. package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -110
  181. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  182. package/templates/nextblock-template/components/theme-icon.tsx +78 -78
  183. package/templates/nextblock-template/components/theme-switcher.tsx +85 -85
  184. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  185. package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -44
  186. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  187. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -77
  188. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +372 -372
  189. package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -309
  190. package/templates/nextblock-template/docs/README.md +42 -42
  191. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12506 -12506
  192. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  193. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  194. package/templates/nextblock-template/index.d.ts +7 -7
  195. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  196. package/templates/nextblock-template/lib/blocks/blockColors.test.ts +134 -134
  197. package/templates/nextblock-template/lib/blocks/blockColors.ts +175 -175
  198. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -761
  199. package/templates/nextblock-template/lib/blocks/inlineScriptNonce.ts +20 -20
  200. package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -64
  201. package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -0
  202. package/templates/nextblock-template/lib/cms/payments-reminder.ts +30 -23
  203. package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -42
  204. package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -162
  205. package/templates/nextblock-template/lib/config/email-settings.ts +323 -323
  206. package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -57
  207. package/templates/nextblock-template/lib/cortex-ai/alt-text-request.ts +86 -86
  208. package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -60
  209. package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -59
  210. package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -39
  211. package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -70
  212. package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -93
  213. package/templates/nextblock-template/lib/messages/thread-token.ts +157 -157
  214. package/templates/nextblock-template/lib/messages/threads.ts +579 -579
  215. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  216. package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -26
  217. package/templates/nextblock-template/lib/search/types.ts +27 -27
  218. package/templates/nextblock-template/lib/seo/alt-text-write-back.test.ts +154 -154
  219. package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -109
  220. package/templates/nextblock-template/lib/seo/block-content.ts +123 -123
  221. package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -242
  222. package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -204
  223. package/templates/nextblock-template/lib/seo/page-audit-context.tsx +140 -140
  224. package/templates/nextblock-template/lib/seo/page-document.test.ts +350 -350
  225. package/templates/nextblock-template/lib/seo/page-document.ts +412 -412
  226. package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -479
  227. package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -466
  228. package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -102
  229. package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -41
  230. package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -370
  231. package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -510
  232. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +177 -177
  233. package/templates/nextblock-template/lib/site-scripts/revisions.ts +71 -71
  234. package/templates/nextblock-template/lib/site-scripts/types.ts +46 -46
  235. package/templates/nextblock-template/lib/site-url.test.ts +89 -89
  236. package/templates/nextblock-template/lib/site-url.ts +102 -102
  237. package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -124
  238. package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -31
  239. package/templates/nextblock-template/lib/themes/tokens.ts +143 -143
  240. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
  241. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
  242. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
  243. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
  244. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
  245. package/templates/nextblock-template/lib/zod-config.ts +5 -5
  246. package/templates/nextblock-template/next-env.d.ts +1 -2
  247. package/templates/nextblock-template/package.json +1 -1
  248. package/templates/nextblock-template/postcss.config.js +6 -6
  249. package/templates/nextblock-template/scripts/backup.js +115 -115
  250. package/templates/nextblock-template/scripts/restore.js +385 -385
  251. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  252. package/templates/nextblock-template/tailwind.config.js +25 -25
  253. package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
  254. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  255. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  256. package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
  257. package/templates/nextblock-template/tools/update.mjs +1303 -1303
  258. package/tsconfig.base.json +3 -3
@@ -1,177 +1,177 @@
1
- // AUTO-GENERATED by tools/scripts/generate-migrations-bundle.ts — do not edit by hand.
2
- // Regenerate after adding a migration: npm run generate:migrations-bundle
3
- //
4
- // Embedded forward-only migrations so the /setup wizard can apply the schema on
5
- // serverless hosts (Vercel) where libs/db isn't on the function filesystem.
6
-
7
- export interface BundledMigration {
8
- /** Numeric version prefix, e.g. "00000000000030". */
9
- version: string;
10
- /** Original migration filename. */
11
- name: string;
12
- /** Raw SQL contents. */
13
- sql: string;
14
- }
15
-
16
- export const MIGRATIONS_BUNDLE: BundledMigration[] = [
17
- {
18
- "version": "00000000000000",
19
- "name": "00000000000000_baseline_schema.sql",
20
- "sql": "-- AUTO-GENERATED baseline (re-baseline of migrations 000..044). Idempotent; safe to replay.\r\n-- 00 · schema: enums, functions, tables, sequences, defaults\r\n-- Regenerate via tools/scripts/rebaseline-transform.mjs. Do not hand-edit.\r\n\r\nSET check_function_bodies = false;\r\n\r\nCREATE SCHEMA IF NOT EXISTS public;\r\n\r\nCOMMENT ON SCHEMA public IS 'standard public schema';\r\n\r\nDO $rb$ BEGIN\r\nCREATE TYPE public.approval_status AS ENUM (\r\n 'pending',\r\n 'approved',\r\n 'denied'\r\n);\r\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\r\n\r\nDO $rb$ BEGIN\r\nCREATE TYPE public.interaction_type AS ENUM (\r\n 'review',\r\n 'comment'\r\n);\r\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\r\n\r\nDO $rb$ BEGIN\r\nCREATE TYPE public.menu_location AS ENUM (\r\n 'HEADER',\r\n 'FOOTER',\r\n 'SIDEBAR'\r\n);\r\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\r\n\r\nDO $rb$ BEGIN\r\nCREATE TYPE public.page_status AS ENUM (\r\n 'draft',\r\n 'published',\r\n 'archived'\r\n);\r\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\r\n\r\nDO $rb$ BEGIN\r\nCREATE TYPE public.revision_type AS ENUM (\r\n 'snapshot',\r\n 'diff'\r\n);\r\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\r\n\r\nDO $rb$ BEGIN\r\nCREATE TYPE public.user_role AS ENUM (\r\n 'ADMIN',\r\n 'WRITER',\r\n 'USER'\r\n);\r\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\r\n\r\nCREATE OR REPLACE FUNCTION public.apply_order_inventory_deduction(p_order_id uuid) RETURNS void\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_track_quantities boolean := public.get_ecommerce_track_quantities();\r\n v_item record;\r\n v_inventory_deducted_at timestamptz;\r\n v_sku text;\r\n v_current_quantity integer;\r\nBEGIN\r\n SELECT inventory_deducted_at\r\n INTO v_inventory_deducted_at\r\n FROM public.orders\r\n WHERE id = p_order_id\r\n FOR UPDATE;\r\n\r\n IF NOT FOUND OR v_inventory_deducted_at IS NOT NULL THEN\r\n RETURN;\r\n END IF;\r\n\r\n IF NOT v_track_quantities THEN\r\n UPDATE public.orders\r\n SET inventory_deducted_at = now()\r\n WHERE id = p_order_id;\r\n\r\n RETURN;\r\n END IF;\r\n\r\n FOR v_item IN\r\n SELECT\r\n product_id,\r\n variant_id,\r\n SUM(quantity)::integer AS quantity\r\n FROM public.order_items\r\n WHERE order_id = p_order_id\r\n GROUP BY product_id, variant_id\r\n LOOP\r\n v_sku := NULL;\r\n v_current_quantity := 0;\r\n\r\n IF v_item.variant_id IS NOT NULL THEN\r\n SELECT\r\n sku,\r\n GREATEST(COALESCE(stock_quantity, 0), 0)\r\n INTO v_sku,\r\n v_current_quantity\r\n FROM public.product_variants\r\n WHERE id = v_item.variant_id\r\n LIMIT 1;\r\n ELSIF v_item.product_id IS NOT NULL THEN\r\n SELECT\r\n sku,\r\n GREATEST(COALESCE(stock, 0), 0)\r\n INTO v_sku,\r\n v_current_quantity\r\n FROM public.products\r\n WHERE id = v_item.product_id\r\n LIMIT 1;\r\n END IF;\r\n\r\n IF NULLIF(trim(v_sku), '') IS NULL THEN\r\n CONTINUE;\r\n END IF;\r\n\r\n INSERT INTO public.inventory_items (sku, quantity)\r\n VALUES (v_sku, v_current_quantity)\r\n ON CONFLICT (sku) DO NOTHING;\r\n\r\n UPDATE public.inventory_items\r\n SET\r\n quantity = GREATEST(COALESCE(quantity, 0) - v_item.quantity, 0),\r\n updated_at = now()\r\n WHERE sku = v_sku;\r\n END LOOP;\r\n\r\n UPDATE public.orders\r\n SET inventory_deducted_at = now()\r\n WHERE id = p_order_id;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.assign_order_invoice_metadata(p_order_id uuid, p_paid_at timestamp with time zone DEFAULT now()) RETURNS TABLE(invoice_number text, paid_at timestamp with time zone)\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_order public.orders%ROWTYPE;\r\n v_effective_paid_at timestamptz;\r\nBEGIN\r\n SELECT *\r\n INTO v_order\r\n FROM public.orders\r\n WHERE id = p_order_id\r\n FOR UPDATE;\r\n\r\n IF NOT FOUND THEN\r\n RAISE EXCEPTION 'Order % not found', p_order_id;\r\n END IF;\r\n\r\n v_effective_paid_at := COALESCE(v_order.paid_at, p_paid_at, now(), v_order.created_at);\r\n\r\n UPDATE public.orders\r\n SET\r\n invoice_number = COALESCE(v_order.invoice_number, public.generate_order_invoice_number()),\r\n paid_at = v_effective_paid_at\r\n WHERE id = p_order_id\r\n RETURNING orders.invoice_number, orders.paid_at\r\n INTO invoice_number, paid_at;\r\n\r\n RETURN NEXT;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.clear_currency_price_overrides(target_currency text) RETURNS void\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nDECLARE\r\n v_target_currency text := upper(trim(target_currency));\r\nBEGIN\r\n IF v_target_currency = '' THEN\r\n RETURN;\r\n END IF;\r\n\r\n UPDATE public.products\r\n SET\r\n prices = COALESCE(prices, '{}'::jsonb) - v_target_currency,\r\n sale_prices = CASE\r\n WHEN sale_prices IS NULL THEN NULL\r\n WHEN sale_prices - v_target_currency = '{}'::jsonb THEN NULL\r\n ELSE sale_prices - v_target_currency\r\n END,\r\n updated_at = now()\r\n WHERE COALESCE(prices, '{}'::jsonb) ? v_target_currency\r\n OR COALESCE(sale_prices, '{}'::jsonb) ? v_target_currency;\r\n\r\n UPDATE public.product_variants\r\n SET\r\n prices = COALESCE(prices, '{}'::jsonb) - v_target_currency,\r\n sale_prices = CASE\r\n WHEN sale_prices IS NULL THEN NULL\r\n WHEN sale_prices - v_target_currency = '{}'::jsonb THEN NULL\r\n ELSE sale_prices - v_target_currency\r\n END,\r\n updated_at = now()\r\n WHERE COALESCE(prices, '{}'::jsonb) ? v_target_currency\r\n OR COALESCE(sale_prices, '{}'::jsonb) ? v_target_currency;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.is_valid_custom_block_fields(candidate jsonb) RETURNS boolean\r\n LANGUAGE sql IMMUTABLE\r\n SET search_path TO ''\r\n AS $_$\r\n SELECT CASE\r\n WHEN jsonb_typeof(candidate) <> 'array' THEN false\r\n ELSE\r\n NOT EXISTS (\r\n SELECT 1\r\n FROM jsonb_array_elements(candidate) AS field(value)\r\n WHERE jsonb_typeof(field.value) <> 'object'\r\n OR jsonb_typeof(field.value -> 'key') IS DISTINCT FROM 'string'\r\n OR jsonb_typeof(field.value -> 'label') IS DISTINCT FROM 'string'\r\n OR jsonb_typeof(field.value -> 'type') IS DISTINCT FROM 'string'\r\n OR field.value ->> 'key' !~ '^[a-z][a-z0-9_]*$'\r\n OR field.value ->> 'type' NOT IN ('text', 'rich-text', 'image_r2', 'db_relation')\r\n )\r\n AND (\r\n SELECT COUNT(*) = COUNT(DISTINCT field.value ->> 'key')\r\n FROM jsonb_array_elements(candidate) AS field(value)\r\n )\r\n END;\r\n$_$;\r\n\r\nCREATE OR REPLACE FUNCTION public.is_valid_custom_block_layout_schema(candidate jsonb) RETURNS boolean\r\n LANGUAGE sql IMMUTABLE\r\n SET search_path TO ''\r\n AS $$\r\n SELECT CASE\r\n WHEN jsonb_typeof(candidate) <> 'object' THEN false\r\n ELSE candidate ->> 'type' IN ('container', 'field_render')\r\n END;\r\n$$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.custom_block_definitions (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n slug text NOT NULL,\r\n name text NOT NULL,\r\n description text DEFAULT ''::text NOT NULL,\r\n fields jsonb DEFAULT '[]'::jsonb NOT NULL,\r\n layout_schema jsonb NOT NULL,\r\n is_original boolean DEFAULT true NOT NULL,\r\n CONSTRAINT custom_block_definitions_fields_check CHECK (public.is_valid_custom_block_fields(fields)),\r\n CONSTRAINT custom_block_definitions_layout_schema_check CHECK (public.is_valid_custom_block_layout_schema(layout_schema)),\r\n CONSTRAINT custom_block_definitions_name_check CHECK ((length(TRIM(BOTH FROM name)) > 0)),\r\n CONSTRAINT custom_block_definitions_slug_check CHECK ((slug ~ '^[a-z][a-z0-9-]*$'::text))\r\n);\r\n\r\nCOMMENT ON TABLE public.custom_block_definitions IS 'Registry for user-created block definitions rendered from database JSONB without runtime code compilation.';\r\n\r\nCOMMENT ON COLUMN public.custom_block_definitions.fields IS 'Strict JSONB field declarations for data-rendered custom blocks.';\r\n\r\nCOMMENT ON COLUMN public.custom_block_definitions.layout_schema IS 'Open-ended recursive layout schema consumed by the dynamic layout renderer.';\r\n\r\nCOMMENT ON COLUMN public.custom_block_definitions.is_original IS 'False when a definition was created by duplicating an existing registry row.';\r\n\r\nCREATE OR REPLACE FUNCTION public.duplicate_block_definition(target_id uuid) RETURNS public.custom_block_definitions\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $_$\r\nDECLARE\r\n source_definition public.custom_block_definitions%ROWTYPE;\r\n copied_definition public.custom_block_definitions%ROWTYPE;\r\n base_slug text;\r\n copy_slug text;\r\n copy_index integer := 1;\r\nBEGIN\r\n IF auth.role() <> 'service_role'\r\n AND COALESCE((SELECT public.get_current_user_role())::text, '') NOT IN ('ADMIN', 'WRITER') THEN\r\n RAISE EXCEPTION 'Not authorized to duplicate custom block definitions.'\r\n USING ERRCODE = '42501';\r\n END IF;\r\n\r\n SELECT *\r\n INTO source_definition\r\n FROM public.custom_block_definitions\r\n WHERE id = target_id;\r\n\r\n IF NOT FOUND THEN\r\n RAISE EXCEPTION 'Custom block definition % not found.', target_id\r\n USING ERRCODE = 'P0002';\r\n END IF;\r\n\r\n base_slug := regexp_replace(source_definition.slug, '-copy(-[0-9]+)?$', '');\r\n copy_slug := base_slug || '-copy';\r\n\r\n WHILE EXISTS (\r\n SELECT 1\r\n FROM public.custom_block_definitions\r\n WHERE slug = copy_slug\r\n ) LOOP\r\n copy_index := copy_index + 1;\r\n copy_slug := base_slug || '-copy-' || copy_index;\r\n END LOOP;\r\n\r\n INSERT INTO public.custom_block_definitions (\r\n id,\r\n slug,\r\n name,\r\n description,\r\n fields,\r\n layout_schema,\r\n is_original\r\n )\r\n VALUES (\r\n gen_random_uuid(),\r\n copy_slug,\r\n source_definition.name || ' Copy',\r\n source_definition.description,\r\n source_definition.fields,\r\n source_definition.layout_schema,\r\n false\r\n )\r\n RETURNING *\r\n INTO copied_definition;\r\n\r\n RETURN copied_definition;\r\nEND;\r\n$_$;\r\n\r\nCREATE OR REPLACE FUNCTION public.format_order_invoice_number(p_value bigint) RETURNS text\r\n LANGUAGE sql IMMUTABLE\r\n SET search_path TO ''\r\n AS $$\r\n SELECT 'INV-' || lpad(p_value::text, 6, '0');\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.generate_order_invoice_number() RETURNS text\r\n LANGUAGE sql\r\n SET search_path TO ''\r\n AS $$\r\n SELECT public.format_order_invoice_number(nextval('public.order_invoice_number_seq'));\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.get_current_user_role() RETURNS public.user_role\r\n LANGUAGE sql STABLE\r\n SET search_path TO 'public'\r\n AS $$\r\n SELECT role FROM public.profiles WHERE id = auth.uid();\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.get_default_currency_code() RETURNS text\r\n LANGUAGE sql STABLE\r\n SET search_path TO ''\r\n AS $$\r\n SELECT COALESCE(\r\n (\r\n SELECT upper(code)\r\n FROM public.currencies\r\n WHERE is_default = true\r\n ORDER BY updated_at DESC, created_at DESC, code ASC\r\n LIMIT 1\r\n ),\r\n 'USD'\r\n );\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.get_ecommerce_track_quantities() RETURNS boolean\r\n LANGUAGE plpgsql STABLE\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_value jsonb;\r\n v_raw text;\r\nBEGIN\r\n SELECT value\r\n INTO v_value\r\n FROM public.site_settings\r\n WHERE key = 'ecommerce_inventory_settings';\r\n\r\n IF v_value IS NULL THEN\r\n RETURN true;\r\n END IF;\r\n\r\n IF jsonb_typeof(v_value) = 'object' THEN\r\n v_raw := NULLIF(v_value->>'track_quantities', '');\r\n ELSE\r\n v_raw := NULLIF(trim(BOTH '\"' FROM v_value::text), '');\r\n END IF;\r\n\r\n IF v_raw IS NULL THEN\r\n RETURN true;\r\n END IF;\r\n\r\n IF lower(v_raw) IN ('false', 'f', '0', 'no', 'off') THEN\r\n RETURN false;\r\n END IF;\r\n\r\n RETURN true;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.get_my_claim(claim text) RETURNS jsonb\r\n LANGUAGE sql STABLE\r\n SET search_path TO ''\r\n AS $$\r\n SELECT COALESCE(current_setting('request.jwt.claims', true)::jsonb ->> claim, NULL)::jsonb\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_blocks_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_coupon_freemius_mappings_write() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.freemius_product_id := btrim(NEW.freemius_product_id);\r\n NEW.freemius_coupon_code := upper(regexp_replace(btrim(NEW.freemius_coupon_code), '\\s+', '', 'g'));\r\n NEW.sync_status := lower(btrim(COALESCE(NEW.sync_status, 'pending')));\r\n NEW.updated_at := now();\r\n\r\n IF NEW.created_at IS NULL THEN\r\n NEW.created_at := now();\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_coupons_write() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.code := upper(regexp_replace(btrim(NEW.code), '\\s+', '', 'g'));\r\n NEW.name := btrim(NEW.name);\r\n NEW.provider_scope := lower(btrim(NEW.provider_scope));\r\n NEW.discount_type := lower(btrim(NEW.discount_type));\r\n NEW.freemius_sync_status := lower(btrim(COALESCE(NEW.freemius_sync_status, 'not_synced')));\r\n NEW.updated_at := now();\r\n\r\n IF NEW.created_at IS NULL THEN\r\n NEW.created_at := now();\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_default_currency_change() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n IF TG_OP = 'INSERT' THEN\r\n IF NEW.is_default THEN\r\n PERFORM public.sync_legacy_price_columns_for_currency(NEW.code);\r\n END IF;\r\n ELSIF NEW.is_default\r\n AND (\r\n OLD.is_default IS DISTINCT FROM NEW.is_default\r\n OR OLD.code IS DISTINCT FROM NEW.code\r\n ) THEN\r\n PERFORM public.sync_legacy_price_columns_for_currency(NEW.code);\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_inventory_item_change() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_sku text := COALESCE(NEW.sku, OLD.sku);\r\nBEGIN\r\n PERFORM public.sync_inventory_cache_for_sku(v_sku);\r\n RETURN COALESCE(NEW, OLD);\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_inventory_items_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_languages_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_media_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_navigation_items_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS trigger\r\n LANGUAGE plpgsql SECURITY DEFINER\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n admin_flag_set boolean := false;\r\n user_role public.user_role;\r\n v_full_name text;\r\n v_avatar_url text;\r\n v_github_username text;\r\n v_provider text;\r\nBEGIN\r\n INSERT INTO public.site_settings (key, value)\r\n VALUES ('is_admin_created', 'false'::jsonb)\r\n ON CONFLICT (key) DO NOTHING;\r\n\r\n SELECT COALESCE(value::jsonb::boolean, false)\r\n INTO admin_flag_set\r\n FROM public.site_settings\r\n WHERE key = 'is_admin_created'\r\n FOR UPDATE;\r\n\r\n IF admin_flag_set = false THEN\r\n user_role := 'ADMIN'::public.user_role;\r\n\r\n UPDATE public.site_settings\r\n SET value = 'true'::jsonb\r\n WHERE key = 'is_admin_created';\r\n ELSE\r\n user_role := 'USER'::public.user_role;\r\n END IF;\r\n\r\n v_full_name := NEW.raw_user_meta_data->>'full_name';\r\n v_avatar_url := NEW.raw_user_meta_data->>'avatar_url';\r\n v_provider := NEW.raw_app_meta_data->>'provider';\r\n\r\n IF v_provider = 'github' OR (NEW.raw_user_meta_data->>'iss') LIKE '%github%' THEN\r\n v_github_username := COALESCE(\r\n NEW.raw_user_meta_data->>'user_name',\r\n NEW.raw_user_meta_data->>'preferred_username'\r\n );\r\n ELSE\r\n v_github_username := NULL;\r\n END IF;\r\n\r\n INSERT INTO public.profiles (\r\n id,\r\n role,\r\n full_name,\r\n avatar_url,\r\n github_username\r\n )\r\n VALUES (\r\n NEW.id,\r\n user_role,\r\n v_full_name,\r\n v_avatar_url,\r\n v_github_username\r\n )\r\n ON CONFLICT (id) DO UPDATE SET\r\n full_name = EXCLUDED.full_name,\r\n avatar_url = EXCLUDED.avatar_url,\r\n github_username = EXCLUDED.github_username;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_pages_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_posts_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_product_freemius_sale_coupons_write() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.freemius_product_id := btrim(NEW.freemius_product_id);\r\n NEW.freemius_coupon_code := upper(regexp_replace(btrim(NEW.freemius_coupon_code), '\\s+', '', 'g'));\r\n NEW.sync_status := lower(btrim(COALESCE(NEW.sync_status, 'pending')));\r\n NEW.updated_at := now();\r\n\r\n IF NEW.created_at IS NULL THEN\r\n NEW.created_at := now();\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_shipping_zone_locations_write() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.country_code = upper(btrim(NEW.country_code));\r\n NEW.state_code = CASE\r\n WHEN NEW.state_code IS NULL OR btrim(NEW.state_code) = '' THEN NULL\r\n ELSE upper(btrim(NEW.state_code))\r\n END;\r\n NEW.postal_code = CASE\r\n WHEN NEW.postal_code IS NULL OR btrim(NEW.postal_code) = '' THEN NULL\r\n ELSE upper(btrim(NEW.postal_code))\r\n END;\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_tax_rates_write() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.country_code = upper(btrim(NEW.country_code));\r\n NEW.state_code = CASE\r\n WHEN NEW.state_code IS NULL OR btrim(NEW.state_code) = '' THEN NULL\r\n ELSE upper(btrim(NEW.state_code))\r\n END;\r\n NEW.tax_name = btrim(NEW.tax_name);\r\n NEW.updated_at = now();\r\n\r\n IF NEW.created_at IS NULL THEN\r\n NEW.created_at = now();\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_ucp_cart_sessions_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.is_admin() RETURNS boolean\r\n LANGUAGE sql STABLE\r\n SET search_path TO 'public'\r\n AS $$\r\n SELECT role = 'ADMIN' FROM public.profiles WHERE id = auth.uid();\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.is_valid_currency_amount_map(amounts jsonb) RETURNS boolean\r\n LANGUAGE sql IMMUTABLE\r\n SET search_path TO ''\r\n AS $_$\r\n SELECT CASE\r\n WHEN amounts IS NULL THEN false\r\n WHEN jsonb_typeof(amounts) <> 'object' THEN false\r\n WHEN amounts = '{}'::jsonb THEN false\r\n ELSE NOT EXISTS (\r\n SELECT 1\r\n FROM jsonb_each(amounts) AS entry\r\n WHERE entry.key !~ '^[A-Z]{3}$'\r\n OR jsonb_typeof(entry.value) <> 'number'\r\n OR entry.value::text !~ '^[0-9]+$'\r\n )\r\n END;\r\n$_$;\r\n\r\nCREATE OR REPLACE FUNCTION public.is_valid_sale_price_map(prices jsonb, sale_prices jsonb) RETURNS boolean\r\n LANGUAGE sql IMMUTABLE\r\n SET search_path TO ''\r\n AS $_$\r\n SELECT CASE\r\n WHEN sale_prices IS NULL THEN true\r\n WHEN jsonb_typeof(sale_prices) <> 'object' THEN false\r\n WHEN sale_prices = '{}'::jsonb THEN true\r\n WHEN prices IS NULL OR jsonb_typeof(prices) <> 'object' THEN false\r\n ELSE NOT EXISTS (\r\n SELECT 1\r\n FROM jsonb_each(sale_prices) AS entry\r\n WHERE entry.key !~ '^[A-Z]{3}$'\r\n OR NOT (prices ? entry.key)\r\n OR jsonb_typeof(entry.value) <> 'number'\r\n OR entry.value::text !~ '^[0-9]+$'\r\n OR entry.value::text::numeric > (prices ->> entry.key)::numeric\r\n )\r\n END;\r\n$_$;\r\n\r\nCREATE OR REPLACE FUNCTION public.normalize_currency_amount_map(amounts jsonb) RETURNS jsonb\r\n LANGUAGE sql IMMUTABLE\r\n SET search_path TO ''\r\n AS $_$\r\n SELECT CASE\r\n WHEN amounts IS NULL THEN '{}'::jsonb\r\n WHEN jsonb_typeof(amounts) <> 'object' THEN amounts\r\n ELSE COALESCE(\r\n (\r\n SELECT jsonb_object_agg(\r\n upper(trim(entry.key)),\r\n CASE\r\n WHEN jsonb_typeof(entry.value) = 'number'\r\n AND entry.value::text ~ '^[0-9]+$' THEN\r\n to_jsonb((entry.value::text)::bigint)\r\n ELSE\r\n entry.value\r\n END\r\n )\r\n FROM jsonb_each(amounts) AS entry\r\n ),\r\n '{}'::jsonb\r\n )\r\n END;\r\n$_$;\r\n\r\nCREATE OR REPLACE FUNCTION public.set_currency_defaults() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.code := upper(trim(NEW.code));\r\n NEW.updated_at := now();\r\n\r\n IF NEW.is_default THEN\r\n NEW.is_active := true;\r\n NEW.exchange_rate := 1;\r\n NEW.auto_update_exchange_rate := false;\r\n NEW.auto_sync_product_prices := false;\r\n NEW.exchange_rate_source := COALESCE(NULLIF(NEW.exchange_rate_source, ''), 'store-default');\r\n NEW.exchange_rate_updated_at := COALESCE(NEW.exchange_rate_updated_at, now());\r\n\r\n UPDATE public.currencies\r\n SET is_default = false,\r\n updated_at = now()\r\n WHERE id IS DISTINCT FROM NEW.id\r\n AND is_default = true;\r\n ELSIF NULLIF(NEW.exchange_rate_source, '') IS NULL THEN\r\n NEW.exchange_rate_source := NULL;\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.set_current_timestamp_updated_at() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nDECLARE\r\n _new record;\r\nBEGIN\r\n _new := NEW;\r\n _new.updated_at = now();\r\n RETURN _new;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.sync_currency_price_maps() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nDECLARE\r\n v_default_currency text := public.get_default_currency_code();\r\n v_price_map_changed boolean := false;\r\n v_legacy_changed boolean := false;\r\nBEGIN\r\n NEW.prices := public.normalize_currency_amount_map(COALESCE(NEW.prices, '{}'::jsonb));\r\n NEW.sale_prices := public.normalize_currency_amount_map(COALESCE(NEW.sale_prices, '{}'::jsonb));\r\n\r\n IF NEW.sale_prices = '{}'::jsonb THEN\r\n NEW.sale_prices := NULL;\r\n END IF;\r\n\r\n IF TG_OP = 'UPDATE' THEN\r\n v_price_map_changed :=\r\n NEW.prices IS DISTINCT FROM OLD.prices\r\n OR NEW.sale_prices IS DISTINCT FROM OLD.sale_prices;\r\n v_legacy_changed :=\r\n NEW.price IS DISTINCT FROM OLD.price\r\n OR NEW.sale_price IS DISTINCT FROM OLD.sale_price;\r\n END IF;\r\n\r\n IF TG_OP = 'INSERT' THEN\r\n IF NEW.prices ? v_default_currency THEN\r\n NEW.price := (NEW.prices ->> v_default_currency)::integer;\r\n ELSE\r\n NEW.prices := NEW.prices || jsonb_build_object(\r\n v_default_currency,\r\n GREATEST(COALESCE(NEW.price, 0), 0)\r\n );\r\n END IF;\r\n\r\n IF NEW.sale_prices IS NOT NULL AND NEW.sale_prices ? v_default_currency THEN\r\n NEW.sale_price := (NEW.sale_prices ->> v_default_currency)::integer;\r\n ELSIF NEW.sale_price IS NOT NULL THEN\r\n NEW.sale_prices := COALESCE(NEW.sale_prices, '{}'::jsonb)\r\n || jsonb_build_object(v_default_currency, GREATEST(NEW.sale_price, 0));\r\n END IF;\r\n\r\n RETURN NEW;\r\n END IF;\r\n\r\n IF v_price_map_changed AND NOT v_legacy_changed THEN\r\n IF NOT (NEW.prices ? v_default_currency) THEN\r\n NEW.prices := NEW.prices || jsonb_build_object(\r\n v_default_currency,\r\n GREATEST(COALESCE(OLD.price, NEW.price, 0), 0)\r\n );\r\n END IF;\r\n\r\n NEW.price := (NEW.prices ->> v_default_currency)::integer;\r\n NEW.sale_price := CASE\r\n WHEN NEW.sale_prices IS NOT NULL AND NEW.sale_prices ? v_default_currency THEN\r\n (NEW.sale_prices ->> v_default_currency)::integer\r\n ELSE\r\n NULL\r\n END;\r\n\r\n RETURN NEW;\r\n END IF;\r\n\r\n NEW.prices := NEW.prices || jsonb_build_object(\r\n v_default_currency,\r\n GREATEST(COALESCE(NEW.price, 0), 0)\r\n );\r\n\r\n IF NEW.sale_price IS NULL THEN\r\n IF NEW.sale_prices IS NOT NULL THEN\r\n NEW.sale_prices := NEW.sale_prices - v_default_currency;\r\n\r\n IF NEW.sale_prices = '{}'::jsonb THEN\r\n NEW.sale_prices := NULL;\r\n END IF;\r\n END IF;\r\n ELSE\r\n NEW.sale_prices := COALESCE(NEW.sale_prices, '{}'::jsonb)\r\n || jsonb_build_object(v_default_currency, GREATEST(NEW.sale_price, 0));\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.sync_inventory_cache_for_sku(p_sku text) RETURNS void\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_quantity integer := 0;\r\nBEGIN\r\n IF NULLIF(trim(p_sku), '') IS NULL THEN\r\n RETURN;\r\n END IF;\r\n\r\n SELECT quantity\r\n INTO v_quantity\r\n FROM public.inventory_items\r\n WHERE sku = p_sku\r\n LIMIT 1;\r\n\r\n v_quantity := COALESCE(v_quantity, 0);\r\n\r\n UPDATE public.product_variants\r\n SET\r\n stock_quantity = v_quantity,\r\n updated_at = now()\r\n WHERE sku = p_sku;\r\n\r\n UPDATE public.products AS products\r\n SET\r\n stock = v_quantity,\r\n updated_at = now()\r\n WHERE products.sku = p_sku\r\n AND NOT EXISTS (\r\n SELECT 1\r\n FROM public.product_variants\r\n WHERE product_id = products.id\r\n );\r\n\r\n UPDATE public.products AS products\r\n SET\r\n stock = COALESCE((\r\n SELECT SUM(COALESCE(inventory.quantity, 0))\r\n FROM public.product_variants AS variants\r\n LEFT JOIN public.inventory_items AS inventory\r\n ON inventory.sku = variants.sku\r\n WHERE variants.product_id = products.id\r\n ), 0),\r\n updated_at = now()\r\n WHERE EXISTS (\r\n SELECT 1\r\n FROM public.product_variants AS variants\r\n WHERE variants.product_id = products.id\r\n AND variants.sku = p_sku\r\n );\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.sync_legacy_price_columns_for_currency(target_currency text) RETURNS void\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nDECLARE\r\n v_target_currency text := upper(trim(target_currency));\r\nBEGIN\r\n UPDATE public.products\r\n SET\r\n prices = CASE\r\n WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN prices\r\n ELSE COALESCE(prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, price)\r\n END,\r\n sale_prices = CASE\r\n WHEN sale_price IS NULL THEN sale_prices\r\n WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN sale_prices\r\n ELSE COALESCE(sale_prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, sale_price)\r\n END,\r\n price = CASE\r\n WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN\r\n (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer\r\n ELSE\r\n price\r\n END,\r\n sale_price = CASE\r\n WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN\r\n (sale_prices ->> v_target_currency)::integer\r\n ELSE\r\n sale_price\r\n END,\r\n updated_at = now()\r\n WHERE\r\n NOT (COALESCE(prices, '{}'::jsonb) ? v_target_currency)\r\n OR (\r\n sale_price IS NOT NULL\r\n AND (sale_prices IS NULL OR NOT (sale_prices ? v_target_currency))\r\n )\r\n OR (\r\n COALESCE(prices, '{}'::jsonb) ? v_target_currency\r\n AND price IS DISTINCT FROM (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer\r\n )\r\n OR (\r\n sale_prices IS NOT NULL\r\n AND sale_prices ? v_target_currency\r\n AND sale_price IS DISTINCT FROM (sale_prices ->> v_target_currency)::integer\r\n );\r\n\r\n UPDATE public.product_variants\r\n SET\r\n prices = CASE\r\n WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN prices\r\n ELSE COALESCE(prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, price)\r\n END,\r\n sale_prices = CASE\r\n WHEN sale_price IS NULL THEN sale_prices\r\n WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN sale_prices\r\n ELSE COALESCE(sale_prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, sale_price)\r\n END,\r\n price = CASE\r\n WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN\r\n (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer\r\n ELSE\r\n price\r\n END,\r\n sale_price = CASE\r\n WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN\r\n (sale_prices ->> v_target_currency)::integer\r\n ELSE\r\n sale_price\r\n END,\r\n updated_at = now()\r\n WHERE\r\n NOT (COALESCE(prices, '{}'::jsonb) ? v_target_currency)\r\n OR (\r\n sale_price IS NOT NULL\r\n AND (sale_prices IS NULL OR NOT (sale_prices ? v_target_currency))\r\n )\r\n OR (\r\n COALESCE(prices, '{}'::jsonb) ? v_target_currency\r\n AND price IS DISTINCT FROM (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer\r\n )\r\n OR (\r\n sale_prices IS NOT NULL\r\n AND sale_prices ? v_target_currency\r\n AND sale_price IS DISTINCT FROM (sale_prices ->> v_target_currency)::integer\r\n );\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.sync_shipping_method_currency_maps() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nDECLARE\r\n v_source_currency text;\r\nBEGIN\r\n v_source_currency := upper(trim(COALESCE(NULLIF(NEW.cost_currency, ''), public.get_default_currency_code())));\r\n\r\n NEW.cost_currency := v_source_currency;\r\n NEW.currency_pricing_mode := COALESCE(NULLIF(lower(trim(NEW.currency_pricing_mode)), ''), 'auto');\r\n NEW.cost_amounts := public.normalize_currency_amount_map(COALESCE(NEW.cost_amounts, '{}'::jsonb));\r\n NEW.min_order_amounts := public.normalize_currency_amount_map(COALESCE(NEW.min_order_amounts, '{}'::jsonb));\r\n\r\n IF NEW.currency_pricing_mode NOT IN ('auto', 'manual') THEN\r\n RAISE EXCEPTION 'Unsupported shipping currency pricing mode: %', NEW.currency_pricing_mode;\r\n END IF;\r\n\r\n IF NEW.cost_amounts = '{}'::jsonb THEN\r\n NEW.cost_amounts := jsonb_build_object(v_source_currency, GREATEST(COALESCE(NEW.cost_amount, 0), 0));\r\n ELSIF NOT (NEW.cost_amounts ? v_source_currency) THEN\r\n NEW.cost_amounts := NEW.cost_amounts || jsonb_build_object(\r\n v_source_currency,\r\n GREATEST(COALESCE(NEW.cost_amount, 0), 0)\r\n );\r\n END IF;\r\n\r\n IF NEW.min_order_amounts = '{}'::jsonb THEN\r\n NEW.min_order_amounts := jsonb_build_object(\r\n v_source_currency,\r\n GREATEST(COALESCE(NEW.min_order_amount, 0), 0)\r\n );\r\n ELSIF NOT (NEW.min_order_amounts ? v_source_currency) THEN\r\n NEW.min_order_amounts := NEW.min_order_amounts || jsonb_build_object(\r\n v_source_currency,\r\n GREATEST(COALESCE(NEW.min_order_amount, 0), 0)\r\n );\r\n END IF;\r\n\r\n IF NEW.currency_pricing_mode = 'auto' THEN\r\n NEW.cost_amounts := jsonb_build_object(\r\n v_source_currency,\r\n GREATEST((NEW.cost_amounts ->> v_source_currency)::integer, 0)\r\n );\r\n NEW.min_order_amounts := jsonb_build_object(\r\n v_source_currency,\r\n GREATEST((NEW.min_order_amounts ->> v_source_currency)::integer, 0)\r\n );\r\n END IF;\r\n\r\n NEW.cost_amount := GREATEST((NEW.cost_amounts ->> v_source_currency)::integer, 0);\r\n NEW.min_order_amount := GREATEST((NEW.min_order_amounts ->> v_source_currency)::integer, 0);\r\n NEW.updated_at := now();\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.update_product_ratings() RETURNS trigger\r\n LANGUAGE plpgsql SECURITY DEFINER\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_product_id uuid;\r\n v_avg numeric(3,2);\r\n v_count integer;\r\nBEGIN\r\n -- Determine which product_id we need to update\r\n IF TG_OP = 'DELETE' THEN\r\n v_product_id := OLD.product_id;\r\n ELSE\r\n v_product_id := NEW.product_id;\r\n END IF;\r\n\r\n IF v_product_id IS NOT NULL THEN\r\n -- Calculate average and count of approved reviews for this product\r\n SELECT COALESCE(avg(rating), 0.00), count(*)\r\n INTO v_avg, v_count\r\n FROM public.cms_interactions\r\n WHERE product_id = v_product_id\r\n AND type = 'review'\r\n AND status = 'approved';\r\n\r\n -- Update products table\r\n UPDATE public.products\r\n SET average_rating = ROUND(COALESCE(v_avg, 0.00), 2),\r\n total_reviews = v_count\r\n WHERE id = v_product_id;\r\n END IF;\r\n\r\n -- Handle old product_id if it changed on UPDATE (e.g. transfer of reviews)\r\n IF TG_OP = 'UPDATE' AND OLD.product_id IS NOT NULL AND OLD.product_id <> NEW.product_id THEN\r\n SELECT COALESCE(avg(rating), 0.00), count(*)\r\n INTO v_avg, v_count\r\n FROM public.cms_interactions\r\n WHERE product_id = OLD.product_id\r\n AND type = 'review'\r\n AND status = 'approved';\r\n\r\n UPDATE public.products\r\n SET average_rating = ROUND(COALESCE(v_avg, 0.00), 2),\r\n total_reviews = v_count\r\n WHERE id = OLD.product_id;\r\n END IF;\r\n\r\n RETURN NULL;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.upsert_product_with_variants(product_payload jsonb) RETURNS uuid\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_product_id uuid := NULLIF(product_payload->>'id', '')::uuid;\r\n v_translation_group_id uuid := NULLIF(product_payload->>'translation_group_id', '')::uuid;\r\n v_product_type text := CASE\r\n WHEN product_payload->>'product_type' IN ('physical', 'digital') THEN\r\n product_payload->>'product_type'\r\n WHEN NULLIF(product_payload->>'freemius_product_id', '') IS NOT NULL\r\n OR NULLIF(product_payload->>'freemius_plan_id', '') IS NOT NULL THEN\r\n 'digital'\r\n ELSE\r\n 'physical'\r\n END;\r\n v_payment_provider text := CASE\r\n WHEN v_product_type = 'digital' THEN 'freemius'\r\n ELSE 'stripe'\r\n END;\r\n v_variants jsonb := COALESCE(product_payload->'variants', '[]'::jsonb);\r\n v_variant jsonb;\r\n v_variant_id uuid;\r\n v_term_id text;\r\n v_has_variants boolean := jsonb_typeof(v_variants) = 'array' AND jsonb_array_length(v_variants) > 0;\r\n v_total_variant_stock integer := 0;\r\nBEGIN\r\n IF NOT public.is_admin() THEN\r\n RAISE EXCEPTION 'Admin access required';\r\n END IF;\r\n\r\n IF v_has_variants THEN\r\n SELECT COALESCE(SUM(COALESCE((value->>'stock_quantity')::integer, 0)), 0)\r\n INTO v_total_variant_stock\r\n FROM jsonb_array_elements(v_variants);\r\n END IF;\r\n\r\n IF v_product_id IS NULL THEN\r\n INSERT INTO public.products (\r\n title,\r\n slug,\r\n sku,\r\n product_type,\r\n payment_provider,\r\n upc,\r\n stock,\r\n status,\r\n short_description,\r\n description_json,\r\n metadata,\r\n price,\r\n prices,\r\n sale_price,\r\n sale_prices,\r\n sale_start_at,\r\n sale_end_at,\r\n freemius_plan_id,\r\n freemius_product_id,\r\n trial_period_days,\r\n trial_requires_payment_method,\r\n language_id,\r\n translation_group_id\r\n )\r\n VALUES (\r\n product_payload->>'title',\r\n product_payload->>'slug',\r\n product_payload->>'sku',\r\n v_product_type,\r\n v_payment_provider,\r\n NULLIF(product_payload->>'upc', ''),\r\n CASE\r\n WHEN v_has_variants THEN v_total_variant_stock\r\n ELSE COALESCE((product_payload->>'stock')::integer, 0)\r\n END,\r\n COALESCE(product_payload->>'status', 'draft'),\r\n NULLIF(product_payload->>'short_description', ''),\r\n product_payload->'description_json',\r\n COALESCE(product_payload->'metadata', '{}'::jsonb),\r\n COALESCE((product_payload->>'price')::integer, 0),\r\n COALESCE(product_payload->'prices', '{}'::jsonb),\r\n CASE\r\n WHEN product_payload ? 'sale_price' AND product_payload->>'sale_price' <> '' THEN\r\n (product_payload->>'sale_price')::integer\r\n ELSE\r\n NULL\r\n END,\r\n CASE\r\n WHEN product_payload ? 'sale_prices' THEN COALESCE(product_payload->'sale_prices', '{}'::jsonb)\r\n ELSE NULL\r\n END,\r\n CASE\r\n WHEN product_payload ? 'sale_start_at' AND product_payload->>'sale_start_at' <> '' THEN\r\n (product_payload->>'sale_start_at')::timestamptz\r\n ELSE\r\n NULL\r\n END,\r\n CASE\r\n WHEN product_payload ? 'sale_end_at' AND product_payload->>'sale_end_at' <> '' THEN\r\n (product_payload->>'sale_end_at')::timestamptz\r\n ELSE\r\n NULL\r\n END,\r\n NULLIF(product_payload->>'freemius_plan_id', ''),\r\n NULLIF(product_payload->>'freemius_product_id', ''),\r\n COALESCE((product_payload->>'trial_period_days')::integer, 0),\r\n COALESCE((product_payload->>'trial_requires_payment_method')::boolean, false),\r\n (product_payload->>'language_id')::bigint,\r\n COALESCE(v_translation_group_id, gen_random_uuid())\r\n )\r\n RETURNING id INTO v_product_id;\r\n ELSE\r\n UPDATE public.products\r\n SET\r\n title = product_payload->>'title',\r\n slug = product_payload->>'slug',\r\n sku = product_payload->>'sku',\r\n product_type = v_product_type,\r\n payment_provider = v_payment_provider,\r\n upc = NULLIF(product_payload->>'upc', ''),\r\n stock = CASE\r\n WHEN v_has_variants THEN v_total_variant_stock\r\n ELSE COALESCE((product_payload->>'stock')::integer, 0)\r\n END,\r\n status = COALESCE(product_payload->>'status', status),\r\n short_description = NULLIF(product_payload->>'short_description', ''),\r\n description_json = product_payload->'description_json',\r\n metadata = COALESCE(product_payload->'metadata', '{}'::jsonb),\r\n price = COALESCE((product_payload->>'price')::integer, 0),\r\n prices = COALESCE(product_payload->'prices', '{}'::jsonb),\r\n sale_price = CASE\r\n WHEN product_payload ? 'sale_price' AND product_payload->>'sale_price' <> '' THEN\r\n (product_payload->>'sale_price')::integer\r\n ELSE\r\n NULL\r\n END,\r\n sale_prices = CASE\r\n WHEN product_payload ? 'sale_prices' THEN COALESCE(product_payload->'sale_prices', '{}'::jsonb)\r\n ELSE NULL\r\n END,\r\n sale_start_at = CASE\r\n WHEN product_payload ? 'sale_start_at' AND product_payload->>'sale_start_at' <> '' THEN\r\n (product_payload->>'sale_start_at')::timestamptz\r\n ELSE\r\n NULL\r\n END,\r\n sale_end_at = CASE\r\n WHEN product_payload ? 'sale_end_at' AND product_payload->>'sale_end_at' <> '' THEN\r\n (product_payload->>'sale_end_at')::timestamptz\r\n ELSE\r\n NULL\r\n END,\r\n freemius_plan_id = NULLIF(product_payload->>'freemius_plan_id', ''),\r\n freemius_product_id = NULLIF(product_payload->>'freemius_product_id', ''),\r\n trial_period_days = COALESCE((product_payload->>'trial_period_days')::integer, 0),\r\n trial_requires_payment_method = COALESCE((product_payload->>'trial_requires_payment_method')::boolean, false),\r\n language_id = COALESCE((product_payload->>'language_id')::bigint, language_id),\r\n translation_group_id = COALESCE(v_translation_group_id, translation_group_id),\r\n updated_at = now()\r\n WHERE id = v_product_id;\r\n\r\n IF NOT FOUND THEN\r\n RAISE EXCEPTION 'Product not found';\r\n END IF;\r\n END IF;\r\n\r\n DELETE FROM public.variant_attribute_mapping\r\n WHERE variant_id IN (\r\n SELECT id\r\n FROM public.product_variants\r\n WHERE product_id = v_product_id\r\n );\r\n\r\n DELETE FROM public.product_variants\r\n WHERE product_id = v_product_id;\r\n\r\n IF v_has_variants THEN\r\n FOR v_variant IN\r\n SELECT value FROM jsonb_array_elements(v_variants)\r\n LOOP\r\n INSERT INTO public.product_variants (\r\n product_id,\r\n sku,\r\n upc,\r\n price,\r\n prices,\r\n sale_price,\r\n sale_prices,\r\n sale_start_at,\r\n sale_end_at,\r\n stock_quantity,\r\n main_media_id\r\n )\r\n VALUES (\r\n v_product_id,\r\n v_variant->>'sku',\r\n NULLIF(v_variant->>'upc', ''),\r\n COALESCE((v_variant->>'price')::integer, 0),\r\n COALESCE(v_variant->'prices', '{}'::jsonb),\r\n CASE\r\n WHEN v_variant ? 'sale_price' AND v_variant->>'sale_price' <> '' THEN\r\n (v_variant->>'sale_price')::integer\r\n ELSE\r\n NULL\r\n END,\r\n CASE\r\n WHEN v_variant ? 'sale_prices' THEN COALESCE(v_variant->'sale_prices', '{}'::jsonb)\r\n ELSE NULL\r\n END,\r\n CASE\r\n WHEN v_variant ? 'sale_start_at' AND v_variant->>'sale_start_at' <> '' THEN\r\n (v_variant->>'sale_start_at')::timestamptz\r\n ELSE\r\n NULL\r\n END,\r\n CASE\r\n WHEN v_variant ? 'sale_end_at' AND v_variant->>'sale_end_at' <> '' THEN\r\n (v_variant->>'sale_end_at')::timestamptz\r\n ELSE\r\n NULL\r\n END,\r\n COALESCE((v_variant->>'stock_quantity')::integer, 0),\r\n NULLIF(v_variant->>'main_media_id', '')::uuid\r\n )\r\n RETURNING id INTO v_variant_id;\r\n\r\n FOR v_term_id IN\r\n SELECT jsonb_array_elements_text(COALESCE(v_variant->'attribute_term_ids', '[]'::jsonb))\r\n LOOP\r\n INSERT INTO public.variant_attribute_mapping (variant_id, attribute_term_id)\r\n VALUES (v_variant_id, v_term_id::uuid);\r\n END LOOP;\r\n END LOOP;\r\n END IF;\r\n\r\n RETURN v_product_id;\r\nEND;\r\n$$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.blocks (\r\n id bigint NOT NULL,\r\n page_id bigint,\r\n post_id bigint,\r\n language_id bigint NOT NULL,\r\n block_type text NOT NULL,\r\n content jsonb,\r\n \"order\" integer DEFAULT 0 NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n product_id uuid,\r\n CONSTRAINT check_exactly_one_parent CHECK ((((page_id IS NOT NULL) AND (post_id IS NULL) AND (product_id IS NULL)) OR ((post_id IS NOT NULL) AND (page_id IS NULL) AND (product_id IS NULL)) OR ((product_id IS NOT NULL) AND (page_id IS NULL) AND (post_id IS NULL))))\r\n);\r\n\r\nCOMMENT ON TABLE public.blocks IS 'Stores content blocks for pages and posts.';\r\n\r\nCOMMENT ON COLUMN public.blocks.block_type IS 'Type of the block, e.g., \"text\", \"image\".';\r\n\r\nCOMMENT ON COLUMN public.blocks.content IS 'JSONB content specific to the block_type.';\r\n\r\nCOMMENT ON COLUMN public.blocks.\"order\" IS 'Sort order of the block.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.blocks'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.blocks ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.blocks_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.categories (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n name text NOT NULL,\r\n slug text NOT NULL,\r\n description text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n name_translations jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n description_translations jsonb DEFAULT '{}'::jsonb NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.categories IS 'Product categories for organizing catalog items.';\r\n\r\nCOMMENT ON COLUMN public.categories.name_translations IS 'Translated category names (e.g. {\"fr\": \"Numérique\"}).';\r\n\r\nCOMMENT ON COLUMN public.categories.description_translations IS 'Translated category descriptions.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.cms_interactions (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n type public.interaction_type NOT NULL,\r\n status public.approval_status DEFAULT 'pending'::public.approval_status NOT NULL,\r\n content text NOT NULL,\r\n rating integer,\r\n user_id uuid NOT NULL,\r\n product_id uuid,\r\n post_id bigint,\r\n reactions jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT check_product_or_post CHECK ((((product_id IS NOT NULL) AND (post_id IS NULL)) OR ((post_id IS NOT NULL) AND (product_id IS NULL)))),\r\n CONSTRAINT check_rating_only_for_review CHECK ((((type = 'review'::public.interaction_type) AND (rating IS NOT NULL) AND (rating >= 1) AND (rating <= 5)) OR ((type = 'comment'::public.interaction_type) AND (rating IS NULL))))\r\n);\r\n\r\nCOMMENT ON TABLE public.cms_interactions IS 'Stores user-submitted product reviews and blog post comments.';\r\n\r\nCOMMENT ON COLUMN public.cms_interactions.rating IS 'Star rating 1-5, only populated for product reviews.';\r\n\r\nCOMMENT ON COLUMN public.cms_interactions.user_id IS 'References public.profiles.id';\r\n\r\nCOMMENT ON COLUMN public.cms_interactions.reactions IS 'JSONB structure tracking counts of reactions (likes, etc.).';\r\n\r\nCREATE TABLE IF NOT EXISTS public.content_drafts (\r\n id bigint NOT NULL,\r\n parent_type text NOT NULL,\r\n parent_id bigint NOT NULL,\r\n author_id uuid,\r\n base_version integer DEFAULT 1 NOT NULL,\r\n meta jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n blocks jsonb DEFAULT '[]'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT content_drafts_blocks_array CHECK ((jsonb_typeof(blocks) = 'array'::text)),\r\n CONSTRAINT content_drafts_meta_object CHECK ((jsonb_typeof(meta) = 'object'::text)),\r\n CONSTRAINT content_drafts_parent_type_check CHECK ((parent_type = ANY (ARRAY['page'::text, 'post'::text])))\r\n);\r\n\r\nCOMMENT ON TABLE public.content_drafts IS 'Draft snapshots used by Draft Mode and front-end visual editing before publishing to live page/post rows.';\r\n\r\nCOMMENT ON COLUMN public.content_drafts.parent_type IS 'The content table this draft belongs to: page or post.';\r\n\r\nCOMMENT ON COLUMN public.content_drafts.parent_id IS 'ID of the page or post being drafted.';\r\n\r\nCOMMENT ON COLUMN public.content_drafts.base_version IS 'Published page/post version the draft was created from.';\r\n\r\nCOMMENT ON COLUMN public.content_drafts.meta IS 'Draft page/post metadata snapshot.';\r\n\r\nCOMMENT ON COLUMN public.content_drafts.blocks IS 'Ordered draft block snapshot, including block ids, block types, content, language ids, and order values.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.content_drafts'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.content_drafts ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.content_drafts_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.cortex_ai_db_mutation_audit (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n actor_user_id uuid,\r\n tool_name text NOT NULL,\r\n action_name text NOT NULL,\r\n target_tables text[] DEFAULT '{}'::text[] NOT NULL,\r\n operation_summary text NOT NULL,\r\n payload_hash text NOT NULL,\r\n payload jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n preview jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n status text NOT NULL,\r\n error_message text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT cortex_ai_db_mutation_audit_status_check CHECK ((status = ANY (ARRAY['success'::text, 'failure'::text])))\r\n);\r\n\r\nCOMMENT ON TABLE public.cortex_ai_db_mutation_audit IS 'Audit trail for confirmed Cortex AI database mutation attempts.';\r\n\r\nCOMMENT ON COLUMN public.cortex_ai_db_mutation_audit.payload IS 'Redacted tool input payload for the confirmed mutation attempt.';\r\n\r\nCOMMENT ON COLUMN public.cortex_ai_db_mutation_audit.preview IS 'Redacted confirmation preview shown before the mutation was confirmed.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.coupon_freemius_mappings (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n coupon_id uuid NOT NULL,\r\n product_id uuid,\r\n freemius_product_id text NOT NULL,\r\n freemius_coupon_id text,\r\n freemius_coupon_code text NOT NULL,\r\n sync_status text DEFAULT 'pending'::text NOT NULL,\r\n sync_error text,\r\n remote_payload jsonb,\r\n last_synced_at timestamp with time zone,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT coupon_freemius_mappings_code_not_blank CHECK ((char_length(btrim(freemius_coupon_code)) > 0)),\r\n CONSTRAINT coupon_freemius_mappings_product_not_blank CHECK ((char_length(btrim(freemius_product_id)) > 0)),\r\n CONSTRAINT coupon_freemius_mappings_sync_status_valid CHECK ((sync_status = ANY (ARRAY['pending'::text, 'synced'::text, 'failed'::text, 'deleted'::text])))\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.coupon_products (\r\n coupon_id uuid NOT NULL,\r\n product_id uuid NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.coupon_products IS 'Optional product allow-list for coupons. No rows means all products in the provider scope are eligible.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.coupon_redemptions (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n coupon_id uuid,\r\n order_id uuid,\r\n coupon_code text NOT NULL,\r\n provider text NOT NULL,\r\n discount_total integer DEFAULT 0 NOT NULL,\r\n user_id uuid,\r\n customer_email text,\r\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n redeemed_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT coupon_redemptions_code_not_blank CHECK ((char_length(btrim(coupon_code)) > 0)),\r\n CONSTRAINT coupon_redemptions_discount_total_check CHECK ((discount_total >= 0)),\r\n CONSTRAINT coupon_redemptions_provider_check CHECK ((provider = ANY (ARRAY['stripe'::text, 'freemius'::text])))\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.coupons (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n code text NOT NULL,\r\n name text NOT NULL,\r\n internal_note text,\r\n provider_scope text DEFAULT 'all'::text NOT NULL,\r\n discount_type text NOT NULL,\r\n discount_amount integer NOT NULL,\r\n is_active boolean DEFAULT true NOT NULL,\r\n starts_at timestamp with time zone,\r\n ends_at timestamp with time zone,\r\n redemption_limit integer,\r\n redemptions_count integer DEFAULT 0 NOT NULL,\r\n freemius_sync_status text DEFAULT 'not_synced'::text NOT NULL,\r\n freemius_sync_error text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT coupons_code_not_blank CHECK ((char_length(btrim(code)) > 0)),\r\n CONSTRAINT coupons_date_window_valid CHECK (((starts_at IS NULL) OR (ends_at IS NULL) OR (starts_at < ends_at))),\r\n CONSTRAINT coupons_discount_amount_positive CHECK ((discount_amount > 0)),\r\n CONSTRAINT coupons_discount_type_valid CHECK ((discount_type = ANY (ARRAY['percent'::text, 'fixed'::text]))),\r\n CONSTRAINT coupons_freemius_sync_status_valid CHECK ((freemius_sync_status = ANY (ARRAY['not_synced'::text, 'pending'::text, 'synced'::text, 'failed'::text, 'not_required'::text]))),\r\n CONSTRAINT coupons_name_not_blank CHECK ((char_length(btrim(name)) > 0)),\r\n CONSTRAINT coupons_percent_amount_valid CHECK (((discount_type <> 'percent'::text) OR (discount_amount <= 100))),\r\n CONSTRAINT coupons_provider_scope_valid CHECK ((provider_scope = ANY (ARRAY['all'::text, 'stripe'::text, 'freemius'::text]))),\r\n CONSTRAINT coupons_redemption_limit_positive CHECK (((redemption_limit IS NULL) OR (redemption_limit > 0))),\r\n CONSTRAINT coupons_redemptions_count_nonnegative CHECK ((redemptions_count >= 0))\r\n);\r\n\r\nCOMMENT ON TABLE public.coupons IS 'Unified commerce coupons managed by NextBlock CMS and applied through provider-aware checkout.';\r\n\r\nCOMMENT ON COLUMN public.coupons.provider_scope IS 'Provider eligibility: all, stripe, or freemius.';\r\n\r\nCOMMENT ON COLUMN public.coupons.discount_amount IS 'Percent value for percent coupons, or fixed minor-unit amount for fixed coupons.';\r\n\r\nCOMMENT ON COLUMN public.coupons.freemius_sync_status IS 'Aggregate status for syncing this coupon to Freemius product coupon records.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.currencies (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n code text NOT NULL,\r\n symbol text NOT NULL,\r\n exchange_rate numeric(20,10) NOT NULL,\r\n is_default boolean DEFAULT false NOT NULL,\r\n is_active boolean DEFAULT true NOT NULL,\r\n rounding_mode text DEFAULT 'none'::text NOT NULL,\r\n rounding_increment integer DEFAULT 1 NOT NULL,\r\n rounding_charm_amount integer,\r\n auto_update_exchange_rate boolean DEFAULT true NOT NULL,\r\n exchange_rate_updated_at timestamp with time zone,\r\n exchange_rate_source text,\r\n auto_sync_product_prices boolean DEFAULT false NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT currencies_charm_requires_amount CHECK (((rounding_mode <> 'charm'::text) OR (rounding_charm_amount IS NOT NULL))),\r\n CONSTRAINT currencies_code_check CHECK ((code ~ '^[A-Z]{3}$'::text)),\r\n CONSTRAINT currencies_default_auto_update_disabled CHECK (((NOT is_default) OR (auto_update_exchange_rate = false))),\r\n CONSTRAINT currencies_default_exchange_rate_is_one CHECK (((NOT is_default) OR (exchange_rate = (1)::numeric))),\r\n CONSTRAINT currencies_default_must_be_active CHECK (((NOT is_default) OR is_active)),\r\n CONSTRAINT currencies_default_product_price_sync_disabled CHECK (((NOT is_default) OR (auto_sync_product_prices = false))),\r\n CONSTRAINT currencies_exchange_rate_check CHECK ((exchange_rate > (0)::numeric)),\r\n CONSTRAINT currencies_rounding_charm_nonnegative CHECK (((rounding_charm_amount IS NULL) OR (rounding_charm_amount >= 0))),\r\n CONSTRAINT currencies_rounding_increment_positive CHECK ((rounding_increment > 0)),\r\n CONSTRAINT currencies_rounding_mode_valid CHECK ((rounding_mode = ANY (ARRAY['none'::text, 'nearest'::text, 'up'::text, 'down'::text, 'charm'::text])))\r\n);\r\n\r\nCOMMENT ON TABLE public.currencies IS 'Store currencies available for storefront display and conversion.';\r\n\r\nCOMMENT ON COLUMN public.currencies.exchange_rate IS 'Relative to the current store default currency. The default currency should have exchange_rate = 1.';\r\n\r\nCOMMENT ON COLUMN public.currencies.rounding_mode IS 'Rounding strategy applied when prices are auto-converted into this currency.';\r\n\r\nCOMMENT ON COLUMN public.currencies.rounding_increment IS 'Rounding step in the currency smallest unit. Example: 5 means 0.05 for USD/CAD.';\r\n\r\nCOMMENT ON COLUMN public.currencies.rounding_charm_amount IS 'Charm ending in the currency smallest unit. Example: 90 means prices like 29.90.';\r\n\r\nCOMMENT ON COLUMN public.currencies.auto_update_exchange_rate IS 'Whether scheduled FX sync jobs should refresh this currency.';\r\n\r\nCOMMENT ON COLUMN public.currencies.exchange_rate_updated_at IS 'When this currency exchange rate was last refreshed or manually set.';\r\n\r\nCOMMENT ON COLUMN public.currencies.exchange_rate_source IS 'Human-readable source for the current exchange rate, such as a provider host or manual override.';\r\n\r\nCOMMENT ON COLUMN public.currencies.auto_sync_product_prices IS 'Whether storefront product and variant prices in this currency are derived automatically from the store default currency using FX and rounding rules.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.email_2fa_challenges (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n user_id uuid NOT NULL,\r\n token_hash text NOT NULL,\r\n expires_at timestamp with time zone NOT NULL,\r\n consumed_at timestamp with time zone,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.email_2fa_challenges IS 'Short-lived SHA-256 hashes of 6-digit email verification codes. Readable/writable only by the service role.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.freemius_plans (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n product_id uuid NOT NULL,\r\n name text NOT NULL,\r\n title text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.freemius_pricing (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n plan_id uuid NOT NULL,\r\n api_monthly_price numeric,\r\n api_annual_price numeric,\r\n api_lifetime_price numeric,\r\n override_monthly_price numeric,\r\n override_annual_price numeric,\r\n override_lifetime_price numeric,\r\n license_quota integer,\r\n is_active boolean DEFAULT true NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.inventory_items (\r\n sku text NOT NULL,\r\n quantity integer DEFAULT 0 NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT inventory_items_quantity_check CHECK ((quantity >= 0))\r\n);\r\n\r\nCOMMENT ON TABLE public.inventory_items IS 'Source-of-truth inventory records keyed by sellable SKU.';\r\n\r\nCOMMENT ON COLUMN public.inventory_items.sku IS 'Global sellable SKU. Matching products or variants share inventory.';\r\n\r\nCOMMENT ON COLUMN public.inventory_items.quantity IS 'Available quantity for this SKU.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.languages (\r\n id bigint NOT NULL,\r\n code text NOT NULL,\r\n name text NOT NULL,\r\n is_default boolean DEFAULT false NOT NULL,\r\n is_active boolean DEFAULT true,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.languages IS 'Stores supported languages for the CMS.';\r\n\r\nCOMMENT ON COLUMN public.languages.code IS 'BCP 47 language code.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.languages'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.languages ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.languages_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.logos (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n name text NOT NULL,\r\n media_id uuid,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.logos IS 'Stores company and brand logos.';\r\n\r\nCOMMENT ON COLUMN public.logos.name IS 'The name of the brand or company for the logo.';\r\n\r\nCOMMENT ON COLUMN public.logos.media_id IS 'Foreign key to the media table for the logo image.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.media (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n uploader_id uuid,\r\n file_name text NOT NULL,\r\n object_key text NOT NULL,\r\n file_type text,\r\n size_bytes bigint,\r\n description text,\r\n width integer,\r\n height integer,\r\n blur_data_url text,\r\n variants jsonb,\r\n file_path text,\r\n folder text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.media IS 'Stores information about uploaded media assets.';\r\n\r\nCOMMENT ON COLUMN public.media.object_key IS 'Unique key (path) in Cloudflare R2.';\r\n\r\nCOMMENT ON COLUMN public.media.width IS 'Width of the image in pixels.';\r\n\r\nCOMMENT ON COLUMN public.media.height IS 'Height of the image in pixels.';\r\n\r\nCOMMENT ON COLUMN public.media.blur_data_url IS 'Base64 encoded string for image blur placeholders.';\r\n\r\nCOMMENT ON COLUMN public.media.variants IS 'Array of image variant objects.';\r\n\r\nCOMMENT ON COLUMN public.media.file_path IS 'Full path to the file in the storage bucket.';\r\n\r\nCOMMENT ON COLUMN public.media.folder IS 'Folder path prefix for the R2 object.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.navigation_items (\r\n id bigint NOT NULL,\r\n language_id bigint NOT NULL,\r\n menu_key public.menu_location NOT NULL,\r\n label text NOT NULL,\r\n url text NOT NULL,\r\n parent_id bigint,\r\n \"order\" integer DEFAULT 0 NOT NULL,\r\n page_id bigint,\r\n translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.navigation_items IS 'Stores navigation menu items.';\r\n\r\nCOMMENT ON COLUMN public.navigation_items.menu_key IS 'Identifies the menu this item belongs to.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.navigation_items'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.navigation_items ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.navigation_items_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE SEQUENCE IF NOT EXISTS public.order_invoice_number_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1;\r\n\r\nCREATE TABLE IF NOT EXISTS public.order_items (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n order_id uuid NOT NULL,\r\n product_id uuid,\r\n variant_id uuid,\r\n quantity integer NOT NULL,\r\n price_at_purchase integer NOT NULL\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.orders (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n user_id uuid,\r\n status text DEFAULT 'pending'::text NOT NULL,\r\n total integer NOT NULL,\r\n stripe_session_id text,\r\n payment_intent_id text,\r\n customer_details jsonb,\r\n provider text DEFAULT 'stripe'::text,\r\n freemius_product_id text,\r\n freemius_plan_id text,\r\n freemius_license_id text,\r\n freemius_subscription_id text,\r\n freemius_trial_id text,\r\n freemius_user_id text,\r\n freemius_trial_ends_at timestamp with time zone,\r\n freemius_last_event_type text,\r\n freemius_last_synced_at timestamp with time zone,\r\n currency text DEFAULT 'USD'::text NOT NULL,\r\n subtotal integer,\r\n shipping_total integer,\r\n tax_total integer DEFAULT 0 NOT NULL,\r\n tax_details jsonb,\r\n exchange_rate_at_purchase numeric(20,10) DEFAULT 1 NOT NULL,\r\n inventory_deducted_at timestamp with time zone,\r\n invoice_number text,\r\n paid_at timestamp with time zone,\r\n created_at timestamp with time zone DEFAULT now(),\r\n coupon_id uuid,\r\n coupon_code text,\r\n discount_total integer DEFAULT 0 NOT NULL,\r\n discount_details jsonb,\r\n CONSTRAINT orders_discount_total_check CHECK ((discount_total >= 0)),\r\n CONSTRAINT orders_exchange_rate_at_purchase_positive CHECK ((exchange_rate_at_purchase > (0)::numeric)),\r\n CONSTRAINT orders_provider_check CHECK ((provider = ANY (ARRAY['stripe'::text, 'freemius'::text]))),\r\n CONSTRAINT orders_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'trial'::text, 'paid'::text, 'shipped'::text, 'cancelled'::text, 'refunded'::text])))\r\n);\r\n\r\nCOMMENT ON COLUMN public.orders.freemius_license_id IS 'Freemius license ID used to reconcile checkout callbacks and webhooks.';\r\n\r\nCOMMENT ON COLUMN public.orders.freemius_subscription_id IS 'Freemius subscription ID when the order is associated with recurring billing.';\r\n\r\nCOMMENT ON COLUMN public.orders.freemius_trial_id IS 'Freemius trial ID when checkout starts in trial mode.';\r\n\r\nCOMMENT ON COLUMN public.orders.freemius_trial_ends_at IS 'Freemius trial expiration timestamp when supplied by checkout or webhook data.';\r\n\r\nCOMMENT ON COLUMN public.orders.freemius_last_event_type IS 'Last Freemius checkout callback or webhook event applied to the order.';\r\n\r\nCOMMENT ON COLUMN public.orders.freemius_last_synced_at IS 'Timestamp when Freemius metadata was last reconciled locally.';\r\n\r\nCOMMENT ON COLUMN public.orders.currency IS 'ISO currency code used for the order totals.';\r\n\r\nCOMMENT ON COLUMN public.orders.subtotal IS 'Subtotal before shipping and tax, in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.orders.shipping_total IS 'Shipping amount before tax, in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.orders.tax_total IS 'Total tax amount collected for the order, in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.orders.tax_details IS 'Normalized tax breakdown payload sourced from manual rates or finalized Stripe tax data.';\r\n\r\nCOMMENT ON COLUMN public.orders.exchange_rate_at_purchase IS 'Exchange rate locked at purchase time relative to the store default currency.';\r\n\r\nCOMMENT ON COLUMN public.orders.invoice_number IS 'Stable printable invoice number assigned once when the order first becomes paid.';\r\n\r\nCOMMENT ON COLUMN public.orders.paid_at IS 'Timestamp when the order was first marked as paid.';\r\n\r\nCOMMENT ON COLUMN public.orders.coupon_code IS 'Coupon code applied to the order at checkout time.';\r\n\r\nCOMMENT ON COLUMN public.orders.discount_total IS 'Total discount applied to this order in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.orders.discount_details IS 'Provider-aware coupon quote details captured at checkout.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.package_activations (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n license_key text NOT NULL,\r\n instance_name text NOT NULL,\r\n package_id text NOT NULL,\r\n status text DEFAULT 'active'::text NOT NULL,\r\n meta jsonb DEFAULT '{}'::jsonb,\r\n last_validated_at timestamp with time zone DEFAULT now(),\r\n created_at timestamp with time zone DEFAULT now()\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.page_revisions (\r\n id bigint NOT NULL,\r\n page_id bigint NOT NULL,\r\n author_id uuid,\r\n version integer NOT NULL,\r\n revision_type public.revision_type NOT NULL,\r\n content jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.page_revisions IS 'Hybrid (snapshot/diff) revisions for pages.';\r\n\r\nCOMMENT ON COLUMN public.page_revisions.content IS 'If snapshot: full content; if diff: JSON Patch array.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.page_revisions'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.page_revisions ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.page_revisions_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.pages (\r\n id bigint NOT NULL,\r\n language_id bigint NOT NULL,\r\n author_id uuid,\r\n title text NOT NULL,\r\n slug text NOT NULL,\r\n status public.page_status DEFAULT 'draft'::public.page_status NOT NULL,\r\n meta_title text,\r\n meta_description text,\r\n version integer DEFAULT 1 NOT NULL,\r\n translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n feature_image_id uuid\r\n);\r\n\r\nCOMMENT ON TABLE public.pages IS 'Stores static pages for the website.';\r\n\r\nCOMMENT ON COLUMN public.pages.slug IS 'URL-friendly identifier, unique per language.';\r\n\r\nCOMMENT ON COLUMN public.pages.version IS 'Monotonic version number for hybrid revisions.';\r\n\r\nCOMMENT ON COLUMN public.pages.translation_group_id IS 'Groups different language versions of the same conceptual page.';\r\n\r\nCOMMENT ON COLUMN public.pages.feature_image_id IS 'ID of the media item to be used as the page feature image.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.pages'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.pages ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.pages_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.post_revisions (\r\n id bigint NOT NULL,\r\n post_id bigint NOT NULL,\r\n author_id uuid,\r\n version integer NOT NULL,\r\n revision_type public.revision_type NOT NULL,\r\n content jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.post_revisions IS 'Hybrid (snapshot/diff) revisions for posts.';\r\n\r\nCOMMENT ON COLUMN public.post_revisions.content IS 'If snapshot: full content; if diff: JSON Patch array.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.post_revisions'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.post_revisions ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.post_revisions_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.posts (\r\n id bigint NOT NULL,\r\n language_id bigint NOT NULL,\r\n author_id uuid,\r\n title text NOT NULL,\r\n slug text NOT NULL,\r\n label text,\r\n excerpt text,\r\n subtitle text,\r\n status public.page_status DEFAULT 'draft'::public.page_status NOT NULL,\r\n published_at timestamp with time zone,\r\n meta_title text,\r\n meta_description text,\r\n feature_image_id uuid,\r\n version integer DEFAULT 1 NOT NULL,\r\n translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.posts IS 'Stores blog posts or news articles.';\r\n\r\nCOMMENT ON COLUMN public.posts.slug IS 'URL-friendly identifier, unique per language.';\r\n\r\nCOMMENT ON COLUMN public.posts.label IS 'Short editorial label rendered as a pill on post hero and article cards.';\r\n\r\nCOMMENT ON COLUMN public.posts.excerpt IS 'Short editorial summary used in post metadata rows and post cards.';\r\n\r\nCOMMENT ON COLUMN public.posts.subtitle IS 'Longer deck shown under the post title.';\r\n\r\nCOMMENT ON COLUMN public.posts.feature_image_id IS 'ID of the media item to be used as the feature image.';\r\n\r\nCOMMENT ON COLUMN public.posts.version IS 'Monotonic version number for hybrid revisions.';\r\n\r\nCOMMENT ON COLUMN public.posts.translation_group_id IS 'Groups different language versions of the same conceptual post.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.posts'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.posts ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.posts_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.privacy_consent_logs (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n consent_token text NOT NULL,\r\n categories jsonb DEFAULT '{\"analytics\": false, \"marketing\": false, \"necessary\": true}'::jsonb NOT NULL,\r\n ip_masked text,\r\n user_agent text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT privacy_consent_logs_categories_check CHECK ((jsonb_typeof(categories) = 'object'::text))\r\n);\r\n\r\nCOMMENT ON TABLE public.privacy_consent_logs IS 'Immutable audit log of visitor consent decisions for Quebec Law 25 / PIPEDA accountability.';\r\n\r\nCOMMENT ON COLUMN public.privacy_consent_logs.consent_token IS 'Opaque token also stored in the nb_consent_preference cookie to correlate a decision with its record.';\r\n\r\nCOMMENT ON COLUMN public.privacy_consent_logs.ip_masked IS 'Partially masked IP (e.g. 203.0.113.x) - never store a full address for an analytics/marketing consent log.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_attribute_terms (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n attribute_id uuid NOT NULL,\r\n value text NOT NULL,\r\n slug text NOT NULL,\r\n sort_order integer DEFAULT 0 NOT NULL,\r\n value_translations jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now(),\r\n updated_at timestamp with time zone DEFAULT now()\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_attributes (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n name text NOT NULL,\r\n slug text NOT NULL,\r\n name_translations jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now(),\r\n updated_at timestamp with time zone DEFAULT now()\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_categories (\r\n product_id uuid NOT NULL,\r\n category_id uuid NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.product_categories IS 'Junction table mapping products to multiple categories.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_drafts (\r\n id bigint NOT NULL,\r\n product_id uuid NOT NULL,\r\n author_id uuid,\r\n meta jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n blocks jsonb DEFAULT '[]'::jsonb NOT NULL,\r\n CONSTRAINT product_drafts_blocks_array CHECK ((jsonb_typeof(blocks) = 'array'::text)),\r\n CONSTRAINT product_drafts_meta_object CHECK ((jsonb_typeof(meta) = 'object'::text))\r\n);\r\n\r\nCOMMENT ON TABLE public.product_drafts IS 'Draft product metadata snapshots used by Draft Mode and front-end visual editing before publishing to live product rows.';\r\n\r\nCOMMENT ON COLUMN public.product_drafts.product_id IS 'ID of the product being drafted.';\r\n\r\nCOMMENT ON COLUMN public.product_drafts.meta IS 'Draft product metadata snapshot. Front-end visual editing currently mutates visible title, short_description, and description_json fields.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.product_drafts'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.product_drafts ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.product_drafts_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_freemius_sale_coupons (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n product_id uuid NOT NULL,\r\n freemius_product_id text NOT NULL,\r\n freemius_plan_id text,\r\n freemius_coupon_id text,\r\n freemius_coupon_code text NOT NULL,\r\n discount_percent integer,\r\n starts_at timestamp with time zone,\r\n ends_at timestamp with time zone,\r\n is_active boolean DEFAULT false NOT NULL,\r\n sync_status text DEFAULT 'pending'::text NOT NULL,\r\n sync_error text,\r\n remote_payload jsonb,\r\n last_synced_at timestamp with time zone,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT product_freemius_sale_coupons_code_not_blank CHECK ((char_length(btrim(freemius_coupon_code)) > 0)),\r\n CONSTRAINT product_freemius_sale_coupons_discount_valid CHECK (((discount_percent IS NULL) OR ((discount_percent > 0) AND (discount_percent <= 100)))),\r\n CONSTRAINT product_freemius_sale_coupons_fm_product_not_blank CHECK ((char_length(btrim(freemius_product_id)) > 0)),\r\n CONSTRAINT product_freemius_sale_coupons_sync_status_valid CHECK ((sync_status = ANY (ARRAY['pending'::text, 'synced'::text, 'failed'::text, 'deleted'::text]))),\r\n CONSTRAINT product_freemius_sale_coupons_window_valid CHECK (((starts_at IS NULL) OR (ends_at IS NULL) OR (starts_at < ends_at)))\r\n);\r\n\r\nCOMMENT ON TABLE public.product_freemius_sale_coupons IS 'Auto-generated, time-bounded Freemius coupons that enforce a scheduled sale on a Freemius product at Freemius-hosted checkout.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_media (\r\n product_id uuid NOT NULL,\r\n media_id uuid NOT NULL,\r\n sort_order integer DEFAULT 0\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_variants (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n product_id uuid NOT NULL,\r\n sku text NOT NULL,\r\n price_adjustment integer DEFAULT 0 NOT NULL,\r\n price integer DEFAULT 0 NOT NULL,\r\n prices jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n sale_price integer,\r\n sale_prices jsonb,\r\n stock_quantity integer DEFAULT 0 NOT NULL,\r\n upc text,\r\n main_media_id uuid,\r\n created_at timestamp with time zone DEFAULT now(),\r\n updated_at timestamp with time zone DEFAULT now(),\r\n sale_start_at timestamp with time zone,\r\n sale_end_at timestamp with time zone,\r\n scheduled_price integer,\r\n scheduled_prices jsonb,\r\n scheduled_price_at timestamp with time zone,\r\n CONSTRAINT product_variants_prices_is_valid CHECK (public.is_valid_currency_amount_map(prices)),\r\n CONSTRAINT product_variants_sale_prices_are_valid CHECK (public.is_valid_sale_price_map(prices, sale_prices)),\r\n CONSTRAINT product_variants_sale_window_valid CHECK (((sale_start_at IS NULL) OR (sale_end_at IS NULL) OR (sale_start_at < sale_end_at)))\r\n);\r\n\r\nCOMMENT ON COLUMN public.product_variants.prices IS 'Variant regular prices by ISO 4217 code in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.product_variants.sale_prices IS 'Variant sale prices by ISO 4217 code in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.product_variants.sale_start_at IS 'Inclusive start of the scheduled sale window (UTC) for this variant. NULL means no lower bound.';\r\n\r\nCOMMENT ON COLUMN public.product_variants.sale_end_at IS 'Exclusive end of the scheduled sale window (UTC) for this variant. NULL means no upper bound.';\r\n\r\nCOMMENT ON COLUMN public.product_variants.scheduled_price IS 'Pending regular price (smallest currency unit) applied once scheduled_price_at has passed.';\r\n\r\nCOMMENT ON COLUMN public.product_variants.scheduled_prices IS 'Pending multi-currency regular prices by ISO 4217 code, applied once scheduled_price_at has passed.';\r\n\r\nCOMMENT ON COLUMN public.product_variants.scheduled_price_at IS 'Effective timestamp (UTC) for the pending regular-price change.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.products (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n language_id bigint NOT NULL,\r\n translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n sku text NOT NULL,\r\n title text NOT NULL,\r\n slug text NOT NULL,\r\n product_type text NOT NULL,\r\n payment_provider text NOT NULL,\r\n price integer NOT NULL,\r\n prices jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n sale_price integer,\r\n sale_prices jsonb,\r\n stock integer DEFAULT 0,\r\n status text DEFAULT 'draft'::text NOT NULL,\r\n meta_title text,\r\n meta_description text,\r\n short_description text,\r\n description_json jsonb,\r\n metadata jsonb,\r\n freemius_plan_id text,\r\n freemius_product_id text,\r\n trial_period_days integer DEFAULT 0 NOT NULL,\r\n trial_requires_payment_method boolean DEFAULT false NOT NULL,\r\n upc text,\r\n is_taxable boolean DEFAULT true NOT NULL,\r\n created_at timestamp with time zone DEFAULT now(),\r\n updated_at timestamp with time zone DEFAULT now(),\r\n sale_start_at timestamp with time zone,\r\n sale_end_at timestamp with time zone,\r\n scheduled_price integer,\r\n scheduled_prices jsonb,\r\n scheduled_price_at timestamp with time zone,\r\n average_rating numeric(3,2) DEFAULT 0.00 NOT NULL,\r\n total_reviews integer DEFAULT 0 NOT NULL,\r\n CONSTRAINT products_payment_provider_check CHECK ((payment_provider = ANY (ARRAY['stripe'::text, 'freemius'::text]))),\r\n CONSTRAINT products_prices_is_valid CHECK (public.is_valid_currency_amount_map(prices)),\r\n CONSTRAINT products_product_type_check CHECK ((product_type = ANY (ARRAY['physical'::text, 'digital'::text]))),\r\n CONSTRAINT products_sale_prices_are_valid CHECK (public.is_valid_sale_price_map(prices, sale_prices)),\r\n CONSTRAINT products_sale_window_valid CHECK (((sale_start_at IS NULL) OR (sale_end_at IS NULL) OR (sale_start_at < sale_end_at))),\r\n CONSTRAINT products_status_check CHECK ((status = ANY (ARRAY['draft'::text, 'active'::text, 'archived'::text]))),\r\n CONSTRAINT products_trial_period_days_check CHECK ((trial_period_days >= 0)),\r\n CONSTRAINT products_type_provider_consistency_check CHECK ((((product_type = 'physical'::text) AND (payment_provider = 'stripe'::text)) OR ((product_type = 'digital'::text) AND (payment_provider = 'freemius'::text))))\r\n);\r\n\r\nCOMMENT ON COLUMN public.products.prices IS 'Regular prices by ISO 4217 code in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.products.sale_prices IS 'Sale prices by ISO 4217 code in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.products.is_taxable IS 'When true, this product participates in Stripe tax calculation.';\r\n\r\nCOMMENT ON COLUMN public.products.sale_start_at IS 'Inclusive start of the scheduled sale window (UTC). NULL means no lower bound.';\r\n\r\nCOMMENT ON COLUMN public.products.sale_end_at IS 'Exclusive end of the scheduled sale window (UTC). NULL means no upper bound. Both NULL = always-on sale.';\r\n\r\nCOMMENT ON COLUMN public.products.scheduled_price IS 'Pending regular price (smallest currency unit) applied once scheduled_price_at has passed.';\r\n\r\nCOMMENT ON COLUMN public.products.scheduled_prices IS 'Pending multi-currency regular prices by ISO 4217 code, applied once scheduled_price_at has passed.';\r\n\r\nCOMMENT ON COLUMN public.products.scheduled_price_at IS 'Effective timestamp (UTC) for the pending regular-price change.';\r\n\r\nCOMMENT ON COLUMN public.products.average_rating IS 'Aggregated average 1-5 rating of approved reviews.';\r\n\r\nCOMMENT ON COLUMN public.products.total_reviews IS 'Total count of approved reviews for this product.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.profiles (\r\n id uuid NOT NULL,\r\n updated_at timestamp with time zone,\r\n full_name text,\r\n avatar_url text,\r\n website text,\r\n github_username text,\r\n phone text,\r\n role public.user_role DEFAULT 'USER'::public.user_role NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.profiles IS 'Profile information for each user, extending auth.users.';\r\n\r\nCOMMENT ON COLUMN public.profiles.id IS 'References auth.users.id';\r\n\r\nCOMMENT ON COLUMN public.profiles.role IS 'User role for RBAC.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.shipping_zone_locations (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n zone_id uuid NOT NULL,\r\n country_code text NOT NULL,\r\n state_code text,\r\n postal_code text,\r\n created_at timestamp with time zone DEFAULT now()\r\n);\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_locations.country_code IS 'ISO 3166-1 alpha-2 country code.';\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_locations.state_code IS 'Optional state/province code within the selected country (for example CA, NY, ON, QC). NULL means the whole country.';\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_locations.postal_code IS 'Optional exact postal code or wildcard pattern. NULL means all postal codes in the matched country/state.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.shipping_zone_methods (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n zone_id uuid NOT NULL,\r\n method_type text NOT NULL,\r\n cost_amount integer DEFAULT 0 NOT NULL,\r\n cost_currency text DEFAULT 'USD'::text NOT NULL,\r\n min_order_amount integer DEFAULT 0 NOT NULL,\r\n name text NOT NULL,\r\n name_translations jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n currency_pricing_mode text DEFAULT 'auto'::text NOT NULL,\r\n cost_amounts jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n min_order_amounts jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now(),\r\n updated_at timestamp with time zone DEFAULT now(),\r\n CONSTRAINT shipping_zone_methods_cost_amounts_include_source CHECK ((cost_amounts ? upper(cost_currency))),\r\n CONSTRAINT shipping_zone_methods_cost_amounts_valid CHECK (public.is_valid_currency_amount_map(cost_amounts)),\r\n CONSTRAINT shipping_zone_methods_cost_currency_format CHECK ((cost_currency ~ '^[A-Z]{3}$'::text)),\r\n CONSTRAINT shipping_zone_methods_currency_pricing_mode_valid CHECK ((currency_pricing_mode = ANY (ARRAY['auto'::text, 'manual'::text]))),\r\n CONSTRAINT shipping_zone_methods_method_type_check CHECK ((method_type = ANY (ARRAY['flat_rate'::text, 'free_shipping'::text]))),\r\n CONSTRAINT shipping_zone_methods_min_order_amounts_include_source CHECK ((min_order_amounts ? upper(cost_currency))),\r\n CONSTRAINT shipping_zone_methods_min_order_amounts_valid CHECK (public.is_valid_currency_amount_map(min_order_amounts))\r\n);\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_methods.name_translations IS 'Localized shipping method labels keyed by language code. Example: {\"fr\": \"Livraison standard\"}.';\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_methods.currency_pricing_mode IS 'Whether this rate uses auto FX conversion from a single source currency or exact manual amounts per currency.';\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_methods.cost_amounts IS 'Shipping costs by ISO 4217 code in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_methods.min_order_amounts IS 'Minimum order thresholds by ISO 4217 code in the smallest currency unit.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.shipping_zones (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n name text NOT NULL,\r\n priority_order integer DEFAULT 0 NOT NULL,\r\n created_at timestamp with time zone DEFAULT now(),\r\n updated_at timestamp with time zone DEFAULT now()\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.site_settings (\r\n key text NOT NULL,\r\n value jsonb\r\n);\r\n\r\nCOMMENT ON TABLE public.site_settings IS 'Key-value store for global site settings. Sensitive keys (Cortex AI BYOK, Bot Protection Secret, Email secret, Payment secret) hold encrypted envelopes and are restricted to ADMIN via row-level policies.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.system_alerts (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n alert_type text NOT NULL,\r\n title text NOT NULL,\r\n message text NOT NULL,\r\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n is_resolved boolean DEFAULT false NOT NULL,\r\n resolved_at timestamp with time zone,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT system_alerts_alert_type_check CHECK ((alert_type = ANY (ARRAY['merge_conflict'::text, 'runtime_update_available'::text])))\r\n);\r\n\r\nCOMMENT ON TABLE public.system_alerts IS 'System notifications for the automated upstream-update architecture (merge conflicts, runtime updates available). Written by service-role; read by ADMINs.';\r\n\r\nCOMMENT ON COLUMN public.system_alerts.alert_type IS 'One of: merge_conflict, runtime_update_available.';\r\n\r\nCOMMENT ON COLUMN public.system_alerts.metadata IS 'Structured deep-link context for the dashboard banner CTA (repo/branch/action_url or latest_version/download_url).';\r\n\r\nCOMMENT ON COLUMN public.system_alerts.is_resolved IS 'When true the alert is hidden from the dashboard banner.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.system_configuration (\r\n id integer DEFAULT 1 NOT NULL,\r\n auto_accept_signups boolean DEFAULT false NOT NULL,\r\n settings jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT system_configuration_singleton CHECK ((id = 1))\r\n);\r\n\r\nCOMMENT ON TABLE public.system_configuration IS 'Singleton (id = 1) of global setup-wizard configuration. ADMIN-only via RLS; never store secrets in settings.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.tax_rates (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n country_code text NOT NULL,\r\n state_code text,\r\n tax_name text NOT NULL,\r\n tax_rate numeric(7,4) NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT tax_rates_tax_name_check CHECK ((char_length(btrim(tax_name)) > 0)),\r\n CONSTRAINT tax_rates_tax_rate_check CHECK (((tax_rate >= (0)::numeric) AND (tax_rate <= (100)::numeric)))\r\n);\r\n\r\nCOMMENT ON TABLE public.tax_rates IS 'Manual tax rates used for Stripe storefront orders. Multiple rows can exist per jurisdiction to support combined taxes such as GST + PST.';\r\n\r\nCOMMENT ON COLUMN public.tax_rates.country_code IS 'ISO 3166-1 alpha-2 country code.';\r\n\r\nCOMMENT ON COLUMN public.tax_rates.state_code IS 'Optional state/province code within country_code. NULL represents a country-wide or federal tax.';\r\n\r\nCOMMENT ON COLUMN public.tax_rates.tax_name IS 'Display name for the tax component, for example GST, PST, HST, or State Sales Tax.';\r\n\r\nCOMMENT ON COLUMN public.tax_rates.tax_rate IS 'Percent value, not decimal fraction. Example: 5.0000 means 5%.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.translations (\r\n key text NOT NULL,\r\n translations jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON COLUMN public.translations.key IS 'A unique, slugified identifier (e.g., \"sign_in_button_text\").';\r\n\r\nCOMMENT ON COLUMN public.translations.translations IS 'Stores translations as key-value pairs (e.g., {\"en\": \"Sign In\", \"fr\": \"S''inscrire\"}).';\r\n\r\nCREATE TABLE IF NOT EXISTS public.ucp_cart_sessions (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n status text DEFAULT 'active'::text NOT NULL,\r\n currency text DEFAULT 'USD'::text NOT NULL,\r\n locale text,\r\n buyer_identity jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n context jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n signals jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n attribution jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n line_items jsonb DEFAULT '[]'::jsonb NOT NULL,\r\n totals jsonb DEFAULT '[]'::jsonb NOT NULL,\r\n checkout_url text,\r\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n expires_at timestamp with time zone DEFAULT (now() + '7 days'::interval) NOT NULL,\r\n CONSTRAINT ucp_cart_sessions_attribution_object CHECK ((jsonb_typeof(attribution) = 'object'::text)),\r\n CONSTRAINT ucp_cart_sessions_buyer_identity_object CHECK ((jsonb_typeof(buyer_identity) = 'object'::text)),\r\n CONSTRAINT ucp_cart_sessions_context_object CHECK ((jsonb_typeof(context) = 'object'::text)),\r\n CONSTRAINT ucp_cart_sessions_line_items_array CHECK ((jsonb_typeof(line_items) = 'array'::text)),\r\n CONSTRAINT ucp_cart_sessions_metadata_object CHECK ((jsonb_typeof(metadata) = 'object'::text)),\r\n CONSTRAINT ucp_cart_sessions_signals_object CHECK ((jsonb_typeof(signals) = 'object'::text)),\r\n CONSTRAINT ucp_cart_sessions_status_check CHECK ((status = ANY (ARRAY['active'::text, 'cancelled'::text, 'completed'::text]))),\r\n CONSTRAINT ucp_cart_sessions_totals_array CHECK ((jsonb_typeof(totals) = 'array'::text))\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.user_addresses (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n user_id uuid NOT NULL,\r\n address_type text NOT NULL,\r\n is_default boolean DEFAULT false NOT NULL,\r\n recipient_name text,\r\n company_name text,\r\n line1 text,\r\n line2 text,\r\n city text,\r\n state text,\r\n postal_code text,\r\n country_code text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT user_addresses_address_type_check CHECK ((address_type = ANY (ARRAY['billing'::text, 'shipping'::text])))\r\n);\r\n\r\nCOMMENT ON COLUMN public.user_addresses.company_name IS 'Optional company or organization name for the address.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.user_security_settings (\r\n user_id uuid NOT NULL,\r\n mfa_enabled boolean DEFAULT false NOT NULL,\r\n mfa_type text,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT user_security_settings_mfa_type_check CHECK ((mfa_type = ANY (ARRAY['totp'::text, 'email'::text])))\r\n);\r\n\r\nCOMMENT ON TABLE public.user_security_settings IS 'Per-user multi-factor configuration. mfa_type is NULL until a factor is enrolled.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.user_trusted_devices (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n user_id uuid NOT NULL,\r\n device_hash text NOT NULL,\r\n browser_metadata text,\r\n expires_at timestamp with time zone NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.user_trusted_devices IS 'SHA-256 hashes of trusted-device tokens. A 2FA bypass is only honoured when a non-expired row matches the cookie token, so deleting a row instantly revokes trust.';\r\n\r\nCOMMENT ON COLUMN public.user_trusted_devices.device_hash IS 'SHA-256 of the raw token held in the nb_trusted_device cookie. The raw token is never stored.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.variant_attribute_mapping (\r\n variant_id uuid NOT NULL,\r\n attribute_term_id uuid NOT NULL\r\n);\r\n"
21
- },
22
- {
23
- "version": "00000000000001",
24
- "name": "00000000000001_baseline_constraints_and_indexes.sql",
25
- "sql": "-- AUTO-GENERATED baseline (re-baseline of migrations 000..044). Idempotent; safe to replay.\r\n-- 01 · constraints (PK / unique / check / FK) + indexes\r\n-- Regenerate via tools/scripts/rebaseline-transform.mjs. Do not hand-edit.\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'blocks_pkey' AND conrelid = 'public.blocks'::regclass) THEN\r\n ALTER TABLE ONLY public.blocks\r\n ADD CONSTRAINT blocks_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'categories_pkey' AND conrelid = 'public.categories'::regclass) THEN\r\n ALTER TABLE ONLY public.categories\r\n ADD CONSTRAINT categories_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'categories_slug_key' AND conrelid = 'public.categories'::regclass) THEN\r\n ALTER TABLE ONLY public.categories\r\n ADD CONSTRAINT categories_slug_key UNIQUE (slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cms_interactions_pkey' AND conrelid = 'public.cms_interactions'::regclass) THEN\r\n ALTER TABLE ONLY public.cms_interactions\r\n ADD CONSTRAINT cms_interactions_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'content_drafts_parent_unique' AND conrelid = 'public.content_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.content_drafts\r\n ADD CONSTRAINT content_drafts_parent_unique UNIQUE (parent_type, parent_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'content_drafts_pkey' AND conrelid = 'public.content_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.content_drafts\r\n ADD CONSTRAINT content_drafts_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cortex_ai_db_mutation_audit_pkey' AND conrelid = 'public.cortex_ai_db_mutation_audit'::regclass) THEN\r\n ALTER TABLE ONLY public.cortex_ai_db_mutation_audit\r\n ADD CONSTRAINT cortex_ai_db_mutation_audit_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_freemius_mappings_pkey' AND conrelid = 'public.coupon_freemius_mappings'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_freemius_mappings\r\n ADD CONSTRAINT coupon_freemius_mappings_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_products_pkey' AND conrelid = 'public.coupon_products'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_products\r\n ADD CONSTRAINT coupon_products_pkey PRIMARY KEY (coupon_id, product_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_redemptions_pkey' AND conrelid = 'public.coupon_redemptions'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_redemptions\r\n ADD CONSTRAINT coupon_redemptions_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupons_pkey' AND conrelid = 'public.coupons'::regclass) THEN\r\n ALTER TABLE ONLY public.coupons\r\n ADD CONSTRAINT coupons_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'currencies_code_key' AND conrelid = 'public.currencies'::regclass) THEN\r\n ALTER TABLE ONLY public.currencies\r\n ADD CONSTRAINT currencies_code_key UNIQUE (code);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'currencies_pkey' AND conrelid = 'public.currencies'::regclass) THEN\r\n ALTER TABLE ONLY public.currencies\r\n ADD CONSTRAINT currencies_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'custom_block_definitions_pkey' AND conrelid = 'public.custom_block_definitions'::regclass) THEN\r\n ALTER TABLE ONLY public.custom_block_definitions\r\n ADD CONSTRAINT custom_block_definitions_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'custom_block_definitions_slug_key' AND conrelid = 'public.custom_block_definitions'::regclass) THEN\r\n ALTER TABLE ONLY public.custom_block_definitions\r\n ADD CONSTRAINT custom_block_definitions_slug_key UNIQUE (slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'email_2fa_challenges_pkey' AND conrelid = 'public.email_2fa_challenges'::regclass) THEN\r\n ALTER TABLE ONLY public.email_2fa_challenges\r\n ADD CONSTRAINT email_2fa_challenges_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'freemius_plans_pkey' AND conrelid = 'public.freemius_plans'::regclass) THEN\r\n ALTER TABLE ONLY public.freemius_plans\r\n ADD CONSTRAINT freemius_plans_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'freemius_pricing_pkey' AND conrelid = 'public.freemius_pricing'::regclass) THEN\r\n ALTER TABLE ONLY public.freemius_pricing\r\n ADD CONSTRAINT freemius_pricing_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'inventory_items_pkey' AND conrelid = 'public.inventory_items'::regclass) THEN\r\n ALTER TABLE ONLY public.inventory_items\r\n ADD CONSTRAINT inventory_items_pkey PRIMARY KEY (sku);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'languages_code_key' AND conrelid = 'public.languages'::regclass) THEN\r\n ALTER TABLE ONLY public.languages\r\n ADD CONSTRAINT languages_code_key UNIQUE (code);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'languages_pkey' AND conrelid = 'public.languages'::regclass) THEN\r\n ALTER TABLE ONLY public.languages\r\n ADD CONSTRAINT languages_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'logos_pkey' AND conrelid = 'public.logos'::regclass) THEN\r\n ALTER TABLE ONLY public.logos\r\n ADD CONSTRAINT logos_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'media_object_key_key' AND conrelid = 'public.media'::regclass) THEN\r\n ALTER TABLE ONLY public.media\r\n ADD CONSTRAINT media_object_key_key UNIQUE (object_key);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'media_pkey' AND conrelid = 'public.media'::regclass) THEN\r\n ALTER TABLE ONLY public.media\r\n ADD CONSTRAINT media_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'navigation_items_pkey' AND conrelid = 'public.navigation_items'::regclass) THEN\r\n ALTER TABLE ONLY public.navigation_items\r\n ADD CONSTRAINT navigation_items_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'order_items_pkey' AND conrelid = 'public.order_items'::regclass) THEN\r\n ALTER TABLE ONLY public.order_items\r\n ADD CONSTRAINT order_items_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'orders_pkey' AND conrelid = 'public.orders'::regclass) THEN\r\n ALTER TABLE ONLY public.orders\r\n ADD CONSTRAINT orders_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'orders_stripe_session_id_key' AND conrelid = 'public.orders'::regclass) THEN\r\n ALTER TABLE ONLY public.orders\r\n ADD CONSTRAINT orders_stripe_session_id_key UNIQUE (stripe_session_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'package_activations_license_key_package_id_key' AND conrelid = 'public.package_activations'::regclass) THEN\r\n ALTER TABLE ONLY public.package_activations\r\n ADD CONSTRAINT package_activations_license_key_package_id_key UNIQUE (license_key, package_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'package_activations_pkey' AND conrelid = 'public.package_activations'::regclass) THEN\r\n ALTER TABLE ONLY public.package_activations\r\n ADD CONSTRAINT package_activations_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'page_revisions_page_version_key' AND conrelid = 'public.page_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.page_revisions\r\n ADD CONSTRAINT page_revisions_page_version_key UNIQUE (page_id, version);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'page_revisions_pkey' AND conrelid = 'public.page_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.page_revisions\r\n ADD CONSTRAINT page_revisions_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pages_language_id_slug_key' AND conrelid = 'public.pages'::regclass) THEN\r\n ALTER TABLE ONLY public.pages\r\n ADD CONSTRAINT pages_language_id_slug_key UNIQUE (language_id, slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pages_pkey' AND conrelid = 'public.pages'::regclass) THEN\r\n ALTER TABLE ONLY public.pages\r\n ADD CONSTRAINT pages_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'post_revisions_pkey' AND conrelid = 'public.post_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.post_revisions\r\n ADD CONSTRAINT post_revisions_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'post_revisions_post_version_key' AND conrelid = 'public.post_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.post_revisions\r\n ADD CONSTRAINT post_revisions_post_version_key UNIQUE (post_id, version);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_language_id_slug_key' AND conrelid = 'public.posts'::regclass) THEN\r\n ALTER TABLE ONLY public.posts\r\n ADD CONSTRAINT posts_language_id_slug_key UNIQUE (language_id, slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_pkey' AND conrelid = 'public.posts'::regclass) THEN\r\n ALTER TABLE ONLY public.posts\r\n ADD CONSTRAINT posts_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'privacy_consent_logs_consent_token_key' AND conrelid = 'public.privacy_consent_logs'::regclass) THEN\r\n ALTER TABLE ONLY public.privacy_consent_logs\r\n ADD CONSTRAINT privacy_consent_logs_consent_token_key UNIQUE (consent_token);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'privacy_consent_logs_pkey' AND conrelid = 'public.privacy_consent_logs'::regclass) THEN\r\n ALTER TABLE ONLY public.privacy_consent_logs\r\n ADD CONSTRAINT privacy_consent_logs_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_attribute_terms_attribute_id_slug_key' AND conrelid = 'public.product_attribute_terms'::regclass) THEN\r\n ALTER TABLE ONLY public.product_attribute_terms\r\n ADD CONSTRAINT product_attribute_terms_attribute_id_slug_key UNIQUE (attribute_id, slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_attribute_terms_pkey' AND conrelid = 'public.product_attribute_terms'::regclass) THEN\r\n ALTER TABLE ONLY public.product_attribute_terms\r\n ADD CONSTRAINT product_attribute_terms_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_attributes_pkey' AND conrelid = 'public.product_attributes'::regclass) THEN\r\n ALTER TABLE ONLY public.product_attributes\r\n ADD CONSTRAINT product_attributes_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_attributes_slug_key' AND conrelid = 'public.product_attributes'::regclass) THEN\r\n ALTER TABLE ONLY public.product_attributes\r\n ADD CONSTRAINT product_attributes_slug_key UNIQUE (slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_categories_pkey' AND conrelid = 'public.product_categories'::regclass) THEN\r\n ALTER TABLE ONLY public.product_categories\r\n ADD CONSTRAINT product_categories_pkey PRIMARY KEY (product_id, category_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_drafts_pkey' AND conrelid = 'public.product_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.product_drafts\r\n ADD CONSTRAINT product_drafts_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_drafts_product_unique' AND conrelid = 'public.product_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.product_drafts\r\n ADD CONSTRAINT product_drafts_product_unique UNIQUE (product_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_freemius_sale_coupons_pkey' AND conrelid = 'public.product_freemius_sale_coupons'::regclass) THEN\r\n ALTER TABLE ONLY public.product_freemius_sale_coupons\r\n ADD CONSTRAINT product_freemius_sale_coupons_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_freemius_sale_coupons_product_unique' AND conrelid = 'public.product_freemius_sale_coupons'::regclass) THEN\r\n ALTER TABLE ONLY public.product_freemius_sale_coupons\r\n ADD CONSTRAINT product_freemius_sale_coupons_product_unique UNIQUE (product_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_media_pkey' AND conrelid = 'public.product_media'::regclass) THEN\r\n ALTER TABLE ONLY public.product_media\r\n ADD CONSTRAINT product_media_pkey PRIMARY KEY (product_id, media_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_variants_pkey' AND conrelid = 'public.product_variants'::regclass) THEN\r\n ALTER TABLE ONLY public.product_variants\r\n ADD CONSTRAINT product_variants_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_variants_product_id_sku_key' AND conrelid = 'public.product_variants'::regclass) THEN\r\n ALTER TABLE ONLY public.product_variants\r\n ADD CONSTRAINT product_variants_product_id_sku_key UNIQUE (product_id, sku);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'products_language_id_sku_key' AND conrelid = 'public.products'::regclass) THEN\r\n ALTER TABLE ONLY public.products\r\n ADD CONSTRAINT products_language_id_sku_key UNIQUE (language_id, sku);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'products_language_id_slug_key' AND conrelid = 'public.products'::regclass) THEN\r\n ALTER TABLE ONLY public.products\r\n ADD CONSTRAINT products_language_id_slug_key UNIQUE (language_id, slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'products_pkey' AND conrelid = 'public.products'::regclass) THEN\r\n ALTER TABLE ONLY public.products\r\n ADD CONSTRAINT products_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'profiles_pkey' AND conrelid = 'public.profiles'::regclass) THEN\r\n ALTER TABLE ONLY public.profiles\r\n ADD CONSTRAINT profiles_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'shipping_zone_locations_pkey' AND conrelid = 'public.shipping_zone_locations'::regclass) THEN\r\n ALTER TABLE ONLY public.shipping_zone_locations\r\n ADD CONSTRAINT shipping_zone_locations_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'shipping_zone_methods_pkey' AND conrelid = 'public.shipping_zone_methods'::regclass) THEN\r\n ALTER TABLE ONLY public.shipping_zone_methods\r\n ADD CONSTRAINT shipping_zone_methods_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'shipping_zones_pkey' AND conrelid = 'public.shipping_zones'::regclass) THEN\r\n ALTER TABLE ONLY public.shipping_zones\r\n ADD CONSTRAINT shipping_zones_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'site_settings_pkey' AND conrelid = 'public.site_settings'::regclass) THEN\r\n ALTER TABLE ONLY public.site_settings\r\n ADD CONSTRAINT site_settings_pkey PRIMARY KEY (key);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'system_alerts_pkey' AND conrelid = 'public.system_alerts'::regclass) THEN\r\n ALTER TABLE ONLY public.system_alerts\r\n ADD CONSTRAINT system_alerts_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'system_configuration_pkey' AND conrelid = 'public.system_configuration'::regclass) THEN\r\n ALTER TABLE ONLY public.system_configuration\r\n ADD CONSTRAINT system_configuration_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'tax_rates_pkey' AND conrelid = 'public.tax_rates'::regclass) THEN\r\n ALTER TABLE ONLY public.tax_rates\r\n ADD CONSTRAINT tax_rates_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'translations_pkey' AND conrelid = 'public.translations'::regclass) THEN\r\n ALTER TABLE ONLY public.translations\r\n ADD CONSTRAINT translations_pkey PRIMARY KEY (key);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'ucp_cart_sessions_pkey' AND conrelid = 'public.ucp_cart_sessions'::regclass) THEN\r\n ALTER TABLE ONLY public.ucp_cart_sessions\r\n ADD CONSTRAINT ucp_cart_sessions_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_addresses_pkey' AND conrelid = 'public.user_addresses'::regclass) THEN\r\n ALTER TABLE ONLY public.user_addresses\r\n ADD CONSTRAINT user_addresses_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_security_settings_pkey' AND conrelid = 'public.user_security_settings'::regclass) THEN\r\n ALTER TABLE ONLY public.user_security_settings\r\n ADD CONSTRAINT user_security_settings_pkey PRIMARY KEY (user_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_trusted_devices_device_hash_key' AND conrelid = 'public.user_trusted_devices'::regclass) THEN\r\n ALTER TABLE ONLY public.user_trusted_devices\r\n ADD CONSTRAINT user_trusted_devices_device_hash_key UNIQUE (device_hash);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_trusted_devices_pkey' AND conrelid = 'public.user_trusted_devices'::regclass) THEN\r\n ALTER TABLE ONLY public.user_trusted_devices\r\n ADD CONSTRAINT user_trusted_devices_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'variant_attribute_mapping_pkey' AND conrelid = 'public.variant_attribute_mapping'::regclass) THEN\r\n ALTER TABLE ONLY public.variant_attribute_mapping\r\n ADD CONSTRAINT variant_attribute_mapping_pkey PRIMARY KEY (variant_id, attribute_term_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE INDEX IF NOT EXISTS content_drafts_author_id_idx ON public.content_drafts USING btree (author_id);\r\n\r\nCREATE INDEX IF NOT EXISTS content_drafts_parent_idx ON public.content_drafts USING btree (parent_type, parent_id);\r\n\r\nCREATE INDEX IF NOT EXISTS content_drafts_updated_at_idx ON public.content_drafts USING btree (updated_at DESC);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS coupon_freemius_mappings_coupon_product_unique ON public.coupon_freemius_mappings USING btree (coupon_id, freemius_product_id);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS coupon_redemptions_order_unique ON public.coupon_redemptions USING btree (order_id) WHERE (order_id IS NOT NULL);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS coupons_code_unique ON public.coupons USING btree (upper(code));\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS ensure_single_default_language_idx ON public.languages USING btree (is_default) WHERE (is_default = true);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_blocks_language_id ON public.blocks USING btree (language_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_blocks_page_id ON public.blocks USING btree (page_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_blocks_post_id ON public.blocks USING btree (post_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_coupon_freemius_mappings_freemius_product_id ON public.coupon_freemius_mappings USING btree (freemius_product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_coupon_freemius_mappings_product_id ON public.coupon_freemius_mappings USING btree (product_id) WHERE (product_id IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_coupon_products_product_id ON public.coupon_products USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_coupon_redemptions_coupon_id ON public.coupon_redemptions USING btree (coupon_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_coupon_redemptions_user_id ON public.coupon_redemptions USING btree (user_id) WHERE (user_id IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_coupons_active_dates ON public.coupons USING btree (is_active, starts_at, ends_at);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS idx_currencies_single_default ON public.currencies USING btree (is_default) WHERE (is_default = true);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_custom_block_definitions_is_original ON public.custom_block_definitions USING btree (is_original);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_email_2fa_challenges_expires_at ON public.email_2fa_challenges USING btree (expires_at);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_email_2fa_challenges_user_id ON public.email_2fa_challenges USING btree (user_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_freemius_plans_product_id ON public.freemius_plans USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_freemius_pricing_plan_id ON public.freemius_pricing USING btree (plan_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_inventory_items_updated_at ON public.inventory_items USING btree (updated_at DESC);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_logos_media_id ON public.logos USING btree (media_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_media_uploader_id ON public.media USING btree (uploader_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_navigation_items_language_id ON public.navigation_items USING btree (language_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_navigation_items_menu_lang_order ON public.navigation_items USING btree (menu_key, language_id, \"order\");\r\n\r\nCREATE INDEX IF NOT EXISTS idx_navigation_items_page_id ON public.navigation_items USING btree (page_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_navigation_items_parent_id ON public.navigation_items USING btree (parent_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_order_items_order_id ON public.order_items USING btree (order_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_order_items_product_id ON public.order_items USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_order_items_variant_id ON public.order_items USING btree (variant_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_orders_coupon_id ON public.orders USING btree (coupon_id) WHERE (coupon_id IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_orders_freemius_license_id ON public.orders USING btree (freemius_license_id) WHERE (freemius_license_id IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_orders_freemius_subscription_id ON public.orders USING btree (freemius_subscription_id) WHERE (freemius_subscription_id IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_orders_freemius_trial_id ON public.orders USING btree (freemius_trial_id) WHERE (freemius_trial_id IS NOT NULL);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS idx_orders_invoice_number_unique ON public.orders USING btree (invoice_number) WHERE (invoice_number IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_orders_user_id ON public.orders USING btree (user_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_package_activations_license_key ON public.package_activations USING btree (license_key);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_package_activations_package_id ON public.package_activations USING btree (package_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_page_revisions_author_id ON public.page_revisions USING btree (author_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_page_revisions_page_id_version ON public.page_revisions USING btree (page_id, version);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_pages_author_id ON public.pages USING btree (author_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_pages_feature_image_id ON public.pages USING btree (feature_image_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_pages_translation_group_id ON public.pages USING btree (translation_group_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_post_revisions_author_id ON public.post_revisions USING btree (author_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_post_revisions_post_id_version ON public.post_revisions USING btree (post_id, version);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_posts_author_id ON public.posts USING btree (author_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_posts_feature_image_id ON public.posts USING btree (feature_image_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_posts_translation_group_id ON public.posts USING btree (translation_group_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_privacy_consent_logs_created_at ON public.privacy_consent_logs USING btree (created_at DESC);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_attribute_terms_attribute_id ON public.product_attribute_terms USING btree (attribute_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_categories_category_id ON public.product_categories USING btree (category_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_categories_product_id ON public.product_categories USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_freemius_sale_coupons_freemius_product_id ON public.product_freemius_sale_coupons USING btree (freemius_product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_media_media_id ON public.product_media USING btree (media_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_media_product_id ON public.product_media USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_variants_main_media_id ON public.product_variants USING btree (main_media_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_variants_prices_gin ON public.product_variants USING gin (prices jsonb_path_ops);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_variants_product_id ON public.product_variants USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_products_prices_gin ON public.products USING gin (prices jsonb_path_ops);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_products_slug ON public.products USING btree (slug);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_products_translation_group_id ON public.products USING btree (translation_group_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_shipping_zone_locations_country_state_postal ON public.shipping_zone_locations USING btree (country_code, state_code, postal_code);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_shipping_zone_locations_zone_id ON public.shipping_zone_locations USING btree (zone_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_shipping_zone_methods_name_translations ON public.shipping_zone_methods USING gin (name_translations);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_shipping_zone_methods_zone_id ON public.shipping_zone_methods USING btree (zone_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_system_alerts_unresolved ON public.system_alerts USING btree (is_resolved, created_at DESC);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_tax_rates_country_state ON public.tax_rates USING btree (country_code, state_code);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_ucp_cart_sessions_created_at ON public.ucp_cart_sessions USING btree (created_at DESC);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_ucp_cart_sessions_status_expires_at ON public.ucp_cart_sessions USING btree (status, expires_at);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS idx_user_addresses_one_default_per_type ON public.user_addresses USING btree (user_id, address_type) WHERE (is_default = true);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_user_addresses_type ON public.user_addresses USING btree (address_type);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_user_addresses_user_id ON public.user_addresses USING btree (user_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_user_trusted_devices_expires_at ON public.user_trusted_devices USING btree (expires_at);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_user_trusted_devices_user_id ON public.user_trusted_devices USING btree (user_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_variant_attribute_mapping_attribute_term_id ON public.variant_attribute_mapping USING btree (attribute_term_id);\r\n\r\nCREATE INDEX IF NOT EXISTS media_folder_idx ON public.media USING btree (folder);\r\n\r\nCREATE INDEX IF NOT EXISTS product_drafts_author_id_idx ON public.product_drafts USING btree (author_id);\r\n\r\nCREATE INDEX IF NOT EXISTS product_drafts_product_id_idx ON public.product_drafts USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS product_drafts_updated_at_idx ON public.product_drafts USING btree (updated_at DESC);\r\n\r\nCREATE INDEX IF NOT EXISTS product_variants_sale_window_idx ON public.product_variants USING btree (sale_start_at, sale_end_at) WHERE ((sale_start_at IS NOT NULL) OR (sale_end_at IS NOT NULL));\r\n\r\nCREATE INDEX IF NOT EXISTS product_variants_scheduled_price_idx ON public.product_variants USING btree (scheduled_price_at) WHERE (scheduled_price_at IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS products_sale_window_idx ON public.products USING btree (sale_start_at, sale_end_at) WHERE ((sale_start_at IS NOT NULL) OR (sale_end_at IS NOT NULL));\r\n\r\nCREATE INDEX IF NOT EXISTS products_scheduled_price_idx ON public.products USING btree (scheduled_price_at) WHERE (scheduled_price_at IS NOT NULL);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS tax_rates_country_state_name_key ON public.tax_rates USING btree (country_code, COALESCE(state_code, ''::text), lower(tax_name));\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'blocks_language_id_fkey' AND conrelid = 'public.blocks'::regclass) THEN\r\n ALTER TABLE ONLY public.blocks\r\n ADD CONSTRAINT blocks_language_id_fkey FOREIGN KEY (language_id) REFERENCES public.languages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'blocks_page_id_fkey' AND conrelid = 'public.blocks'::regclass) THEN\r\n ALTER TABLE ONLY public.blocks\r\n ADD CONSTRAINT blocks_page_id_fkey FOREIGN KEY (page_id) REFERENCES public.pages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'blocks_post_id_fkey' AND conrelid = 'public.blocks'::regclass) THEN\r\n ALTER TABLE ONLY public.blocks\r\n ADD CONSTRAINT blocks_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'blocks_product_id_fkey' AND conrelid = 'public.blocks'::regclass) THEN\r\n ALTER TABLE ONLY public.blocks\r\n ADD CONSTRAINT blocks_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cms_interactions_post_id_fkey' AND conrelid = 'public.cms_interactions'::regclass) THEN\r\n ALTER TABLE ONLY public.cms_interactions\r\n ADD CONSTRAINT cms_interactions_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cms_interactions_product_id_fkey' AND conrelid = 'public.cms_interactions'::regclass) THEN\r\n ALTER TABLE ONLY public.cms_interactions\r\n ADD CONSTRAINT cms_interactions_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cms_interactions_user_id_profiles_fkey' AND conrelid = 'public.cms_interactions'::regclass) THEN\r\n ALTER TABLE ONLY public.cms_interactions\r\n ADD CONSTRAINT cms_interactions_user_id_profiles_fkey FOREIGN KEY (user_id) REFERENCES public.profiles(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'content_drafts_author_id_fkey' AND conrelid = 'public.content_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.content_drafts\r\n ADD CONSTRAINT content_drafts_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cortex_ai_db_mutation_audit_actor_user_id_fkey' AND conrelid = 'public.cortex_ai_db_mutation_audit'::regclass) THEN\r\n ALTER TABLE ONLY public.cortex_ai_db_mutation_audit\r\n ADD CONSTRAINT cortex_ai_db_mutation_audit_actor_user_id_fkey FOREIGN KEY (actor_user_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_freemius_mappings_coupon_id_fkey' AND conrelid = 'public.coupon_freemius_mappings'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_freemius_mappings\r\n ADD CONSTRAINT coupon_freemius_mappings_coupon_id_fkey FOREIGN KEY (coupon_id) REFERENCES public.coupons(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_freemius_mappings_product_id_fkey' AND conrelid = 'public.coupon_freemius_mappings'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_freemius_mappings\r\n ADD CONSTRAINT coupon_freemius_mappings_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_products_coupon_id_fkey' AND conrelid = 'public.coupon_products'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_products\r\n ADD CONSTRAINT coupon_products_coupon_id_fkey FOREIGN KEY (coupon_id) REFERENCES public.coupons(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_products_product_id_fkey' AND conrelid = 'public.coupon_products'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_products\r\n ADD CONSTRAINT coupon_products_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_redemptions_coupon_id_fkey' AND conrelid = 'public.coupon_redemptions'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_redemptions\r\n ADD CONSTRAINT coupon_redemptions_coupon_id_fkey FOREIGN KEY (coupon_id) REFERENCES public.coupons(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_redemptions_order_id_fkey' AND conrelid = 'public.coupon_redemptions'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_redemptions\r\n ADD CONSTRAINT coupon_redemptions_order_id_fkey FOREIGN KEY (order_id) REFERENCES public.orders(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_redemptions_user_id_fkey' AND conrelid = 'public.coupon_redemptions'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_redemptions\r\n ADD CONSTRAINT coupon_redemptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'email_2fa_challenges_user_id_fkey' AND conrelid = 'public.email_2fa_challenges'::regclass) THEN\r\n ALTER TABLE ONLY public.email_2fa_challenges\r\n ADD CONSTRAINT email_2fa_challenges_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'freemius_plans_product_id_fkey' AND conrelid = 'public.freemius_plans'::regclass) THEN\r\n ALTER TABLE ONLY public.freemius_plans\r\n ADD CONSTRAINT freemius_plans_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'freemius_pricing_plan_id_fkey' AND conrelid = 'public.freemius_pricing'::regclass) THEN\r\n ALTER TABLE ONLY public.freemius_pricing\r\n ADD CONSTRAINT freemius_pricing_plan_id_fkey FOREIGN KEY (plan_id) REFERENCES public.freemius_plans(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'logos_media_id_fkey' AND conrelid = 'public.logos'::regclass) THEN\r\n ALTER TABLE ONLY public.logos\r\n ADD CONSTRAINT logos_media_id_fkey FOREIGN KEY (media_id) REFERENCES public.media(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'media_uploader_id_fkey' AND conrelid = 'public.media'::regclass) THEN\r\n ALTER TABLE ONLY public.media\r\n ADD CONSTRAINT media_uploader_id_fkey FOREIGN KEY (uploader_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'navigation_items_language_id_fkey' AND conrelid = 'public.navigation_items'::regclass) THEN\r\n ALTER TABLE ONLY public.navigation_items\r\n ADD CONSTRAINT navigation_items_language_id_fkey FOREIGN KEY (language_id) REFERENCES public.languages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'navigation_items_page_id_fkey' AND conrelid = 'public.navigation_items'::regclass) THEN\r\n ALTER TABLE ONLY public.navigation_items\r\n ADD CONSTRAINT navigation_items_page_id_fkey FOREIGN KEY (page_id) REFERENCES public.pages(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'navigation_items_parent_id_fkey' AND conrelid = 'public.navigation_items'::regclass) THEN\r\n ALTER TABLE ONLY public.navigation_items\r\n ADD CONSTRAINT navigation_items_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES public.navigation_items(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'order_items_order_id_fkey' AND conrelid = 'public.order_items'::regclass) THEN\r\n ALTER TABLE ONLY public.order_items\r\n ADD CONSTRAINT order_items_order_id_fkey FOREIGN KEY (order_id) REFERENCES public.orders(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'order_items_product_id_fkey' AND conrelid = 'public.order_items'::regclass) THEN\r\n ALTER TABLE ONLY public.order_items\r\n ADD CONSTRAINT order_items_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'order_items_variant_id_fkey' AND conrelid = 'public.order_items'::regclass) THEN\r\n ALTER TABLE ONLY public.order_items\r\n ADD CONSTRAINT order_items_variant_id_fkey FOREIGN KEY (variant_id) REFERENCES public.product_variants(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'orders_coupon_id_fkey' AND conrelid = 'public.orders'::regclass) THEN\r\n ALTER TABLE ONLY public.orders\r\n ADD CONSTRAINT orders_coupon_id_fkey FOREIGN KEY (coupon_id) REFERENCES public.coupons(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'orders_user_id_fkey' AND conrelid = 'public.orders'::regclass) THEN\r\n ALTER TABLE ONLY public.orders\r\n ADD CONSTRAINT orders_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'page_revisions_author_id_fkey' AND conrelid = 'public.page_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.page_revisions\r\n ADD CONSTRAINT page_revisions_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'page_revisions_page_id_fkey' AND conrelid = 'public.page_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.page_revisions\r\n ADD CONSTRAINT page_revisions_page_id_fkey FOREIGN KEY (page_id) REFERENCES public.pages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pages_author_id_fkey' AND conrelid = 'public.pages'::regclass) THEN\r\n ALTER TABLE ONLY public.pages\r\n ADD CONSTRAINT pages_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pages_feature_image_id_fkey' AND conrelid = 'public.pages'::regclass) THEN\r\n ALTER TABLE ONLY public.pages\r\n ADD CONSTRAINT pages_feature_image_id_fkey FOREIGN KEY (feature_image_id) REFERENCES public.media(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pages_language_id_fkey' AND conrelid = 'public.pages'::regclass) THEN\r\n ALTER TABLE ONLY public.pages\r\n ADD CONSTRAINT pages_language_id_fkey FOREIGN KEY (language_id) REFERENCES public.languages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'post_revisions_author_id_fkey' AND conrelid = 'public.post_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.post_revisions\r\n ADD CONSTRAINT post_revisions_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'post_revisions_post_id_fkey' AND conrelid = 'public.post_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.post_revisions\r\n ADD CONSTRAINT post_revisions_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_author_id_fkey' AND conrelid = 'public.posts'::regclass) THEN\r\n ALTER TABLE ONLY public.posts\r\n ADD CONSTRAINT posts_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_feature_image_id_fkey' AND conrelid = 'public.posts'::regclass) THEN\r\n ALTER TABLE ONLY public.posts\r\n ADD CONSTRAINT posts_feature_image_id_fkey FOREIGN KEY (feature_image_id) REFERENCES public.media(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_language_id_fkey' AND conrelid = 'public.posts'::regclass) THEN\r\n ALTER TABLE ONLY public.posts\r\n ADD CONSTRAINT posts_language_id_fkey FOREIGN KEY (language_id) REFERENCES public.languages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_attribute_terms_attribute_id_fkey' AND conrelid = 'public.product_attribute_terms'::regclass) THEN\r\n ALTER TABLE ONLY public.product_attribute_terms\r\n ADD CONSTRAINT product_attribute_terms_attribute_id_fkey FOREIGN KEY (attribute_id) REFERENCES public.product_attributes(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_categories_category_id_fkey' AND conrelid = 'public.product_categories'::regclass) THEN\r\n ALTER TABLE ONLY public.product_categories\r\n ADD CONSTRAINT product_categories_category_id_fkey FOREIGN KEY (category_id) REFERENCES public.categories(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_categories_product_id_fkey' AND conrelid = 'public.product_categories'::regclass) THEN\r\n ALTER TABLE ONLY public.product_categories\r\n ADD CONSTRAINT product_categories_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_drafts_author_id_fkey' AND conrelid = 'public.product_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.product_drafts\r\n ADD CONSTRAINT product_drafts_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_drafts_product_id_fkey' AND conrelid = 'public.product_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.product_drafts\r\n ADD CONSTRAINT product_drafts_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_freemius_sale_coupons_product_id_fkey' AND conrelid = 'public.product_freemius_sale_coupons'::regclass) THEN\r\n ALTER TABLE ONLY public.product_freemius_sale_coupons\r\n ADD CONSTRAINT product_freemius_sale_coupons_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_media_media_id_fkey' AND conrelid = 'public.product_media'::regclass) THEN\r\n ALTER TABLE ONLY public.product_media\r\n ADD CONSTRAINT product_media_media_id_fkey FOREIGN KEY (media_id) REFERENCES public.media(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_media_product_id_fkey' AND conrelid = 'public.product_media'::regclass) THEN\r\n ALTER TABLE ONLY public.product_media\r\n ADD CONSTRAINT product_media_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_variants_main_media_id_fkey' AND conrelid = 'public.product_variants'::regclass) THEN\r\n ALTER TABLE ONLY public.product_variants\r\n ADD CONSTRAINT product_variants_main_media_id_fkey FOREIGN KEY (main_media_id) REFERENCES public.media(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_variants_product_id_fkey' AND conrelid = 'public.product_variants'::regclass) THEN\r\n ALTER TABLE ONLY public.product_variants\r\n ADD CONSTRAINT product_variants_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'products_language_id_fkey' AND conrelid = 'public.products'::regclass) THEN\r\n ALTER TABLE ONLY public.products\r\n ADD CONSTRAINT products_language_id_fkey FOREIGN KEY (language_id) REFERENCES public.languages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'profiles_id_fkey' AND conrelid = 'public.profiles'::regclass) THEN\r\n ALTER TABLE ONLY public.profiles\r\n ADD CONSTRAINT profiles_id_fkey FOREIGN KEY (id) REFERENCES auth.users(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'shipping_zone_locations_zone_id_fkey' AND conrelid = 'public.shipping_zone_locations'::regclass) THEN\r\n ALTER TABLE ONLY public.shipping_zone_locations\r\n ADD CONSTRAINT shipping_zone_locations_zone_id_fkey FOREIGN KEY (zone_id) REFERENCES public.shipping_zones(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'shipping_zone_methods_zone_id_fkey' AND conrelid = 'public.shipping_zone_methods'::regclass) THEN\r\n ALTER TABLE ONLY public.shipping_zone_methods\r\n ADD CONSTRAINT shipping_zone_methods_zone_id_fkey FOREIGN KEY (zone_id) REFERENCES public.shipping_zones(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_addresses_user_id_fkey' AND conrelid = 'public.user_addresses'::regclass) THEN\r\n ALTER TABLE ONLY public.user_addresses\r\n ADD CONSTRAINT user_addresses_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.profiles(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_security_settings_user_id_fkey' AND conrelid = 'public.user_security_settings'::regclass) THEN\r\n ALTER TABLE ONLY public.user_security_settings\r\n ADD CONSTRAINT user_security_settings_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_trusted_devices_user_id_fkey' AND conrelid = 'public.user_trusted_devices'::regclass) THEN\r\n ALTER TABLE ONLY public.user_trusted_devices\r\n ADD CONSTRAINT user_trusted_devices_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'variant_attribute_mapping_attribute_term_id_fkey' AND conrelid = 'public.variant_attribute_mapping'::regclass) THEN\r\n ALTER TABLE ONLY public.variant_attribute_mapping\r\n ADD CONSTRAINT variant_attribute_mapping_attribute_term_id_fkey FOREIGN KEY (attribute_term_id) REFERENCES public.product_attribute_terms(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'variant_attribute_mapping_variant_id_fkey' AND conrelid = 'public.variant_attribute_mapping'::regclass) THEN\r\n ALTER TABLE ONLY public.variant_attribute_mapping\r\n ADD CONSTRAINT variant_attribute_mapping_variant_id_fkey FOREIGN KEY (variant_id) REFERENCES public.product_variants(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n"
26
- },
27
- {
28
- "version": "00000000000002",
29
- "name": "00000000000002_baseline_security_and_grants.sql",
30
- "sql": "-- AUTO-GENERATED baseline (re-baseline of migrations 000..044). Idempotent; safe to replay.\r\n-- 02 · row-level security, policies, triggers, grants\r\n-- Regenerate via tools/scripts/rebaseline-transform.mjs. Do not hand-edit.\r\n\r\nDROP TRIGGER IF EXISTS on_blocks_update ON public.blocks;\r\nCREATE TRIGGER on_blocks_update BEFORE UPDATE ON public.blocks FOR EACH ROW EXECUTE FUNCTION public.handle_blocks_update();\r\n\r\nDROP TRIGGER IF EXISTS on_content_drafts_update ON public.content_drafts;\r\nCREATE TRIGGER on_content_drafts_update BEFORE UPDATE ON public.content_drafts FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\r\n\r\nDROP TRIGGER IF EXISTS on_coupon_freemius_mappings_write ON public.coupon_freemius_mappings;\r\nCREATE TRIGGER on_coupon_freemius_mappings_write BEFORE INSERT OR UPDATE ON public.coupon_freemius_mappings FOR EACH ROW EXECUTE FUNCTION public.handle_coupon_freemius_mappings_write();\r\n\r\nDROP TRIGGER IF EXISTS on_coupons_write ON public.coupons;\r\nCREATE TRIGGER on_coupons_write BEFORE INSERT OR UPDATE ON public.coupons FOR EACH ROW EXECUTE FUNCTION public.handle_coupons_write();\r\n\r\nDROP TRIGGER IF EXISTS on_inventory_item_change ON public.inventory_items;\r\nCREATE TRIGGER on_inventory_item_change AFTER INSERT OR DELETE OR UPDATE OF quantity ON public.inventory_items FOR EACH ROW EXECUTE FUNCTION public.handle_inventory_item_change();\r\n\r\nDROP TRIGGER IF EXISTS on_inventory_items_update ON public.inventory_items;\r\nCREATE TRIGGER on_inventory_items_update BEFORE UPDATE ON public.inventory_items FOR EACH ROW EXECUTE FUNCTION public.handle_inventory_items_update();\r\n\r\nDROP TRIGGER IF EXISTS on_languages_update ON public.languages;\r\nCREATE TRIGGER on_languages_update BEFORE UPDATE ON public.languages FOR EACH ROW EXECUTE FUNCTION public.handle_languages_update();\r\n\r\nDROP TRIGGER IF EXISTS on_media_update ON public.media;\r\nCREATE TRIGGER on_media_update BEFORE UPDATE ON public.media FOR EACH ROW EXECUTE FUNCTION public.handle_media_update();\r\n\r\nDROP TRIGGER IF EXISTS on_navigation_items_update ON public.navigation_items;\r\nCREATE TRIGGER on_navigation_items_update BEFORE UPDATE ON public.navigation_items FOR EACH ROW EXECUTE FUNCTION public.handle_navigation_items_update();\r\n\r\nDROP TRIGGER IF EXISTS on_pages_update ON public.pages;\r\nCREATE TRIGGER on_pages_update BEFORE UPDATE ON public.pages FOR EACH ROW EXECUTE FUNCTION public.handle_pages_update();\r\n\r\nDROP TRIGGER IF EXISTS on_posts_update ON public.posts;\r\nCREATE TRIGGER on_posts_update BEFORE UPDATE ON public.posts FOR EACH ROW EXECUTE FUNCTION public.handle_posts_update();\r\n\r\nDROP TRIGGER IF EXISTS on_product_drafts_update ON public.product_drafts;\r\nCREATE TRIGGER on_product_drafts_update BEFORE UPDATE ON public.product_drafts FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\r\n\r\nDROP TRIGGER IF EXISTS on_product_freemius_sale_coupons_write ON public.product_freemius_sale_coupons;\r\nCREATE TRIGGER on_product_freemius_sale_coupons_write BEFORE INSERT OR UPDATE ON public.product_freemius_sale_coupons FOR EACH ROW EXECUTE FUNCTION public.handle_product_freemius_sale_coupons_write();\r\n\r\nDROP TRIGGER IF EXISTS on_shipping_zone_locations_write ON public.shipping_zone_locations;\r\nCREATE TRIGGER on_shipping_zone_locations_write BEFORE INSERT OR UPDATE ON public.shipping_zone_locations FOR EACH ROW EXECUTE FUNCTION public.handle_shipping_zone_locations_write();\r\n\r\nDROP TRIGGER IF EXISTS on_tax_rates_write ON public.tax_rates;\r\nCREATE TRIGGER on_tax_rates_write BEFORE INSERT OR UPDATE ON public.tax_rates FOR EACH ROW EXECUTE FUNCTION public.handle_tax_rates_write();\r\n\r\nDROP TRIGGER IF EXISTS set_updated_at ON public.cms_interactions;\r\nCREATE TRIGGER set_updated_at BEFORE UPDATE ON public.cms_interactions FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\r\n\r\nDROP TRIGGER IF EXISTS set_updated_at ON public.translations;\r\nCREATE TRIGGER set_updated_at BEFORE UPDATE ON public.translations FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\r\n\r\nDROP TRIGGER IF EXISTS trg_handle_default_currency_change ON public.currencies;\r\nCREATE TRIGGER trg_handle_default_currency_change AFTER INSERT OR UPDATE ON public.currencies FOR EACH ROW EXECUTE FUNCTION public.handle_default_currency_change();\r\n\r\nDROP TRIGGER IF EXISTS trg_handle_ucp_cart_sessions_update ON public.ucp_cart_sessions;\r\nCREATE TRIGGER trg_handle_ucp_cart_sessions_update BEFORE UPDATE ON public.ucp_cart_sessions FOR EACH ROW EXECUTE FUNCTION public.handle_ucp_cart_sessions_update();\r\n\r\nDROP TRIGGER IF EXISTS trg_set_currency_defaults ON public.currencies;\r\nCREATE TRIGGER trg_set_currency_defaults BEFORE INSERT OR UPDATE ON public.currencies FOR EACH ROW EXECUTE FUNCTION public.set_currency_defaults();\r\n\r\nDROP TRIGGER IF EXISTS trg_sync_product_variants_currency_prices ON public.product_variants;\r\nCREATE TRIGGER trg_sync_product_variants_currency_prices BEFORE INSERT OR UPDATE OF price, sale_price, prices, sale_prices ON public.product_variants FOR EACH ROW EXECUTE FUNCTION public.sync_currency_price_maps();\r\n\r\nDROP TRIGGER IF EXISTS trg_sync_products_currency_prices ON public.products;\r\nCREATE TRIGGER trg_sync_products_currency_prices BEFORE INSERT OR UPDATE OF price, sale_price, prices, sale_prices ON public.products FOR EACH ROW EXECUTE FUNCTION public.sync_currency_price_maps();\r\n\r\nDROP TRIGGER IF EXISTS trg_sync_shipping_method_currency_maps ON public.shipping_zone_methods;\r\nCREATE TRIGGER trg_sync_shipping_method_currency_maps BEFORE INSERT OR UPDATE OF cost_amount, cost_currency, min_order_amount, currency_pricing_mode, cost_amounts, min_order_amounts ON public.shipping_zone_methods FOR EACH ROW EXECUTE FUNCTION public.sync_shipping_method_currency_maps();\r\n\r\nDROP TRIGGER IF EXISTS trg_system_alerts_updated_at ON public.system_alerts;\r\nCREATE TRIGGER trg_system_alerts_updated_at BEFORE UPDATE ON public.system_alerts FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\r\n\r\nDROP TRIGGER IF EXISTS trigger_update_product_ratings ON public.cms_interactions;\r\nCREATE TRIGGER trigger_update_product_ratings AFTER INSERT OR DELETE OR UPDATE ON public.cms_interactions FOR EACH ROW EXECUTE FUNCTION public.update_product_ratings();\r\n\r\nDROP POLICY IF EXISTS \"Admin can delete categories\" ON public.categories;\r\nCREATE POLICY \"Admin can delete categories\" ON public.categories FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS \"Admin can delete product_categories\" ON public.product_categories;\r\nCREATE POLICY \"Admin can delete product_categories\" ON public.product_categories FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS \"Admin can insert categories\" ON public.categories;\r\nCREATE POLICY \"Admin can insert categories\" ON public.categories FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS \"Admin can insert product_categories\" ON public.product_categories;\r\nCREATE POLICY \"Admin can insert product_categories\" ON public.product_categories FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS \"Admin can update categories\" ON public.categories;\r\nCREATE POLICY \"Admin can update categories\" ON public.categories FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS \"Admin can update product_categories\" ON public.product_categories;\r\nCREATE POLICY \"Admin can update product_categories\" ON public.product_categories FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS \"Allow authenticated read access\" ON public.package_activations;\r\nCREATE POLICY \"Allow authenticated read access\" ON public.package_activations FOR SELECT TO authenticated USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Allow service role full access\" ON public.package_activations;\r\nCREATE POLICY \"Allow service role full access\" ON public.package_activations TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Public can view categories\" ON public.categories;\r\nCREATE POLICY \"Public can view categories\" ON public.categories FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public can view inventory items\" ON public.inventory_items;\r\nCREATE POLICY \"Public can view inventory items\" ON public.inventory_items FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public can view product media\" ON public.product_media;\r\nCREATE POLICY \"Public can view product media\" ON public.product_media FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public can view product_categories\" ON public.product_categories;\r\nCREATE POLICY \"Public can view product_categories\" ON public.product_categories FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public can view products\" ON public.products;\r\nCREATE POLICY \"Public can view products\" ON public.products FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read access for freemius_plans\" ON public.freemius_plans;\r\nCREATE POLICY \"Public read access for freemius_plans\" ON public.freemius_plans FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read access for freemius_pricing\" ON public.freemius_pricing;\r\nCREATE POLICY \"Public read access for freemius_pricing\" ON public.freemius_pricing FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read active currencies\" ON public.currencies;\r\nCREATE POLICY \"Public read active currencies\" ON public.currencies FOR SELECT TO authenticated, anon USING ((is_active = true));\r\n\r\nDROP POLICY IF EXISTS \"Public read product_attribute_terms\" ON public.product_attribute_terms;\r\nCREATE POLICY \"Public read product_attribute_terms\" ON public.product_attribute_terms FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read product_attributes\" ON public.product_attributes;\r\nCREATE POLICY \"Public read product_attributes\" ON public.product_attributes FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read product_variants\" ON public.product_variants;\r\nCREATE POLICY \"Public read product_variants\" ON public.product_variants FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read shipping_zone_locations\" ON public.shipping_zone_locations;\r\nCREATE POLICY \"Public read shipping_zone_locations\" ON public.shipping_zone_locations FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read shipping_zone_methods\" ON public.shipping_zone_methods;\r\nCREATE POLICY \"Public read shipping_zone_methods\" ON public.shipping_zone_methods FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read shipping_zones\" ON public.shipping_zones;\r\nCREATE POLICY \"Public read shipping_zones\" ON public.shipping_zones FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read tax_rates\" ON public.tax_rates;\r\nCREATE POLICY \"Public read tax_rates\" ON public.tax_rates FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read variant_attribute_mapping\" ON public.variant_attribute_mapping;\r\nCREATE POLICY \"Public read variant_attribute_mapping\" ON public.variant_attribute_mapping FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Service Role manages inventory items\" ON public.inventory_items;\r\nCREATE POLICY \"Service Role manages inventory items\" ON public.inventory_items TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Service Role manages order items\" ON public.order_items;\r\nCREATE POLICY \"Service Role manages order items\" ON public.order_items TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Service Role manages orders\" ON public.orders;\r\nCREATE POLICY \"Service Role manages orders\" ON public.orders TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Service Role manages tax_rates\" ON public.tax_rates;\r\nCREATE POLICY \"Service Role manages tax_rates\" ON public.tax_rates TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Service role manages all addresses\" ON public.user_addresses;\r\nCREATE POLICY \"Service role manages all addresses\" ON public.user_addresses TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Service role manages currencies\" ON public.currencies;\r\nCREATE POLICY \"Service role manages currencies\" ON public.currencies TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Users can manage own addresses\" ON public.user_addresses;\r\nCREATE POLICY \"Users can manage own addresses\" ON public.user_addresses TO authenticated USING ((user_id = ( SELECT auth.uid() AS uid))) WITH CHECK ((user_id = ( SELECT auth.uid() AS uid)));\r\n\r\nDROP POLICY IF EXISTS \"Users can view own order items\" ON public.order_items;\r\nCREATE POLICY \"Users can view own order items\" ON public.order_items FOR SELECT TO authenticated USING (((( SELECT public.is_admin() AS is_admin) IS TRUE) OR (EXISTS ( SELECT 1\r\n FROM public.orders\r\n WHERE ((orders.id = order_items.order_id) AND (orders.user_id = ( SELECT auth.uid() AS uid)))))));\r\n\r\nDROP POLICY IF EXISTS \"Users can view own orders\" ON public.orders;\r\nCREATE POLICY \"Users can view own orders\" ON public.orders FOR SELECT TO authenticated USING (((( SELECT public.is_admin() AS is_admin) IS TRUE) OR (user_id = ( SELECT auth.uid() AS uid))));\r\n\r\nALTER TABLE public.blocks ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS blocks_anon_read_policy ON public.blocks;\r\nCREATE POLICY blocks_anon_read_policy ON public.blocks FOR SELECT TO anon USING ((((page_id IS NOT NULL) AND (EXISTS ( SELECT 1\r\n FROM public.pages p\r\n WHERE ((p.id = blocks.page_id) AND (p.status = 'published'::public.page_status))))) OR ((post_id IS NOT NULL) AND (EXISTS ( SELECT 1\r\n FROM public.posts pt\r\n WHERE ((pt.id = blocks.post_id) AND (pt.status = 'published'::public.page_status) AND ((pt.published_at IS NULL) OR (pt.published_at <= now())))))) OR ((product_id IS NOT NULL) AND (EXISTS ( SELECT 1\r\n FROM public.products pr\r\n WHERE ((pr.id = blocks.product_id) AND (pr.status = 'active'::text)))))));\r\n\r\nDROP POLICY IF EXISTS blocks_delete_policy ON public.blocks;\r\nCREATE POLICY blocks_delete_policy ON public.blocks FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS blocks_insert_policy ON public.blocks;\r\nCREATE POLICY blocks_insert_policy ON public.blocks FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS blocks_read_policy ON public.blocks;\r\nCREATE POLICY blocks_read_policy ON public.blocks FOR SELECT TO authenticated USING (((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])) OR (((page_id IS NOT NULL) AND (EXISTS ( SELECT 1\r\n FROM public.pages p\r\n WHERE ((p.id = blocks.page_id) AND (p.status = 'published'::public.page_status))))) OR ((post_id IS NOT NULL) AND (EXISTS ( SELECT 1\r\n FROM public.posts pt\r\n WHERE ((pt.id = blocks.post_id) AND (pt.status = 'published'::public.page_status) AND ((pt.published_at IS NULL) OR (pt.published_at <= now())))))) OR ((product_id IS NOT NULL) AND (EXISTS ( SELECT 1\r\n FROM public.products pr\r\n WHERE ((pr.id = blocks.product_id) AND (pr.status = 'active'::text))))))));\r\n\r\nDROP POLICY IF EXISTS blocks_update_policy ON public.blocks;\r\nCREATE POLICY blocks_update_policy ON public.blocks FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.categories ENABLE ROW LEVEL SECURITY;\r\n\r\nALTER TABLE public.cms_interactions ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS cms_interactions_delete_policy ON public.cms_interactions;\r\nCREATE POLICY cms_interactions_delete_policy ON public.cms_interactions FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS cms_interactions_insert_policy ON public.cms_interactions;\r\nCREATE POLICY cms_interactions_insert_policy ON public.cms_interactions FOR INSERT TO authenticated WITH CHECK (((auth.uid() = user_id) AND ((status = 'pending'::public.approval_status) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])))));\r\n\r\nDROP POLICY IF EXISTS cms_interactions_read_policy ON public.cms_interactions;\r\nCREATE POLICY cms_interactions_read_policy ON public.cms_interactions FOR SELECT USING (((status = 'approved'::public.approval_status) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))));\r\n\r\nDROP POLICY IF EXISTS cms_interactions_update_policy ON public.cms_interactions;\r\nCREATE POLICY cms_interactions_update_policy ON public.cms_interactions FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.content_drafts ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS content_drafts_delete_policy ON public.content_drafts;\r\nCREATE POLICY content_drafts_delete_policy ON public.content_drafts FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS content_drafts_insert_policy ON public.content_drafts;\r\nCREATE POLICY content_drafts_insert_policy ON public.content_drafts FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS content_drafts_select_policy ON public.content_drafts;\r\nCREATE POLICY content_drafts_select_policy ON public.content_drafts FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS content_drafts_update_policy ON public.content_drafts;\r\nCREATE POLICY content_drafts_update_policy ON public.content_drafts FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.cortex_ai_db_mutation_audit ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS cortex_ai_db_mutation_audit_admin_read_policy ON public.cortex_ai_db_mutation_audit;\r\nCREATE POLICY cortex_ai_db_mutation_audit_admin_read_policy ON public.cortex_ai_db_mutation_audit FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS cortex_ai_db_mutation_audit_service_role_policy ON public.cortex_ai_db_mutation_audit;\r\nCREATE POLICY cortex_ai_db_mutation_audit_service_role_policy ON public.cortex_ai_db_mutation_audit TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.coupon_freemius_mappings ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS coupon_freemius_mappings_admin_policy ON public.coupon_freemius_mappings;\r\nCREATE POLICY coupon_freemius_mappings_admin_policy ON public.coupon_freemius_mappings TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupon_freemius_mappings_service_role_policy ON public.coupon_freemius_mappings;\r\nCREATE POLICY coupon_freemius_mappings_service_role_policy ON public.coupon_freemius_mappings TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.coupon_products ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS coupon_products_admin_policy ON public.coupon_products;\r\nCREATE POLICY coupon_products_admin_policy ON public.coupon_products TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupon_products_service_role_policy ON public.coupon_products;\r\nCREATE POLICY coupon_products_service_role_policy ON public.coupon_products TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.coupon_redemptions ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS coupon_redemptions_admin_select_policy ON public.coupon_redemptions;\r\nCREATE POLICY coupon_redemptions_admin_select_policy ON public.coupon_redemptions FOR SELECT TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupon_redemptions_service_role_policy ON public.coupon_redemptions;\r\nCREATE POLICY coupon_redemptions_service_role_policy ON public.coupon_redemptions TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.coupons ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS coupons_admin_delete_policy ON public.coupons;\r\nCREATE POLICY coupons_admin_delete_policy ON public.coupons FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupons_admin_insert_policy ON public.coupons;\r\nCREATE POLICY coupons_admin_insert_policy ON public.coupons FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupons_admin_select_policy ON public.coupons;\r\nCREATE POLICY coupons_admin_select_policy ON public.coupons FOR SELECT TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupons_admin_update_policy ON public.coupons;\r\nCREATE POLICY coupons_admin_update_policy ON public.coupons FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupons_service_role_policy ON public.coupons;\r\nCREATE POLICY coupons_service_role_policy ON public.coupons TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.currencies ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS currencies_delete_policy ON public.currencies;\r\nCREATE POLICY currencies_delete_policy ON public.currencies FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS currencies_insert_policy ON public.currencies;\r\nCREATE POLICY currencies_insert_policy ON public.currencies FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS currencies_update_policy ON public.currencies;\r\nCREATE POLICY currencies_update_policy ON public.currencies FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.custom_block_definitions ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS custom_block_definitions_delete_policy ON public.custom_block_definitions;\r\nCREATE POLICY custom_block_definitions_delete_policy ON public.custom_block_definitions FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS custom_block_definitions_insert_policy ON public.custom_block_definitions;\r\nCREATE POLICY custom_block_definitions_insert_policy ON public.custom_block_definitions FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS custom_block_definitions_public_read_policy ON public.custom_block_definitions;\r\nCREATE POLICY custom_block_definitions_public_read_policy ON public.custom_block_definitions FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS custom_block_definitions_service_role_policy ON public.custom_block_definitions;\r\nCREATE POLICY custom_block_definitions_service_role_policy ON public.custom_block_definitions TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS custom_block_definitions_update_policy ON public.custom_block_definitions;\r\nCREATE POLICY custom_block_definitions_update_policy ON public.custom_block_definitions FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.email_2fa_challenges ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS email_2fa_challenges_service_role_policy ON public.email_2fa_challenges;\r\nCREATE POLICY email_2fa_challenges_service_role_policy ON public.email_2fa_challenges TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.freemius_plans ENABLE ROW LEVEL SECURITY;\r\n\r\nALTER TABLE public.freemius_pricing ENABLE ROW LEVEL SECURITY;\r\n\r\nALTER TABLE public.inventory_items ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS inventory_items_delete_policy ON public.inventory_items;\r\nCREATE POLICY inventory_items_delete_policy ON public.inventory_items FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS inventory_items_insert_policy ON public.inventory_items;\r\nCREATE POLICY inventory_items_insert_policy ON public.inventory_items FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS inventory_items_update_policy ON public.inventory_items;\r\nCREATE POLICY inventory_items_update_policy ON public.inventory_items FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.languages ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS languages_delete_policy ON public.languages;\r\nCREATE POLICY languages_delete_policy ON public.languages FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS languages_insert_policy ON public.languages;\r\nCREATE POLICY languages_insert_policy ON public.languages FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS languages_read_policy ON public.languages;\r\nCREATE POLICY languages_read_policy ON public.languages FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS languages_update_policy ON public.languages;\r\nCREATE POLICY languages_update_policy ON public.languages FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nALTER TABLE public.logos ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS logos_delete_policy ON public.logos;\r\nCREATE POLICY logos_delete_policy ON public.logos FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS logos_insert_policy ON public.logos;\r\nCREATE POLICY logos_insert_policy ON public.logos FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS logos_read_policy ON public.logos;\r\nCREATE POLICY logos_read_policy ON public.logos FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS logos_update_policy ON public.logos;\r\nCREATE POLICY logos_update_policy ON public.logos FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nALTER TABLE public.media ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS media_delete_policy ON public.media;\r\nCREATE POLICY media_delete_policy ON public.media FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS media_insert_policy ON public.media;\r\nCREATE POLICY media_insert_policy ON public.media FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS media_read_policy ON public.media;\r\nCREATE POLICY media_read_policy ON public.media FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS media_service_role_policy ON public.media;\r\nCREATE POLICY media_service_role_policy ON public.media TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS media_update_policy ON public.media;\r\nCREATE POLICY media_update_policy ON public.media FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.navigation_items ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS navigation_items_delete_policy ON public.navigation_items;\r\nCREATE POLICY navigation_items_delete_policy ON public.navigation_items FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS navigation_items_insert_policy ON public.navigation_items;\r\nCREATE POLICY navigation_items_insert_policy ON public.navigation_items FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS navigation_items_update_policy ON public.navigation_items;\r\nCREATE POLICY navigation_items_update_policy ON public.navigation_items FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS navigation_read_policy ON public.navigation_items;\r\nCREATE POLICY navigation_read_policy ON public.navigation_items FOR SELECT USING (true);\r\n\r\nALTER TABLE public.order_items ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS order_items_delete_policy ON public.order_items;\r\nCREATE POLICY order_items_delete_policy ON public.order_items FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS order_items_insert_policy ON public.order_items;\r\nCREATE POLICY order_items_insert_policy ON public.order_items FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS order_items_update_policy ON public.order_items;\r\nCREATE POLICY order_items_update_policy ON public.order_items FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS orders_delete_policy ON public.orders;\r\nCREATE POLICY orders_delete_policy ON public.orders FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS orders_insert_policy ON public.orders;\r\nCREATE POLICY orders_insert_policy ON public.orders FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS orders_update_policy ON public.orders;\r\nCREATE POLICY orders_update_policy ON public.orders FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.package_activations ENABLE ROW LEVEL SECURITY;\r\n\r\nALTER TABLE public.page_revisions ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS page_revisions_delete_policy ON public.page_revisions;\r\nCREATE POLICY page_revisions_delete_policy ON public.page_revisions FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS page_revisions_insert_policy ON public.page_revisions;\r\nCREATE POLICY page_revisions_insert_policy ON public.page_revisions FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS page_revisions_read_policy ON public.page_revisions;\r\nCREATE POLICY page_revisions_read_policy ON public.page_revisions FOR SELECT TO authenticated USING (true);\r\n\r\nDROP POLICY IF EXISTS page_revisions_update_policy ON public.page_revisions;\r\nCREATE POLICY page_revisions_update_policy ON public.page_revisions FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.pages ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS pages_anon_read_policy ON public.pages;\r\nCREATE POLICY pages_anon_read_policy ON public.pages FOR SELECT TO anon USING ((status = 'published'::public.page_status));\r\n\r\nDROP POLICY IF EXISTS pages_delete_policy ON public.pages;\r\nCREATE POLICY pages_delete_policy ON public.pages FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS pages_insert_policy ON public.pages;\r\nCREATE POLICY pages_insert_policy ON public.pages FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS pages_read_policy ON public.pages;\r\nCREATE POLICY pages_read_policy ON public.pages FOR SELECT TO authenticated USING (((status = 'published'::public.page_status) OR ((author_id = ( SELECT auth.uid() AS uid)) AND (status <> 'published'::public.page_status)) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))));\r\n\r\nDROP POLICY IF EXISTS pages_update_policy ON public.pages;\r\nCREATE POLICY pages_update_policy ON public.pages FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.post_revisions ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS post_revisions_delete_policy ON public.post_revisions;\r\nCREATE POLICY post_revisions_delete_policy ON public.post_revisions FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS post_revisions_insert_policy ON public.post_revisions;\r\nCREATE POLICY post_revisions_insert_policy ON public.post_revisions FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS post_revisions_read_policy ON public.post_revisions;\r\nCREATE POLICY post_revisions_read_policy ON public.post_revisions FOR SELECT TO authenticated USING (true);\r\n\r\nDROP POLICY IF EXISTS post_revisions_update_policy ON public.post_revisions;\r\nCREATE POLICY post_revisions_update_policy ON public.post_revisions FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS posts_anon_read_policy ON public.posts;\r\nCREATE POLICY posts_anon_read_policy ON public.posts FOR SELECT TO anon USING (((status = 'published'::public.page_status) AND ((published_at IS NULL) OR (published_at <= now()))));\r\n\r\nDROP POLICY IF EXISTS posts_delete_policy ON public.posts;\r\nCREATE POLICY posts_delete_policy ON public.posts FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS posts_insert_policy ON public.posts;\r\nCREATE POLICY posts_insert_policy ON public.posts FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS posts_read_policy ON public.posts;\r\nCREATE POLICY posts_read_policy ON public.posts FOR SELECT TO authenticated USING ((((status = 'published'::public.page_status) AND ((published_at IS NULL) OR (published_at <= now()))) OR ((author_id = ( SELECT auth.uid() AS uid)) AND (status <> 'published'::public.page_status)) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))));\r\n\r\nDROP POLICY IF EXISTS posts_update_policy ON public.posts;\r\nCREATE POLICY posts_update_policy ON public.posts FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.privacy_consent_logs ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS privacy_consent_logs_admin_read_policy ON public.privacy_consent_logs;\r\nCREATE POLICY privacy_consent_logs_admin_read_policy ON public.privacy_consent_logs FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS privacy_consent_logs_service_role_policy ON public.privacy_consent_logs;\r\nCREATE POLICY privacy_consent_logs_service_role_policy ON public.privacy_consent_logs TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.product_attribute_terms ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS product_attribute_terms_delete_policy ON public.product_attribute_terms;\r\nCREATE POLICY product_attribute_terms_delete_policy ON public.product_attribute_terms FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_attribute_terms_insert_policy ON public.product_attribute_terms;\r\nCREATE POLICY product_attribute_terms_insert_policy ON public.product_attribute_terms FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_attribute_terms_update_policy ON public.product_attribute_terms;\r\nCREATE POLICY product_attribute_terms_update_policy ON public.product_attribute_terms FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.product_attributes ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS product_attributes_delete_policy ON public.product_attributes;\r\nCREATE POLICY product_attributes_delete_policy ON public.product_attributes FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_attributes_insert_policy ON public.product_attributes;\r\nCREATE POLICY product_attributes_insert_policy ON public.product_attributes FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_attributes_update_policy ON public.product_attributes;\r\nCREATE POLICY product_attributes_update_policy ON public.product_attributes FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.product_categories ENABLE ROW LEVEL SECURITY;\r\n\r\nALTER TABLE public.product_drafts ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS product_drafts_delete_policy ON public.product_drafts;\r\nCREATE POLICY product_drafts_delete_policy ON public.product_drafts FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS product_drafts_insert_policy ON public.product_drafts;\r\nCREATE POLICY product_drafts_insert_policy ON public.product_drafts FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS product_drafts_select_policy ON public.product_drafts;\r\nCREATE POLICY product_drafts_select_policy ON public.product_drafts FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS product_drafts_update_policy ON public.product_drafts;\r\nCREATE POLICY product_drafts_update_policy ON public.product_drafts FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.product_freemius_sale_coupons ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS product_freemius_sale_coupons_admin_policy ON public.product_freemius_sale_coupons;\r\nCREATE POLICY product_freemius_sale_coupons_admin_policy ON public.product_freemius_sale_coupons TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_freemius_sale_coupons_service_role_policy ON public.product_freemius_sale_coupons;\r\nCREATE POLICY product_freemius_sale_coupons_service_role_policy ON public.product_freemius_sale_coupons TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.product_media ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS product_media_delete_policy ON public.product_media;\r\nCREATE POLICY product_media_delete_policy ON public.product_media FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_media_insert_policy ON public.product_media;\r\nCREATE POLICY product_media_insert_policy ON public.product_media FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_media_update_policy ON public.product_media;\r\nCREATE POLICY product_media_update_policy ON public.product_media FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.product_variants ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS product_variants_delete_policy ON public.product_variants;\r\nCREATE POLICY product_variants_delete_policy ON public.product_variants FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_variants_insert_policy ON public.product_variants;\r\nCREATE POLICY product_variants_insert_policy ON public.product_variants FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_variants_update_policy ON public.product_variants;\r\nCREATE POLICY product_variants_update_policy ON public.product_variants FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.products ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS products_delete_policy ON public.products;\r\nCREATE POLICY products_delete_policy ON public.products FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS products_insert_policy ON public.products;\r\nCREATE POLICY products_insert_policy ON public.products FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS products_update_policy ON public.products;\r\nCREATE POLICY products_update_policy ON public.products FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS profiles_insert_policy ON public.profiles;\r\nCREATE POLICY profiles_insert_policy ON public.profiles FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS profiles_read_policy ON public.profiles;\r\nCREATE POLICY profiles_read_policy ON public.profiles FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS profiles_service_role_policy ON public.profiles;\r\nCREATE POLICY profiles_service_role_policy ON public.profiles TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS profiles_update_policy ON public.profiles;\r\nCREATE POLICY profiles_update_policy ON public.profiles FOR UPDATE TO authenticated USING (((id = ( SELECT auth.uid() AS uid)) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))) WITH CHECK (((id = ( SELECT auth.uid() AS uid)) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)));\r\n\r\nALTER TABLE public.shipping_zone_locations ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS shipping_zone_locations_delete_policy ON public.shipping_zone_locations;\r\nCREATE POLICY shipping_zone_locations_delete_policy ON public.shipping_zone_locations FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS shipping_zone_locations_insert_policy ON public.shipping_zone_locations;\r\nCREATE POLICY shipping_zone_locations_insert_policy ON public.shipping_zone_locations FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS shipping_zone_locations_update_policy ON public.shipping_zone_locations;\r\nCREATE POLICY shipping_zone_locations_update_policy ON public.shipping_zone_locations FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.shipping_zone_methods ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS shipping_zone_methods_delete_policy ON public.shipping_zone_methods;\r\nCREATE POLICY shipping_zone_methods_delete_policy ON public.shipping_zone_methods FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS shipping_zone_methods_insert_policy ON public.shipping_zone_methods;\r\nCREATE POLICY shipping_zone_methods_insert_policy ON public.shipping_zone_methods FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS shipping_zone_methods_update_policy ON public.shipping_zone_methods;\r\nCREATE POLICY shipping_zone_methods_update_policy ON public.shipping_zone_methods FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.shipping_zones ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS shipping_zones_delete_policy ON public.shipping_zones;\r\nCREATE POLICY shipping_zones_delete_policy ON public.shipping_zones FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS shipping_zones_insert_policy ON public.shipping_zones;\r\nCREATE POLICY shipping_zones_insert_policy ON public.shipping_zones FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS shipping_zones_update_policy ON public.shipping_zones;\r\nCREATE POLICY shipping_zones_update_policy ON public.shipping_zones FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.site_settings ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;\r\nCREATE POLICY site_settings_delete_policy ON public.site_settings FOR DELETE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nDROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;\r\nCREATE POLICY site_settings_insert_policy ON public.site_settings FOR INSERT TO authenticated WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nDROP POLICY IF EXISTS site_settings_read_policy ON public.site_settings;\r\nCREATE POLICY site_settings_read_policy ON public.site_settings FOR SELECT USING (((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT auth.role() AS role) = 'authenticated'::text) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nDROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;\r\nCREATE POLICY site_settings_update_policy ON public.site_settings FOR UPDATE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)))) WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nALTER TABLE public.system_alerts ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS system_alerts_select_admin ON public.system_alerts;\r\nCREATE POLICY system_alerts_select_admin ON public.system_alerts FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS system_alerts_update_admin ON public.system_alerts;\r\nCREATE POLICY system_alerts_update_admin ON public.system_alerts FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nALTER TABLE public.system_configuration ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS system_configuration_admin_delete ON public.system_configuration;\r\nCREATE POLICY system_configuration_admin_delete ON public.system_configuration FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS system_configuration_admin_insert ON public.system_configuration;\r\nCREATE POLICY system_configuration_admin_insert ON public.system_configuration FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS system_configuration_admin_select ON public.system_configuration;\r\nCREATE POLICY system_configuration_admin_select ON public.system_configuration FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS system_configuration_admin_update ON public.system_configuration;\r\nCREATE POLICY system_configuration_admin_update ON public.system_configuration FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS system_configuration_service_role_all ON public.system_configuration;\r\nCREATE POLICY system_configuration_service_role_all ON public.system_configuration TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.tax_rates ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS tax_rates_delete_policy ON public.tax_rates;\r\nCREATE POLICY tax_rates_delete_policy ON public.tax_rates FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS tax_rates_insert_policy ON public.tax_rates;\r\nCREATE POLICY tax_rates_insert_policy ON public.tax_rates FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS tax_rates_update_policy ON public.tax_rates;\r\nCREATE POLICY tax_rates_update_policy ON public.tax_rates FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.translations ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS translations_delete_policy ON public.translations;\r\nCREATE POLICY translations_delete_policy ON public.translations FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS translations_insert_policy ON public.translations;\r\nCREATE POLICY translations_insert_policy ON public.translations FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS translations_read_policy ON public.translations;\r\nCREATE POLICY translations_read_policy ON public.translations FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS translations_update_policy ON public.translations;\r\nCREATE POLICY translations_update_policy ON public.translations FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.ucp_cart_sessions ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS ucp_cart_sessions_service_role_policy ON public.ucp_cart_sessions;\r\nCREATE POLICY ucp_cart_sessions_service_role_policy ON public.ucp_cart_sessions TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.user_addresses ENABLE ROW LEVEL SECURITY;\r\n\r\nALTER TABLE public.user_security_settings ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS user_security_settings_insert_own_policy ON public.user_security_settings;\r\nCREATE POLICY user_security_settings_insert_own_policy ON public.user_security_settings FOR INSERT TO authenticated WITH CHECK ((( SELECT auth.uid() AS uid) = user_id));\r\n\r\nDROP POLICY IF EXISTS user_security_settings_select_own_policy ON public.user_security_settings;\r\nCREATE POLICY user_security_settings_select_own_policy ON public.user_security_settings FOR SELECT TO authenticated USING ((( SELECT auth.uid() AS uid) = user_id));\r\n\r\nDROP POLICY IF EXISTS user_security_settings_service_role_policy ON public.user_security_settings;\r\nCREATE POLICY user_security_settings_service_role_policy ON public.user_security_settings TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS user_security_settings_update_own_policy ON public.user_security_settings;\r\nCREATE POLICY user_security_settings_update_own_policy ON public.user_security_settings FOR UPDATE TO authenticated USING ((( SELECT auth.uid() AS uid) = user_id)) WITH CHECK ((( SELECT auth.uid() AS uid) = user_id));\r\n\r\nALTER TABLE public.user_trusted_devices ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS user_trusted_devices_delete_own_policy ON public.user_trusted_devices;\r\nCREATE POLICY user_trusted_devices_delete_own_policy ON public.user_trusted_devices FOR DELETE TO authenticated USING ((( SELECT auth.uid() AS uid) = user_id));\r\n\r\nDROP POLICY IF EXISTS user_trusted_devices_select_own_policy ON public.user_trusted_devices;\r\nCREATE POLICY user_trusted_devices_select_own_policy ON public.user_trusted_devices FOR SELECT TO authenticated USING ((( SELECT auth.uid() AS uid) = user_id));\r\n\r\nDROP POLICY IF EXISTS user_trusted_devices_service_role_policy ON public.user_trusted_devices;\r\nCREATE POLICY user_trusted_devices_service_role_policy ON public.user_trusted_devices TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.variant_attribute_mapping ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS variant_attribute_mapping_delete_policy ON public.variant_attribute_mapping;\r\nCREATE POLICY variant_attribute_mapping_delete_policy ON public.variant_attribute_mapping FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS variant_attribute_mapping_insert_policy ON public.variant_attribute_mapping;\r\nCREATE POLICY variant_attribute_mapping_insert_policy ON public.variant_attribute_mapping FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS variant_attribute_mapping_update_policy ON public.variant_attribute_mapping;\r\nCREATE POLICY variant_attribute_mapping_update_policy ON public.variant_attribute_mapping FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nGRANT USAGE ON SCHEMA public TO postgres;\r\n\r\nGRANT USAGE ON SCHEMA public TO anon;\r\n\r\nGRANT USAGE ON SCHEMA public TO authenticated;\r\n\r\nGRANT USAGE ON SCHEMA public TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.apply_order_inventory_deduction(p_order_id uuid) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.apply_order_inventory_deduction(p_order_id uuid) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.apply_order_inventory_deduction(p_order_id uuid) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.assign_order_invoice_metadata(p_order_id uuid, p_paid_at timestamp with time zone) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.assign_order_invoice_metadata(p_order_id uuid, p_paid_at timestamp with time zone) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.assign_order_invoice_metadata(p_order_id uuid, p_paid_at timestamp with time zone) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.clear_currency_price_overrides(target_currency text) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.clear_currency_price_overrides(target_currency text) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.clear_currency_price_overrides(target_currency text) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_custom_block_fields(candidate jsonb) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_custom_block_fields(candidate jsonb) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_custom_block_fields(candidate jsonb) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_custom_block_layout_schema(candidate jsonb) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_custom_block_layout_schema(candidate jsonb) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_custom_block_layout_schema(candidate jsonb) TO service_role;\r\n\r\nGRANT ALL ON TABLE public.custom_block_definitions TO anon;\r\n\r\nGRANT ALL ON TABLE public.custom_block_definitions TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.custom_block_definitions TO service_role;\r\n\r\nREVOKE ALL ON FUNCTION public.duplicate_block_definition(target_id uuid) FROM PUBLIC;\r\n\r\nGRANT ALL ON FUNCTION public.duplicate_block_definition(target_id uuid) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.duplicate_block_definition(target_id uuid) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.duplicate_block_definition(target_id uuid) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.format_order_invoice_number(p_value bigint) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.format_order_invoice_number(p_value bigint) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.format_order_invoice_number(p_value bigint) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.generate_order_invoice_number() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.generate_order_invoice_number() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.generate_order_invoice_number() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.get_current_user_role() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.get_current_user_role() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.get_current_user_role() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.get_default_currency_code() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.get_default_currency_code() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.get_default_currency_code() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.get_ecommerce_track_quantities() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.get_ecommerce_track_quantities() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.get_ecommerce_track_quantities() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.get_my_claim(claim text) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.get_my_claim(claim text) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.get_my_claim(claim text) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_blocks_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_blocks_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_blocks_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_coupon_freemius_mappings_write() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_coupon_freemius_mappings_write() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_coupon_freemius_mappings_write() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_coupons_write() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_coupons_write() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_coupons_write() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_default_currency_change() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_default_currency_change() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_default_currency_change() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_inventory_item_change() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_inventory_item_change() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_inventory_item_change() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_inventory_items_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_inventory_items_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_inventory_items_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_languages_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_languages_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_languages_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_media_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_media_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_media_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_navigation_items_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_navigation_items_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_navigation_items_update() TO service_role;\r\n\r\nREVOKE ALL ON FUNCTION public.handle_new_user() FROM PUBLIC;\r\n\r\nGRANT ALL ON FUNCTION public.handle_new_user() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_new_user() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_new_user() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_pages_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_pages_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_pages_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_posts_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_posts_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_posts_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_product_freemius_sale_coupons_write() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_product_freemius_sale_coupons_write() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_product_freemius_sale_coupons_write() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_shipping_zone_locations_write() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_shipping_zone_locations_write() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_shipping_zone_locations_write() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_tax_rates_write() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_tax_rates_write() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_tax_rates_write() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_ucp_cart_sessions_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_ucp_cart_sessions_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_ucp_cart_sessions_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.is_admin() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.is_admin() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.is_admin() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_currency_amount_map(amounts jsonb) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_currency_amount_map(amounts jsonb) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_currency_amount_map(amounts jsonb) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_sale_price_map(prices jsonb, sale_prices jsonb) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_sale_price_map(prices jsonb, sale_prices jsonb) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_sale_price_map(prices jsonb, sale_prices jsonb) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.normalize_currency_amount_map(amounts jsonb) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.normalize_currency_amount_map(amounts jsonb) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.normalize_currency_amount_map(amounts jsonb) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.set_currency_defaults() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.set_currency_defaults() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.set_currency_defaults() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.set_current_timestamp_updated_at() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.set_current_timestamp_updated_at() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.set_current_timestamp_updated_at() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.sync_currency_price_maps() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.sync_currency_price_maps() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.sync_currency_price_maps() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.sync_inventory_cache_for_sku(p_sku text) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.sync_inventory_cache_for_sku(p_sku text) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.sync_inventory_cache_for_sku(p_sku text) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.sync_legacy_price_columns_for_currency(target_currency text) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.sync_legacy_price_columns_for_currency(target_currency text) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.sync_legacy_price_columns_for_currency(target_currency text) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.sync_shipping_method_currency_maps() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.sync_shipping_method_currency_maps() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.sync_shipping_method_currency_maps() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.update_product_ratings() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.update_product_ratings() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.update_product_ratings() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.upsert_product_with_variants(product_payload jsonb) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.upsert_product_with_variants(product_payload jsonb) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.upsert_product_with_variants(product_payload jsonb) TO service_role;\r\n\r\nGRANT ALL ON TABLE public.blocks TO anon;\r\n\r\nGRANT ALL ON TABLE public.blocks TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.blocks TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.blocks_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.blocks_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.blocks_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.categories TO anon;\r\n\r\nGRANT ALL ON TABLE public.categories TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.categories TO service_role;\r\n\r\nGRANT ALL ON TABLE public.cms_interactions TO anon;\r\n\r\nGRANT ALL ON TABLE public.cms_interactions TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.cms_interactions TO service_role;\r\n\r\nGRANT ALL ON TABLE public.content_drafts TO anon;\r\n\r\nGRANT ALL ON TABLE public.content_drafts TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.content_drafts TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.content_drafts_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.content_drafts_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.content_drafts_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.cortex_ai_db_mutation_audit TO anon;\r\n\r\nGRANT ALL ON TABLE public.cortex_ai_db_mutation_audit TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.cortex_ai_db_mutation_audit TO service_role;\r\n\r\nGRANT ALL ON TABLE public.coupon_freemius_mappings TO anon;\r\n\r\nGRANT ALL ON TABLE public.coupon_freemius_mappings TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.coupon_freemius_mappings TO service_role;\r\n\r\nGRANT ALL ON TABLE public.coupon_products TO anon;\r\n\r\nGRANT ALL ON TABLE public.coupon_products TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.coupon_products TO service_role;\r\n\r\nGRANT ALL ON TABLE public.coupon_redemptions TO anon;\r\n\r\nGRANT ALL ON TABLE public.coupon_redemptions TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.coupon_redemptions TO service_role;\r\n\r\nGRANT ALL ON TABLE public.coupons TO anon;\r\n\r\nGRANT ALL ON TABLE public.coupons TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.coupons TO service_role;\r\n\r\nGRANT ALL ON TABLE public.currencies TO anon;\r\n\r\nGRANT ALL ON TABLE public.currencies TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.currencies TO service_role;\r\n\r\nGRANT ALL ON TABLE public.email_2fa_challenges TO anon;\r\n\r\nGRANT ALL ON TABLE public.email_2fa_challenges TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.email_2fa_challenges TO service_role;\r\n\r\nGRANT ALL ON TABLE public.freemius_plans TO anon;\r\n\r\nGRANT ALL ON TABLE public.freemius_plans TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.freemius_plans TO service_role;\r\n\r\nGRANT ALL ON TABLE public.freemius_pricing TO anon;\r\n\r\nGRANT ALL ON TABLE public.freemius_pricing TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.freemius_pricing TO service_role;\r\n\r\nGRANT ALL ON TABLE public.inventory_items TO anon;\r\n\r\nGRANT ALL ON TABLE public.inventory_items TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.inventory_items TO service_role;\r\n\r\nGRANT ALL ON TABLE public.languages TO anon;\r\n\r\nGRANT ALL ON TABLE public.languages TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.languages TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.languages_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.languages_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.languages_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.logos TO anon;\r\n\r\nGRANT ALL ON TABLE public.logos TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.logos TO service_role;\r\n\r\nGRANT ALL ON TABLE public.media TO anon;\r\n\r\nGRANT ALL ON TABLE public.media TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.media TO service_role;\r\n\r\nGRANT ALL ON TABLE public.navigation_items TO anon;\r\n\r\nGRANT ALL ON TABLE public.navigation_items TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.navigation_items TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.navigation_items_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.navigation_items_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.navigation_items_id_seq TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.order_invoice_number_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.order_invoice_number_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.order_invoice_number_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.order_items TO anon;\r\n\r\nGRANT ALL ON TABLE public.order_items TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.order_items TO service_role;\r\n\r\nGRANT ALL ON TABLE public.orders TO anon;\r\n\r\nGRANT ALL ON TABLE public.orders TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.orders TO service_role;\r\n\r\nGRANT ALL ON TABLE public.package_activations TO anon;\r\n\r\nGRANT ALL ON TABLE public.package_activations TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.package_activations TO service_role;\r\n\r\nGRANT ALL ON TABLE public.page_revisions TO anon;\r\n\r\nGRANT ALL ON TABLE public.page_revisions TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.page_revisions TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.page_revisions_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.page_revisions_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.page_revisions_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.pages TO anon;\r\n\r\nGRANT ALL ON TABLE public.pages TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.pages TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.pages_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.pages_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.pages_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.post_revisions TO anon;\r\n\r\nGRANT ALL ON TABLE public.post_revisions TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.post_revisions TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.post_revisions_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.post_revisions_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.post_revisions_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.posts TO anon;\r\n\r\nGRANT ALL ON TABLE public.posts TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.posts TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.posts_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.posts_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.posts_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.privacy_consent_logs TO anon;\r\n\r\nGRANT ALL ON TABLE public.privacy_consent_logs TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.privacy_consent_logs TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_attribute_terms TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_attribute_terms TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_attribute_terms TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_attributes TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_attributes TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_attributes TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_categories TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_categories TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_categories TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_drafts TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_drafts TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_drafts TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.product_drafts_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.product_drafts_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.product_drafts_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_freemius_sale_coupons TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_freemius_sale_coupons TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_freemius_sale_coupons TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_media TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_media TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_media TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_variants TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_variants TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_variants TO service_role;\r\n\r\nGRANT ALL ON TABLE public.products TO anon;\r\n\r\nGRANT ALL ON TABLE public.products TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.products TO service_role;\r\n\r\nGRANT ALL ON TABLE public.profiles TO anon;\r\n\r\nGRANT ALL ON TABLE public.profiles TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.profiles TO service_role;\r\n\r\nGRANT ALL ON TABLE public.shipping_zone_locations TO anon;\r\n\r\nGRANT ALL ON TABLE public.shipping_zone_locations TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.shipping_zone_locations TO service_role;\r\n\r\nGRANT ALL ON TABLE public.shipping_zone_methods TO anon;\r\n\r\nGRANT ALL ON TABLE public.shipping_zone_methods TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.shipping_zone_methods TO service_role;\r\n\r\nGRANT ALL ON TABLE public.shipping_zones TO anon;\r\n\r\nGRANT ALL ON TABLE public.shipping_zones TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.shipping_zones TO service_role;\r\n\r\nGRANT ALL ON TABLE public.site_settings TO anon;\r\n\r\nGRANT ALL ON TABLE public.site_settings TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.site_settings TO service_role;\r\n\r\nGRANT ALL ON TABLE public.system_alerts TO anon;\r\n\r\nGRANT ALL ON TABLE public.system_alerts TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.system_alerts TO service_role;\r\n\r\nGRANT ALL ON TABLE public.system_configuration TO anon;\r\n\r\nGRANT ALL ON TABLE public.system_configuration TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.system_configuration TO service_role;\r\n\r\nGRANT ALL ON TABLE public.tax_rates TO anon;\r\n\r\nGRANT ALL ON TABLE public.tax_rates TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.tax_rates TO service_role;\r\n\r\nGRANT ALL ON TABLE public.translations TO anon;\r\n\r\nGRANT ALL ON TABLE public.translations TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.translations TO service_role;\r\n\r\nGRANT ALL ON TABLE public.ucp_cart_sessions TO anon;\r\n\r\nGRANT ALL ON TABLE public.ucp_cart_sessions TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.ucp_cart_sessions TO service_role;\r\n\r\nGRANT ALL ON TABLE public.user_addresses TO anon;\r\n\r\nGRANT ALL ON TABLE public.user_addresses TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.user_addresses TO service_role;\r\n\r\nGRANT ALL ON TABLE public.user_security_settings TO anon;\r\n\r\nGRANT ALL ON TABLE public.user_security_settings TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.user_security_settings TO service_role;\r\n\r\nGRANT ALL ON TABLE public.user_trusted_devices TO anon;\r\n\r\nGRANT ALL ON TABLE public.user_trusted_devices TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.user_trusted_devices TO service_role;\r\n\r\nGRANT ALL ON TABLE public.variant_attribute_mapping TO anon;\r\n\r\nGRANT ALL ON TABLE public.variant_attribute_mapping TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.variant_attribute_mapping TO service_role;\r\n\r\n-- Re-attached: trigger lives on auth.users, which a public-only pg_dump omits (see migration 005).\r\nDROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;\r\nCREATE TRIGGER on_auth_user_created\r\n AFTER INSERT ON auth.users\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.handle_new_user();\r\nREVOKE EXECUTE ON FUNCTION public.handle_new_user() FROM PUBLIC, anon, authenticated;\r\n"
31
- },
32
- {
33
- "version": "00000000000003",
34
- "name": "00000000000003_baseline_seed.sql",
35
- "sql": "-- AUTO-GENERATED baseline (re-baseline of migrations 000..044). Idempotent; safe to replay.\n-- 03 · seed: canonical NextBlock demo content (no users, no secrets)\n-- Regenerate via tools/scripts/rebaseline-transform.mjs. Do not hand-edit.\n\nINSERT INTO public.languages (id, code, name, is_default, is_active, created_at, updated_at) VALUES (1, 'en', 'English', true, true, '2026-07-03 17:52:15.444652+00', '2026-07-03 17:52:15.444652+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.languages (id, code, name, is_default, is_active, created_at, updated_at) VALUES (2, 'fr', 'Français', false, true, '2026-07-03 17:52:15.444652+00', '2026-07-03 17:52:15.444652+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.media (id, uploader_id, file_name, object_key, file_type, size_bytes, description, width, height, blur_data_url, variants, file_path, folder, created_at, updated_at) VALUES ('ea6fdaf5-f8de-416c-9f2b-b689b7a4ed38', NULL, 'nextblock-logo-small.webp', 'images/nextblock-logo-small.webp', 'image/webp', 10000, 'NextBlock™ Site Logo', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.media (id, uploader_id, file_name, object_key, file_type, size_bytes, description, width, height, blur_data_url, variants, file_path, folder, created_at, updated_at) VALUES ('5bb07d4c-ca14-49ab-96bb-febbed31aded', NULL, 'NBcover.webp', 'images/NBcover.webp', 'image/webp', 180000, 'NextBlock™ architecture overview cover image', 1024, 572, NULL, NULL, NULL, NULL, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.media (id, uploader_id, file_name, object_key, file_type, size_bytes, description, width, height, blur_data_url, variants, file_path, folder, created_at, updated_at) VALUES ('641ddf75-5c90-41df-8b83-e7c298f30a6a', NULL, 'extensibility.webp', 'images/extensibility.webp', 'image/webp', 246808, 'NextBlock™ extensibility editorial artwork', 1024, 559, NULL, NULL, NULL, NULL, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.media (id, uploader_id, file_name, object_key, file_type, size_bytes, description, width, height, blur_data_url, variants, file_path, folder, created_at, updated_at) VALUES ('e6ed9c34-6ef6-47a8-b836-e6dbf73511fa', NULL, 'included.webp', 'images/included.webp', 'image/webp', 237478, 'NextBlock™ getting-started platform artwork', 1024, 559, NULL, NULL, NULL, NULL, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.media (id, uploader_id, file_name, object_key, file_type, size_bytes, description, width, height, blur_data_url, variants, file_path, folder, created_at, updated_at) VALUES ('77b9da47-4182-470f-9070-fa6cd11c6aea', NULL, 'programmer-upscaled.webp', 'images/programmer-upscaled.webp', 'image/webp', 780000, 'NextBlock™ setup guide cover image', 8192, 2632, NULL, NULL, NULL, NULL, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.media (id, uploader_id, file_name, object_key, file_type, size_bytes, description, width, height, blur_data_url, variants, file_path, folder, created_at, updated_at) VALUES ('dfa0e881-70b6-4767-b9b0-7a6293db5009', NULL, 'commerce-plan.webp', 'images/commerce-plan.webp', 'image/webp', 269854, 'NextBlock™ commerce roadmap artwork', 1024, 559, NULL, NULL, NULL, NULL, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.media (id, uploader_id, file_name, object_key, file_type, size_bytes, description, width, height, blur_data_url, variants, file_path, folder, created_at, updated_at) VALUES ('5f5a0d6d-aed4-4b6f-b966-ea5d5b17b7a6', NULL, 'commerce-wide.webp', 'images/commerce-wide.webp', 'image/webp', 250584, 'NextBlock™ Commerce editorial feature image', 1024, 434, NULL, NULL, NULL, NULL, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.media (id, uploader_id, file_name, object_key, file_type, size_bytes, description, width, height, blur_data_url, variants, file_path, folder, created_at, updated_at) VALUES ('38ec38c2-899f-47b0-8364-56a574527078', NULL, 'cortex-ai.webp', 'images/cortex-ai.webp', 'image/webp', 298588, 'NextBlock Cortex AI editorial feature image', 1024, 571, NULL, NULL, 'images/cortex-ai.webp', 'images', '2026-07-03 17:52:15.643901+00', '2026-07-03 17:52:15.643901+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.pages (id, language_id, author_id, title, slug, status, meta_title, meta_description, version, translation_group_id, created_at, updated_at, feature_image_id) VALUES (1, 1, NULL, 'Home', 'home', 'published', NULL, NULL, 1, '0098e4f0-e5f3-4e28-acfc-bb9fdb3a4a6b', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.pages (id, language_id, author_id, title, slug, status, meta_title, meta_description, version, translation_group_id, created_at, updated_at, feature_image_id) VALUES (2, 2, NULL, 'Accueil', 'accueil', 'published', NULL, NULL, 1, '0098e4f0-e5f3-4e28-acfc-bb9fdb3a4a6b', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.pages (id, language_id, author_id, title, slug, status, meta_title, meta_description, version, translation_group_id, created_at, updated_at, feature_image_id) VALUES (3, 1, NULL, 'Articles', 'articles', 'published', NULL, NULL, 1, '50adeea5-5040-4ae7-bacd-992647920982', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.pages (id, language_id, author_id, title, slug, status, meta_title, meta_description, version, translation_group_id, created_at, updated_at, feature_image_id) VALUES (4, 2, NULL, 'Articles', 'articles', 'published', NULL, NULL, 1, '50adeea5-5040-4ae7-bacd-992647920982', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.pages (id, language_id, author_id, title, slug, status, meta_title, meta_description, version, translation_group_id, created_at, updated_at, feature_image_id) VALUES (5, 1, NULL, 'Contact Us', 'contact', 'published', NULL, NULL, 1, 'e3b28669-7e38-47bd-9189-7db2353b52dc', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.pages (id, language_id, author_id, title, slug, status, meta_title, meta_description, version, translation_group_id, created_at, updated_at, feature_image_id) VALUES (6, 2, NULL, 'Contactez-nous', 'contact', 'published', NULL, NULL, 1, 'e3b28669-7e38-47bd-9189-7db2353b52dc', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.pages (id, language_id, author_id, title, slug, status, meta_title, meta_description, version, translation_group_id, created_at, updated_at, feature_image_id) VALUES (7, 1, NULL, 'Privacy Policy', 'privacy-policy', 'published', 'Privacy Policy', 'How we collect, use, disclose, and protect your personal information under Quebec Law 25, PIPEDA, and CASL.', 1, '8cd91839-518d-4b57-87db-441e59a71a95', '2026-07-03 17:52:15.699946+00', '2026-07-03 17:52:15.699946+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.pages (id, language_id, author_id, title, slug, status, meta_title, meta_description, version, translation_group_id, created_at, updated_at, feature_image_id) VALUES (8, 2, NULL, 'Politique de confidentialité', 'politique-de-confidentialite', 'published', 'Politique de confidentialité', 'Comment nous recueillons, utilisons, communiquons et protégeons vos renseignements personnels en vertu de la Loi 25, de la LPRPDE et de la LCAP.', 1, '8cd91839-518d-4b57-87db-441e59a71a95', '2026-07-03 17:52:15.699946+00', '2026-07-03 17:52:15.699946+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.pages (id, language_id, author_id, title, slug, status, meta_title, meta_description, version, translation_group_id, created_at, updated_at, feature_image_id) VALUES (9, 1, NULL, 'Terms of Service', 'terms-of-service', 'published', 'Terms of Service', 'The terms governing your use of NextBlock™ CMS, free and open-source software licensed under the AGPL-3.0.', 1, 'b5eb82c7-95d6-4308-82d8-3ac2816f5d46', '2026-07-03 17:52:15.699946+00', '2026-07-03 17:52:15.699946+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.pages (id, language_id, author_id, title, slug, status, meta_title, meta_description, version, translation_group_id, created_at, updated_at, feature_image_id) VALUES (10, 2, NULL, 'Conditions d''utilisation', 'conditions-utilisation', 'published', 'Conditions d''utilisation', 'Les conditions régissant votre utilisation de NextBlock™ CMS, un logiciel libre sous licence AGPL-3.0.', 1, 'b5eb82c7-95d6-4308-82d8-3ac2816f5d46', '2026-07-03 17:52:15.699946+00', '2026-07-03 17:52:15.699946+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.posts (id, language_id, author_id, title, slug, label, excerpt, subtitle, status, published_at, meta_title, meta_description, feature_image_id, version, translation_group_id, created_at, updated_at) VALUES (1, 1, NULL, 'How NextBlock™ Works: A Look Under the Hood', 'how-nextblock-works', 'Architecture', 'Under the hood of the monorepo, block registry, and editor stack that power NextBlock.', 'A guided tour of the monorepo, block registry, editor stack, and open-core architecture behind NextBlock.', 'published', NULL, NULL, NULL, '5bb07d4c-ca14-49ab-96bb-febbed31aded', 1, 'de8b8593-1ef6-4e66-8d1f-8b3e7ffb811d', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.posts (id, language_id, author_id, title, slug, label, excerpt, subtitle, status, published_at, meta_title, meta_description, feature_image_id, version, translation_group_id, created_at, updated_at) VALUES (2, 2, NULL, 'Comment NextBlock™ Fonctionne : Regard Sous le Capot', 'comment-nextblock-fonctionne', 'Architecture', 'Sous le capot du monorepo, du registre de blocs et de l editeur qui propulsent NextBlock.', 'Une visite guidee du monorepo, du registre de blocs, de l editeur et de l architecture open-core de NextBlock.', 'published', NULL, NULL, NULL, '5bb07d4c-ca14-49ab-96bb-febbed31aded', 1, 'de8b8593-1ef6-4e66-8d1f-8b3e7ffb811d', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.posts (id, language_id, author_id, title, slug, label, excerpt, subtitle, status, published_at, meta_title, meta_description, feature_image_id, version, translation_group_id, created_at, updated_at) VALUES (5, 1, NULL, 'NextBlock™ Commerce: Multi-Currency, Tax Sync & Beyond', 'nextblock-commerce-guide', 'Commerce', 'Storefront architecture, checkout flows, and premium commerce capabilities inside NextBlock.', 'A closer look at the commerce module, from multi-currency and tax sync to shipping, inventory, and provider-aware checkout.', 'published', NULL, NULL, NULL, '5f5a0d6d-aed4-4b6f-b966-ea5d5b17b7a6', 1, '23773a34-e54b-40c6-89b5-b09ead17ae60', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.posts (id, language_id, author_id, title, slug, label, excerpt, subtitle, status, published_at, meta_title, meta_description, feature_image_id, version, translation_group_id, created_at, updated_at) VALUES (6, 2, NULL, 'NextBlock™ Commerce : Multi-Devises, Taxes Automatiques et Plus', 'guide-commerce-nextblock', 'Commerce', 'Architecture boutique, parcours de paiement et fonctions commerce premium au coeur de NextBlock.', 'Un apercu du module commerce : multi-devises, sync taxes, expedition, inventaire et paiements connectes.', 'published', NULL, NULL, NULL, '5f5a0d6d-aed4-4b6f-b966-ea5d5b17b7a6', 1, '23773a34-e54b-40c6-89b5-b09ead17ae60', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.posts (id, language_id, author_id, title, slug, label, excerpt, subtitle, status, published_at, meta_title, meta_description, feature_image_id, version, translation_group_id, created_at, updated_at) VALUES (7, 1, NULL, 'NextBlock Cortex AI Guide', 'nextblock-cortex-ai-guide', 'AI Copilot', 'A practical guide to Cortex AI, the block-aware assistant for model routing, BYOK controls, and faster editorial production inside NextBlock.', 'See how Cortex AI brings structured generation, provider choice, and safer content workflows directly into the NextBlock editor.', 'published', '2026-07-03 17:52:15.643901+00', 'NextBlock Cortex AI Guide', 'Learn how NextBlock Cortex AI helps teams generate, refine, and translate structured block content with model routing and BYOK controls.', '38ec38c2-899f-47b0-8364-56a574527078', 1, '669489e7-5900-496e-8b73-2aba7514a71b', '2026-07-03 17:52:15.643901+00', '2026-07-03 17:52:15.643901+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.posts (id, language_id, author_id, title, slug, label, excerpt, subtitle, status, published_at, meta_title, meta_description, feature_image_id, version, translation_group_id, created_at, updated_at) VALUES (3, 1, NULL, 'How to Install NextBlock: Every Setup Option Explained', 'how-to-setup-nextblock', 'Getting Started', 'Every way to install NextBlock — one-click cloud deploy, CLI scaffold, git clone, or self-hosted Docker — with copy-paste steps for each.', 'Four ways to launch NextBlock: a one-click Vercel deploy, npm create nextblock, git clone with the browser setup wizard, or a fully local Docker stack.', 'published', NULL, 'How to Install NextBlock CMS — Vercel, CLI, Git or Docker', 'Install NextBlock in minutes — one-click Vercel deploy, npm create nextblock, git clone, or self-hosted Docker. No config files, no manual SQL.', '77b9da47-4182-470f-9070-fa6cd11c6aea', 1, '461cde55-74cb-4112-8c3d-a25882eeedce', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.799297+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.posts (id, language_id, author_id, title, slug, label, excerpt, subtitle, status, published_at, meta_title, meta_description, feature_image_id, version, translation_group_id, created_at, updated_at) VALUES (4, 2, NULL, 'Installer NextBlock : toutes les options expliquées', 'comment-configurer-nextblock', 'Mise En Route', 'Toutes les façons d''installer NextBlock — cloud en un clic, CLI, git clone ou Docker auto-hébergé — avec les étapes à copier-coller.', 'Quatre façons de lancer NextBlock : déploiement Vercel en un clic, npm create nextblock, git clone avec l''assistant dans le navigateur, ou une pile Docker 100 % locale.', 'published', NULL, 'Installer NextBlock — Vercel, CLI, Git ou Docker', 'Installez NextBlock en quelques minutes : déploiement Vercel en un clic, npm create nextblock, git clone ou Docker auto-hébergé. Sans config ni SQL.', '77b9da47-4182-470f-9070-fa6cd11c6aea', 1, '461cde55-74cb-4112-8c3d-a25882eeedce', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.799297+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (2, 1, NULL, 1, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"none\"}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"level\": 2, \"textAlign\": \"center\", \"text_content\": \"Key Features: The Three Pillars of NextBlock™\"}, \"block_type\": \"heading\"}, {\"content\": {\"html_content\": \"<p class=''text-lg text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto''>NextBlock™ is a holistic platform that unites performance, editorial experience, and developer control so every stakeholder delivers their best work.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<div class=''grid gap-8 md:grid-cols-3 mt-12''><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 backdrop-blur-sm hover:bg-slate-100 dark:hover:bg-white/10 transition-colors duration-300''><div class=''w-12 h-12 rounded-xl flex items-center justify-center text-black dark:text-white mb-6''><svg class=''w-6 h-6'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M13 10V3L4 14h7v7l9-11h-7z''></path></svg></div><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Built for Speed.</h3><p class=''text-sm text-slate-600 dark:text-slate-400 leading-relaxed''>Architected for 100% Lighthouse scores with global delivery and near-instant FCP.</p><ul class=''mt-6 space-y-3 text-sm text-slate-600 dark:text-slate-400''><li><strong class=''text-slate-800 dark:text-slate-200''>Edge Caching &amp; ISR:</strong> Serve pages worldwide.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Critical CSS:</strong> Inline styles to eliminate blocking.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Image Opt:</strong> AVIF &amp; blurred placeholders.</li></ul></div><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 backdrop-blur-sm hover:bg-slate-100 dark:hover:bg-white/10 transition-colors duration-300''><div class=''w-12 h-12 rounded-xl flex items-center justify-center text-black dark:text-white mb-6''><svg class=''w-6 h-6'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z''></path></svg></div><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Editor-First Experience.</h3><p class=''text-sm text-slate-600 dark:text-slate-400 leading-relaxed''>A low-code, Notion-style block editor empowers teams to ship pages without engineering help.</p><ul class=''mt-6 space-y-3 text-sm text-slate-600 dark:text-slate-400''><li><strong class=''text-slate-800 dark:text-slate-200''>Notion-Style:</strong> Slash commands &amp; drag-and-drop.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Bilingual:</strong> Manage locales from one interface.</li><li><strong class=''text-slate-800 dark:text-slate-200''>History:</strong> Restore any version with a click.</li></ul></div><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 backdrop-blur-sm hover:bg-slate-100 dark:hover:bg-white/10 transition-colors duration-300''><div class=''w-12 h-12 bg-white/50 dark:bg-white/10 rounded-xl flex items-center justify-center mb-6''><svg class=''w-6 h-6 text-slate-900 dark:text-white'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10''></path></svg></div><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Infinitely Extensible.</h3><p class=''text-sm text-slate-700 dark:text-slate-200 leading-relaxed''>Open-source control with a clean Nx monorepo and a typed SDK for limitless customization.</p><ul class=''mt-6 space-y-3 text-sm text-slate-700 dark:text-slate-200''><li><strong class=''text-slate-900 dark:text-white''>Open Source:</strong> Own the code &amp; data forever.</li><li><strong class=''text-slate-900 dark:text-white''>Nx Monorepo:</strong> Scale confidently.</li><li><strong class=''text-slate-900 dark:text-white''>Developer SDK:</strong> Scaffold blocks in minutes.</li></ul></div></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 1, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (3, 1, NULL, 1, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#0f172a\", \"position\": 0}, {\"color\": \"#020817\", \"position\": 100}], \"direction\": \"180deg\"}}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<h2 class=''text-3xl md:text-4xl font-bold text-white text-center mb-6''>Built with the Best.</h2>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<p class=''text-slate-400 text-center max-w-2xl mx-auto''>Every layer of NextBlock™ leans on proven developer-first technology so the platform feels familiar, performant, and trustworthy from day one.</p><div class=''grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-8 gap-4 mt-10 text-sm font-semibold text-center text-white''><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Next.js</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>React</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Supabase</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Stripe</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Tailwind</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Tiptap</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Vercel</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Nx</div></div>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<h2 class=''text-3xl md:text-4xl font-bold text-white text-center mb-6 mt-16''>Powerful for Developers. Intuitive for Editors.</h2>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<div class=''grid md:grid-cols-2 gap-8 mt-10 text-white''><div class=''p-8 rounded-3xl border border-white/10 bg-white/5 backdrop-blur-sm''><h3 class=''text-xl font-bold mb-6 text-blue-400''>For Content Creators</h3><ul class=''space-y-4 text-sm text-slate-300''><li><strong class=''text-white block mb-1''>Intuitive Block Editor</strong>Drag-and-drop layouts with a Notion-like interface.</li><li><strong class=''text-white block mb-1''>Rich Content Blocks</strong>Deploy heroes, galleries, testimonials, and more in one click.</li><li><strong class=''text-white block mb-1''>Effortless Media Management</strong>Organize assets with folders, tags, and bulk actions.</li><li><strong class=''text-white block mb-1''>Worry-Free Revisions</strong>Automatic version history with instant restore.</li></ul></div><div class=''p-8 rounded-3xl border border-white/10 bg-gradient-to-br from-white/5 to-white/[0.02] backdrop-blur-sm''><h3 class=''text-xl font-bold mb-6 text-purple-400''>For Developers</h3><ul class=''space-y-4 text-sm text-slate-300''><li><strong class=''text-white block mb-1''>Next.js 16 Core</strong>Server Components, ISR, and Edge Functions ready out of the box.</li><li><strong class=''text-white block mb-1''>Supabase Integration</strong>Postgres, auth, storage, and real-time APIs without glue code.</li><li><strong class=''text-white block mb-1''>Monorepo Ready</strong>Nx-powered dev experience for scalable architectures.</li><li><strong class=''text-white block mb-1''>Extensible Block SDK</strong>Ship fully typed custom blocks and widgets.</li></ul></div></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 2, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (4, 1, NULL, 1, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#022c22\", \"position\": 0}, {\"color\": \"#0f172a\", \"position\": 50}, {\"color\": \"#020817\", \"position\": 100}], \"direction\": \"135deg\"}}, \"column_gap\": \"xl\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<p class=''text-xs uppercase tracking-[0.25em] text-emerald-400 font-bold mb-4''>Now Available — Premium Module</p><h2 class=''text-4xl md:text-5xl font-bold text-white mb-6 leading-tight''>Turn Your CMS Into<br/>a Full Storefront.</h2><p class=''text-lg text-slate-300 max-w-2xl leading-relaxed mb-8''>NextBlock™ Commerce transforms your content platform into a complete e-commerce engine. Products, checkout, multi-currency, taxes, shipping, invoices — all natively integrated into the block editor you already know.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"url\": \"/article/nextblock-commerce-guide\", \"size\": \"lg\", \"text\": \"Explore Commerce Features →\", \"variant\": \"default\"}, \"block_type\": \"button\"}, {\"content\": {\"url\": \"https://nextblock.dev/product/nextblock-commerce-pro-commerce-license\", \"size\": \"lg\", \"text\": \"Get a License\", \"variant\": \"outline\"}, \"block_type\": \"button\"}], [{\"content\": {\"html_content\": \"<div class=''rounded-3xl overflow-hidden border border-emerald-500/20 bg-gradient-to-br from-white/5 to-emerald-500/5 shadow-2xl p-6 backdrop-blur-sm''><img src=''/images/commerce-square.webp'' alt=''NextBlock™ Commerce dashboard showing product management'' class=''w-full h-auto rounded-2xl shadow-lg'' /><div class=''mt-4 grid grid-cols-3 gap-3 text-center''><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-emerald-400''>∞</p><p class=''text-xs text-slate-400''>Currencies</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-emerald-400''>2</p><p class=''text-xs text-slate-400''>Providers</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-emerald-400''>Auto</p><p class=''text-xs text-slate-400''>Tax Sync</p></div></div></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 2}, \"vertical_alignment\": \"center\"}', 3, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (5, 1, NULL, 1, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"none\"}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"level\": 2, \"textAlign\": \"center\", \"text_content\": \"Everything You Need to Sell Online\"}, \"block_type\": \"heading\"}, {\"content\": {\"html_content\": \"<p class=''text-lg text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto mb-12''>NextBlock™ Commerce ships a complete e-commerce toolkit so you can go from catalog to checkout without third-party plugins.</p><div class=''grid gap-6 md:grid-cols-2 lg:grid-cols-3''><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><div class=''w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-500/10 flex items-center justify-center mb-4''><svg class=''w-5 h-5 text-emerald-600 dark:text-emerald-400'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z''></path></svg></div><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Multi-Currency</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Real-time FX rates, rounding modes, charm pricing, and automatic product price sync across unlimited currencies.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><div class=''w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-500/10 flex items-center justify-center mb-4''><svg class=''w-5 h-5 text-emerald-600 dark:text-emerald-400'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M9 14l6-6m-5.5.5h.01m4.99 5h.01M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16l3.5-2 3.5 2 3.5-2 3.5 2z''></path></svg></div><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Tax Automation</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Manual stacked tax rates (GST + PST) or fully automatic calculation via Stripe Tax — you choose.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><div class=''w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-500/10 flex items-center justify-center mb-4''><svg class=''w-5 h-5 text-emerald-600 dark:text-emerald-400'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4''></path></svg></div><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Shipping Zones</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Zone-based rate resolution with country and state matching, per-currency pricing, and free-shipping thresholds.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><div class=''w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-500/10 flex items-center justify-center mb-4''><svg class=''w-5 h-5 text-emerald-600 dark:text-emerald-400'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z''></path></svg></div><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Stripe &amp; Freemius Checkout</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Stripe for physical products, Freemius for digital licensing — provider-aware checkout with inventory validation.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><div class=''w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-500/10 flex items-center justify-center mb-4''><svg class=''w-5 h-5 text-emerald-600 dark:text-emerald-400'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4''></path></svg></div><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Inventory Tracking</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Automatic quantity deduction on payment with resilient fallback paths and variant-level stock management.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><div class=''w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-500/10 flex items-center justify-center mb-4''><svg class=''w-5 h-5 text-emerald-600 dark:text-emerald-400'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z''></path></svg></div><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Orders &amp; Invoices</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Full order lifecycle management, stable invoice numbering, printable documents, and exportable order reports.</p></div></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 4, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (6, 1, NULL, 1, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#1e1b4b\", \"position\": 0}, {\"color\": \"#0f172a\", \"position\": 50}, {\"color\": \"#020817\", \"position\": 100}], \"direction\": \"135deg\"}}, \"column_gap\": \"xl\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<div class=''rounded-3xl overflow-hidden border border-violet-500/20 bg-gradient-to-br from-white/5 to-violet-500/5 shadow-2xl p-6 backdrop-blur-sm''><img src=''/images/cortex-ai-square.webp'' alt=''NextBlock™ Cortex AI dashboard showing block generator'' class=''w-full h-auto rounded-2xl shadow-lg'' /><div class=''mt-4 grid grid-cols-3 gap-3 text-center''><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-violet-400''>OpenRouter</p><p class=''text-xs text-slate-400''>AI Gateway</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-violet-400''>BYOK</p><p class=''text-xs text-slate-400''>Cost Control</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-violet-400''>Zod</p><p class=''text-xs text-slate-400''>Typed Blocks</p></div></div></div>\"}, \"block_type\": \"text\"}], [{\"content\": {\"html_content\": \"<p class=''text-xs uppercase tracking-[0.25em] text-violet-400 font-bold mb-4''>Now Available — AI Copilot</p><h2 class=''text-4xl md:text-5xl font-bold text-white mb-6 leading-tight''>Supercharge Your<br/>Content with AI.</h2><p class=''text-lg text-slate-300 max-w-2xl leading-relaxed mb-8''>NextBlock™ Cortex AI brings native block-level intelligence directly to your editor. Generate copy, refactor structures, and automate translations in one click, built directly on our high-performance architecture.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"url\": \"/article/nextblock-cortex-ai-guide\", \"size\": \"lg\", \"text\": \"Explore AI Capabilities →\", \"variant\": \"default\"}, \"block_type\": \"button\"}, {\"content\": {\"url\": \"https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license\", \"size\": \"lg\", \"text\": \"Get a License\", \"variant\": \"outline\"}, \"block_type\": \"button\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 2}, \"vertical_alignment\": \"center\"}', 5, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (7, 1, NULL, 1, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"none\"}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"level\": 2, \"textAlign\": \"center\", \"text_content\": \"More Than a CMS. An Ecosystem.\"}, \"block_type\": \"heading\"}, {\"content\": {\"html_content\": \"<p class=''text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto''>NextBlock™ is building a sustainable open-core roadmap so the platform grows with your business.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<div class=''grid gap-6 lg:grid-cols-[0.75fr_1.25fr] mt-10 items-stretch''><div class=''overflow-hidden rounded-[2rem] border border-slate-200 dark:border-white/10 bg-slate-950 shadow-2xl''><img src=''/images/goals.webp'' alt=''Roadmap board outlining the NextBlock™ ecosystem and premium module direction'' class=''h-full w-full object-cover'' /><div class=''border-t border-white/10 bg-slate-950/95 px-6 py-5''><p class=''text-xs uppercase tracking-[0.24em] text-emerald-300 mb-2 font-bold''>Roadmap in motion</p><p class=''text-sm text-slate-300 mb-0''>Commerce ships first, then the broader ecosystem grows around plugins, blocks, and partner-built modules.</p></div></div><div class=''grid gap-6''><div class=''p-10 rounded-3xl border border-emerald-500/20 bg-gradient-to-br from-emerald-50 to-white dark:from-emerald-500/5 dark:to-white/5 hover:border-emerald-500/40 transition-colors''><p class=''text-xs uppercase tracking-wide text-emerald-600 dark:text-emerald-400 mb-2 font-bold''>Available now</p><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>NextBlock™ Commerce</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Transform your site into a composable storefront with products, checkout, multi-currency pricing, tax automation, and commerce blocks that live beside your editorial content.</p></div><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-violet-500/30 transition-colors''><p class=''text-xs uppercase tracking-wide text-violet-700 dark:text-violet-300 mb-2 font-bold''>Build the future</p><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Plugin and block marketplace</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>A community marketplace gives developers room to publish, sell, and distribute custom blocks, themes, integrations, and partner modules.</p></div></div></div>\"}, \"block_type\": \"text\"}, {\"content\": {\"level\": 2, \"textAlign\": \"center\", \"text_content\": \"Join Our Community.\"}, \"block_type\": \"heading\"}, {\"content\": {\"html_content\": \"<p class=''text-slate-600 dark:text-slate-400 text-center mx-auto''>NextBlock™ is being built in the open. Star the repo, share feedback, and help define the future of performance-first content management.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<div class=''grid gap-4 md:grid-cols-3 mt-10 text-sm''><a class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 transition-all hover:scale-[1.02]'' href=''https://github.com/nextblock-cms'' target=''_blank'' rel=''noopener noreferrer''><strong class=''block text-base text-slate-900 dark:text-white mb-1''>GitHub</strong><span class=''text-slate-600 dark:text-slate-400''>Star the repo &amp; contribute</span></a><a class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 transition-all hover:scale-[1.02]'' href=''https://x.com/NextBlockCMS'' target=''_blank'' rel=''noopener noreferrer''><strong class=''block text-base text-slate-900 dark:text-white mb-1''>X (Twitter)</strong><span class=''text-slate-600 dark:text-slate-400''>Follow updates &amp; announcements</span></a><a class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 transition-all hover:scale-[1.02]'' href=''https://dev.to/nextblockcms'' target=''_blank'' rel=''noopener noreferrer''><strong class=''block text-base text-slate-900 dark:text-white mb-1''>Dev.to</strong><span class=''text-slate-600 dark:text-slate-400''>Read technical deep dives</span></a></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 6, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (1, 1, NULL, 1, 'section', '{\"is_hero\": true, \"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#020817\", \"position\": 0}, {\"color\": \"#0f172a\", \"position\": 50}, {\"color\": \"#1e293b\", \"position\": 100}], \"direction\": \"135deg\"}}, \"column_gap\": \"xl\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<h1 class=''text-5xl md:text-6xl font-extrabold tracking-tight text-white text-center leading-tight''>Build <span class=''relative inline-block mx-1 group''><span class=''absolute inset-0 bg-gradient-to-r from-blue-600 to-cyan-400 translate-y-1 md:translate-y-2 transform -skew-x-12 rounded-sm shadow-lg group-hover:skew-x-0 transition-transform duration-300 ease-out''></span><span class=''relative text-white italic px-1''>Blazing-Fast</span></span><br class=''md:hidden'' /> Websites.</h1>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<p class=''text-xl text-slate-300 text-center max-w-3xl mx-auto mt-4 leading-relaxed''>NextBlock™ is the open-source, developer-first Next.js CMS that merges 100% Lighthouse scores with a powerful visual block editor.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"url\": \"/article/how-to-setup-nextblock\", \"size\": \"lg\", \"text\": \"Get Started\", \"variant\": \"default\", \"position\": \"center\"}, \"block_type\": \"button\"}, {\"content\": {\"url\": \"https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&project-name=nextblock&repository-name=nextblock&stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D\", \"size\": \"lg\", \"text\": \"Deploy on Vercel\", \"variant\": \"outline\", \"position\": \"center\"}, \"block_type\": \"button\"}, {\"content\": {\"html_content\": \"<div class=''flex flex-wrap justify-center gap-6 text-sm uppercase tracking-wide text-slate-400 mt-8''><a href=''https://github.com/nextblock-cms'' target=''_blank'' rel=''noopener noreferrer'' class=''hover:text-white transition-colors''>GitHub</a><a href=''https://x.com/NextBlockCMS'' target=''_blank'' rel=''noopener noreferrer'' class=''hover:text-white transition-colors''>X</a><a href=''https://www.linkedin.com/in/nextblock/'' target=''_blank'' rel=''noopener noreferrer'' class=''hover:text-white transition-colors''>LinkedIn</a><a href=''https://dev.to/nextblockcms'' target=''_blank'' rel=''noopener noreferrer'' class=''hover:text-white transition-colors''>Dev.to</a><a href=''https://www.npmjs.com/~nextblockcms'' target=''_blank'' rel=''noopener noreferrer'' class=''hover:text-white transition-colors''>npm</a></div>\"}, \"block_type\": \"text\"}], [{\"content\": {\"html_content\": \"<div class=''p-10 border border-white/10 rounded-3xl bg-white/5 backdrop-blur-xl shadow-2xl relative overflow-hidden group''><div class=''absolute inset-0 bg-gradient-to-br from-blue-500/10 to-purple-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-500''></div><div class=''relative z-10''><p class=''text-xs text-white uppercase tracking-widest font-semibold mb-2''>Why teams switch</p><p class=''text-3xl font-bold text-white mb-2''>100% Lighthouse</p><p class=''text-base text-slate-300 mb-6''>Edge-rendered marketing sites, launches, and docs with uncompromising performance.</p><ul class=''space-y-3 text-sm text-slate-200''><li><span class=''text-blue-400 mr-2''>&#10003;</span> Next.js 16 with ISR and edge caching</li><li><span class=''text-blue-400 mr-2''>&#10003;</span> Supabase auth, data, and storage</li><li><span class=''text-blue-400 mr-2''>&#10003;</span> Notion-style block editor powered by Tiptap</li></ul><div class=''mt-6 rounded-2xl overflow-hidden border border-white/10 shadow-lg''><div class=''relative w-full aspect-video''><iframe class=''absolute inset-0 h-full w-full border-0'' src=''https://www.youtube.com/embed/71MyfoL4YVM?si=jtlDXV6cSC8rDgz0'' title=''NextBlock demo video'' allow=''accelerated-motion; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'' referrerpolicy=''strict-origin-when-cross-origin'' loading=''lazy'' allowfullscreen></iframe></div></div></div></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 2}, \"vertical_alignment\": \"center\"}', 0, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.851553+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (10, 3, NULL, 1, 'posts_grid', '{\"title\": \"Latest Deep Dives\", \"columns\": 3, \"postsPerPage\": 6, \"showPagination\": true}', 1, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (12, 5, NULL, 1, 'section', '{\"padding\": {\"top\": \"lg\", \"bottom\": \"lg\"}, \"background\": {\"type\": \"none\"}, \"column_blocks\": [[{\"content\": {\"html_content\": \"<div class=''max-w-2xl mx-auto text-center''><h2 class=''text-2xl font-bold mb-4''>Open Source & Community Driven</h2><p class=''text-slate-600 dark:text-slate-400 mb-6''>NextBlock™ is built in the open. We rely on developers and editors like you to help us define the roadmap. Whether it''s a bug report, a feature request, or just a shoutout, every message helps us move faster.</p></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 1, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (15, 6, NULL, 2, 'section', '{\"padding\": {\"top\": \"lg\", \"bottom\": \"lg\"}, \"background\": {\"type\": \"none\"}, \"column_blocks\": [[{\"content\": {\"html_content\": \"<div class=''max-w-2xl mx-auto text-center''><h2 class=''text-2xl font-bold mb-4''>Open Source & Communautaire</h2><p class=''text-slate-600 dark:text-slate-400 mb-6''>NextBlock™ est construit en public. Nous comptons sur les développeurs et éditeurs comme vous pour définir notre roadmap. Qu''il s''agisse d''un bug, d''une suggestion ou d''un simple salut, chaque message compte.</p></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 1, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (13, 5, NULL, 1, 'form', '{\"fields\": [{\"label\": \"Name\", \"temp_id\": \"name\", \"field_type\": \"text\", \"is_required\": true, \"placeholder\": \"Your name\"}, {\"label\": \"Email\", \"temp_id\": \"email\", \"field_type\": \"email\", \"is_required\": true, \"placeholder\": \"your@email.com\"}, {\"label\": \"Message\", \"temp_id\": \"message\", \"field_type\": \"textarea\", \"is_required\": true, \"placeholder\": \"How can we help?\"}], \"recipient_email\": \"contact@example.com\", \"success_message\": \"Thank you for your feedback! We''ll get back to you soon.\", \"submit_button_text\": \"Send Message\"}', 2, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.760236+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (16, 6, NULL, 2, 'form', '{\"fields\": [{\"label\": \"Nom\", \"temp_id\": \"nom\", \"field_type\": \"text\", \"is_required\": true, \"placeholder\": \"Votre nom\"}, {\"label\": \"Email\", \"temp_id\": \"email\", \"field_type\": \"email\", \"is_required\": true, \"placeholder\": \"votre@email.com\"}, {\"label\": \"Message\", \"temp_id\": \"message\", \"field_type\": \"textarea\", \"is_required\": true, \"placeholder\": \"Comment pouvons-nous vous aider ?\"}], \"recipient_email\": \"contact@example.com\", \"success_message\": \"Merci pour vos retours ! Nous vous répondrons bientôt.\", \"submit_button_text\": \"Envoyer le message\"}', 2, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.760236+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (17, 2, NULL, 2, 'section', '{\"is_hero\": true, \"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#020817\", \"position\": 0}, {\"color\": \"#0f172a\", \"position\": 50}, {\"color\": \"#1e293b\", \"position\": 100}], \"direction\": \"135deg\"}}, \"column_gap\": \"xl\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<h1 class=''text-5xl md:text-6xl font-bold tracking-tight text-white text-center drop-shadow-lg''>Créez des sites <span class=''relative inline-block mx-1 group''><span class=''absolute inset-0 bg-gradient-to-r from-blue-600 to-cyan-400 translate-y-1 md:translate-y-2 transform -skew-x-12 rounded-sm shadow-lg group-hover:skew-x-0 transition-transform duration-300 ease-out''></span><span class=''relative text-white italic px-1''>Ultra-Rapides</span></span><br class=''md:hidden'' />.</h1>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<p class=''text-xl text-slate-300 text-center max-w-3xl mx-auto mt-4 leading-relaxed''>NextBlock™ est le CMS Next.js open-source alliant scores Lighthouse parfaits et éditeur visuel puissant.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"url\": \"/article/comment-configurer-nextblock\", \"size\": \"lg\", \"text\": \"Commencer\", \"variant\": \"default\", \"position\": \"center\"}, \"block_type\": \"button\"}, {\"content\": {\"url\": \"https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&project-name=nextblock&repository-name=nextblock&stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D\", \"size\": \"lg\", \"text\": \"Déployer sur Vercel\", \"variant\": \"outline\", \"position\": \"center\"}, \"block_type\": \"button\"}, {\"content\": {\"html_content\": \"<div class=''flex flex-wrap justify-center gap-6 text-sm uppercase tracking-wide text-slate-400 mt-8''><a href=''https://github.com/nextblock-cms'' target=''_blank'' rel=''noopener noreferrer'' class=''hover:text-white transition-colors''>GitHub</a><a href=''https://x.com/NextBlockCMS'' target=''_blank'' rel=''noopener noreferrer'' class=''hover:text-white transition-colors''>X</a><a href=''https://www.linkedin.com/in/nextblock/'' target=''_blank'' rel=''noopener noreferrer'' class=''hover:text-white transition-colors''>LinkedIn</a><a href=''https://dev.to/nextblockcms'' target=''_blank'' rel=''noopener noreferrer'' class=''hover:text-white transition-colors''>Dev.to</a><a href=''https://www.npmjs.com/~nextblockcms'' target=''_blank'' rel=''noopener noreferrer'' class=''hover:text-white transition-colors''>npm</a></div>\"}, \"block_type\": \"text\"}], [{\"content\": {\"html_content\": \"<div class=''p-10 border border-white/10 rounded-3xl bg-white/5 backdrop-blur-xl shadow-2xl relative overflow-hidden group''><div class=''absolute inset-0 bg-gradient-to-br from-blue-500/10 to-purple-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-500''></div><div class=''relative z-10''><p class=''text-xs text-white uppercase tracking-widest font-semibold mb-2''>Pourquoi migrer</p><p class=''text-3xl font-bold text-white mb-2''>100% Lighthouse</p><p class=''text-base text-slate-300 mb-6''>Sites marketing et docs rendus à l''edge avec des performances irréprochables.</p><ul class=''space-y-3 text-sm text-slate-200''><li><span class=''text-blue-400 mr-2''>&#10003;</span> Next.js 16 avec ISR et cache edge</li><li><span class=''text-blue-400 mr-2''>&#10003;</span> Supabase pour l''auth, les données et le stockage</li><li><span class=''text-blue-400 mr-2''>&#10003;</span> Éditeur de blocs type Notion sur Tiptap</li></ul><div class=''mt-6 rounded-2xl overflow-hidden border border-white/10 shadow-lg''><div class=''relative w-full aspect-video''><iframe class=''absolute inset-0 h-full w-full border-0'' src=''https://www.youtube.com/embed/71MyfoL4YVM?si=jtlDXV6cSC8rDgz0'' title=''Vidéo de démonstration NextBlock'' allow=''accelerated-motion; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'' referrerpolicy=''strict-origin-when-cross-origin'' loading=''lazy'' allowfullscreen></iframe></div></div></div></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 2}, \"vertical_alignment\": \"center\"}', 0, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.851553+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (18, 2, NULL, 2, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"none\"}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"level\": 2, \"textAlign\": \"center\", \"text_content\": \"Fonctionnalités clés : les trois piliers de NextBlock™\"}, \"block_type\": \"heading\"}, {\"content\": {\"html_content\": \"<p class=''text-lg text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto''>NextBlock™ unifie performances, expérience éditoriale et contrôle développeur pour que chaque équipe livre son meilleur travail.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<div class=''grid gap-8 md:grid-cols-3 mt-12''><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 backdrop-blur-sm hover:bg-slate-100 dark:hover:bg-white/10 transition-colors duration-300''><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Vitesse Extrême.</h3><p class=''text-sm text-slate-600 dark:text-slate-400 leading-relaxed''>Pensé pour des scores Lighthouse parfaits avec une diffusion mondiale.</p><ul class=''mt-6 space-y-3 text-sm text-slate-600 dark:text-slate-400''><li><strong class=''text-slate-800 dark:text-slate-200''>Edge Caching:</strong> Servez vos pages partout.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Critical CSS:</strong> Styles en ligne pour éviter les blocages.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Images Opt:</strong> AVIF et placeholders floutés.</li></ul></div><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 backdrop-blur-sm hover:bg-slate-100 dark:hover:bg-white/10 transition-colors duration-300''><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Expérience Éditeur.</h3><p class=''text-sm text-slate-600 dark:text-slate-400 leading-relaxed''>Un éditeur façon Notion pour publier sans dépendre des développeurs.</p><ul class=''mt-6 space-y-3 text-sm text-slate-600 dark:text-slate-400''><li><strong class=''text-slate-800 dark:text-slate-200''>Visuel:</strong> Héros, galeries, témoignages.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Média:</strong> Dossiers, tags et actions groupées.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Historique:</strong> Restauration complète.</li></ul></div><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 backdrop-blur-sm hover:bg-slate-100 dark:hover:bg-white/10 transition-colors duration-300''><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Extensible à l''Infini.</h3><p class=''text-sm text-slate-700 dark:text-slate-200 leading-relaxed''>Un socle Next.js + Supabase modulaire, extensible et auto-hébergeable.</p><ul class=''mt-6 space-y-3 text-sm text-slate-700 dark:text-slate-200''><li><strong class=''text-slate-900 dark:text-white''>SDK de blocs:</strong> Composants typés.</li><li><strong class=''text-slate-900 dark:text-white''>CLI:</strong> Générez modules en minutes.</li><li><strong class=''text-slate-900 dark:text-white''>Monorepo Nx:</strong> Dépendances maintenables.</li></ul></div></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 1, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (19, 2, NULL, 2, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#0f172a\", \"position\": 0}, {\"color\": \"#020817\", \"position\": 100}], \"direction\": \"180deg\"}}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<h2 class=''text-3xl md:text-4xl font-bold text-white text-center mb-6''>Conçu avec les meilleurs outils.</h2>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<p class=''text-slate-400 text-center max-w-2xl mx-auto''>Chaque couche de NextBlock™ repose sur des technologies éprouvées pour une expérience familière et performante.</p><div class=''grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-8 gap-4 mt-10 text-sm font-semibold text-center text-white''><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Next.js</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>React</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Supabase</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Stripe</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Tailwind</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Tiptap</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Vercel</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Nx</div></div>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<h2 class=''text-3xl md:text-4xl font-bold text-white text-center mb-6 mt-16''>Puissant pour les développeurs. Intuitif pour les éditeurs.</h2>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<div class=''grid md:grid-cols-2 gap-8 mt-10 text-white''><div class=''p-8 rounded-3xl border border-white/10 bg-white/5 backdrop-blur-sm''><h3 class=''text-xl font-bold mb-6 text-blue-400''>Pour les créateurs</h3><ul class=''space-y-4 text-sm text-slate-300''><li><strong class=''text-white block mb-1''>Éditeur de blocs</strong>Glisser-déposer façon Notion.</li><li><strong class=''text-white block mb-1''>Blocs riches</strong>Héros, galeries, témoignages.</li><li><strong class=''text-white block mb-1''>Médiathèque</strong>Dossiers, tags et actions groupées.</li><li><strong class=''text-white block mb-1''>Versions sécurisées</strong>Historique et restauration instantanée.</li></ul></div><div class=''p-8 rounded-3xl border border-white/10 bg-gradient-to-br from-white/5 to-white/[0.02] backdrop-blur-sm''><h3 class=''text-xl font-bold mb-6 text-purple-400''>Pour les développeurs</h3><ul class=''space-y-4 text-sm text-slate-300''><li><strong class=''text-white block mb-1''>Next.js 16</strong>Server Components, ISR et Edge prêts à l''emploi.</li><li><strong class=''text-white block mb-1''>Supabase</strong>Postgres, auth, stockage, temps réel.</li><li><strong class=''text-white block mb-1''>Monorepo Nx</strong>Dépendances lisibles et centrales.</li><li><strong class=''text-white block mb-1''>SDK de blocs</strong>Widgets typés et extensibles.</li></ul></div></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 2, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (20, 2, NULL, 2, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#022c22\", \"position\": 0}, {\"color\": \"#0f172a\", \"position\": 50}, {\"color\": \"#020817\", \"position\": 100}], \"direction\": \"135deg\"}}, \"column_gap\": \"xl\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<p class=''text-xs uppercase tracking-[0.25em] text-emerald-400 font-bold mb-4''>Disponible — Module Premium</p><h2 class=''text-4xl md:text-5xl font-bold text-white mb-6 leading-tight''>Transformez votre CMS<br/>en vitrine complète.</h2><p class=''text-lg text-slate-300 max-w-2xl leading-relaxed mb-8''>NextBlock™ Commerce transforme votre plateforme de contenu en moteur e-commerce complet. Produits, checkout, multi-devises, taxes, expédition, factures — le tout intégré nativement dans l''éditeur de blocs que vous connaissez déjà.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"url\": \"/article/guide-commerce-nextblock\", \"size\": \"lg\", \"text\": \"Découvrir Commerce →\", \"variant\": \"default\"}, \"block_type\": \"button\"}, {\"content\": {\"url\": \"https://nextblock.dev/product/nextblock-commerce-pro-commerce-license\", \"size\": \"lg\", \"text\": \"Obtenir une licence\", \"variant\": \"outline\"}, \"block_type\": \"button\"}], [{\"content\": {\"html_content\": \"<div class=''rounded-3xl overflow-hidden border border-emerald-500/20 bg-gradient-to-br from-white/5 to-emerald-500/5 shadow-2xl p-6 backdrop-blur-sm''><img src=''/images/commerce-square.webp'' alt=''Tableau de bord NextBlock™ Commerce'' class=''w-full h-auto rounded-2xl shadow-lg'' /><div class=''mt-4 grid grid-cols-3 gap-3 text-center''><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-emerald-400''>∞</p><p class=''text-xs text-slate-400''>Devises</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-emerald-400''>2</p><p class=''text-xs text-slate-400''>Fournisseurs</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-emerald-400''>Auto</p><p class=''text-xs text-slate-400''>Taxes</p></div></div></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 2}, \"vertical_alignment\": \"center\"}', 3, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (21, 2, NULL, 2, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"none\"}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"level\": 2, \"textAlign\": \"center\", \"text_content\": \"Tout pour vendre en ligne\"}, \"block_type\": \"heading\"}, {\"content\": {\"html_content\": \"<p class=''text-lg text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto mb-12''>NextBlock™ Commerce livre une boîte à outils e-commerce complète pour aller du catalogue au paiement sans plugins tiers.</p><div class=''grid gap-6 md:grid-cols-2 lg:grid-cols-3''><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Multi-Devises</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Taux de change en temps réel, modes d''arrondi, prix charme et synchronisation automatique sur toutes les devises.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Taxes Automatiques</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Taux manuels empilés (TPS + TVQ) ou calcul automatique via Stripe Tax — à vous de choisir.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Zones d''Expédition</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Résolution par pays et état, tarification par devise et seuils de livraison gratuite.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Stripe &amp; Freemius</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Stripe pour les produits physiques, Freemius pour les licences numériques — checkout intelligent avec validation d''inventaire.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Suivi d''Inventaire</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Déduction automatique des quantités au paiement avec gestion des stocks par variante.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Commandes &amp; Factures</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Gestion du cycle de vie des commandes, numérotation stable des factures et rapports de commandes exportables.</p></div></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 4, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (22, 2, NULL, 2, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#1e1b4b\", \"position\": 0}, {\"color\": \"#0f172a\", \"position\": 50}, {\"color\": \"#020817\", \"position\": 100}], \"direction\": \"135deg\"}}, \"column_gap\": \"xl\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<div class=''rounded-3xl overflow-hidden border border-violet-500/20 bg-gradient-to-br from-white/5 to-violet-500/5 shadow-2xl p-6 backdrop-blur-sm''><img src=''/images/cortex-ai-square.webp'' alt=''Tableau de bord NextBlock™ Cortex AI montrant le générateur de blocs'' class=''w-full h-auto rounded-2xl shadow-lg'' /><div class=''mt-4 grid grid-cols-3 gap-3 text-center''><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-violet-400''>OpenRouter</p><p class=''text-xs text-slate-400''>Passerelle IA</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-violet-400''>BYOK</p><p class=''text-xs text-slate-400''>Contrôle des coûts</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-violet-400''>Zod</p><p class=''text-xs text-slate-400''>Blocs typés</p></div></div></div>\"}, \"block_type\": \"text\"}], [{\"content\": {\"html_content\": \"<p class=''text-xs uppercase tracking-[0.25em] text-violet-400 font-bold mb-4''>Disponible — Copilote IA</p><h2 class=''text-4xl md:text-5xl font-bold text-white mb-6 leading-tight''>Boostez votre<br/>contenu avec l''IA.</h2><p class=''text-lg text-slate-300 max-w-2xl leading-relaxed mb-8''>NextBlock™ Cortex AI apporte une intelligence native au niveau des blocs directement dans votre éditeur. Générez du texte, restructurez vos contenus et automatisez les traductions en un clic, le tout propulsé par notre architecture haute performance.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"url\": \"/article/nextblock-cortex-ai-guide\", \"size\": \"lg\", \"text\": \"Découvrir l''IA →\", \"variant\": \"default\"}, \"block_type\": \"button\"}, {\"content\": {\"url\": \"https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license\", \"size\": \"lg\", \"text\": \"Obtenir une licence\", \"variant\": \"outline\"}, \"block_type\": \"button\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 2}, \"vertical_alignment\": \"center\"}', 5, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (23, 2, NULL, 2, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"none\"}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"level\": 2, \"textAlign\": \"center\", \"text_content\": \"Plus qu''un CMS. Un écosystème.\"}, \"block_type\": \"heading\"}, {\"content\": {\"html_content\": \"<p class=''text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto''>NextBlock™ construit une feuille de route open-core durable qui évolue avec votre activité.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<div class=''grid gap-6 lg:grid-cols-[0.75fr_1.25fr] mt-10 items-stretch''><div class=''overflow-hidden rounded-[2rem] border border-slate-200 dark:border-white/10 bg-slate-950 shadow-2xl''><img src=''/images/goals.webp'' alt=''Tableau de roadmap montrant la direction de l''ecosysteme NextBlock™ et des modules premium'' class=''h-full w-full object-cover'' /><div class=''border-t border-white/10 bg-slate-950/95 px-6 py-5''><p class=''text-xs uppercase tracking-[0.24em] text-emerald-300 mb-2 font-bold''>Roadmap en mouvement</p><p class=''text-sm text-slate-300 mb-0''>Le commerce arrive en premier, puis l''ecosysteme s''etend avec des plugins, des blocs et des modules construits par les partenaires.</p></div></div><div class=''grid gap-6''><div class=''p-10 rounded-3xl border border-emerald-500/20 bg-gradient-to-br from-emerald-50 to-white dark:from-emerald-500/5 dark:to-white/5 hover:border-emerald-500/40 transition-colors''><p class=''text-xs uppercase tracking-wide text-emerald-600 dark:text-emerald-400 mb-2 font-bold''>Disponible maintenant</p><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>NextBlock™ Commerce</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Transformez votre site en vitrine composable avec produits, checkout, tarification multi-devise, taxes automatiques et blocs commerce relies a votre contenu editorial.</p></div><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-violet-500/30 transition-colors''><p class=''text-xs uppercase tracking-wide text-violet-700 dark:text-violet-300 mb-2 font-bold''>Construire la suite</p><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Marketplace de plugins et blocs</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Une marketplace communautaire ouvrira la voie a la publication, la vente et la distribution de blocs, themes, integrations et modules partenaires.</p></div></div></div>\"}, \"block_type\": \"text\"}, {\"content\": {\"level\": 2, \"textAlign\": \"center\", \"text_content\": \"Rejoignez la communauté.\"}, \"block_type\": \"heading\"}, {\"content\": {\"html_content\": \"<p class=''text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto''>NextBlock™ se construit en public. Ajoutez une étoile, partagez vos retours et façonnez l''avenir du CMS orienté performance.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<div class=''grid gap-4 md:grid-cols-3 mt-10 text-sm''><a class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 transition-all hover:scale-[1.02]'' href=''https://github.com/nextblock-cms'' target=''_blank'' rel=''noopener noreferrer''><strong class=''block text-base text-slate-900 dark:text-white mb-1''>GitHub</strong><span class=''text-slate-600 dark:text-slate-400''>Ajoutez une étoile &amp; contribuez</span></a><a class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 transition-all hover:scale-[1.02]'' href=''https://x.com/NextBlockCMS'' target=''_blank'' rel=''noopener noreferrer''><strong class=''block text-base text-slate-900 dark:text-white mb-1''>X (Twitter)</strong><span class=''text-slate-600 dark:text-slate-400''>Suivez les annonces</span></a><a class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 transition-all hover:scale-[1.02]'' href=''https://dev.to/nextblockcms'' target=''_blank'' rel=''noopener noreferrer''><strong class=''block text-base text-slate-900 dark:text-white mb-1''>Dev.to</strong><span class=''text-slate-600 dark:text-slate-400''>Lisez nos articles techniques</span></a></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 6, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (26, 4, NULL, 2, 'posts_grid', '{\"title\": \"Derniers articles\", \"columns\": 3, \"postsPerPage\": 6, \"showPagination\": true}', 1, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (9, 3, NULL, 1, 'section', '{\"is_hero\": true, \"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#020817\", \"position\": 0}, {\"color\": \"#1e293b\", \"position\": 100}], \"direction\": \"135deg\"}}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<p class=''text-sm uppercase tracking-[0.3em] text-blue-400 font-bold text-center md:text-left mb-4''>The Nextblock Journal</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<h2 class=''text-4xl md:text-5xl font-bold text-white text-center md:text-left mb-6''>Deep dives into performance, DX, and visual editing.</h2>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<p class=''text-slate-300 text-lg max-w-xl mx-auto md:mx-0 text-center md:text-left leading-relaxed''>Explore architectural walkthroughs, Supabase recipes, and block editor experiments written by the Nextblock core team.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"url\": \"/articles#latest\", \"size\": \"lg\", \"text\": \"Explore Articles\", \"variant\": \"default\"}, \"block_type\": \"button\"}, {\"content\": {\"url\": \"https://github.com/nextblock-cms/nextblock/discussions\", \"size\": \"lg\", \"text\": \"Subscribe for Updates\", \"variant\": \"outline\"}, \"block_type\": \"button\"}], [{\"content\": {\"html_content\": \"<div class=''h-full flex items-center justify-center rounded-3xl overflow-hidden border border-white/10 bg-white/5 shadow-2xl p-4 backdrop-blur-sm''><img src=''/images/developer.webp'' alt=''Developer working with the Nextblock stack'' class=''w-full object-cover rounded-2xl shadow-lg'' style=''max-width: 400px;'' /></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 2}}', 0, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (11, 5, NULL, 1, 'section', '{\"is_hero\": true, \"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#020817\", \"position\": 0}, {\"color\": \"#0f172a\", \"position\": 100}], \"direction\": \"135deg\"}}, \"column_blocks\": [[{\"content\": {\"level\": 1, \"textAlign\": \"center\", \"textColor\": \"white\", \"text_content\": \"Let''s Build the Future Together\"}, \"block_type\": \"heading\"}, {\"content\": {\"html_content\": \"<p class=''text-xl text-slate-300 text-center max-w-3xl mx-auto mt-4''>NextBlock™ is an open-source project driven by community feedback. We''d love to hear your thoughts, ideas, or questions.</p>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 0, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (14, 6, NULL, 2, 'section', '{\"is_hero\": true, \"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#020817\", \"position\": 0}, {\"color\": \"#0f172a\", \"position\": 100}], \"direction\": \"135deg\"}}, \"column_blocks\": [[{\"content\": {\"level\": 1, \"textAlign\": \"center\", \"textColor\": \"white\", \"text_content\": \"Bâtissons le futur ensemble\"}, \"block_type\": \"heading\"}, {\"content\": {\"html_content\": \"<p class=''text-xl text-slate-300 text-center max-w-3xl mx-auto mt-4''>NextBlock™ est un projet open-source propulsé par vos retours. Nous serions ravis d''entendre vos idées ou vos questions.</p>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 0, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (24, 2, NULL, 2, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#020817\", \"position\": 0}, {\"color\": \"#0f172a\", \"position\": 100}], \"direction\": \"180deg\"}}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<h2 class=''text-3xl md:text-4xl font-bold text-center text-white mb-4''>Des questions ?</h2>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<p class=''text-center text-base text-slate-300 max-w-2xl mx-auto''>NextBlock™ co-construit avec des partenaires : fonctionnalités, modules sponsorisés et direction produit.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"url\": \"/contact\", \"size\": \"lg\", \"text\": \"Nous contacter\", \"variant\": \"default\", \"position\": \"center\"}, \"block_type\": \"button\"}, {\"content\": {\"url\": \"https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&project-name=nextblock&repository-name=nextblock&stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D\", \"size\": \"lg\", \"text\": \"Déployer sur Vercel\", \"variant\": \"outline\", \"position\": \"center\"}, \"block_type\": \"button\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 7, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.840619+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (25, 4, NULL, 2, 'section', '{\"is_hero\": true, \"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#020817\", \"position\": 0}, {\"color\": \"#1e293b\", \"position\": 100}], \"direction\": \"135deg\"}}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<p class=''text-sm uppercase tracking-[0.3em] text-blue-400 font-bold text-center md:text-left mb-4''>Le journal Nextblock</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<h2 class=''text-4xl md:text-5xl font-bold text-white text-center md:text-left mb-6''>Plongées dans la performance, l''expérience dev et l''édition visuelle.</h2>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<p class=''text-lg max-w-xl mx-auto md:mx-0 text-center md:text-left text-slate-300 leading-relaxed''>Walkthroughs d''architecture, recettes Supabase et expérimentations éditeur écrits par l''équipe Nextblock.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"url\": \"/articles#latest\", \"size\": \"lg\", \"text\": \"Explorer les articles\", \"variant\": \"default\"}, \"block_type\": \"button\"}, {\"content\": {\"url\": \"https://github.com/nextblock-cms/nextblock/discussions\", \"size\": \"lg\", \"text\": \"S''abonner aux mises à jour\", \"variant\": \"outline\"}, \"block_type\": \"button\"}], [{\"content\": {\"html_content\": \"<div class=''rounded-3xl overflow-hidden border border-white/10 bg-white/5 shadow-2xl p-4 backdrop-blur-sm''><img src=''/images/developer.webp'' alt=''Développeur travaillant avec la stack Nextblock'' class=''w-full object-cover rounded-2xl shadow-lg'' style=''max-width: 400px;'' /></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 2}}', 0, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (31, NULL, 5, 1, 'text', '{\"html_content\": \"<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock™ Commerce is the first premium module in the ecosystem: a source-available storefront layer that plugs directly into the same editorial system as the CMS. It is built for teams that want content, catalog, and checkout to live inside one product surface instead of three disconnected tools.</p>\\n\\n<div class=''grid gap-4 md:grid-cols-3 my-10''>\\n <div class=''rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Commerce core</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Catalog + checkout</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Products, variants, orders, shipping, and invoices all plug into the existing CMS shell.</p>\\n </div>\\n <div class=''rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200''>Global selling</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Multi-currency ready</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Automatic FX sync, rounding strategies, and per-product overrides keep international pricing practical.</p>\\n </div>\\n <div class=''rounded-3xl border border-indigo-200/70 bg-indigo-50/70 p-6 dark:border-indigo-500/20 dark:bg-indigo-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:text-indigo-200''>Operator workflow</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Provider-aware flow</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Stripe and Freemius are handled differently so the storefront can stay clean without hiding complexity.</p>\\n </div>\\n</div>\\n\\n<h2>Product Catalog</h2>\\n<p>Commerce supports physical and digital products with variants, attributes, localized product media, independent pricing, SKUs, and stock levels. Product assets stay in the same media library editors already use for marketing pages, so content and commerce teams are not working in separate silos.</p>\\n\\n<h2>Multi-Currency Engine</h2>\\n<p>The pricing engine is built for real-world stores, not just a demo checkout:</p>\\n<ul>\\n <li><strong>Unlimited currencies</strong> with ISO codes, symbols, and stored exchange rates</li>\\n <li><strong>Automatic FX sync</strong> from Frankfurter or a custom provider via <code>FX_API_BASE_URL</code></li>\\n <li><strong>Rounding modes</strong> including nearest, up, down, and charm pricing like <code>9.99</code></li>\\n <li><strong>Store-managed auto-sync</strong> so product prices convert when rates refresh</li>\\n <li><strong>Rebasing</strong> when the default currency changes</li>\\n <li><strong>Per-product overrides</strong> when a catalog item needs explicit pricing in specific markets</li>\\n</ul>\\n\\n<h2>Tax Automation</h2>\\n<p>Teams can stay manual when they need control, or delegate tax math to Stripe Tax when they want automation:</p>\\n<div class=''grid md:grid-cols-2 gap-6 my-6''>\\n <div class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5''>\\n <h4 class=''font-bold text-slate-900 dark:text-white mb-2''>Manual mode</h4>\\n <p class=''text-sm text-slate-600 dark:text-slate-400''>Define rates by country and optional state or province. Stacked taxes such as GST + PST are supported, and tax lines are stored in <code>orders.tax_details</code>.</p>\\n </div>\\n <div class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5''>\\n <h4 class=''font-bold text-slate-900 dark:text-white mb-2''>Automatic mode</h4>\\n <p class=''text-sm text-slate-600 dark:text-slate-400''>Stripe Tax calculates the final amounts. Product and shipping tax codes travel with the line items, and the webhook resync replaces provisional values with final totals.</p>\\n </div>\\n</div>\\n\\n<h2>Shipping and Checkout</h2>\\n<p>Shipping zones match by country and state or province, support localized method names, per-currency pricing, free-shipping thresholds, and priority-based fallbacks when an exact match is not found.</p>\\n<p>The checkout layer is provider-aware:</p>\\n<ul>\\n <li><strong>Stripe</strong> handles physical goods, inventory checks, shipping calculation, tax, customer upserts, and Checkout Sessions</li>\\n <li><strong>Freemius</strong> handles digital licensing, plan resolution, and checkout URLs with sandbox support</li>\\n <li>Mixed-provider carts are rejected so the buyer journey stays understandable</li>\\n</ul>\\n\\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\\n <img src=''/images/commerce-plan.webp'' alt=''Commerce roadmap board outlining premium module goals and future storefront capabilities for NextBlock™'' class=''w-full h-auto object-cover'' />\\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>Commerce is positioned as the first premium module in a larger roadmap, which makes it feel like part of a growing platform instead of a bolt-on add-on.</figcaption>\\n</figure>\\n\\n<h2>Inventory, Orders, and Invoices</h2>\\n<p>When quantity tracking is enabled, checkout validates requested quantities against <code>inventory_items</code>. On payment confirmation, <code>apply_order_inventory_deduction()</code> reduces stock with a resilient fallback path that can use direct SQL if the RPC layer fails.</p>\\n<ul>\\n <li>Order statuses move from <code>pending</code> to <code>paid</code> to <code>shipped</code>, with cancellation and refund states available too</li>\\n <li>Invoice numbering is generated through database functions for consistency</li>\\n <li>Printable invoice documents pull from <code>invoice_settings</code></li>\\n <li>Customers can review order history and invoice access from the storefront side</li>\\n <li><strong>Coming soon:</strong> exportable order reporting and analytics dashboards</li>\\n</ul>\\n\\n<h2>Commerce Surfaces Inside the CMS</h2>\\n<p>When the ecommerce package is active, the CMS exposes product list, create, and edit views with media and variants, inventory management, order detail screens, shipping configuration, payment provider settings, tax setup, and currency management. The important part is not only that those screens exist, but that they feel native inside the same shell your content team is already using.</p>\"}', 0, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (32, NULL, 6, 2, 'text', '{\"html_content\": \"<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock™ Commerce est le premier module premium de l''ecosysteme : une couche storefront source-available qui se branche directement sur le meme systeme editorial que le CMS. L''objectif est de rapprocher contenu, catalogue et checkout dans une seule surface produit.</p>\\n\\n<div class=''grid gap-4 md:grid-cols-3 my-10''>\\n <div class=''rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Base commerce</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Catalogue + checkout</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Produits, variantes, commandes, livraison et factures vivent dans le meme shell CMS.</p>\\n </div>\\n <div class=''rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200''>Vente globale</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Multi-devise</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Sync FX automatique, strategies d''arrondi et overrides par produit rendent les prix internationaux realistes.</p>\\n </div>\\n <div class=''rounded-3xl border border-indigo-200/70 bg-indigo-50/70 p-6 dark:border-indigo-500/20 dark:bg-indigo-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:text-indigo-200''>Workflow operateur</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Par fournisseur</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Stripe et Freemius sont traites differemment pour garder un parcours d''achat propre sans cacher la complexite.</p>\\n </div>\\n</div>\\n\\n<h2>Catalogue produits</h2>\\n<p>Le module gere produits physiques et numeriques avec variantes, attributs, medias localises, prix independants, SKU et niveaux de stock. Les assets produits restent dans la meme bibliotheque media que les pages marketing, ce qui evite de separer equipes contenu et equipes commerce.</p>\\n\\n<h2>Moteur multi-devise</h2>\\n<p>Le moteur tarifaire vise un vrai usage boutique, pas seulement une demo :</p>\\n<ul>\\n <li><strong>Devises illimitees</strong> avec codes ISO, symboles et taux stockes</li>\\n <li><strong>Synchronisation FX automatique</strong> depuis Frankfurter ou un provider custom via <code>FX_API_BASE_URL</code></li>\\n <li><strong>Modes d''arrondi</strong> dont nearest, up, down et prix charme comme <code>9.99</code></li>\\n <li><strong>Auto-sync magasin</strong> pour convertir les prix quand les taux changent</li>\\n <li><strong>Rebasement</strong> lorsqu''on change la devise par defaut</li>\\n <li><strong>Overrides par produit</strong> quand un article demande un prix fixe sur certains marches</li>\\n</ul>\\n\\n<h2>Taxes automatiques</h2>\\n<p>Les equipes peuvent rester en mode manuel ou confier le calcul a Stripe Tax :</p>\\n<div class=''grid md:grid-cols-2 gap-6 my-6''>\\n <div class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5''>\\n <h4 class=''font-bold text-slate-900 dark:text-white mb-2''>Mode manuel</h4>\\n <p class=''text-sm text-slate-600 dark:text-slate-400''>Definition des taux par pays et eventuellement par province. Les taxes empilees comme TPS + TVQ sont supportees, avec stockage dans <code>orders.tax_details</code>.</p>\\n </div>\\n <div class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5''>\\n <h4 class=''font-bold text-slate-900 dark:text-white mb-2''>Mode automatique</h4>\\n <p class=''text-sm text-slate-600 dark:text-slate-400''>Stripe Tax calcule les montants finaux. Les codes fiscaux voyagent avec les line items et le webhook remplace les valeurs provisoires par les montants definitifs.</p>\\n </div>\\n</div>\\n\\n<h2>Livraison et checkout</h2>\\n<p>Les zones de livraison correspondent par pays et etat ou province, gerent des noms localises, des prix par devise, des seuils de livraison gratuite, et des fallbacks par priorite quand aucune correspondance exacte n''est trouvee.</p>\\n<p>Le checkout est conscient du fournisseur :</p>\\n<ul>\\n <li><strong>Stripe</strong> gere les biens physiques, les verifications d''inventaire, la livraison, les taxes, les clients et les Checkout Sessions</li>\\n <li><strong>Freemius</strong> gere les licences numeriques, la resolution des plans et les URLs de checkout avec support sandbox</li>\\n <li>Les paniers melangeant plusieurs fournisseurs sont refuses pour garder un parcours plus clair</li>\\n</ul>\\n\\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\\n <img src=''/images/commerce-plan.webp'' alt=''Tableau de roadmap commerce montrant les objectifs premium et les futures capacites storefront de NextBlock™'' class=''w-full h-auto object-cover'' />\\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>Le commerce est presente comme le premier module premium d''une feuille de route plus large, ce qui renforce l''idee d''une vraie plateforme en croissance.</figcaption>\\n</figure>\\n\\n<h2>Inventaire, commandes et factures</h2>\\n<p>Quand le suivi des quantites est actif, le checkout valide les demandes contre <code>inventory_items</code>. A la confirmation du paiement, <code>apply_order_inventory_deduction()</code> retire le stock avec un chemin de repli resilient si la couche RPC echoue.</p>\\n<ul>\\n <li>Les statuts de commande passent de <code>pending</code> a <code>paid</code> puis <code>shipped</code>, avec annulation et remboursement si besoin</li>\\n <li>La numerotation des factures est geree par des fonctions SQL pour rester coherente</li>\\n <li>Les documents facture tirent leurs informations de <code>invoice_settings</code></li>\\n <li>Les clients peuvent consulter leur historique et leurs factures</li>\\n <li><strong>Bientot :</strong> exports de commandes et tableaux de bord analytiques</li>\\n</ul>\\n\\n<h2>Surfaces commerce dans le CMS</h2>\\n<p>Quand le package ecommerce est actif, le CMS expose les vues produit, edition avec medias et variantes, gestion d''inventaire, detail des commandes, configuration livraison, parametres de paiement, taxes et devises. L''enjeu principal est que tout cela paraisse natif dans le meme shell que l''equipe contenu utilise deja.</p>\"}', 0, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (33, NULL, 7, 1, 'text', '{\"html_content\": \"\\n<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock Cortex AI is the AI layer built for the way NextBlock pages are actually composed. Instead of treating your site like a blank document, it understands blocks, sections, editor constraints, and the model choices your team wants to use.</p>\\n\\n<div class=''grid gap-4 md:grid-cols-3 my-10''>\\n <div class=''rounded-3xl border border-violet-200/70 bg-violet-50/80 p-6 dark:border-violet-500/20 dark:bg-violet-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Model routing</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Pick the right model</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Route generation through OpenRouter or your configured provider strategy so each task can balance speed, quality, and cost.</p>\\n </div>\\n <div class=''rounded-3xl border border-sky-200/70 bg-sky-50/80 p-6 dark:border-sky-500/20 dark:bg-sky-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200''>BYOK control</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Use your own keys</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Keep provider credentials under your control while still giving editors a clean, guided AI workflow in the CMS.</p>\\n </div>\\n <div class=''rounded-3xl border border-emerald-200/70 bg-emerald-50/80 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Typed output</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Generate valid blocks</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Schema-aware generation helps AI output land as usable page structure instead of loose copy that needs a rebuild.</p>\\n </div>\\n</div>\\n\\n<h2>Why Cortex AI Belongs Inside the Editor</h2>\\n<p>Generic chat tools can draft copy, but they do not know the difference between a hero, a card grid, a product description, and a localized article. Cortex AI lives closer to the editing surface, so generation can respect the same block contracts that render on the public site.</p>\\n<p>That makes AI useful for practical production work: drafting a landing section, tightening an excerpt, expanding a product story, translating a post, or reshaping a rough idea into a block layout that already fits the system.</p>\\n\\n<div class=''rounded-[2rem] border border-slate-200/80 bg-slate-50/90 p-6 my-10 dark:border-white/10 dark:bg-slate-900/70''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Editorial workflow</p>\\n <div class=''grid gap-5 md:grid-cols-2 mt-5''>\\n <div class=''rounded-2xl border border-slate-200 bg-white p-5 dark:border-white/10 dark:bg-slate-950/50''>\\n <h3 class=''mt-0 text-xl text-slate-900 dark:text-white''>Faster first drafts</h3>\\n <p class=''text-sm text-slate-600 dark:text-slate-300''>Start with a structured prompt and get a section, article outline, or product narrative that already matches the site voice.</p>\\n </div>\\n <div class=''rounded-2xl border border-slate-200 bg-white p-5 dark:border-white/10 dark:bg-slate-950/50''>\\n <h3 class=''mt-0 text-xl text-slate-900 dark:text-white''>Cleaner revisions</h3>\\n <p class=''text-sm text-slate-600 dark:text-slate-300''>Ask for shorter, clearer, more technical, more polished, or locale-ready output without leaving the content screen.</p>\\n </div>\\n </div>\\n</div>\\n\\n<h2>Model Routing and Cost Control</h2>\\n<p>Cortex AI is designed around provider choice. Teams can route requests through the configured AI gateway, choose task-appropriate models, and keep bring-your-own-key setups separate from the editor experience. Editors get a simple control surface while developers keep the operational knobs.</p>\\n<ul>\\n <li>Use fast, inexpensive models for drafts, variations, and rewrites</li>\\n <li>Reserve stronger models for long-form strategy, technical copy, or difficult transformations</li>\\n <li>Keep provider keys and routing defaults in the server-side configuration layer</li>\\n <li>Make cost and quality decisions without changing the public rendering system</li>\\n</ul>\\n\\n<h2>Block-Aware Generation</h2>\\n<p>The important shift is that Cortex AI is not just writing text. It is meant to produce content that can map back to NextBlock surfaces: section copy, article bodies, product descriptions, headings, calls to action, and localized variants. That reduces the cleanup step that usually happens after copying AI text from a separate tool.</p>\\n<p>Because the output is shaped around existing block contracts, the generated content feels native in the editor and predictable on the frontend.</p>\\n\\n<h2>Safer Team Workflows</h2>\\n<p>AI works best when it is helpful without becoming invisible infrastructure. Cortex AI keeps humans in the loop: editors review the output, developers control the available providers, and the CMS keeps the generated content inside the same revision and publishing flow as everything else.</p>\\n\\n<h2>A Practical Launch Flow</h2>\\n<ol>\\n <li>Draft the article, landing section, or product story from a focused prompt.</li>\\n <li>Refine the result against the brand voice and target audience.</li>\\n <li>Generate a localized version or shorter excerpt for cards and metadata.</li>\\n <li>Review the content in the NextBlock editor, publish, and keep iterating through normal revisions.</li>\\n</ol>\\n\\n<p>Cortex AI turns the CMS into a more capable production surface: not a replacement for editorial judgment, but a faster way to shape good ideas into structured pages that are ready to ship.</p>\\n\"}', 0, '2026-07-03 17:52:15.643901+00', '2026-07-03 17:52:15.643901+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (34, 7, NULL, 1, 'text', '{\"html_content\": \"\\n<h1>Privacy Policy</h1>\\n<p><em>Last updated: June 4, 2026</em></p>\\n<p>NextBlock™ CMS (\\\"we\\\", \\\"us\\\", or \\\"our\\\") respects your privacy and is committed to protecting your personal information in accordance with Quebec''s <em>Act respecting the protection of personal information in the private sector</em> (Law 25), the federal <em>Personal Information Protection and Electronic Documents Act</em> (PIPEDA), and Canada''s Anti-Spam Legislation (CASL).</p>\\n\\n<h2>1. Person responsible for personal information</h2>\\n<p>Our Privacy Officer is responsible for our compliance with applicable privacy laws. You may reach them at <a href=\\\"mailto:{{privacy_email}}\\\">{{privacy_email}}</a>.</p>\\n\\n<h2>2. What we collect</h2>\\n<ul>\\n <li><strong>Account information</strong> &mdash; name, email address, and credentials when you register.</li>\\n <li><strong>Usage and device data</strong> &mdash; collected only with your consent through analytics technologies.</li>\\n <li><strong>Communications</strong> &mdash; messages you send us and your marketing preferences.</li>\\n</ul>\\n\\n<h2>3. Why we collect it and your consent</h2>\\n<p>We collect personal information for clearly identified purposes: to provide and secure our services, to communicate with you, and &mdash; only with your express, opt-in consent &mdash; for analytics and marketing. Consistent with Law 25, non-essential cookies and trackers remain disabled until you actively accept them, and you may withdraw your consent at any time.</p>\\n\\n<h2>4. Cookies and tracking technologies</h2>\\n<p>Strictly necessary cookies keep the site working and require no consent. Analytics and marketing technologies are loaded <strong>only after</strong> you opt in through our consent banner. Your choice is recorded so we can honour it and demonstrate accountability.</p>\\n\\n<h2>5. Disclosure and sharing</h2>\\n<p>We do not sell your personal information. We share it only with service providers who help us operate the platform under contractual confidentiality obligations, or where required by law.</p>\\n\\n<h2>6. Retention</h2>\\n<p>We keep personal information only for as long as necessary to fulfil the purposes described above or as required by law, after which it is securely destroyed or anonymized.</p>\\n\\n<h2>7. Your rights</h2>\\n<p>Subject to applicable law, you have the right to access, rectify, and delete your personal information, to withdraw consent, to data portability, and to be informed about automated processing. To exercise these rights, contact our Privacy Officer at <a href=\\\"mailto:{{privacy_email}}\\\">{{privacy_email}}</a>.</p>\\n\\n<h2>8. Commercial electronic messages (CASL)</h2>\\n<p>We send commercial electronic messages only with your consent. Every message identifies us and includes a working unsubscribe mechanism that we honour promptly.</p>\\n\\n<h2>9. Safeguards</h2>\\n<p>We use appropriate physical, organizational, and technological measures &mdash; including encryption in transit and access controls &mdash; to protect personal information against loss, theft, and unauthorized access.</p>\\n\\n<h2>10. Open-source software</h2>\\n<p>NextBlock™ CMS is free, open-source software distributed under the GNU Affero General Public License v3. When you self-host NextBlock, you are the operator responsible for the personal information processed by your own deployment, and this policy serves as a starting point you may adapt to your organization.</p>\\n\\n<h2>11. Changes to this policy</h2>\\n<p>We may update this policy from time to time. Material changes will be communicated through the site, and the \\\"last updated\\\" date will be revised.</p>\\n\\n<h2>12. Contact us</h2>\\n<p>Questions or complaints? Contact NextBlock™ CMS at <a href=\\\"mailto:{{privacy_email}}\\\">{{privacy_email}}</a>. You may also contact the Commission d''accès à l''information du Québec or the Office of the Privacy Commissioner of Canada.</p>\\n\"}', 0, '2026-07-03 17:52:15.699946+00', '2026-07-03 17:52:15.760236+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (35, 8, NULL, 2, 'text', '{\"html_content\": \"\\n<h1>Politique de confidentialité</h1>\\n<p><em>Dernière mise à jour : 4 juin 2026</em></p>\\n<p>NextBlock™ CMS (« nous ») respecte votre vie privée et s''engage à protéger vos renseignements personnels conformément à la <em>Loi sur la protection des renseignements personnels dans le secteur privé</em> du Québec (Loi 25), à la <em>Loi sur la protection des renseignements personnels et les documents électroniques</em> (LPRPDE) et à la Loi canadienne anti-pourriel (LCAP).</p>\\n\\n<h2>1. Responsable de la protection des renseignements personnels</h2>\\n<p>Notre responsable de la protection des renseignements personnels veille au respect des lois applicables. Vous pouvez le joindre à <a href=\\\"mailto:{{privacy_email}}\\\">{{privacy_email}}</a>.</p>\\n\\n<h2>2. Renseignements que nous recueillons</h2>\\n<ul>\\n <li><strong>Renseignements de compte</strong> &mdash; nom, adresse courriel et identifiants lors de l''inscription.</li>\\n <li><strong>Données d''utilisation et d''appareil</strong> &mdash; recueillies uniquement avec votre consentement au moyen de technologies d''analyse.</li>\\n <li><strong>Communications</strong> &mdash; les messages que vous nous envoyez et vos préférences marketing.</li>\\n</ul>\\n\\n<h2>3. Finalités et consentement</h2>\\n<p>Nous recueillons des renseignements personnels à des fins clairement déterminées : fournir et sécuriser nos services, communiquer avec vous et &mdash; uniquement avec votre consentement exprès &mdash; à des fins d''analyse et de marketing. Conformément à la Loi 25, les témoins et traceurs non essentiels demeurent désactivés tant que vous ne les avez pas acceptés, et vous pouvez retirer votre consentement en tout temps.</p>\\n\\n<h2>4. Témoins et technologies de suivi</h2>\\n<p>Les témoins strictement nécessaires assurent le fonctionnement du site et ne requièrent aucun consentement. Les technologies d''analyse et de marketing ne sont chargées qu''<strong>après</strong> votre consentement explicite. Votre choix est enregistré afin de le respecter.</p>\\n\\n<h2>5. Communication à des tiers</h2>\\n<p>Nous ne vendons pas vos renseignements personnels. Nous ne les communiquons qu''à des fournisseurs qui nous aident à exploiter la plateforme, sous obligation de confidentialité, ou lorsque la loi l''exige.</p>\\n\\n<h2>6. Conservation</h2>\\n<p>Nous ne conservons les renseignements personnels que le temps nécessaire aux fins décrites ou exigé par la loi, après quoi ils sont détruits ou anonymisés de façon sécuritaire.</p>\\n\\n<h2>7. Vos droits</h2>\\n<p>Sous réserve de la loi applicable, vous avez le droit d''accéder à vos renseignements, de les rectifier et de les supprimer, de retirer votre consentement, à la portabilité de vos données et d''être informé du traitement automatisé. Pour exercer ces droits, écrivez à <a href=\\\"mailto:{{privacy_email}}\\\">{{privacy_email}}</a>.</p>\\n\\n<h2>8. Messages électroniques commerciaux (LCAP)</h2>\\n<p>Nous n''envoyons des messages électroniques commerciaux qu''avec votre consentement. Chaque message nous identifie et comporte un mécanisme de désabonnement fonctionnel que nous respectons rapidement.</p>\\n\\n<h2>9. Mesures de sécurité</h2>\\n<p>Nous employons des mesures physiques, organisationnelles et technologiques appropriées &mdash; dont le chiffrement en transit et le contrôle des accès &mdash; pour protéger vos renseignements.</p>\\n\\n<h2>10. Logiciel libre</h2>\\n<p>NextBlock™ CMS est un logiciel libre et à code source ouvert distribué sous la licence publique générale GNU Affero v3. Lorsque vous hébergez NextBlock vous-même, vous êtes l''exploitant responsable des renseignements personnels traités par votre propre instance, et la présente politique vous sert de point de départ adaptable à votre organisation.</p>\\n\\n<h2>11. Modifications</h2>\\n<p>Nous pouvons mettre à jour cette politique. Les changements importants seront communiqués sur le site et la date de mise à jour sera révisée.</p>\\n\\n<h2>12. Nous joindre</h2>\\n<p>Des questions ou des plaintes ? Contactez NextBlock™ CMS à <a href=\\\"mailto:{{privacy_email}}\\\">{{privacy_email}}</a>. Vous pouvez aussi vous adresser à la Commission d''accès à l''information du Québec.</p>\\n\"}', 0, '2026-07-03 17:52:15.699946+00', '2026-07-03 17:52:15.760236+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (36, 9, NULL, 1, 'text', '{\"html_content\": \"\\n<h1>Terms of Service</h1>\\n<p><em>Last updated: June 4, 2026</em></p>\\n\\n<h2>1. Acceptance of terms</h2>\\n<p>By accessing or using NextBlock™ CMS and the services we provide (the \\\"Services\\\"), you agree to be bound by these Terms of Service. If you do not agree, do not use the Services.</p>\\n\\n<h2>2. Free and open-source software</h2>\\n<p>NextBlock™ CMS is free, open-source software licensed under the <strong>GNU Affero General Public License, version 3 (AGPL-3.0)</strong> or, at your option, any later version. You are free to run, study, share, and modify the software under the terms of that license. A copy of the license is distributed with the software and is also available at <a href=\\\"https://www.gnu.org/licenses/agpl-3.0.html\\\">gnu.org/licenses/agpl-3.0.html</a>.</p>\\n<p>Copyright © 2025 NextBlock™ CMS.</p>\\n\\n<h2>3. Source code availability</h2>\\n<p>In accordance with section 13 of the AGPL-3.0, if you run a modified version of NextBlock™ CMS and make it available to users over a network, you must prominently offer those users access to the Corresponding Source of your modified version, free of charge, through a standard or customary means of facilitating copying of software.</p>\\n\\n<h2>4. Trademarks</h2>\\n<p>The AGPL-3.0 grants broad rights to the software''s source code but does <strong>not</strong> grant any rights to our trade names, trademarks, or service marks. \\\"NextBlock™\\\", the NextBlock™ CMS name, and associated logos remain our property and may not be used in a way that suggests endorsement or affiliation without our prior written permission.</p>\\n\\n<h2>5. Accounts and acceptable use</h2>\\n<p>If you create an account, you are responsible for safeguarding your credentials and for all activity under your account, and you agree to notify us promptly of any unauthorized use. You agree not to misuse the Services, including by attempting to disrupt them, access them without authorization, or use them for unlawful purposes.</p>\\n\\n<h2>6. No warranty</h2>\\n<p>As stated in section 15 of the AGPL-3.0, the software is provided \\\"as is\\\", without warranty of any kind, either expressed or implied, including, without limitation, the implied warranties of merchantability and fitness for a particular purpose. The entire risk as to the quality and performance of the software is with you.</p>\\n\\n<h2>7. Limitation of liability</h2>\\n<p>As stated in section 16 of the AGPL-3.0, and to the fullest extent permitted by applicable law, in no event will any copyright holder, or any other party who modifies or conveys the software, be liable to you for damages, including any general, special, incidental, or consequential damages arising out of the use or inability to use the software.</p>\\n\\n<h2>8. Governing law</h2>\\n<p>These Terms are governed by the laws of the Province of Quebec and the federal laws of Canada applicable therein, without regard to conflict-of-law principles. Nothing in these Terms limits any mandatory consumer-protection rights you may have under those laws.</p>\\n\\n<h2>9. Changes</h2>\\n<p>We may revise these Terms from time to time. Material changes will be communicated through the Services, and continued use of the Services after changes take effect constitutes acceptance of the revised Terms.</p>\\n\\n<h2>10. Contact</h2>\\n<p>Questions about these Terms? Contact NextBlock™ CMS at <a href=\\\"mailto:{{privacy_email}}\\\">{{privacy_email}}</a>.</p>\\n\"}', 0, '2026-07-03 17:52:15.699946+00', '2026-07-03 17:52:15.760236+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (37, 10, NULL, 2, 'text', '{\"html_content\": \"\\n<h1>Conditions d''utilisation</h1>\\n<p><em>Dernière mise à jour : 4 juin 2026</em></p>\\n\\n<h2>1. Acceptation des conditions</h2>\\n<p>En accédant à NextBlock™ CMS et aux services que nous fournissons (les « Services ») ou en les utilisant, vous acceptez d''être lié par les présentes conditions d''utilisation. Si vous n''êtes pas d''accord, n''utilisez pas les Services.</p>\\n\\n<h2>2. Logiciel libre et à code source ouvert</h2>\\n<p>NextBlock™ CMS est un logiciel libre et à code source ouvert distribué sous la <strong>licence publique générale GNU Affero, version 3 (AGPL-3.0)</strong> ou, à votre choix, toute version ultérieure. Vous êtes libre d''exécuter, d''étudier, de partager et de modifier le logiciel selon les termes de cette licence. Une copie de la licence est fournie avec le logiciel et est aussi disponible à <a href=\\\"https://www.gnu.org/licenses/agpl-3.0.html\\\">gnu.org/licenses/agpl-3.0.html</a>.</p>\\n<p>Droit d''auteur © 2025 NextBlock™ CMS.</p>\\n\\n<h2>3. Disponibilité du code source</h2>\\n<p>Conformément à l''article 13 de l''AGPL-3.0, si vous exploitez une version modifiée de NextBlock™ CMS et la rendez accessible à des utilisateurs sur un réseau, vous devez offrir clairement à ces utilisateurs l''accès au code source correspondant de votre version modifiée, gratuitement, par un moyen usuel de copie de logiciels.</p>\\n\\n<h2>4. Marques de commerce</h2>\\n<p>L''AGPL-3.0 accorde de larges droits sur le code source du logiciel, mais <strong>n''accorde aucun droit</strong> sur nos noms commerciaux, marques de commerce ou marques de service. « NextBlock™ », le nom NextBlock™ CMS et les logos associés demeurent notre propriété et ne peuvent être utilisés d''une manière laissant entendre une approbation ou une affiliation sans notre autorisation écrite préalable.</p>\\n\\n<h2>5. Comptes et utilisation acceptable</h2>\\n<p>Si vous créez un compte, vous êtes responsable de la protection de vos identifiants et de toute activité effectuée à partir de votre compte, et vous vous engagez à nous aviser rapidement de toute utilisation non autorisée. Vous vous engagez à ne pas détourner les Services, notamment en tentant de les perturber, d''y accéder sans autorisation ou de les utiliser à des fins illégales.</p>\\n\\n<h2>6. Absence de garantie</h2>\\n<p>Comme l''énonce l''article 15 de l''AGPL-3.0, le logiciel est fourni « tel quel », sans garantie d''aucune sorte, expresse ou implicite, y compris, sans s''y limiter, les garanties implicites de qualité marchande et d''adéquation à un usage particulier. Vous assumez l''entièreté du risque quant à la qualité et au rendement du logiciel.</p>\\n\\n<h2>7. Limitation de responsabilité</h2>\\n<p>Comme l''énonce l''article 16 de l''AGPL-3.0, et dans toute la mesure permise par la loi applicable, en aucun cas un titulaire de droits d''auteur ou toute autre partie qui modifie ou transmet le logiciel ne saurait être tenu responsable envers vous de dommages, y compris tout dommage général, spécial, accessoire ou consécutif découlant de l''utilisation ou de l''impossibilité d''utiliser le logiciel.</p>\\n\\n<h2>8. Droit applicable</h2>\\n<p>Les présentes conditions sont régies par les lois de la province de Québec et les lois fédérales du Canada qui y sont applicables, sans égard aux règles de conflit de lois. Rien dans les présentes conditions ne limite les droits impératifs de protection du consommateur dont vous pourriez bénéficier en vertu de ces lois.</p>\\n\\n<h2>9. Modifications</h2>\\n<p>Nous pouvons réviser ces conditions de temps à autre. Les changements importants seront communiqués au moyen des Services, et l''utilisation continue des Services après leur entrée en vigueur vaut acceptation.</p>\\n\\n<h2>10. Nous joindre</h2>\\n<p>Des questions sur ces conditions ? Contactez NextBlock™ CMS à <a href=\\\"mailto:{{privacy_email}}\\\">{{privacy_email}}</a>.</p>\\n\"}', 0, '2026-07-03 17:52:15.699946+00', '2026-07-03 17:52:15.760236+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (40, NULL, 3, 1, 'text', '{\"html_content\": \"<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock is an open-source, AI-native Next.js CMS built on Supabase — and installing it no longer involves config files, terminal wizards, or manual SQL. There are four ways to get running, and they all end in the same place: a browser <strong>setup wizard</strong> that connects your database, configures media storage, and creates your admin account for you. Pick the path that fits, follow the steps, and you will be publishing in minutes.</p>\\n\\n<div class=''grid gap-5 md:grid-cols-2 my-10''>\\n <a href=''#one-click-vercel'' class=''block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10''>\\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200''>Fastest &middot; about 3 minutes</p>\\n <h3 class=''mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>One-click deploy on Vercel</h3>\\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>A live production site with a managed database. No terminal, no environment variables, nothing to copy.</p>\\n </a>\\n <a href=''#npm-create'' class=''block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10''>\\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Recommended for new projects</p>\\n <h3 class=''mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock</h3>\\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Scaffold a standalone Next.js app with NextBlock built in, then finish setup in your browser.</p>\\n </a>\\n <a href=''#git-clone'' class=''block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10''>\\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Full source code</p>\\n <h3 class=''mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Clone the repository</h3>\\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Run the complete monorepo — the CMS, every package, and the docs. Made for contributors and platform teams.</p>\\n </a>\\n <a href=''#docker'' class=''block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10''>\\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200''>100% local &middot; no accounts</p>\\n <h3 class=''mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Self-hosted with Docker</h3>\\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>One command boots the entire stack on your machine — database, auth, storage, and the CMS. No cloud services at all.</p>\\n </a>\\n</div>\\n\\n<p class=''text-sm text-slate-500 dark:text-slate-400''>Not sure which to pick? If you want a live website with the least effort, choose <a href=''#one-click-vercel''>Vercel</a>. If you want a local project to build on, choose <a href=''#npm-create''>npm create nextblock</a>.</p>\\n\\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\\n <img src=''/images/included.webp'' alt=''NextBlock CMS platform overview showing the block editor, CMS dashboard, and integrations included with every installation'' class=''w-full h-auto object-cover'' />\\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>Whichever path you choose, you get the same block editor, the same CMS, and the same database schema.</figcaption>\\n</figure>\\n\\n<h2 id=''one-click-vercel''>Option 1: One-Click Deploy on Vercel</h2>\\n<p>The fastest way to get a production NextBlock site. One button creates your own copy of NextBlock on GitHub, provisions a managed Supabase database, and deploys the site — you never open a terminal or copy a single key.</p>\\n<ol class=''space-y-2''>\\n <li><strong>Click Deploy to Vercel</strong> and sign in — Vercel clones NextBlock into a new repository you own.</li>\\n <li><strong>Create the Supabase database</strong> when prompted: pick a name and a region. Vercel connects it to the project and injects the keys before the first build.</li>\\n <li><strong>Open your new site</strong> once the build finishes. Every fresh instance takes you straight to the setup wizard.</li>\\n <li><strong>Create your administrator account.</strong> It is confirmed instantly — no verification email — and you land in the CMS dashboard.</li>\\n</ol>\\n<div class=''my-8''>\\n <a href=''https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200''>Deploy to Vercel &rarr;</a>\\n</div>\\n<div class=''rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200''>Zero configuration</p>\\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>There are no environment variables to fill in. Media storage automatically uses your connected Supabase project, security secrets are derived for you, and database migrations run automatically on every production build. Adding a custom domain later? Set <code>NEXT_PUBLIC_URL</code> in your Vercel project and redeploy.</p>\\n</div>\\n<p>Your site also keeps itself current: the dashboard checklist includes a one-click <strong>Connect GitHub</strong> step that installs a daily workflow syncing your copy with the latest NextBlock release.</p>\\n\\n<h2 id=''npm-create''>Option 2: Scaffold a Project with npm create nextblock</h2>\\n<p>The best starting point for building your own site. The CLI scaffolds a standalone Next.js application with NextBlock already wired in — no monorepo, no workspace tooling — and hands everything else to the browser wizard.</p>\\n<p>Before you start, install <a href=''https://nodejs.org'' target=''_blank'' rel=''noopener''>Node.js 20 or newer</a> (it includes npm), then create a free project at <a href=''https://supabase.com'' target=''_blank'' rel=''noopener''>supabase.com</a> (or pick the CLI''s Docker mode during creation and skip cloud accounts entirely).</p>\\n<pre><code>npm create nextblock@latest my-site\\ncd my-site\\nnpm run dev</code></pre>\\n<p>Open <code>http://localhost:3000/setup</code> and let the wizard take over:</p>\\n<ol class=''space-y-2''>\\n <li><strong>Connect Supabase</strong> — paste your project URL, publishable (anon) key, secret (service role) key, and a personal access token so the wizard can apply the database schema for you.</li>\\n <li><strong>Choose media storage</strong> — plug in a Cloudflare R2 bucket for images and files, or leave it blank to use your connected Supabase project''s storage.</li>\\n <li><strong>Create your administrator</strong> — the wizard applies every migration, generates the app secrets, writes <code>.env.local</code>, creates your confirmed admin account, and signs you in. Restart <code>npm run dev</code> once afterwards so the fresh environment is baked into the app.</li>\\n</ol>\\n<div class=''rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Premium modules</p>\\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Need a store? One command adds products, checkout, orders, and coupons — license-gated and ready when you are: <code>npx create-nextblock activate ecommerce</code></p>\\n</div>\\n\\n<h2 id=''git-clone''>Option 3: Clone the Repository</h2>\\n<p>Run the full Nx monorepo: the CMS application, every shared package, the CLI source, and the documentation. This is the path for contributors, plugin authors, and teams that customize the platform itself.</p>\\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\\ncd nextblock\\nnpm install\\nnpx nx serve nextblock</code></pre>\\n<p>Open <code>http://localhost:4200</code> — a fresh install redirects every page to <code>/setup</code>, where the same three-step wizard connects Supabase, configures storage, and creates your admin. It validates your keys, writes <code>.env.local</code> with generated secrets, and applies all migrations over the Supabase Management API — no Supabase CLI required.</p>\\n<div class=''rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>No terminal setup</p>\\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Configuration moved entirely to the browser — there is no interactive terminal step anymore. When the wizard finishes you are signed in as the administrator; restart the dev server once afterwards so the fresh environment is baked into the app bundle.</p>\\n</div>\\n\\n<h2 id=''docker''>Option 4: Self-Hosted with Docker</h2>\\n<p>Everything runs on your machine: Supabase''s Postgres and auth engines, a PostgREST API behind a Kong gateway, S3-compatible MinIO storage, and the CMS itself. No Supabase account, no Vercel, no email service — ideal for evaluations, air-gapped environments, and anyone who wants a fully self-hosted CMS with complete data ownership.</p>\\n<p>With <a href=''https://www.docker.com/products/docker-desktop/'' target=''_blank'' rel=''noopener''>Docker Desktop</a> installed and running:</p>\\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\\ncd nextblock\\nnpm install\\nnpm run docker:setup</code></pre>\\n<p>The command asks nothing: it generates secure keys, builds the stack, applies every migration, and starts the services. When it finishes, open <code>http://localhost:3000</code> — the setup wizard already has the database and MinIO storage wired up, so the only step left is creating your administrator (confirmed instantly, no email required).</p>\\n<div class=''rounded-3xl border border-amber-200 bg-amber-50/80 p-6 my-8 dark:border-amber-500/20 dark:bg-amber-500/10''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200''>Day-2 commands</p>\\n <pre class=''mt-4 mb-0''><code># rebuild and restart the stack\\nnpm run docker:up\\n\\n# stop the stack (your data persists in Docker volumes)\\nnpm run docker:down\\n\\n# follow the application logs\\nnpm run docker:logs</code></pre>\\n</div>\\n\\n<h2 id=''after-install''>After You Install: Your First 10 Minutes</h2>\\n<p>Every path drops you at <code>/cms/dashboard</code>, signed in as the first administrator. A built-in onboarding checklist walks you through the rest:</p>\\n<ul class=''space-y-2''>\\n <li><strong>Add your branding</strong> — upload your logo and set the site title.</li>\\n <li><strong>Set your footer</strong> — copyright line and footer navigation.</li>\\n <li><strong>Configure email (SMTP)</strong> — under Settings, so password resets and invitations can send.</li>\\n <li><strong>Optional extras</strong> — connect analytics, enable bot protection, and (on Vercel) turn on automatic updates.</li>\\n</ul>\\n<p>From there, see how the platform fits together in <a href=''/article/how-nextblock-works''>How NextBlock Works</a>, add a storefront with the <a href=''/article/nextblock-commerce-guide''>Commerce guide</a>, or meet your AI copilot in the <a href=''/article/nextblock-cortex-ai-guide''>Cortex AI guide</a>.</p>\\n\\n<h2 id=''faq''>Installation FAQ</h2>\\n<h3>What do I need installed?</h3>\\n<p>Nothing for the Vercel path — it runs entirely in the browser. For <code>npm create nextblock</code>: <a href=''https://nodejs.org'' target=''_blank'' rel=''noopener''>Node.js 20+</a> (which includes npm). For the cloned repository: Node.js 20+ and git. For Docker: those plus Docker Desktop.</p>\\n<h3>Is NextBlock free?</h3>\\n<p>Yes — the core of NextBlock is a 100% free, open-source CMS (AGPL). Premium packages such as e-commerce and Cortex AI are optional and activate with a license key. Both Vercel and Supabase offer free tiers, so a starter site can run at no cost.</p>\\n<h3>Do I need a Supabase account?</h3>\\n<p>On Vercel, the database is created for you during the deploy. For <code>npm create nextblock</code> and the cloned repository you bring a free Supabase project. With Docker you need no cloud accounts at all.</p>\\n<h3>Do I have to run migrations or SQL by hand?</h3>\\n<p>No. The setup wizard, the Vercel build, and the Docker stack all apply the database schema automatically — and re-running is always safe.</p>\\n<h3>Can I switch paths later?</h3>\\n<p>Yes. Every path runs the same application and the same database schema, so you can prototype locally with Docker today and deploy to Vercel tomorrow. NextBlock deploys like any standard Next.js app.</p>\\n<h3>How do I update NextBlock?</h3>\\n<p>On Vercel, the Connect GitHub onboarding step enables a daily automatic sync with upstream. On a cloned repository, <code>git pull</code>, run <code>npm run db:migrate</code>, then restart (on production builds, pending migrations apply automatically). With Docker, pull the latest code and run <code>npm run docker:up</code>.</p>\\n\\n<div class=''rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5''>\\n <p class=''mt-0 text-2xl font-semibold text-slate-900 dark:text-white''>Ready to launch?</p>\\n <p class=''text-sm text-slate-600 dark:text-slate-300''>Pick your path above, or jump straight to the fastest one.</p>\\n <div class=''mt-5 flex flex-wrap justify-center gap-3''>\\n <a href=''https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200''>Deploy on Vercel</a>\\n <a href=''https://github.com/nextblock-cms/nextblock'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50''>View on GitHub</a>\\n </div>\\n</div>\"}', 0, '2026-07-03 17:52:15.799297+00', '2026-07-03 17:52:15.799297+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (41, NULL, 4, 2, 'text', '{\"html_content\": \"<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock est un CMS open source et natif IA, construit sur Next.js et Supabase — et son installation ne passe plus par des fichiers de configuration, des assistants en ligne de commande ou du SQL manuel. Il existe quatre façons de démarrer, et elles aboutissent toutes au même endroit : un <strong>assistant de configuration</strong> dans le navigateur qui connecte votre base de données, configure le stockage des médias et crée votre compte administrateur. Choisissez le chemin qui vous convient, suivez les étapes, et vous publierez en quelques minutes.</p>\\n\\n<div class=''grid gap-5 md:grid-cols-2 my-10''>\\n <a href=''#one-click-vercel'' class=''block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10''>\\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200''>Le plus rapide &middot; environ 3 minutes</p>\\n <h3 class=''mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Déploiement Vercel en un clic</h3>\\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Un site de production en ligne avec une base de données gérée. Pas de terminal, pas de variables d''environnement, rien à copier.</p>\\n </a>\\n <a href=''#npm-create'' class=''block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10''>\\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Recommandé pour les nouveaux projets</p>\\n <h3 class=''mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock</h3>\\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Générez une app Next.js autonome avec NextBlock intégré, puis terminez la configuration dans votre navigateur.</p>\\n </a>\\n <a href=''#git-clone'' class=''block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10''>\\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Code source complet</p>\\n <h3 class=''mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Cloner le dépôt</h3>\\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Faites tourner le monorepo complet — le CMS, tous les packages et la documentation. Conçu pour les contributeurs et les équipes plateforme.</p>\\n </a>\\n <a href=''#docker'' class=''block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10''>\\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200''>100 % local &middot; aucun compte</p>\\n <h3 class=''mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Auto-hébergé avec Docker</h3>\\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Une seule commande démarre toute la pile sur votre machine — base de données, auth, stockage et le CMS. Aucun service cloud.</p>\\n </a>\\n</div>\\n\\n<p class=''text-sm text-slate-500 dark:text-slate-400''>Vous hésitez ? Pour un site en ligne avec le minimum d''effort, choisissez <a href=''#one-click-vercel''>Vercel</a>. Pour un projet local à personnaliser, choisissez <a href=''#npm-create''>npm create nextblock</a>.</p>\\n\\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\\n <img src=''/images/included.webp'' alt=''Aperçu de la plateforme NextBlock : éditeur de blocs, tableau de bord CMS et intégrations incluses dans chaque installation'' class=''w-full h-auto object-cover'' />\\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>Quel que soit le chemin choisi, vous obtenez le même éditeur de blocs, le même CMS et le même schéma de base de données.</figcaption>\\n</figure>\\n\\n<h2 id=''one-click-vercel''>Option 1 : Déploiement Vercel en un clic</h2>\\n<p>Le moyen le plus rapide d''obtenir un site NextBlock en production. Un seul bouton crée votre propre copie de NextBlock sur GitHub, provisionne une base de données Supabase gérée et déploie le site — sans jamais ouvrir un terminal ni copier la moindre clé.</p>\\n<ol class=''space-y-2''>\\n <li><strong>Cliquez sur Deploy to Vercel</strong> et connectez-vous — Vercel clone NextBlock dans un nouveau dépôt qui vous appartient.</li>\\n <li><strong>Créez la base de données Supabase</strong> quand on vous le demande : choisissez un nom et une région. Vercel la connecte au projet et injecte les clés avant le premier build.</li>\\n <li><strong>Ouvrez votre nouveau site</strong> une fois le build terminé. Toute nouvelle instance vous amène directement à l''assistant de configuration.</li>\\n <li><strong>Créez votre compte administrateur.</strong> Il est confirmé instantanément — aucun email de vérification — et vous arrivez dans le tableau de bord du CMS.</li>\\n</ol>\\n<div class=''my-8''>\\n <a href=''https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200''>Déployer sur Vercel &rarr;</a>\\n</div>\\n<div class=''rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200''>Zéro configuration</p>\\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Aucune variable d''environnement à remplir. Le stockage des médias utilise automatiquement votre projet Supabase connecté, les secrets de sécurité sont dérivés pour vous, et les migrations de base de données s''exécutent automatiquement à chaque build de production. Un domaine personnalisé plus tard ? Définissez <code>NEXT_PUBLIC_URL</code> dans votre projet Vercel et redéployez.</p>\\n</div>\\n<p>Par ailleurs, votre site reste à jour tout seul : la checklist du tableau de bord inclut une étape <strong>Connect GitHub</strong> en un clic qui installe un workflow quotidien synchronisant votre copie avec la dernière version de NextBlock.</p>\\n\\n<h2 id=''npm-create''>Option 2 : Créer un projet avec npm create nextblock</h2>\\n<p>Le meilleur point de départ pour construire votre propre site. Le CLI génère une application Next.js autonome avec NextBlock déjà intégré — sans monorepo ni outillage de workspace — et confie tout le reste à l''assistant dans le navigateur.</p>\\n<p>Avant de commencer, installez <a href=''https://nodejs.org'' target=''_blank'' rel=''noopener''>Node.js 20 ou plus récent</a> (npm inclus), puis créez un projet gratuit sur <a href=''https://supabase.com'' target=''_blank'' rel=''noopener''>supabase.com</a> (ou choisissez le mode Docker du CLI pendant la création et passez-vous entièrement de comptes cloud).</p>\\n<pre><code>npm create nextblock@latest mon-site\\ncd mon-site\\nnpm run dev</code></pre>\\n<p>Ouvrez <code>http://localhost:3000/setup</code> et laissez l''assistant faire le travail :</p>\\n<ol class=''space-y-2''>\\n <li><strong>Connectez Supabase</strong> — collez l''URL du projet, la clé publiable (anon), la clé secrète (service role) et un jeton d''accès personnel pour que l''assistant applique le schéma de base de données à votre place.</li>\\n <li><strong>Choisissez le stockage des médias</strong> — branchez un bucket Cloudflare R2 pour les images et les fichiers, ou laissez les champs vides pour utiliser le stockage de votre projet Supabase.</li>\\n <li><strong>Créez votre administrateur</strong> — l''assistant applique toutes les migrations, génère les secrets de l''application, écrit <code>.env.local</code>, crée votre compte admin confirmé et vous connecte. Redémarrez ensuite <code>npm run dev</code> une fois pour que le nouvel environnement soit intégré à l''application.</li>\\n</ol>\\n<div class=''rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Modules premium</p>\\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Besoin d''une boutique ? Une seule commande ajoute produits, paiement, commandes et coupons — activés par clé de licence, prêts quand vous l''êtes : <code>npx create-nextblock activate ecommerce</code></p>\\n</div>\\n\\n<h2 id=''git-clone''>Option 3 : Cloner le dépôt</h2>\\n<p>Faites tourner le monorepo Nx complet : l''application CMS, tous les packages partagés, le code du CLI et la documentation. C''est le chemin des contributeurs, des auteurs de plugins et des équipes qui personnalisent la plateforme elle-même.</p>\\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\\ncd nextblock\\nnpm install\\nnpx nx serve nextblock</code></pre>\\n<p>Ouvrez <code>http://localhost:4200</code> — une nouvelle installation redirige chaque page vers <code>/setup</code>, où le même assistant en trois étapes connecte Supabase, configure le stockage et crée votre admin. Il valide vos clés, écrit <code>.env.local</code> avec des secrets générés, et applique toutes les migrations via l''API de management Supabase — sans CLI Supabase.</p>\\n<div class=''rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Zéro configuration dans le terminal</p>\\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>La configuration se fait entièrement dans le navigateur — il n''y a plus d''étape interactive en ligne de commande. Quand l''assistant termine, vous êtes connecté en administrateur ; redémarrez ensuite le serveur de développement une fois pour que le nouvel environnement soit intégré au bundle de l''application.</p>\\n</div>\\n\\n<h2 id=''docker''>Option 4 : Auto-hébergé avec Docker</h2>\\n<p>Tout tourne sur votre machine : les moteurs Postgres et auth de Supabase, une API PostgREST derrière une passerelle Kong, un stockage MinIO compatible S3 et le CMS lui-même. Pas de compte Supabase, pas de Vercel, pas de service d''email — idéal pour les évaluations, les environnements isolés et la pleine propriété de vos données.</p>\\n<p>Avec <a href=''https://www.docker.com/products/docker-desktop/'' target=''_blank'' rel=''noopener''>Docker Desktop</a> installé et démarré :</p>\\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\\ncd nextblock\\nnpm install\\nnpm run docker:setup</code></pre>\\n<p>La commande ne pose aucune question : elle génère des clés sécurisées, construit la pile, applique toutes les migrations et démarre les services. Quand elle se termine, ouvrez <code>http://localhost:3000</code> — la base de données et le stockage MinIO sont déjà connectés dans l''assistant de configuration, il ne reste qu''à créer votre administrateur (confirmé instantanément, sans email).</p>\\n<div class=''rounded-3xl border border-amber-200 bg-amber-50/80 p-6 my-8 dark:border-amber-500/20 dark:bg-amber-500/10''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200''>Commandes du quotidien</p>\\n <pre class=''mt-4 mb-0''><code># reconstruire et redémarrer la pile\\nnpm run docker:up\\n\\n# arrêter la pile (vos données persistent dans les volumes Docker)\\nnpm run docker:down\\n\\n# suivre les logs de l''application\\nnpm run docker:logs</code></pre>\\n</div>\\n\\n<h2 id=''after-install''>Après l''installation : vos 10 premières minutes</h2>\\n<p>Chaque chemin vous dépose sur <code>/cms/dashboard</code>, connecté en tant que premier administrateur. Une checklist de démarrage intégrée vous guide pour la suite :</p>\\n<ul class=''space-y-2''>\\n <li><strong>Ajoutez votre identité visuelle</strong> — téléversez votre logo et définissez le titre du site.</li>\\n <li><strong>Réglez votre pied de page</strong> — mention de copyright et navigation du pied de page.</li>\\n <li><strong>Configurez l''email (SMTP)</strong> — dans les réglages, pour que les réinitialisations de mot de passe et les invitations partent bien.</li>\\n <li><strong>Extras optionnels</strong> — connectez vos outils d''analytics, activez la protection anti-bots et (sur Vercel) les mises à jour automatiques.</li>\\n</ul>\\n<p>Ensuite, découvrez comment la plateforme s''articule dans <a href=''/article/comment-nextblock-fonctionne''>Comment NextBlock fonctionne</a>, ou ajoutez une boutique avec le <a href=''/article/guide-commerce-nextblock''>guide Commerce</a>.</p>\\n\\n<h2 id=''faq''>FAQ d''installation</h2>\\n<h3>Que dois-je installer ?</h3>\\n<p>Rien pour le chemin Vercel — tout se passe dans le navigateur. Pour <code>npm create nextblock</code> : <a href=''https://nodejs.org'' target=''_blank'' rel=''noopener''>Node.js 20+</a> (npm inclus). Pour le dépôt cloné : Node.js 20+ et git. Pour Docker : ajoutez Docker Desktop.</p>\\n<h3>NextBlock est-il gratuit ?</h3>\\n<p>Oui — le cœur du CMS est 100 % gratuit et open source (AGPL). Les packages premium comme l''e-commerce et Cortex AI sont optionnels et s''activent avec une clé de licence. Vercel et Supabase proposent chacun une offre gratuite : un site de départ peut donc tourner sans frais.</p>\\n<h3>Ai-je besoin d''un compte Supabase ?</h3>\\n<p>Sur Vercel, la base de données est créée pour vous pendant le déploiement. Pour <code>npm create nextblock</code> et le dépôt cloné, il vous faut un projet Supabase gratuit. Avec Docker, aucun compte cloud n''est nécessaire.</p>\\n<h3>Dois-je exécuter des migrations ou du SQL à la main ?</h3>\\n<p>Non. L''assistant de configuration, le build Vercel et la pile Docker appliquent tous le schéma de base de données automatiquement — et relancer l''opération est toujours sans risque.</p>\\n<h3>Puis-je changer de chemin plus tard ?</h3>\\n<p>Oui. Chaque chemin exécute la même application et le même schéma de base de données : vous pouvez prototyper en local avec Docker aujourd''hui et déployer sur Vercel demain. NextBlock se déploie comme n''importe quelle app Next.js.</p>\\n<h3>Comment mettre à jour NextBlock ?</h3>\\n<p>Sur Vercel, l''étape Connect GitHub de la checklist active une synchronisation quotidienne automatique. Sur un dépôt cloné, <code>git pull</code>, lancez <code>npm run db:migrate</code>, puis redémarrez (lors des builds de production, les migrations en attente s''appliquent automatiquement). Avec Docker, récupérez le dernier code et lancez <code>npm run docker:up</code>.</p>\\n\\n<div class=''rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5''>\\n <p class=''mt-0 text-2xl font-semibold text-slate-900 dark:text-white''>Prêt à vous lancer ?</p>\\n <p class=''text-sm text-slate-600 dark:text-slate-300''>Choisissez votre chemin ci-dessus, ou passez directement au plus rapide.</p>\\n <div class=''mt-5 flex flex-wrap justify-center gap-3''>\\n <a href=''https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200''>Déployer sur Vercel</a>\\n <a href=''https://github.com/nextblock-cms/nextblock'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50''>Voir sur GitHub</a>\\n </div>\\n</div>\"}', 0, '2026-07-03 17:52:15.799297+00', '2026-07-03 17:52:15.799297+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (8, 1, NULL, 1, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#020817\", \"position\": 0}, {\"color\": \"#0f172a\", \"position\": 100}], \"direction\": \"180deg\"}}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<h2 class=''text-3xl md:text-4xl font-bold text-center text-white mb-4''>Have Questions?</h2>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<p class=''text-center text-lg text-slate-300 mx-auto mb-8''>NextBlock™ partners with early adopters to co-build features, sponsor modules, and shape the product direction.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"url\": \"/contact\", \"size\": \"lg\", \"text\": \"Get in Touch\", \"variant\": \"default\", \"position\": \"center\"}, \"block_type\": \"button\"}, {\"content\": {\"url\": \"https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&project-name=nextblock&repository-name=nextblock&stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D\", \"size\": \"lg\", \"text\": \"Deploy on Vercel\", \"variant\": \"outline\", \"position\": \"center\"}, \"block_type\": \"button\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 6, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.840619+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (27, NULL, 1, 1, 'text', '{\"html_content\": \"<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock™ is designed so the hosted CMS, the open-source starter, and the developer tooling all feel like the same product. The shared Nx workspace, typed block contracts, and reusable editor package keep product polish and developer velocity moving together.</p>\\n\\n<div class=''grid gap-4 md:grid-cols-3 my-10''>\\n <div class=''rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200''>One codebase</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Shared foundation</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Marketing pages, CMS screens, and the starter template evolve together instead of drifting apart.</p>\\n </div>\\n <div class=''rounded-3xl border border-indigo-200/70 bg-indigo-50/70 p-6 dark:border-indigo-500/20 dark:bg-indigo-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:text-indigo-200''>Typed content</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Blocks with guardrails</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Zod schemas, defaults, and renderer contracts make every custom block safer to ship.</p>\\n </div>\\n <div class=''rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Editorial UX</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Product-grade editing</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>The Tiptap layer gives editors a richer surface without hiding the underlying HTML power.</p>\\n </div>\\n</div>\\n\\n<div class=''flex flex-col md:flex-row gap-8 items-center my-12''>\\n <div class=''w-full md:w-1/2 space-y-4''>\\n <h2>Monorepo Layout and Dependency Flow</h2>\\n <p>The <code>apps/nextblock</code> directory contains the production Next.js experience, including the public site and authenticated CMS shell. The <code>apps/create-nextblock</code> CLI mirrors that foundation so teams can start from the same product decisions instead of rebuilding them from scratch.</p>\\n <ul class=''list-disc pl-6 space-y-2 text-sm''>\\n <li><strong>@nextblock-cms/ui</strong> - UI components, tokens, and shared design primitives</li>\\n <li><strong>@nextblock-cms/utils</strong> - translations, environment guards, and storage helpers</li>\\n <li><strong>@nextblock-cms/db</strong> - migrations, typed database access, and generated types</li>\\n <li><strong>@nextblock-cms/editor</strong> - the reusable Tiptap v3 editing surface</li>\\n <li><strong>@nextblock-cms/sdk</strong> - typed contracts for block authorship and validation</li>\\n <li><strong>@nextblock-cms/ecommerce</strong> - the premium commerce module when activated</li>\\n </ul>\\n <p>Run <code>nx graph</code> and you can see exactly how changes ripple through the workspace. Path aliases from <code>tsconfig.base.json</code> and the shared Tailwind setup help keep design parity between marketing pages, admin screens, and generated projects.</p>\\n </div>\\n <aside class=''w-full md:w-1/2 rounded-[2rem] border border-slate-200/80 bg-white p-4 shadow-xl dark:border-white/10 dark:bg-white/5''>\\n <div class=''relative aspect-video overflow-hidden rounded-2xl''><iframe class=''absolute inset-0 h-full w-full border-0'' src=''https://www.youtube.com/embed/DNqU8ez9qjs?si=p2oIy0f-n7wiaBmO'' title=''How NextBlock™ Works'' allow=''accelerated-motion; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'' referrerpolicy=''strict-origin-when-cross-origin'' loading=''lazy'' allowfullscreen></iframe></div>\\n <p class=''mt-3 text-sm text-slate-500 dark:text-slate-400''>Nx makes every workspace relationship visible, which is exactly why the starter, CMS, and packages stay aligned.</p>\\n </aside>\\n</div>\\n\\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\\n <img src=''/images/extensibility.webp'' alt=''NextBlock™ extensibility artwork showing the CMS connected to reusable modules and integrations'' class=''w-full h-auto object-cover'' />\\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>A single visual system spans content modeling, editing, and future premium modules like commerce.</figcaption>\\n</figure>\\n\\n<h2>Block Registry as Product Surface</h2>\\n<p>The block registry in <code>apps/nextblock/lib/blocks/blockRegistry.ts</code> is the source of truth for available block types, Zod schemas, starter content, and editor or renderer components. Today that includes everything from <code>text</code> and <code>heading</code> to <code>section</code>, <code>posts_grid</code>, <code>checkout</code>, and <code>product_details</code>.</p>\\n<p>Sections support nested column arrays, so layouts can be composed like real pages instead of flat content lists. Helpers such as <code>getBlockDefinition()</code>, <code>getInitialContent()</code>, and <code>validateBlockContent()</code> keep that flexibility strongly typed.</p>\\n\\n<h2>The Editing Layer</h2>\\n<p>The <code>@nextblock-cms/editor</code> package wraps Tiptap v3 into a reusable editorial surface with slash commands, floating and bubble menus, drag handles, tables, task lists, character counts, and syntax-highlighted code blocks. It deliberately preserves richer HTML so advanced teams are not boxed into a simplified subset.</p>\\n\\n<h2>Inside the CMS Shell</h2>\\n<p>Within <code>apps/nextblock/app/cms</code>, each feature area follows a repeatable pattern: list pages, create and edit routes, scoped client components, and server actions that wrap Supabase mutations. The result feels consistent for editors while keeping credentials and permissions on the server side.</p>\\n\\n<h2>Open Core Without Product Drift</h2>\\n<p>The core CMS is open source under AGPL. Premium modules like <code>@nextblock-cms/ecommerce</code> remain source-available but are activated through <code>package_activations</code> and <code>verifyPackageOnline()</code>. That means the same shell can stay clean for open-source users while revealing commerce surfaces only when the license is active.</p>\\n\\n<h2>Why It Holds Together</h2>\\n<p>The Nx workspace keeps libraries honest, the Next.js app enforces UI consistency, Supabase migrations codify access rules, and the Tiptap editor gives collaborators the same authoring experience regardless of deployment. When a team runs <code>npm create nextblock</code>, they inherit the full operating model, not just a pile of files.</p>\"}', 0, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.859357+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (28, NULL, 2, 2, 'text', '{\"html_content\": \"<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock™ relie le CMS h&eacute;berg&eacute;, le starter open source et les outils dev dans un m&ecirc;me socle produit. Le workspace Nx, les contrats de blocs typ&eacute;s et l''&eacute;diteur partag&eacute; permettent d''avancer vite sans sacrifier la coh&eacute;rence.</p>\\n\\n<div class=''grid gap-4 md:grid-cols-3 my-10''>\\n <div class=''rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200''>Socle unique</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Une m&ecirc;me base</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Le site public, le shell CMS et le starter gardent les m&ecirc;mes choix produit et la m&ecirc;me direction visuelle.</p>\\n </div>\\n <div class=''rounded-3xl border border-indigo-200/70 bg-indigo-50/70 p-6 dark:border-indigo-500/20 dark:bg-indigo-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:text-indigo-200''>Contenu typ&eacute;</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Blocs avec garde-fous</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Sch&eacute;mas Zod, contenus par d&eacute;faut et contrats de rendu rendent les extensions plus s&ucirc;res &agrave; maintenir.</p>\\n </div>\\n <div class=''rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Exp&eacute;rience &eacute;ditoriale</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Edition premium</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>La couche Tiptap donne aux &eacute;diteurs une interface riche sans masquer la puissance HTML pour les cas avanc&eacute;s.</p>\\n </div>\\n</div>\\n\\n<div class=''flex flex-col md:flex-row gap-8 items-center my-12''>\\n <div class=''w-full md:w-1/2 space-y-4''>\\n <h2>Architecture monorepo et flux de d&eacute;pendances</h2>\\n <p>Le dossier <code>apps/nextblock</code> contient l''exp&eacute;rience Next.js en production, incluant le site public et le shell CMS authentifi&eacute;. Le CLI <code>apps/create-nextblock</code> reprend cette base pour que les nouveaux projets partent des m&ecirc;mes d&eacute;cisions produit.</p>\\n <ul class=''list-disc pl-6 space-y-2 text-sm''>\\n <li><strong>@nextblock-cms/ui</strong> - composants UI, tokens et primitives visuelles partag&eacute;es</li>\\n <li><strong>@nextblock-cms/utils</strong> - traductions, gardes d''environnement et helpers de stockage</li>\\n <li><strong>@nextblock-cms/db</strong> - migrations, acc&egrave;s base typ&eacute; et types g&eacute;n&eacute;r&eacute;s</li>\\n <li><strong>@nextblock-cms/editor</strong> - la surface d''&eacute;dition Tiptap v3 r&eacute;utilisable</li>\\n <li><strong>@nextblock-cms/sdk</strong> - contrats typ&eacute;s pour l''auteuring et la validation des blocs</li>\\n <li><strong>@nextblock-cms/ecommerce</strong> - le module commerce premium lorsqu''il est activ&eacute;</li>\\n </ul>\\n <p>Lancez <code>nx graph</code> et vous voyez imm&eacute;diatement comment un changement se propage. Les alias de <code>tsconfig.base.json</code> et la configuration Tailwind partag&eacute;e aident &agrave; garder une vraie parit&eacute; entre marketing, back-office et projets g&eacute;n&eacute;r&eacute;s.</p>\\n </div>\\n <aside class=''w-full md:w-1/2 rounded-[2rem] border border-slate-200/80 bg-white p-4 shadow-xl dark:border-white/10 dark:bg-white/5''>\\n <div class=''relative aspect-video overflow-hidden rounded-2xl''><iframe class=''absolute inset-0 h-full w-full border-0'' src=''https://www.youtube.com/embed/DNqU8ez9qjs?si=p2oIy0f-n7wiaBmO'' title=''Comment NextBlock™ fonctionne'' allow=''accelerated-motion; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'' referrerpolicy=''strict-origin-when-cross-origin'' loading=''lazy'' allowfullscreen></iframe></div>\\n <p class=''mt-3 text-sm text-slate-500 dark:text-slate-400''>Nx rend visibles les relations du workspace, ce qui aide le starter, le CMS et les packages a rester alignes.</p>\\n </aside>\\n</div>\\n\\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\\n <img src=''/images/extensibility.webp'' alt=''Visuel NextBlock™ montrant le CMS relie a des modules reutilisables et des integrations'' class=''w-full h-auto object-cover'' />\\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>Un seul langage visuel relie la modelisation de contenu, l''edition et les futurs modules premium comme le commerce.</figcaption>\\n</figure>\\n\\n<h2>Le registre de blocs comme surface produit</h2>\\n<p>Le registre dans <code>apps/nextblock/lib/blocks/blockRegistry.ts</code> d&eacute;finit les types disponibles, les sch&eacute;mas Zod, les contenus de d&eacute;part et les composants d''&eacute;dition ou de rendu. On y trouve aujourd''hui des blocs comme <code>text</code>, <code>heading</code>, <code>section</code>, <code>posts_grid</code>, <code>checkout</code> et <code>product_details</code>.</p>\\n<p>Les sections supportent des colonnes imbriqu&eacute;es, ce qui permet de composer de vraies pages plut&ocirc;t qu''une simple liste de contenu. Des helpers comme <code>getBlockDefinition()</code>, <code>getInitialContent()</code> and <code>validateBlockContent()</code> gardent cette flexibilit&eacute; bien typ&eacute;e.</p>\\n\\n<h2>La couche d''edition</h2>\\n<p>Le package <code>@nextblock-cms/editor</code> enveloppe Tiptap v3 dans une surface &eacute;ditoriale r&eacute;utilisable avec slash commands, menus contextuels, drag handles, tableaux, listes de taches, compteurs et blocs de code. Le but est de conserver un HTML riche quand une equipe en a besoin.</p>\\n\\n<h2>A l''interieur du shell CMS</h2>\\n<p>Dans <code>apps/nextblock/app/cms</code>, chaque zone suit un motif lisible : pages de liste, routes de creation et d''edition, composants clients cibles et server actions qui encapsulent les mutations Supabase. Les editeurs y gagnent une interface coherente et les identifiants restent cote serveur.</p>\\n\\n<h2>Open core sans derive produit</h2>\\n<p>Le coeur du CMS est open source sous AGPL. Les modules premium comme <code>@nextblock-cms/ecommerce</code> restent disponibles en source mais sont actives via <code>package_activations</code> et <code>verifyPackageOnline()</code>. Le meme shell peut donc rester simple pour l''open source tout en deverrouillant les surfaces commerce au bon moment.</p>\\n\\n<h2>Pourquoi l''ensemble tient</h2>\\n<p>Le workspace Nx garde les librairies honnetes, l''app Next.js maintient la coherence UI, les migrations Supabase codifient les regles d''acces, et l''editeur Tiptap donne la meme experience de contribution quel que soit le deploiement. Quand une equipe lance <code>npm create nextblock</code>, elle recupere une facon de travailler complete, pas juste des fichiers.</p>\"}', 0, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.859357+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.currencies (id, code, symbol, exchange_rate, is_default, is_active, rounding_mode, rounding_increment, rounding_charm_amount, auto_update_exchange_rate, exchange_rate_updated_at, exchange_rate_source, auto_sync_product_prices, created_at, updated_at) VALUES ('f26e8339-d3cd-46f2-b11e-12569c30d4a8', 'USD', '$', 1.0000000000, true, true, 'none', 1, NULL, false, '2026-07-03 17:52:15.444652+00', 'store-default', false, '2026-07-03 17:52:15.444652+00', '2026-07-03 17:52:15.444652+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.logos (id, name, media_id, created_at) VALUES ('d45ef03d-27fc-4099-8b46-1cdf82d658d2', 'NextBlock™ Logo', 'ea6fdaf5-f8de-416c-9f2b-b689b7a4ed38', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.navigation_items (id, language_id, menu_key, label, url, parent_id, \"order\", page_id, translation_group_id, created_at, updated_at) VALUES (1, 1, 'HEADER', 'Home', '/', NULL, 0, 1, 'b6bd9818-a8d1-4a17-ac6b-d5b1f178a297', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.navigation_items (id, language_id, menu_key, label, url, parent_id, \"order\", page_id, translation_group_id, created_at, updated_at) VALUES (2, 1, 'HEADER', 'Articles', '/articles', NULL, 1, 3, '8c269d8b-db64-47ab-8ce0-29f65dbbe867', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.navigation_items (id, language_id, menu_key, label, url, parent_id, \"order\", page_id, translation_group_id, created_at, updated_at) VALUES (3, 1, 'HEADER', 'Contact', '/contact', NULL, 3, 5, 'cd630aed-aeb8-4acf-934b-9ccbbe8ee9bd', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.navigation_items (id, language_id, menu_key, label, url, parent_id, \"order\", page_id, translation_group_id, created_at, updated_at) VALUES (4, 2, 'HEADER', 'Accueil', '/accueil', NULL, 0, 2, 'aaa0c842-c996-4a07-bb3a-7562f865b6a4', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.navigation_items (id, language_id, menu_key, label, url, parent_id, \"order\", page_id, translation_group_id, created_at, updated_at) VALUES (5, 2, 'HEADER', 'Articles', '/articles', NULL, 1, 4, 'af69b017-3762-4c11-b709-37fb56081b99', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.navigation_items (id, language_id, menu_key, label, url, parent_id, \"order\", page_id, translation_group_id, created_at, updated_at) VALUES (6, 2, 'HEADER', 'Contact', '/contact', NULL, 3, 6, '21a91d4b-a494-4b3a-8d28-c21c414042cf', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.navigation_items (id, language_id, menu_key, label, url, parent_id, \"order\", page_id, translation_group_id, created_at, updated_at) VALUES (7, 1, 'FOOTER', 'Privacy Policy', '/privacy-policy', NULL, 0, 7, 'c8f2e6a3-7d28-4174-a8f7-0179226ae79a', '2026-07-03 17:52:15.753698+00', '2026-07-03 17:52:15.753698+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.navigation_items (id, language_id, menu_key, label, url, parent_id, \"order\", page_id, translation_group_id, created_at, updated_at) VALUES (8, 1, 'FOOTER', 'Terms of Service', '/terms-of-service', NULL, 1, 9, '4838ca4b-eaad-495c-ad7c-1750984bc8d9', '2026-07-03 17:52:15.753698+00', '2026-07-03 17:52:15.753698+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.navigation_items (id, language_id, menu_key, label, url, parent_id, \"order\", page_id, translation_group_id, created_at, updated_at) VALUES (9, 2, 'FOOTER', 'Politique de confidentialité', '/politique-de-confidentialite', NULL, 0, 8, 'c8f2e6a3-7d28-4174-a8f7-0179226ae79a', '2026-07-03 17:52:15.753698+00', '2026-07-03 17:52:15.753698+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.navigation_items (id, language_id, menu_key, label, url, parent_id, \"order\", page_id, translation_group_id, created_at, updated_at) VALUES (10, 2, 'FOOTER', 'Conditions d''utilisation', '/conditions-utilisation', NULL, 1, 10, '4838ca4b-eaad-495c-ad7c-1750984bc8d9', '2026-07-03 17:52:15.753698+00', '2026-07-03 17:52:15.753698+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.shipping_zones (id, name, priority_order, created_at, updated_at) VALUES ('4026af0e-5752-44fb-b7f7-ba5ac722cadc', 'North America', 10, '2026-07-03 17:52:15.25122+00', '2026-07-03 17:52:15.25122+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.shipping_zone_locations (id, zone_id, country_code, state_code, postal_code, created_at) VALUES ('c25d8fee-f0b6-43d5-8af5-5af4a47cb123', '4026af0e-5752-44fb-b7f7-ba5ac722cadc', 'US', NULL, NULL, '2026-07-03 17:52:15.25122+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.shipping_zone_locations (id, zone_id, country_code, state_code, postal_code, created_at) VALUES ('3d3d4bf6-7cb1-41b9-a5e1-1eb9098e640f', '4026af0e-5752-44fb-b7f7-ba5ac722cadc', 'CA', NULL, NULL, '2026-07-03 17:52:15.25122+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.shipping_zone_locations (id, zone_id, country_code, state_code, postal_code, created_at) VALUES ('63d47fff-7148-48ff-8cc1-5d3ba99fa022', '4026af0e-5752-44fb-b7f7-ba5ac722cadc', 'MX', NULL, NULL, '2026-07-03 17:52:15.25122+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.shipping_zone_methods (id, zone_id, method_type, cost_amount, cost_currency, min_order_amount, name, name_translations, currency_pricing_mode, cost_amounts, min_order_amounts, created_at, updated_at) VALUES ('ea112a82-e05c-46bc-a5e7-5bb62528aa5d', '4026af0e-5752-44fb-b7f7-ba5ac722cadc', 'flat_rate', 1500, 'USD', 0, 'Standard Shipping', '{\"fr\": \"Livraison standard\"}', 'auto', '{\"USD\": 1500}', '{\"USD\": 0}', '2026-07-03 17:52:15.25122+00', '2026-07-03 17:52:15.25122+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.shipping_zone_methods (id, zone_id, method_type, cost_amount, cost_currency, min_order_amount, name, name_translations, currency_pricing_mode, cost_amounts, min_order_amounts, created_at, updated_at) VALUES ('32d473d1-e3d4-4741-a7f5-63d87a387e08', '4026af0e-5752-44fb-b7f7-ba5ac722cadc', 'free_shipping', 0, 'USD', 10000, 'Free Shipping (Orders over $100)', '{\"fr\": \"Livraison gratuite (commandes de plus de 100 $)\"}', 'auto', '{\"USD\": 0}', '{\"USD\": 10000}', '2026-07-03 17:52:15.25122+00', '2026-07-03 17:52:15.25122+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('footer_copyright', '{\"en\": \"© {year} Nextblock CMS. All rights reserved.\", \"fr\": \"© {year} Nextblock CMS. Tous droits réservés.\"}') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('enabled_payment_providers', '{\"stripe\": false, \"freemius\": false}') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('site_title', '\"NextBlock™ CMS\"') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('site_description', '\"NextBlock™ is an open-source CMS on Next.js + Supabase — a visual block editor, blazing-fast multilingual pages, and built-in e-commerce.\"') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('site_keywords', '\"NextBlock, CMS, Next.js, Supabase, headless CMS, block editor, visual page builder, multilingual, e-commerce, open source\"') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('ecommerce_inventory_settings', '{\"enable_taxes\": false, \"track_quantities\": true}') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('invoice_settings', '{\"email\": \"\", \"phone\": \"\", \"address\": {\"city\": \"\", \"line1\": \"\", \"line2\": \"\", \"state\": \"\", \"postal_code\": \"\", \"country_code\": \"CA\"}, \"business_name\": \"\", \"tax_registrations\": []}') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('privacy_settings', '{\"gtm_id\": \"\", \"corporate\": {\"address\": \"\", \"legal_name\": \"\", \"support_email\": \"\"}, \"banner_enabled\": true, \"custom_scripts\": \"\", \"ga_measurement_id\": \"\"}') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('email_public', '{\"host\": \"\", \"port\": \"\", \"secure\": true, \"fromName\": \"\", \"fromEmail\": \"\"}') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('payment_public', '{\"stripe\": {\"publishableKey\": \"\"}, \"freemius\": {\"productId\": \"\", \"publicKey\": \"\", \"developerId\": \"\", \"sandboxEnabled\": false}}') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('onboarding_state', '{\"skipped\": [], \"dismissed\": false}') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('security_settings', '{\"enforce_staff_2fa\": true, \"trusted_device_days\": 30}') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('is_admin_created', 'false') ON CONFLICT DO NOTHING;\nINSERT INTO public.system_configuration (id, auto_accept_signups, settings) VALUES (1, false, '{}'::jsonb) ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('continue_with_github', '{\"en\": \"Continue with GitHub\", \"es\": \"Continuar con GitHub\", \"fr\": \"Continuer avec GitHub\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('or_continue_with', '{\"en\": \"Or continue with\", \"es\": \"O continuar con\", \"fr\": \"Ou continuer avec\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('customer_profile', '{\"en\": \"Customer Profile\", \"es\": \"Perfil de Cliente\", \"fr\": \"Profil Client\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('personal_information', '{\"en\": \"Personal Information\", \"es\": \"Información Personal\", \"fr\": \"Informations Personnelles\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('full_name', '{\"en\": \"Full Name\", \"es\": \"Nombre Completo\", \"fr\": \"Nom Complet\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('github_username', '{\"en\": \"GitHub Username\", \"es\": \"Nombre de usuario de GitHub\", \"fr\": \"Nom d''utilisateur GitHub\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('github_username_help', '{\"en\": \"Required only for purchasing developer licenses.\", \"es\": \"Requerido solo para comprar licencias de desarrollador.\", \"fr\": \"Requis uniquement pour l''achat de licences développeur.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('phone_number', '{\"en\": \"Phone Number\", \"es\": \"Número de Teléfono\", \"fr\": \"Numéro de Téléphone\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('billing_address', '{\"en\": \"Billing Address\", \"es\": \"Dirección de Facturación\", \"fr\": \"Adresse de Facturation\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('address_line_1', '{\"en\": \"Address Line 1\", \"es\": \"Dirección Línea 1\", \"fr\": \"Adresse Ligne 1\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('address_line_2', '{\"en\": \"Address Line 2 (Optional)\", \"es\": \"Dirección Línea 2 (Opcional)\", \"fr\": \"Adresse Ligne 2 (Optionnel)\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('city', '{\"en\": \"City\", \"es\": \"Ciudad\", \"fr\": \"Ville\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('state_province', '{\"en\": \"State / Province\", \"es\": \"Estado / Provincia\", \"fr\": \"État / Province\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('postal_zip_code', '{\"en\": \"Postal / Zip Code\", \"es\": \"Código Postal\", \"fr\": \"Code Postal\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('country', '{\"en\": \"Country\", \"es\": \"País\", \"fr\": \"Pays\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('save_profile', '{\"en\": \"Save Profile\", \"es\": \"Guardar Perfil\", \"fr\": \"Enregistrer le Profil\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('saving', '{\"en\": \"Saving...\", \"es\": \"Guardando...\", \"fr\": \"Enregistrement...\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_updated_success', '{\"en\": \"Profile updated successfully\", \"es\": \"Perfil actualizado con éxito\", \"fr\": \"Profil mis à jour avec succès\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_update_failed', '{\"en\": \"Failed to update profile\", \"es\": \"Error al actualizar el perfil\", \"fr\": \"Échec de la mise à jour du profil\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('address_required', '{\"en\": \"Address is required\", \"es\": \"La dirección es obligatoria\", \"fr\": \"L''adresse est requise\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('city_required', '{\"en\": \"City is required\", \"es\": \"La ciudad es obligatoria\", \"fr\": \"La ville est requise\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('zip_code_required', '{\"en\": \"Zip Code is required\", \"es\": \"El código postal es obligatorio\", \"fr\": \"Le code postal est requis\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('country_required', '{\"en\": \"Country is required\", \"es\": \"El país es obligatorio\", \"fr\": \"Le pays est requis\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('enter_valid_json', '{\"en\": \"Enter valid JSON for billing address.\", \"es\": \"Ingrese JSON válido para la dirección de facturación.\", \"fr\": \"Entrez un JSON valide pour l''adresse de facturation.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('public_profile', '{\"en\": \"Public Profile\", \"es\": \"Perfil Público\", \"fr\": \"Profil Public\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('details', '{\"en\": \"Account Details\", \"es\": \"Detalles de la Cuenta\", \"fr\": \"Détails du Compte\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('identity', '{\"en\": \"Identity\", \"es\": \"Identidad\", \"fr\": \"Identité\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('website', '{\"en\": \"Website\", \"es\": \"Sitio Web\", \"fr\": \"Site Web\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('avatar_url', '{\"en\": \"Avatar URL\", \"es\": \"URL del Avatar\", \"fr\": \"URL de l''Avatar\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('connect_github', '{\"en\": \"Connect GitHub\", \"es\": \"Conectar GitHub\", \"fr\": \"Connecter GitHub\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('github_link_failed', '{\"en\": \"Failed to link GitHub account\", \"es\": \"Error al vincular cuenta de GitHub\", \"fr\": \"Échec de la liaison du compte GitHub\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('save_changes', '{\"en\": \"Save Changes\", \"es\": \"Guardar Cambios\", \"fr\": \"Enregistrer les Modifications\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('github_connected', '{\"en\": \"GitHub Connected\", \"es\": \"GitHub Conectado\", \"fr\": \"GitHub Connecté\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('linked_to', '{\"en\": \"Linked to\", \"es\": \"Vinculado a\", \"fr\": \"Lié à\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('optional', '{\"en\": \"Optional\", \"fr\": \"Optionnel\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('shipping_address', '{\"en\": \"Shipping Address\", \"fr\": \"Adresse de livraison\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_settings_title', '{\"en\": \"Profile Settings\", \"fr\": \"Paramètres du profil\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_settings_description', '{\"en\": \"Keep your contact details and default addresses up to date for faster checkout.\", \"fr\": \"Gardez vos coordonnées et adresses par défaut à jour pour un paiement plus rapide.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_not_found', '{\"en\": \"Profile not found.\", \"fr\": \"Profil introuvable.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_basic_info_help', '{\"en\": \"This information appears on your account and helps us prepare your orders.\", \"fr\": \"Ces informations apparaissent sur votre compte et nous aident à préparer vos commandes.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_address_defaults_help', '{\"en\": \"These default addresses are prefilled during checkout and can still be edited for each order.\", \"fr\": \"Ces adresses par défaut sont préremplies au paiement et restent modifiables pour chaque commande.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('use_billing_for_shipping', '{\"en\": \"Use billing address for shipping\", \"fr\": \"Utiliser l''adresse de facturation pour la livraison\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_use_billing_for_shipping_help', '{\"en\": \"Keep one default address unless you regularly ship somewhere else.\", \"fr\": \"Gardez une seule adresse par défaut sauf si vous faites souvent livrer ailleurs.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('checkout_complete_billing_address', '{\"en\": \"Please complete your billing address before continuing.\", \"fr\": \"Veuillez compléter votre adresse de facturation avant de continuer.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('checkout_complete_shipping_address', '{\"en\": \"Please complete your shipping address before continuing.\", \"fr\": \"Veuillez compléter votre adresse de livraison avant de continuer.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('checkout_prefill_notice', '{\"en\": \"Using your saved account details for {email}. You can still adjust them for this order.\", \"fr\": \"Nous utilisons les renseignements enregistrés pour {email}. Vous pouvez toujours les ajuster pour cette commande.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('checkout_billing_address_help', '{\"en\": \"We use this address for payment verification and invoicing.\", \"fr\": \"Nous utilisons cette adresse pour la vérification du paiement et la facturation.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('checkout_use_billing_for_shipping_help', '{\"en\": \"Uncheck this if you want your order delivered to a different address.\", \"fr\": \"Décochez ceci si vous souhaitez faire livrer votre commande à une autre adresse.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('checkout_shipping_address_help', '{\"en\": \"Choose where physical items should be delivered.\", \"fr\": \"Choisissez où les articles physiques doivent être livrés.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('checkout_payment_only_notice', '{\"en\": \"Stripe checkout is kept focused on payment because your address details are already collected here.\", \"fr\": \"Le paiement Stripe reste centré sur le paiement puisque vos coordonnées sont déjà recueillies ici.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.signup_existing_account_hint', '{\"en\": \"That email may already be registered. Try signing in or resetting your password.\", \"fr\": \"Cette adresse e-mail est peut-être déjà utilisée. Essayez de vous connecter ou de réinitialiser votre mot de passe.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.signup_check_email_profile', '{\"en\": \"Check your email to confirm your account. We''ll bring you to your profile next so you can finish setting up your details.\", \"fr\": \"Vérifiez votre e-mail pour confirmer votre compte. Nous vous amènerons ensuite à votre profil pour terminer la configuration de vos renseignements.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.add_to_cart', '{\"en\": \"Add to Cart\", \"fr\": \"Ajouter au panier\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.added_to_cart', '{\"en\": \"Added to cart\", \"fr\": \"Ajouté au panier\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.no_image', '{\"en\": \"No Image\", \"fr\": \"Pas d''image\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.view_details', '{\"en\": \"View Details\", \"fr\": \"Voir les détails\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.item_added_desc', '{\"en\": \"The item has been added to your cart.\", \"fr\": \"L''article a été ajouté à votre panier.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout', '{\"en\": \"Checkout\", \"fr\": \"Paiement\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.cart', '{\"en\": \"Cart\", \"fr\": \"Panier\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.subtotal', '{\"en\": \"Subtotal\", \"fr\": \"Sous-total\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shipping', '{\"en\": \"Shipping\", \"fr\": \"Livraison\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.tax', '{\"en\": \"Tax\", \"fr\": \"Taxes\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.total', '{\"en\": \"Total\", \"fr\": \"Total\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.order_summary', '{\"en\": \"Order Summary\", \"fr\": \"Résumé de la commande\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.order_summary_desc', '{\"en\": \"Review your items before proceeding to payment.\", \"fr\": \"Vérifiez vos articles avant de procéder au paiement.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.payment_details', '{\"en\": \"Payment Details\", \"fr\": \"Détails du paiement\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shipping_info', '{\"en\": \"Shipping Information\", \"fr\": \"Informations de livraison\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.delivery_notice', '{\"en\": \"Digital product - No shipping required\", \"fr\": \"Produit numérique - Aucune livraison requise\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.back_to_shop', '{\"en\": \"Back to Shop\", \"fr\": \"Retour à la boutique\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.empty_cart', '{\"en\": \"Your cart is empty\", \"fr\": \"Votre panier est vide\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.no_products_found', '{\"en\": \"No products found\", \"fr\": \"Aucun produit trouvé\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.featured_products', '{\"en\": \"Featured Products\", \"fr\": \"Produits vedettes\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.latest_products', '{\"en\": \"Latest Products\", \"fr\": \"Derniers produits\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.search_products', '{\"en\": \"Search products...\", \"fr\": \"Rechercher des produits...\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.price_low_to_high', '{\"en\": \"Price: Low to High\", \"fr\": \"Prix : Croissant\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.price_high_to_low', '{\"en\": \"Price: High to Low\", \"fr\": \"Prix : Décroissant\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.newest', '{\"en\": \"Newest\", \"fr\": \"Nouveautés\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.filters', '{\"en\": \"Filters\", \"fr\": \"Filtres\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.apply_filters', '{\"en\": \"Apply Filters\", \"fr\": \"Appliquer les filtres\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.clear_all', '{\"en\": \"Clear All\", \"fr\": \"Tout effacer\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.digital_notice', '{\"en\": \"This is a digital product. You will receive access instructions via email after purchase.\", \"fr\": \"Ceci est un produit numérique. Vous recevrez les instructions d''accès par e-mail après l''achat.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shopping_cart', '{\"en\": \"Shopping Cart\", \"fr\": \"Panier d''achat\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.cart_empty', '{\"en\": \"Your cart is empty\", \"fr\": \"Votre panier est vide\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.cart_empty_description', '{\"en\": \"Looks like you haven''t added anything to your cart yet.\", \"fr\": \"On dirait que vous n''avez encore rien ajouté à votre panier.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.continue_shopping', '{\"en\": \"Continue Shopping\", \"fr\": \"Continuer vos achats\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.go_to_shop', '{\"en\": \"Go to Shop\", \"fr\": \"Aller à la boutique\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_successful', '{\"en\": \"Checkout Successful\", \"fr\": \"Paiement Réussi\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.sandbox_notice', '{\"en\": \"This is a Sandbox environment. The Freemius checkout is skipped here for demo purposes.\", \"fr\": \"Ceci est un environnement de bac à sable. Le paiement Freemius est sauté ici à des fins de démonstration.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.license_notice', '{\"en\": \"To purchase a real license for your self-hosted NextBlock™ instance, visit:\", \"fr\": \"Pour acheter une vraie licence pour votre instance NextBlock™ auto-hébergée, visitez :\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.purchase_at', '{\"en\": \"Purchase at nextblock.dev\", \"fr\": \"Acheter sur nextblock.dev\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.qty', '{\"en\": \"Qty\", \"fr\": \"Qté\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.quantity', '{\"en\": \"Quantity\", \"fr\": \"Quantité\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.product', '{\"en\": \"Product\", \"fr\": \"Produit\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.price', '{\"en\": \"Price\", \"fr\": \"Prix\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.secure_payment', '{\"en\": \"Secure payment processing\", \"fr\": \"Traitement sécurisé du paiement\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shipping_taxes_notice', '{\"en\": \"* Taxes and shipping will be calculated on the next step.\", \"fr\": \"* Les taxes et les frais de livraison seront calculés à l''étape suivante.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shipping_taxes_calculated', '{\"en\": \"Shipping & taxes calculated at checkout.\", \"fr\": \"Livraison et taxes calculées lors du paiement.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.email_address', '{\"en\": \"Email Address\", \"fr\": \"Adresse e-mail\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.pay_now', '{\"en\": \"Pay Now\", \"fr\": \"Payer maintenant\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.proceed_to_checkout', '{\"en\": \"Proceed to Checkout\", \"fr\": \"Passer à la caisse\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.processing', '{\"en\": \"Processing...\", \"fr\": \"Traitement...\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.invalid_email', '{\"en\": \"Please enter a valid email address.\", \"fr\": \"Veuillez entrer une adresse e-mail valide.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_failed', '{\"en\": \"Checkout failed: \", \"fr\": \"Le paiement a échoué : \"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.generic_error', '{\"en\": \"An error occurred. Please try again.\", \"fr\": \"Une erreur est survenue. Veuillez réessayer.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_popup_blocked', '{\"en\": \"Checkout popup blocked or failed to load. Falling back to direct link.\", \"fr\": \"Le popup de paiement a été bloqué ou n''a pas pu être chargé. Retour au lien direct.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.view_full_cart', '{\"en\": \"View Full Cart\", \"fr\": \"Voir le panier complet\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.ready_to_checkout', '{\"en\": \"Ready to Checkout?\", \"fr\": \"Prêt à passer au paiement ?\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.sale_badge', '{\"en\": \"Sale {percent}% Off\", \"fr\": \"Solde {percent}% de rabais\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.low_stock', '{\"en\": \"Only {count} left\", \"fr\": \"Plus que {count} en stock\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.instant_digital_delivery', '{\"en\": \"Instant Digital Delivery\", \"fr\": \"Livraison numérique instantanée\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.free_shipping', '{\"en\": \"Free Shipping\", \"fr\": \"Livraison gratuite\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.secure_checkout', '{\"en\": \"Secure Checkout\", \"fr\": \"Paiement sécurisé\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.no_description', '{\"en\": \"No description available.\", \"fr\": \"Aucune description disponible.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_overlay_title', '{\"en\": \"Order Checkout\", \"fr\": \"Paiement de la commande\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.email_placeholder', '{\"en\": \"you@example.com\", \"fr\": \"vous@exemple.com\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.contact_information', '{\"en\": \"Contact Information\", \"fr\": \"Informations de contact\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shipping_address', '{\"en\": \"Shipping Address\", \"fr\": \"Adresse de livraison\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shipping_method', '{\"en\": \"Shipping Method\", \"fr\": \"Mode de livraison\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.available_rates', '{\"en\": \"Available Rates\", \"fr\": \"Tarifs disponibles\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.calculating', '{\"en\": \"Calculating...\", \"fr\": \"Calcul en cours...\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.select_rate', '{\"en\": \"Select a shipping rate\", \"fr\": \"Sélectionnez un tarif de livraison\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.enter_postal_code', '{\"en\": \"Enter postal code\", \"fr\": \"Entrez le code postal\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.free', '{\"en\": \"Free\", \"fr\": \"Gratuit\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.first_last_name', '{\"en\": \"First & Last Name\", \"fr\": \"Nom et prénom\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.address', '{\"en\": \"Address\", \"fr\": \"Adresse\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.city', '{\"en\": \"City\", \"fr\": \"Ville\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.state_province', '{\"en\": \"State / Province\", \"fr\": \"État / Province\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.zip_postal', '{\"en\": \"ZIP / Postal Code\", \"fr\": \"Code postal\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.postal_code', '{\"en\": \"Postal Code\", \"fr\": \"Code postal\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.zip_postal_code', '{\"en\": \"ZIP / Postal Code\", \"fr\": \"Code postal\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.estimate_shipping', '{\"en\": \"Estimate Shipping\", \"fr\": \"Estimer la livraison\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.calculate', '{\"en\": \"Calculate\", \"fr\": \"Calculer\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.no_rates_found', '{\"en\": \"No shipping rates found for this region.\", \"fr\": \"Aucun tarif de livraison trouvé pour cette région.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.no_rates_for_region', '{\"en\": \"No shipping rates found for this region.\", \"fr\": \"Aucun tarif de livraison trouvé pour cette région.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.enter_address_for_rates', '{\"en\": \"Enter your address to see shipping rates.\", \"fr\": \"Entrez votre adresse pour voir les tarifs de livraison.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.secure_checkout_guarantee', '{\"en\": \"Secure checkout guaranteed\", \"fr\": \"Paiement sécurisé garanti\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.full_name', '{\"en\": \"Full Name\", \"fr\": \"Nom complet\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.country', '{\"en\": \"Country\", \"fr\": \"Pays\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.freemius_trial_preference_title', '{\"en\": \"How would you like to start your trial?\", \"es\": \"¿Cómo te gustaría comenzar tu prueba?\", \"fr\": \"Comment souhaitez-vous commencer votre essai ?\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.pricing_unavailable', '{\"en\": \"Pricing Unavailable\", \"es\": \"Precios no disponibles\", \"fr\": \"Tarification indisponible\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.monthly', '{\"en\": \"Monthly\", \"es\": \"Mensual\", \"fr\": \"Mensuel\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.annual', '{\"en\": \"Annual\", \"es\": \"Anual\", \"fr\": \"Annuel\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.lifetime', '{\"en\": \"Lifetime\", \"es\": \"De por vida\", \"fr\": \"À vie\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.year', '{\"en\": \"year\", \"es\": \"año\", \"fr\": \"an\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.month', '{\"en\": \"month\", \"es\": \"mes\", \"fr\": \"mois\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.freemius_trial_no_card', '{\"en\": \"Start Free Trial (No card required)\", \"es\": \"Comenzar prueba gratuita (Sin tarjeta)\", \"fr\": \"Commencer l''essai gratuit (Sans carte requise)\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.freemius_trial_with_card', '{\"en\": \"Enter Payment Details Now (Still get full trial length free)\", \"es\": \"Ingresar detalles de pago ahora (Aún obtienes toda la duración de la prueba gratis)\", \"fr\": \"Entrer les détails de paiement maintenant (Vous bénéficiez toujours de toute la durée de l''essai gratuitement)\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.freemius_trial_with_card_help', '{\"en\": \"You will not be billed until the trial ends. Cancel anytime.\", \"es\": \"No se te cobrará hasta que termine la prueba. Cancela en cualquier momento.\", \"fr\": \"Vous ne serez pas facturé avant la fin de l''essai. Annulez à tout moment.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.choose_your_options', '{\"en\": \"Choose Your Options\", \"fr\": \"Choisissez vos options\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.variant_availability_help', '{\"en\": \"Select a combination to resolve the exact variant price and availability.\", \"fr\": \"Selectionnez une combinaison pour afficher le prix exact et la disponibilite de la variante.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.in_stock', '{\"en\": \"{count} in stock\", \"fr\": \"{count} en stock\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.out_of_stock', '{\"en\": \"Out of stock\", \"fr\": \"Rupture de stock\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.select_options', '{\"en\": \"Select Options\", \"fr\": \"Choisir des options\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.variant_selection_required', '{\"en\": \"Select one term from every dropdown to resolve a variation.\", \"fr\": \"Selectionnez une valeur dans chaque liste pour afficher la variante correspondante.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.tax_calculated_on_stripe', '{\"en\": \"Calculated on Stripe\", \"es\": \"Calculado en Stripe\", \"fr\": \"Calculé sur Stripe\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('checkout_stripe_tax_finalized_notice', '{\"en\": \"Tax will be finalized by Stripe Tax on the payment step.\", \"es\": \"El impuesto se finalizará con Stripe Tax en el paso de pago.\", \"fr\": \"La taxe sera finalisée par Stripe Tax à l''étape du paiement.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('branding', '{\"en\": \"Branding\", \"fr\": \"Image de marque\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('company_name', '{\"en\": \"Company name\", \"fr\": \"Nom de l''entreprise\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('invoice', '{\"en\": \"Invoice\", \"fr\": \"Facture\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('invoice_number', '{\"en\": \"Invoice #\", \"fr\": \"Facture no\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('paid_on', '{\"en\": \"Paid on\", \"fr\": \"Paye le\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('bill_to', '{\"en\": \"Bill to\", \"fr\": \"Facturer a\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ship_to', '{\"en\": \"Ship to\", \"fr\": \"Livrer a\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('print_invoice', '{\"en\": \"Print / Save as PDF\", \"fr\": \"Imprimer / Enregistrer en PDF\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('tax_registrations', '{\"en\": \"Tax registrations\", \"fr\": \"Inscriptions fiscales\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('invoice_settings', '{\"en\": \"Invoice settings\", \"fr\": \"Parametres de facture\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('business_name', '{\"en\": \"Business name\", \"fr\": \"Nom de l''entreprise\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('order_number', '{\"en\": \"Order #\", \"fr\": \"Commande no\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('print_invoice_help', '{\"en\": \"Use your browser print dialog to save this invoice as a PDF.\", \"fr\": \"Utilisez la boite de dialogue d''impression de votre navigateur pour enregistrer cette facture en PDF.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('return_home', '{\"en\": \"Return to Home\", \"fr\": \"Retour a l''accueil\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('receipt_finalizing', '{\"en\": \"Finalizing your invoice and payment details...\", \"fr\": \"Finalisation de votre facture et des details du paiement...\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('receipt_not_ready', '{\"en\": \"Your invoice will appear here once the payment sync is complete.\", \"fr\": \"Votre facture apparaitra ici une fois la synchronisation du paiement terminee.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('tax_breakdown', '{\"en\": \"Tax breakdown\", \"fr\": \"Detail des taxes\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('amount', '{\"en\": \"Amount\", \"fr\": \"Montant\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('price', '{\"en\": \"Price\", \"fr\": \"Prix\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('from', '{\"en\": \"From\", \"fr\": \"De\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('account_navigation', '{\"en\": \"Account\", \"fr\": \"Compte\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('account_orders', '{\"en\": \"Orders\", \"fr\": \"Commandes\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('change_my_password', '{\"en\": \"Change my password\", \"fr\": \"Changer mon mot de passe\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_orders_title', '{\"en\": \"My orders\", \"fr\": \"Mes commandes\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_orders_description', '{\"en\": \"Review your recent purchases and open printable invoices.\", \"fr\": \"Consultez vos achats récents et ouvrez vos factures imprimables.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_orders_empty', '{\"en\": \"You do not have any orders yet.\", \"fr\": \"Vous n''avez pas encore de commandes.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_order_detail_title', '{\"en\": \"Order invoice\", \"fr\": \"Facture de commande\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_order_detail_description', '{\"en\": \"Review and print your finalized invoice.\", \"fr\": \"Consultez et imprimez votre facture finalisée.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_order_invoice_pending', '{\"en\": \"The printable invoice will appear here once this order has been finalized.\", \"fr\": \"La facture imprimable apparaîtra ici une fois que cette commande aura été finalisée.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_password_title', '{\"en\": \"Change your password\", \"fr\": \"Changer votre mot de passe\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_password_description', '{\"en\": \"Update your account password without leaving your profile.\", \"fr\": \"Mettez à jour le mot de passe de votre compte sans quitter votre profil.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('new_password', '{\"en\": \"New password\", \"fr\": \"Nouveau mot de passe\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('confirm_new_password', '{\"en\": \"Confirm new password\", \"fr\": \"Confirmer le nouveau mot de passe\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('password_updated_success', '{\"en\": \"Password updated successfully.\", \"fr\": \"Mot de passe mis à jour avec succès.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('password_update_failed', '{\"en\": \"Password update failed.\", \"fr\": \"La mise à jour du mot de passe a échoué.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('passwords_do_not_match', '{\"en\": \"Passwords do not match.\", \"fr\": \"Les mots de passe ne correspondent pas.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('order_date', '{\"en\": \"Date\", \"fr\": \"Date\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('order_status_paid', '{\"en\": \"Paid\", \"fr\": \"Payée\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('order_status_pending', '{\"en\": \"Pending\", \"fr\": \"En attente\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('order_status_trial', '{\"en\": \"Trial\", \"fr\": \"Essai\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('order_status_shipped', '{\"en\": \"Shipped\", \"fr\": \"Expédiée\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('order_status_cancelled', '{\"en\": \"Cancelled\", \"fr\": \"Annulée\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('order_status_refunded', '{\"en\": \"Refunded\", \"fr\": \"Remboursée\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('back_to_orders', '{\"en\": \"Back to orders\", \"fr\": \"Retour aux commandes\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_trial_started', '{\"en\": \"Trial started\", \"fr\": \"Essai demarre\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_order_pending', '{\"en\": \"Order pending\", \"fr\": \"Commande en attente\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('select_an_option', '{\"en\": \"Select an option\", \"es\": \"Selecciona una opcion\", \"fr\": \"Selectionnez une option\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shipping_method_required', '{\"en\": \"Please select a shipping method before continuing.\", \"es\": \"Selecciona un metodo de envio antes de continuar.\", \"fr\": \"Veuillez selectionner un mode de livraison avant de continuer.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.waiting_on_address_info', '{\"en\": \"Complete your shipping address to view available shipping options.\", \"es\": \"Completa tu direccion de envio para ver las opciones de envio disponibles.\", \"fr\": \"Completez votre adresse de livraison pour voir les options de livraison disponibles.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.calculating_shipping', '{\"en\": \"Calculating shipping...\", \"es\": \"Calculando el envio...\", \"fr\": \"Calcul de la livraison...\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.sandbox_checkout_stripe_description', '{\"en\": \"This simulated step represents the Stripe checkout for physical products.\", \"es\": \"Este paso simulado representa el pago de Stripe para productos fisicos.\", \"fr\": \"Cette etape simulee represente le paiement Stripe pour les produits physiques.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.sandbox_checkout_freemius_description', '{\"en\": \"This simulated step represents the Freemius checkout for digital products.\", \"es\": \"Este paso simulado representa el pago de Freemius para productos digitales.\", \"fr\": \"Cette etape simulee represente le paiement Freemius pour les produits numeriques.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.digital_label', '{\"en\": \"Digital\", \"es\": \"Digital\", \"fr\": \"Numerique\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.physical_label', '{\"en\": \"Physical\", \"es\": \"Fisico\", \"fr\": \"Physique\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.physical_products', '{\"en\": \"Physical products\", \"es\": \"Productos fisicos\", \"fr\": \"Produits physiques\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.digital_products', '{\"en\": \"Digital products\", \"es\": \"Productos digitales\", \"fr\": \"Produits numeriques\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.estimated_total', '{\"en\": \"Estimated total\", \"es\": \"Total estimado\", \"fr\": \"Total estime\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.stripe_checkout_title', '{\"en\": \"Stripe Checkout\", \"es\": \"Pago con Stripe\", \"fr\": \"Paiement Stripe\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.stripe_checkout_description', '{\"en\": \"Pay for physical products in one Stripe checkout session.\", \"es\": \"Paga los productos fisicos en una sola sesion de Stripe.\", \"fr\": \"Payez les produits physiques dans une seule session Stripe.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.item_count_one', '{\"en\": \"{count} item\", \"es\": \"{count} articulo\", \"fr\": \"{count} article\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.item_count_other', '{\"en\": \"{count} items\", \"es\": \"{count} articulos\", \"fr\": \"{count} articles\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.physical_subtotal', '{\"en\": \"Physical subtotal\", \"es\": \"Subtotal fisico\", \"fr\": \"Sous-total physique\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.total_on_stripe', '{\"en\": \"Total on Stripe\", \"es\": \"Total en Stripe\", \"fr\": \"Total sur Stripe\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_physical_products', '{\"en\": \"Checkout Physical Products\", \"es\": \"Pagar productos fisicos\", \"fr\": \"Payer les produits physiques\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shipping_taxes_collected_on_stripe', '{\"en\": \"Shipping and taxes are only collected during the Stripe step for physical products.\", \"es\": \"El envio y los impuestos solo se cobran durante el paso de Stripe para productos fisicos.\", \"fr\": \"La livraison et les taxes sont percues uniquement a l''etape Stripe pour les produits physiques.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.freemius_checkout_title', '{\"en\": \"Freemius Checkout\", \"es\": \"Pago con Freemius\", \"fr\": \"Paiement Freemius\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.freemius_checkout_description', '{\"en\": \"Digital products use the Freemius checkout flow.\", \"es\": \"Los productos digitales usan el flujo de pago de Freemius.\", \"fr\": \"Les produits numeriques utilisent le flux de paiement Freemius.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.license_count_one', '{\"en\": \"{count} license\", \"es\": \"{count} licencia\", \"fr\": \"{count} licence\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.license_count_other', '{\"en\": \"{count} licenses\", \"es\": \"{count} licencias\", \"fr\": \"{count} licences\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_billing_cycle_monthly', '{\"en\": \"Monthly subscription\", \"es\": \"Suscripcion mensual\", \"fr\": \"Abonnement mensuel\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_billing_cycle_annual', '{\"en\": \"Annual subscription\", \"es\": \"Suscripcion anual\", \"fr\": \"Abonnement annuel\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_billing_cycle_lifetime', '{\"en\": \"Lifetime subscription\", \"es\": \"Suscripcion de por vida\", \"fr\": \"Abonnement a vie\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_product', '{\"en\": \"Checkout {title}\", \"es\": \"Pagar {title}\", \"fr\": \"Paiement de {title}\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_digital_product', '{\"en\": \"Checkout Digital Product\", \"es\": \"Pagar producto digital\", \"fr\": \"Payer le produit numerique\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.digital_subtotal', '{\"en\": \"Digital subtotal\", \"es\": \"Subtotal digital\", \"fr\": \"Sous-total numerique\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.freemius_multi_checkout_notice', '{\"en\": \"Freemius licenses are completed one at a time, so each digital product gets its own checkout action.\", \"es\": \"Las licencias de Freemius se completan una por una, por lo que cada producto digital tiene su propia accion de pago.\", \"fr\": \"Les licences Freemius se finalisent une a la fois, donc chaque produit numerique a sa propre action de paiement.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.freemius_tax_notice', '{\"en\": \"Taxes and compliance for digital products are handled inside the Freemius checkout.\", \"es\": \"Los impuestos y la conformidad para los productos digitales se gestionan dentro del pago de Freemius.\", \"fr\": \"Les taxes et la conformite pour les produits numeriques sont gerees dans le paiement Freemius.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('continue_checkout', '{\"en\": \"Continue Checkout\", \"fr\": \"Continuer le paiement\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('checkout_success_sync_failed', '{\"en\": \"We could not finalize your invoice yet. Please refresh shortly.\", \"fr\": \"Nous n''avons pas encore pu finaliser votre facture. Veuillez rafraichir la page sous peu.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shipping_country_required', '{\"en\": \"Country is required to calculate shipping.\", \"fr\": \"Le pays est requis pour calculer la livraison.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.signup_success_title', '{\"en\": \"Check your inbox\", \"fr\": \"Vérifiez votre boîte de réception\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shipping_calculation_failed', '{\"en\": \"We couldn''t calculate shipping right now. Please try again.\", \"fr\": \"Nous n''avons pas pu calculer la livraison pour le moment. Veuillez reessayer.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_license_inactive', '{\"en\": \"The ecommerce module license is inactive.\", \"fr\": \"La licence du module ecommerce est inactive.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_invalid_items', '{\"en\": \"Your checkout items could not be processed.\", \"fr\": \"Les articles de votre commande n''ont pas pu etre traites.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_provider_items_required', '{\"en\": \"Each checkout step must include items assigned to a payment provider.\", \"fr\": \"Chaque etape de paiement doit inclure des articles associes a un fournisseur de paiement.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_mixed_provider_steps', '{\"en\": \"Products with different payment providers must be purchased in separate checkout steps.\", \"fr\": \"Les produits utilisant differents fournisseurs de paiement doivent etre achetes en etapes separees.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_freemius_single_item', '{\"en\": \"Freemius products must be purchased one at a time.\", \"fr\": \"Les produits Freemius doivent etre achetes un a la fois.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_billing_address_required', '{\"en\": \"A billing address is required to continue checkout.\", \"fr\": \"Une adresse de facturation est requise pour continuer le paiement.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_internal_server_error', '{\"en\": \"Something went wrong while preparing your checkout. Please try again.\", \"fr\": \"Une erreur s''est produite lors de la preparation de votre paiement. Veuillez reessayer.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_missing_session_id', '{\"en\": \"We couldn''t find a checkout session to finalize.\", \"fr\": \"Nous n''avons pas trouve de session de paiement a finaliser.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_payment_pending', '{\"en\": \"Your payment is still pending.\", \"fr\": \"Votre paiement est toujours en attente.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_success_order_not_found', '{\"en\": \"We couldn''t find the order linked to this checkout.\", \"fr\": \"Nous n''avons pas trouve la commande liee a ce paiement.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_success_invalid_reference', '{\"en\": \"This checkout reference can''t be finalized from this page.\", \"fr\": \"Cette reference de paiement ne peut pas etre finalisee depuis cette page.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_success_inventory_update_failed', '{\"en\": \"We couldn''t update inventory for this order.\", \"fr\": \"Nous n''avons pas pu mettre a jour l''inventaire pour cette commande.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_success_status_update_failed', '{\"en\": \"We couldn''t update the order status.\", \"fr\": \"Nous n''avons pas pu mettre a jour le statut de la commande.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.unknown_error', '{\"en\": \"Unknown error\", \"es\": \"Error desconocido\", \"fr\": \"Erreur inconnue\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.get_license', '{\"en\": \"Get License\", \"es\": \"Obtener Licencia\", \"fr\": \"Obtenir la licence\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.added_to_cart_success', '{\"en\": \"{item} added to your cart.\", \"es\": \"{item} añadido al carrito.\", \"fr\": \"{item} ajouté à votre panier.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.added_to_cart_error', '{\"en\": \"Could not add item to cart.\", \"es\": \"No se pudo añadir el artículo al carrito.\", \"fr\": \"Impossible d''ajouter l''article au panier.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('edit_product', '{\"en\": \"Edit Product\", \"fr\": \"Modifier le produit\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.trigger', '{\"en\": \"Search\", \"fr\": \"Rechercher\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.title', '{\"en\": \"Search\", \"fr\": \"Rechercher\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.description', '{\"en\": \"Search published pages, posts, and products.\", \"fr\": \"Rechercher dans les pages, articles et produits publies.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.placeholder', '{\"en\": \"Search...\", \"fr\": \"Rechercher...\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.filter_all', '{\"en\": \"All\", \"fr\": \"Tout\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.filter_pages', '{\"en\": \"Pages\", \"fr\": \"Pages\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.filter_posts', '{\"en\": \"Posts\", \"fr\": \"Articles\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.filter_products', '{\"en\": \"Products\", \"fr\": \"Produits\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.result_page', '{\"en\": \"Page\", \"fr\": \"Page\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.result_post', '{\"en\": \"Post\", \"fr\": \"Article\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.result_product', '{\"en\": \"Product\", \"fr\": \"Produit\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.recent', '{\"en\": \"Recent\", \"fr\": \"Recents\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.error_title', '{\"en\": \"Search is unavailable.\", \"fr\": \"La recherche est indisponible.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.error_description', '{\"en\": \"Please try again in a moment.\", \"fr\": \"Veuillez reessayer dans un instant.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.empty_title', '{\"en\": \"No results found.\", \"fr\": \"Aucun resultat trouve.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.empty_description', '{\"en\": \"Try another search term.\", \"fr\": \"Essayez un autre terme de recherche.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('sign_in', '{\"en\": \"Sign in\", \"fr\": \"Connexion\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('sign_up', '{\"en\": \"Sign up\", \"fr\": \"Inscription\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('sign_out', '{\"en\": \"Sign out\", \"fr\": \"Déconnexion\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('dont_have_account', '{\"en\": \"Don''t have an account?\", \"fr\": \"Pas encore de compte ?\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('email', '{\"en\": \"Email\", \"fr\": \"Email\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('you_at_example_com', '{\"en\": \"you@example.com\", \"fr\": \"vous@example.com\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('password', '{\"en\": \"Password\", \"fr\": \"Mot de passe\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('forgot_password', '{\"en\": \"Forgot Password?\", \"fr\": \"Mot de passe oublié ?\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('your_password', '{\"en\": \"Your password\", \"fr\": \"Votre mot de passe\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('signing_in_pending', '{\"en\": \"Signing In...\", \"fr\": \"Connexion en cours...\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('already_have_account', '{\"en\": \"Already have an account?\", \"fr\": \"Déjà un compte ?\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('signing_up_pending', '{\"en\": \"Signing up...\", \"fr\": \"Inscription en cours...\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reset_password', '{\"en\": \"Reset Password\", \"fr\": \"Réinitialiser le mot de passe\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.signup_form_description', '{\"en\": \"Create your account in one quick step. We''ll email you a confirmation link before you finish your profile.\", \"fr\": \"Créez votre compte en une étape rapide. Nous vous enverrons un lien de confirmation avant de terminer votre profil.\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.signup_success_badge', '{\"en\": \"Signup received\", \"fr\": \"Inscription reçue\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.signup_success_step_confirm', '{\"en\": \"Open the email we sent and confirm your address.\", \"fr\": \"Ouvrez l''e-mail envoyé et confirmez votre adresse.\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.signup_success_step_profile', '{\"en\": \"After confirmation, we''ll bring you to your profile to finish setup.\", \"fr\": \"Après confirmation, nous vous amènerons à votre profil pour terminer la configuration.\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.signup_success_step_spam', '{\"en\": \"If it doesn''t arrive soon, check spam, junk, or promotions.\", \"fr\": \"S''il n''arrive pas bientôt, vérifiez les dossiers spam, indésirables ou promotions.\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.signup_use_different_email', '{\"en\": \"Use a different email\", \"fr\": \"Utiliser une autre adresse e-mail\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.back_to_sign_in', '{\"en\": \"Back to sign in\", \"fr\": \"Retour à la connexion\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.signup_rate_limit', '{\"en\": \"You''re trying too quickly. Please wait a moment before requesting another confirmation email.\", \"fr\": \"Vous allez trop vite. Veuillez attendre un instant avant de demander un nouvel e-mail de confirmation.\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('blog_prefix', '{\"en\": \"article\", \"fr\": \"article\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('edit_page', '{\"en\": \"Edit Page\", \"fr\": \"Éditer la page\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('edit_post', '{\"en\": \"Edit Post\", \"fr\": \"Éditer l''article\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('open_main_menu', '{\"en\": \"Open main menu\", \"fr\": \"Ouvrir le menu principal\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('mobile_navigation_menu', '{\"en\": \"Mobile navigation menu\", \"fr\": \"Menu de navigation mobile\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('cms_dashboard', '{\"en\": \"CMS Dashboard\", \"fr\": \"Tableau de bord CMS\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('update_env_file_warning', '{\"en\": \"Please update .env.local file with anon key and url\", \"fr\": \"Veuillez mettre à jour .env.local avec l''anon key et l''URL\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('greeting', '{\"en\": \"Hey, {username}!\", \"fr\": \"Salut, {username} !\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('theme_switcher', '{\"en\": \"Theme Switcher\", \"fr\": \"Sélecteur de thème\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('theme_light', '{\"en\": \"Light\", \"fr\": \"Clair\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('theme_dark', '{\"en\": \"Dark\", \"fr\": \"Sombre\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('theme_system', '{\"en\": \"System\", \"fr\": \"Système\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('theme_vibrant', '{\"en\": \"Vibrant\", \"fr\": \"Vibrant\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('sandbox_mode_banner', '{\"en\": \"Sandbox Mode: Data is public and resets every 15 minutes.\", \"fr\": \"Mode Sandbox : Les données sont publiques et réinitialisées toutes les 15 minutes.\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('demo_access_title', '{\"en\": \"Demo Access\", \"fr\": \"Accès Démo\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('demo_access_desc', '{\"en\": \"This is a demo site. You may use the following credentials to access the admin section:\", \"fr\": \"Ceci est un site de démonstration. Vous pouvez utiliser les identifiants suivants pour accéder à l''administration :\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('demo_user_label', '{\"en\": \"User:\", \"fr\": \"Utilisateur :\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('demo_password_label', '{\"en\": \"Password:\", \"fr\": \"Mot de passe :\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.on_sale', '{\"en\": \"On Sale\", \"es\": \"En oferta\", \"fr\": \"En solde\"}', '2026-07-03 17:52:15.69428+00', '2026-07-03 17:52:15.69428+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('remember_this_device', '{\"en\": \"Remember this device\", \"fr\": \"Se souvenir de cet appareil\"}', '2026-07-03 17:52:15.699946+00', '2026-07-03 17:52:15.699946+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.aria_label', '{\"en\": \"Privacy consent\", \"fr\": \"Consentement à la confidentialité\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.title', '{\"en\": \"We value your privacy\", \"fr\": \"Nous respectons votre vie privée\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.description_before_policy_link', '{\"en\": \"We use only essential cookies by default. With your consent we also use analytics to improve the site. See our\", \"fr\": \"Nous utilisons seulement les témoins essentiels par défaut. Avec votre consentement, nous utilisons aussi l''analytique pour améliorer le site. Consultez notre\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.privacy_policy_link', '{\"en\": \"Privacy Policy\", \"fr\": \"politique de confidentialité\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.description_after_policy_link', '{\"en\": \".\", \"fr\": \".\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.privacy_policy_href', '{\"en\": \"/privacy-policy\", \"fr\": \"/politique-de-confidentialite\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.necessary_label', '{\"en\": \"Necessary\", \"fr\": \"Nécessaires\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.necessary_help', '{\"en\": \"Always on\", \"fr\": \"Toujours actifs\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.analytics_label', '{\"en\": \"Analytics\", \"fr\": \"Analytique\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.analytics_help', '{\"en\": \"Usage insights\", \"fr\": \"Statistiques d''utilisation\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.marketing_label', '{\"en\": \"Marketing\", \"fr\": \"Marketing\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.marketing_help', '{\"en\": \"Personalized content\", \"fr\": \"Contenu personnalisé\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.save_choices', '{\"en\": \"Save choices\", \"fr\": \"Enregistrer mes choix\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.accept_all', '{\"en\": \"Accept all\", \"fr\": \"Tout accepter\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.reject_all', '{\"en\": \"Reject all\", \"fr\": \"Tout refuser\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.hide_options', '{\"en\": \"Hide options\", \"fr\": \"Masquer les options\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.manage_options', '{\"en\": \"Manage options\", \"fr\": \"Gérer les options\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.customer_reviews', '{\"en\": \"Customer Reviews\", \"fr\": \"Avis clients\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.share_thoughts', '{\"en\": \"Share your thoughts and experience with this product.\", \"fr\": \"Partagez vos impressions et votre expérience avec ce produit.\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.write_review', '{\"en\": \"Write a Review\", \"fr\": \"Rédiger un avis\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.cancel_review', '{\"en\": \"Cancel Review\", \"fr\": \"Annuler l''avis\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.login_to_write', '{\"en\": \"Please log in to write a review.\", \"fr\": \"Veuillez vous connecter pour rédiger un avis.\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.write_your_review', '{\"en\": \"Write your review\", \"fr\": \"Rédigez votre avis\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.rating', '{\"en\": \"Rating\", \"fr\": \"Note\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.description', '{\"en\": \"Review Description\", \"fr\": \"Description de l''avis\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.description_placeholder', '{\"en\": \"What did you like or dislike? How does it perform?\", \"fr\": \"Qu''avez-vous aimé ou déploré ? Comment se comporte-t-il ?\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.submit_review', '{\"en\": \"Submit Review\", \"fr\": \"Soumettre l''avis\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.submitting', '{\"en\": \"Submitting...\", \"fr\": \"Envoi en cours...\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.success_pending', '{\"en\": \"Your review has been submitted successfully and is pending moderation.\", \"fr\": \"Votre avis a été soumis avec succès et est en attente de modération.\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.cancel', '{\"en\": \"Cancel\", \"fr\": \"Annuler\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.helpful', '{\"en\": \"Helpful\", \"fr\": \"Utile\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.no_reviews', '{\"en\": \"No reviews yet\", \"fr\": \"Aucun avis pour le moment\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.be_the_first', '{\"en\": \"There are no reviews for this product yet. Be the first to write one!\", \"fr\": \"Il n''y a pas encore d''avis sur ce produit. Soyez le premier à en rédiger un !\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.load_more', '{\"en\": \"Load More Reviews\", \"fr\": \"Charger plus d''avis\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.review_count_one', '{\"en\": \"{count} review\", \"fr\": \"{count} avis\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.review_count_other', '{\"en\": \"{count} reviews\", \"fr\": \"{count} avis\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.discussion', '{\"en\": \"Discussion & Comments\", \"fr\": \"Discussion & Commentaires\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.join_conversation', '{\"en\": \"Join the conversation and express your thoughts.\", \"fr\": \"Rejoignez la conversation et exprimez vos pensées.\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.write_comment', '{\"en\": \"Write a Comment\", \"fr\": \"Écrire un commentaire\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.cancel_comment', '{\"en\": \"Cancel Comment\", \"fr\": \"Annuler le commentaire\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.login_to_write', '{\"en\": \"Please log in to write a comment.\", \"fr\": \"Veuillez vous connecter pour écrire un commentaire.\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.join_discussion', '{\"en\": \"Join the Discussion\", \"fr\": \"Rejoindre la discussion\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.your_message', '{\"en\": \"Your Message\", \"fr\": \"Votre message\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.message_placeholder', '{\"en\": \"What are your thoughts on this article?\", \"fr\": \"Quelles sont vos pensées sur cet article ?\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.post_comment', '{\"en\": \"Post Comment\", \"fr\": \"Publier le commentaire\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.success_pending', '{\"en\": \"Your comment has been submitted successfully and is pending moderation.\", \"fr\": \"Votre commentaire a été soumis avec succès et est en attente de modération.\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.like', '{\"en\": \"Like\", \"fr\": \"J''aime\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.no_comments', '{\"en\": \"No comments yet\", \"fr\": \"Aucun commentaire pour le moment\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.be_the_first', '{\"en\": \"Be the first to share your thoughts!\", \"fr\": \"Soyez le premier à partager vos pensées !\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.load_more', '{\"en\": \"Load More Comments\", \"fr\": \"Charger plus de commentaires\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nSELECT pg_catalog.setval('public.blocks_id_seq', COALESCE((SELECT MAX(id) FROM public.blocks), 1), true);\nSELECT pg_catalog.setval('public.languages_id_seq', COALESCE((SELECT MAX(id) FROM public.languages), 1), true);\nSELECT pg_catalog.setval('public.navigation_items_id_seq', COALESCE((SELECT MAX(id) FROM public.navigation_items), 1), true);\nSELECT pg_catalog.setval('public.pages_id_seq', COALESCE((SELECT MAX(id) FROM public.pages), 1), true);\nSELECT pg_catalog.setval('public.posts_id_seq', COALESCE((SELECT MAX(id) FROM public.posts), 1), true);\n"
36
- },
37
- {
38
- "version": "00000000000004",
39
- "name": "00000000000004_default_logo_email_safe.sql",
40
- "sql": "-- Swap the seeded DEFAULT site logo from the WebP asset to a bundled PNG.\r\n--\r\n-- WebP (and AVIF) don't render in Outlook and several other email clients, so the default\r\n-- logo shown in transactional emails was a broken image out of the box. The PNG is a\r\n-- /public bundled asset (registered in resolveMediaUrl's BUNDLED_PUBLIC_MEDIA_KEYS), so it\r\n-- resolves to `<site>/images/nextblock-logo-button-tiny.png` and renders everywhere.\r\n--\r\n-- Forward-only, data-only (no schema change), idempotent, and GUARDED so it never\r\n-- overrides a logo an operator has since chosen: it only repoints the seeded default logo\r\n-- row while that row still points at the seeded WebP media.\r\n\r\nINSERT INTO public.media (\r\n id, uploader_id, file_name, object_key, file_type, size_bytes, description,\r\n width, height, blur_data_url, variants, file_path, folder, created_at, updated_at\r\n) VALUES (\r\n 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d', NULL, 'nextblock-logo-button-tiny.png',\r\n 'images/nextblock-logo-button-tiny.png', 'image/png', 10000, 'Default site logo',\r\n NULL, NULL, NULL, NULL, NULL, NULL, now(), now()\r\n) ON CONFLICT DO NOTHING;\r\n\r\nUPDATE public.logos\r\nSET media_id = 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d'\r\nWHERE id = 'd45ef03d-27fc-4099-8b46-1cdf82d658d2'\r\n AND media_id = 'ea6fdaf5-f8de-416c-9f2b-b689b7a4ed38';\r\n"
41
- },
42
- {
43
- "version": "00000000000005",
44
- "name": "00000000000005_add_custom_canonical.sql",
45
- "sql": "-- Add a nullable `custom_canonical` column to pages, posts and products.\r\n--\r\n-- This is the per-content manual canonical override used by each route's\r\n-- generateMetadata(): when set, it wins over the default self-referencing\r\n-- `<site_url>/<slug>` canonical (see app/lib/seo.ts buildCanonicalUrl). NULL/empty\r\n-- keeps the self-referencing default, so existing rows are unaffected.\r\n--\r\n-- Forward-only and idempotent (ADD COLUMN IF NOT EXISTS). No backfill: a NULL value\r\n-- is the \"use the default canonical\" sentinel.\r\n\r\nALTER TABLE public.pages ADD COLUMN IF NOT EXISTS custom_canonical text;\r\nALTER TABLE public.posts ADD COLUMN IF NOT EXISTS custom_canonical text;\r\nALTER TABLE public.products ADD COLUMN IF NOT EXISTS custom_canonical text;\r\n"
46
- },
47
- {
48
- "version": "00000000000006",
49
- "name": "00000000000006_home_live_demo_promo.sql",
50
- "sql": "-- 00000000000006_home_sandbox_promo.sql\n-- Adds a \"Live Demo\" promo section to the English (slug 'home') and French\n-- (slug 'accueil') home pages that drives visitors to the public sandbox at\n-- https://cms.nextblock.dev -- production (nextblock.dev) never mentioned it before.\n--\n-- The section is inserted right after the hero (order 1); existing home-page blocks\n-- shift down by one so relative order is preserved. Each block carries a sentinel\n-- comment (nb-sandbox-promo) inside its HTML so the sandbox reset route can strip it\n-- there: a CTA pointing at the sandbox is pointless *inside* the sandbox itself.\n--\n-- Forward-only and idempotent: the NOT EXISTS sentinel guard makes a re-run a no-op,\n-- and page lookups are by (language code, slug) so explicit IDs never collide with\n-- user-created blocks on a live database.\n\nDO $body$\nDECLARE\n v_en_lang integer;\n v_fr_lang integer;\n v_en_page integer;\n v_fr_page integer;\nBEGIN\n SELECT id INTO v_en_lang FROM public.languages WHERE code = 'en' LIMIT 1;\n SELECT id INTO v_fr_lang FROM public.languages WHERE code = 'fr' LIMIT 1;\n\n -- English home page\n SELECT id INTO v_en_page\n FROM public.pages\n WHERE slug = 'home' AND language_id = v_en_lang\n ORDER BY id\n LIMIT 1;\n\n IF v_en_page IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM public.blocks\n WHERE page_id = v_en_page AND content::text LIKE '%nb-sandbox-promo%'\n ) THEN\n UPDATE public.blocks\n SET \"order\" = \"order\" + 1\n WHERE page_id = v_en_page AND \"order\" >= 1;\n\n INSERT INTO public.blocks (page_id, language_id, block_type, content, \"order\")\n VALUES (v_en_page, v_en_lang, 'section', $en$\n{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#0b1e3d\",\"position\":50},{\"color\":\"#0e2a4d\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"column_gap\":\"xl\",\"vertical_alignment\":\"center\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<!--nb-sandbox-promo--><p class='text-xs uppercase tracking-[0.25em] text-cyan-400 font-bold mb-4'>Live Demo &middot; No Signup</p><h2 class='text-4xl md:text-5xl font-bold text-white mb-6 leading-tight'>Try the full CMS,<br/>live right now.</h2><p class='text-lg text-slate-300 max-w-xl leading-relaxed mb-8'>Don't just read about NextBlock&trade; &mdash; take the wheel. Our public sandbox is a complete, pre-loaded install where you can build pages with the block editor, manage bilingual content, and browse a working demo store. It resets every hour, so explore freely and break whatever you like.</p>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='flex flex-col sm:flex-row gap-4'><a href='https://cms.nextblock.dev' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white bg-gradient-to-r from-blue-600 to-cyan-500 hover:from-blue-500 hover:to-cyan-400 shadow-lg shadow-blue-500/25 transition-all no-underline'>Explore the Live Demo &rarr;</a><a href='https://cms.nextblock.dev/sign-in' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white border border-white/20 hover:bg-white/10 backdrop-blur-sm transition-all no-underline'>Open the CMS Dashboard &rarr;</a></div><p class='text-xs text-slate-400 mt-4'>Demo login <span class='font-mono text-slate-300'>demo@nextblock.dev</span> / <span class='font-mono text-slate-300'>password</span> &middot; Resets hourly</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='rounded-3xl overflow-hidden border border-white/10 bg-white/5 backdrop-blur-xl shadow-2xl'><div class='flex items-center gap-2 px-4 py-3 border-b border-white/10 bg-white/5'><span class='w-3 h-3 rounded-full bg-red-400/70'></span><span class='w-3 h-3 rounded-full bg-yellow-400/70'></span><span class='w-3 h-3 rounded-full bg-green-400/70'></span><span class='ml-3 flex-1 truncate rounded-md bg-black/30 px-3 py-1 text-xs font-mono text-slate-300'>cms.nextblock.dev</span></div><div class='p-6 sm:p-8'><p class='text-sm text-slate-300 mb-5'>A living NextBlock&trade; install &mdash; everything here is real and clickable:</p><ul class='space-y-4 text-sm text-slate-200'><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Full block editor</strong> with slash commands &amp; drag-and-drop.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Bilingual</strong> English / French content management.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>A complete demo storefront</strong> with live checkout.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Wipes clean every hour</strong> &mdash; always a fresh start.</span></li></ul></div></div>\"}}]]}\n$en$::jsonb, 1);\n END IF;\n\n -- French home page\n SELECT id INTO v_fr_page\n FROM public.pages\n WHERE slug = 'accueil' AND language_id = v_fr_lang\n ORDER BY id\n LIMIT 1;\n\n IF v_fr_page IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM public.blocks\n WHERE page_id = v_fr_page AND content::text LIKE '%nb-sandbox-promo%'\n ) THEN\n UPDATE public.blocks\n SET \"order\" = \"order\" + 1\n WHERE page_id = v_fr_page AND \"order\" >= 1;\n\n INSERT INTO public.blocks (page_id, language_id, block_type, content, \"order\")\n VALUES (v_fr_page, v_fr_lang, 'section', $fr$\n{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#0b1e3d\",\"position\":50},{\"color\":\"#0e2a4d\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"column_gap\":\"xl\",\"vertical_alignment\":\"center\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<!--nb-sandbox-promo--><p class='text-xs uppercase tracking-[0.25em] text-cyan-400 font-bold mb-4'>D&eacute;mo en direct &middot; Sans inscription</p><h2 class='text-4xl md:text-5xl font-bold text-white mb-6 leading-tight'>Essayez le CMS complet,<br/>en direct d&egrave;s maintenant.</h2><p class='text-lg text-slate-300 max-w-xl leading-relaxed mb-8'>Ne vous contentez pas de lire &agrave; propos de NextBlock&trade; &mdash; prenez les commandes. Notre bac &agrave; sable public est une installation compl&egrave;te et pr&eacute;charg&eacute;e o&ugrave; vous pouvez cr&eacute;er des pages avec l'&eacute;diteur de blocs, g&eacute;rer du contenu bilingue et parcourir une vraie boutique de d&eacute;monstration. Il se r&eacute;initialise chaque heure : explorez librement et cassez tout ce que vous voulez.</p>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='flex flex-col sm:flex-row gap-4'><a href='https://cms.nextblock.dev' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white bg-gradient-to-r from-blue-600 to-cyan-500 hover:from-blue-500 hover:to-cyan-400 shadow-lg shadow-blue-500/25 transition-all no-underline'>D&eacute;couvrir la d&eacute;mo &rarr;</a><a href='https://cms.nextblock.dev/sign-in' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white border border-white/20 hover:bg-white/10 backdrop-blur-sm transition-all no-underline'>Ouvrir le tableau de bord &rarr;</a></div><p class='text-xs text-slate-400 mt-4'>Identifiants d&eacute;mo <span class='font-mono text-slate-300'>demo@nextblock.dev</span> / <span class='font-mono text-slate-300'>password</span> &middot; R&eacute;initialis&eacute; chaque heure</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='rounded-3xl overflow-hidden border border-white/10 bg-white/5 backdrop-blur-xl shadow-2xl'><div class='flex items-center gap-2 px-4 py-3 border-b border-white/10 bg-white/5'><span class='w-3 h-3 rounded-full bg-red-400/70'></span><span class='w-3 h-3 rounded-full bg-yellow-400/70'></span><span class='w-3 h-3 rounded-full bg-green-400/70'></span><span class='ml-3 flex-1 truncate rounded-md bg-black/30 px-3 py-1 text-xs font-mono text-slate-300'>cms.nextblock.dev</span></div><div class='p-6 sm:p-8'><p class='text-sm text-slate-300 mb-5'>Une installation NextBlock&trade; bien vivante &mdash; tout ici est r&eacute;el et cliquable :</p><ul class='space-y-4 text-sm text-slate-200'><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>&Eacute;diteur de blocs complet</strong> : commandes slash et glisser-d&eacute;poser.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Gestion bilingue</strong> du contenu fran&ccedil;ais / anglais.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Une boutique de d&eacute;monstration compl&egrave;te</strong> avec paiement r&eacute;el.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Remis &agrave; z&eacute;ro chaque heure</strong> &mdash; toujours un nouveau d&eacute;part.</span></li></ul></div></div>\"}}]]}\n$fr$::jsonb, 1);\n END IF;\nEND\n$body$;\n"
51
- },
52
- {
53
- "version": "00000000000007",
54
- "name": "00000000000007_home_live_demo_promo_copy_fix.sql",
55
- "sql": "-- 00000000000007_home_live_demo_promo_copy_fix.sql\n-- Corrects the \"Live Demo\" promo that migration 006 seeded on the EN (slug 'home')\n-- and FR (slug 'accueil') home pages. 006 was already applied to production and the\n-- sandbox, so per the append-only rule this ships the corrections as a forward-only\n-- UPDATE instead of editing 006 (which would never re-run and would desync the file\n-- from what was actually applied).\n--\n-- Fixes: reset cadence \"hourly\" -> \"daily\" (the reset cron in vercel.json is daily,\n-- 0 3 * * *, not hourly); FR \"avec paiement reel\" -> \"un parcours de paiement complet\"\n-- (sandbox checkout is simulated); FR anglicism \"lire a propos de\" -> \"lire des\n-- articles sur\"; and an accessible WCAG-AA primary CTA (solid blue-600, was white on\n-- a low-contrast cyan gradient).\n--\n-- Idempotent: re-running sets identical content. Targets exactly the promo blocks via\n-- the nb-sandbox-promo sentinel, scoped by language. On the sandbox the promo is still\n-- stripped afterward by removeSandboxPromoSections in the reset-sandbox route.\n--\n-- NOTE: keep the word \"sandbox\" OUT of this filename -- generate-sandbox-reset.ts\n-- excludes any migration whose filename contains \"sandbox\" from the reset bundle.\n\nDO $body$\nDECLARE\n v_en_lang integer;\n v_fr_lang integer;\nBEGIN\n SELECT id INTO v_en_lang FROM public.languages WHERE code = 'en' LIMIT 1;\n SELECT id INTO v_fr_lang FROM public.languages WHERE code = 'fr' LIMIT 1;\n\n UPDATE public.blocks\n SET content = $en$\n{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#0b1e3d\",\"position\":50},{\"color\":\"#0e2a4d\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"column_gap\":\"xl\",\"vertical_alignment\":\"center\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<!--nb-sandbox-promo--><p class='text-xs uppercase tracking-[0.25em] text-cyan-400 font-bold mb-4'>Live Demo &middot; No Signup</p><h2 class='text-4xl md:text-5xl font-bold text-white mb-6 leading-tight'>Try the full CMS,<br/>live right now.</h2><p class='text-lg text-slate-300 max-w-xl leading-relaxed mb-8'>Don't just read about NextBlock&trade; &mdash; take the wheel. Our public sandbox is a complete, pre-loaded install where you can build pages with the block editor, manage bilingual content, and browse a working demo store. It resets daily, so explore freely and break whatever you like.</p>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='flex flex-col sm:flex-row gap-4'><a href='https://cms.nextblock.dev' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 shadow-lg shadow-cyan-500/20 transition-all no-underline'>Explore the Live Demo &rarr;</a><a href='https://cms.nextblock.dev/sign-in' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white border border-white/20 hover:bg-white/10 backdrop-blur-sm transition-all no-underline'>Open the CMS Dashboard &rarr;</a></div><p class='text-xs text-slate-400 mt-4'>Demo login <span class='font-mono text-slate-300'>demo@nextblock.dev</span> / <span class='font-mono text-slate-300'>password</span> &middot; Resets daily</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='rounded-3xl overflow-hidden border border-white/10 bg-white/5 backdrop-blur-xl shadow-2xl'><div class='flex items-center gap-2 px-4 py-3 border-b border-white/10 bg-white/5'><span class='w-3 h-3 rounded-full bg-red-400/70'></span><span class='w-3 h-3 rounded-full bg-yellow-400/70'></span><span class='w-3 h-3 rounded-full bg-green-400/70'></span><span class='ml-3 flex-1 truncate rounded-md bg-black/30 px-3 py-1 text-xs font-mono text-slate-300'>cms.nextblock.dev</span></div><div class='p-6 sm:p-8'><p class='text-sm text-slate-300 mb-5'>A living NextBlock&trade; install &mdash; everything here is real and clickable:</p><ul class='space-y-4 text-sm text-slate-200'><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Full block editor</strong> with slash commands &amp; drag-and-drop.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Bilingual</strong> English / French content management.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>A complete demo storefront</strong> with a full checkout flow.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Wipes clean daily</strong> &mdash; always a fresh start.</span></li></ul></div></div>\"}}]]}\n$en$::jsonb,\n updated_at = now()\n WHERE block_type = 'section'\n AND page_id IS NOT NULL\n AND language_id = v_en_lang\n AND content::text LIKE '%nb-sandbox-promo%';\n\n UPDATE public.blocks\n SET content = $fr$\n{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#0b1e3d\",\"position\":50},{\"color\":\"#0e2a4d\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"column_gap\":\"xl\",\"vertical_alignment\":\"center\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<!--nb-sandbox-promo--><p class='text-xs uppercase tracking-[0.25em] text-cyan-400 font-bold mb-4'>D&eacute;mo en direct &middot; Sans inscription</p><h2 class='text-4xl md:text-5xl font-bold text-white mb-6 leading-tight'>Essayez le CMS complet,<br/>en direct d&egrave;s maintenant.</h2><p class='text-lg text-slate-300 max-w-xl leading-relaxed mb-8'>Ne vous contentez pas de lire des articles sur NextBlock&trade; &mdash; prenez les commandes. Notre bac &agrave; sable public est une installation compl&egrave;te et pr&eacute;charg&eacute;e o&ugrave; vous pouvez cr&eacute;er des pages avec l'&eacute;diteur de blocs, g&eacute;rer du contenu bilingue et parcourir une vraie boutique de d&eacute;monstration. Il se r&eacute;initialise chaque jour : explorez librement et cassez tout ce que vous voulez.</p>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='flex flex-col sm:flex-row gap-4'><a href='https://cms.nextblock.dev' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 shadow-lg shadow-cyan-500/20 transition-all no-underline'>D&eacute;couvrir la d&eacute;mo &rarr;</a><a href='https://cms.nextblock.dev/sign-in' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white border border-white/20 hover:bg-white/10 backdrop-blur-sm transition-all no-underline'>Ouvrir le tableau de bord &rarr;</a></div><p class='text-xs text-slate-400 mt-4'>Identifiants d&eacute;mo <span class='font-mono text-slate-300'>demo@nextblock.dev</span> / <span class='font-mono text-slate-300'>password</span> &middot; R&eacute;initialis&eacute; chaque jour</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='rounded-3xl overflow-hidden border border-white/10 bg-white/5 backdrop-blur-xl shadow-2xl'><div class='flex items-center gap-2 px-4 py-3 border-b border-white/10 bg-white/5'><span class='w-3 h-3 rounded-full bg-red-400/70'></span><span class='w-3 h-3 rounded-full bg-yellow-400/70'></span><span class='w-3 h-3 rounded-full bg-green-400/70'></span><span class='ml-3 flex-1 truncate rounded-md bg-black/30 px-3 py-1 text-xs font-mono text-slate-300'>cms.nextblock.dev</span></div><div class='p-6 sm:p-8'><p class='text-sm text-slate-300 mb-5'>Une installation NextBlock&trade; bien vivante &mdash; tout ici est r&eacute;el et cliquable :</p><ul class='space-y-4 text-sm text-slate-200'><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>&Eacute;diteur de blocs complet</strong> : commandes slash et glisser-d&eacute;poser.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Gestion bilingue</strong> du contenu fran&ccedil;ais / anglais.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Une boutique de d&eacute;monstration compl&egrave;te</strong> avec un parcours de paiement complet.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Remis &agrave; z&eacute;ro chaque jour</strong> &mdash; toujours un nouveau d&eacute;part.</span></li></ul></div></div>\"}}]]}\n$fr$::jsonb,\n updated_at = now()\n WHERE block_type = 'section'\n AND page_id IS NOT NULL\n AND language_id = v_fr_lang\n AND content::text LIKE '%nb-sandbox-promo%';\nEND\n$body$;\n"
56
- },
57
- {
58
- "version": "00000000000008",
59
- "name": "00000000000008_setup_article_reorder.sql",
60
- "sql": "-- 00000000000008_setup_article_reorder.sql\r\n-- Reorders and rewrites the \"How to Install NextBlock\" setup guide on BOTH language\r\n-- twins: EN (post slug 'how-to-setup-nextblock', post_id=3 / block id=40) and FR\r\n-- (post slug 'comment-configurer-nextblock', post_id=4 / block id=41), both seeded in\r\n-- the baseline seed 00000000000003. New path order, presented as a complexity ladder\r\n-- (simplest -> most hands-on): (1) One-click Vercel, (2) npm create nextblock -> Docker\r\n-- (100% local), (3) npm create nextblock -> your own Supabase + Cloudflare R2, (4) git\r\n-- clone the monorepo (with a note that Docker works there too). The Vercel section now\r\n-- tells users to make the new GitHub repo Public so automatic updates work.\r\n--\r\n-- Forward-only and append-only: the baseline 003 is already applied everywhere and\r\n-- never replays, so this ships the new copy as an idempotent UPDATE (re-running sets\r\n-- identical content). Data-only -- no schema change. blocks.content is JSONB; written\r\n-- with PostgreSQL dollar-quoting to avoid single-quote doubling (same style as 006/007).\r\n\r\nUPDATE public.blocks\r\n SET content = $nben$\r\n{\"html_content\":\"<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock is an open-source, AI-native Next.js CMS built on Supabase — and installing it no longer involves config files, terminal wizards, or manual SQL. Below are four ways to get running, <strong>ordered from the simplest to the most hands-on</strong>: the first is a single click, the last is the full source for people who want to help build NextBlock. They all end in the same place — a browser <strong>setup wizard</strong> that connects your database, configures media storage, and creates your admin account for you.</p>\\n\\n<div class='mt-8 mb-6 flex items-center gap-4 text-[0.7rem] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>\\n <span class='flex-shrink-0'>Simplest</span>\\n <span class='h-1.5 flex-1 rounded-full bg-gradient-to-r from-blue-400 via-violet-400 to-emerald-400'></span>\\n <span class='flex-shrink-0'>Most control</span>\\n</div>\\n\\n<div class='grid gap-5 md:grid-cols-2 my-6'>\\n <a href='#one-click-vercel' class='block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 text-sm font-bold text-white'>1</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-blue-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Easiest &middot; one click</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Deploy on Vercel</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>A live production site with a managed database. No terminal, no accounts to wire up, nothing to copy.</p>\\n </a>\\n <a href='#npm-docker' class='block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-amber-500 text-sm font-bold text-white'>2</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-amber-500'></span><span class='h-1.5 w-5 rounded-full bg-amber-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Easy &middot; 100% local</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Docker</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>One prompt scaffolds a project and boots the whole stack — database, auth, storage, CMS — on your machine. No cloud accounts.</p>\\n </a>\\n <a href='#npm-cloud' class='block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-violet-600 text-sm font-bold text-white'>3</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Intermediate &middot; your own cloud</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Supabase + R2</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Scaffold a standalone app on your own managed Supabase project and Cloudflare R2, ready to deploy anywhere.</p>\\n </a>\\n <a href='#git-clone' class='block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-emerald-600 text-sm font-bold text-white'>4</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Advanced &middot; for contributors</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Clone the repository</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Run the complete monorepo — the CMS, every package, and the docs. For people who want to help build NextBlock (Docker works here too).</p>\\n </a>\\n</div>\\n\\n<p class='text-sm text-slate-500 dark:text-slate-400'>Not sure where to start? Work down the list — pick the first option whose requirements you already have. Most people should start with <a href='#one-click-vercel'>Vercel</a>.</p>\\n\\n<figure class='my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10'>\\n <img src='/images/included.webp' alt='NextBlock CMS platform overview showing the block editor, CMS dashboard, and integrations included with every installation' class='w-full h-auto object-cover' />\\n <figcaption class='border-t border-white/10 px-6 py-4 text-sm text-slate-300'>Whichever path you choose, you get the same block editor, the same CMS, and the same database schema.</figcaption>\\n</figure>\\n\\n<h2 id='one-click-vercel'>Option 1: One-Click Deploy on Vercel</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-blue-700 dark:border-blue-500/20 dark:bg-blue-500/10 dark:text-blue-200'>Step 1 &middot; Easiest</span><span class='text-slate-500 dark:text-slate-400'>Best for a live site with the least possible effort — no terminal, no accounts to wire up.</span></p>\\n<p>The fastest way to get a production NextBlock site. One button creates your own copy of NextBlock on GitHub, provisions a managed Supabase database, and deploys the site — you never open a terminal or copy a single key.</p>\\n<ol class='space-y-2'>\\n <li><strong>Click Deploy to Vercel</strong> and sign in — Vercel clones NextBlock into a new repository you own.</li>\\n <li><strong>Name the repository — and make it Public.</strong> On Vercel&rsquo;s first step you choose the repository name; set its visibility to <strong>Public</strong> rather than Private. A public repo is what unlocks one-click automatic updates later, so it&rsquo;s the recommended choice.</li>\\n <li><strong>Create the Supabase database</strong> when prompted: pick a name and a region. Vercel connects it to the project and injects the keys before the first build.</li>\\n <li><strong>Open your new site</strong> once the build finishes. Every fresh instance takes you straight to the setup wizard.</li>\\n <li><strong>Create your administrator account.</strong> It is confirmed instantly — no verification email — and you land in the CMS dashboard.</li>\\n</ol>\\n<div class='my-8'>\\n <a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' target='_blank' rel='noopener' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Deploy to Vercel &rarr;</a>\\n</div>\\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Recommended &middot; make the repo public</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>When Vercel asks you to name the new repository, choose <strong>Public</strong>. It costs nothing and it&rsquo;s what lets the dashboard&rsquo;s one-click <strong>Connect GitHub</strong> step install a daily workflow that keeps your site synced with the latest NextBlock release. You can still switch a private repo to public later in GitHub — but starting public is the smoothest path to automatic updates.</p>\\n</div>\\n<div class='rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Zero configuration</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>There are no environment variables to fill in. Media storage automatically uses your connected Supabase project, security secrets are derived for you, and database migrations run automatically on every production build. Adding a custom domain later? Set <code>NEXT_PUBLIC_URL</code> in your Vercel project and redeploy.</p>\\n</div>\\n\\n<h2 id='npm-docker'>Option 2: npm create nextblock &rarr; Docker (100% Local)</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-amber-700 dark:border-amber-500/20 dark:bg-amber-500/10 dark:text-amber-200'>Step 2 &middot; Easy</span><span class='text-slate-500 dark:text-slate-400'>Best for trying everything on your own machine with zero cloud accounts. Needs Docker Desktop.</span></p>\\n<p>Run NextBlock entirely on your own machine with no cloud accounts at all. The CLI scaffolds a standalone Next.js app and then boots the full stack in Docker for you: Supabase&rsquo;s Postgres and auth engines, a PostgREST API behind a Kong gateway, S3-compatible MinIO storage, and the CMS itself — ideal for evaluations, air-gapped environments, and anyone who wants complete data ownership.</p>\\n<p>Before you start, install <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20 or newer</a> (it includes npm) and <a href='https://www.docker.com/products/docker-desktop/' target='_blank' rel='noopener'>Docker Desktop</a>, and make sure Docker Desktop is running.</p>\\n<pre><code>npm create nextblock@latest my-site</code></pre>\\n<ol class='space-y-2'>\\n <li><strong>Choose the hosting profile.</strong> At the first prompt, pick <em>Local Self-Hosted Docker Mode (One-Click Local Sandbox)</em>, then confirm that Docker Desktop is installed and running.</li>\\n <li><strong>Let it run.</strong> The CLI copies the template, installs dependencies, generates secure keys, and boots the whole stack with a single Docker command — no questions asked. The first run pulls images and builds the app, so give it a few minutes; every database migration is applied automatically as the stack comes up.</li>\\n <li><strong>Open <code>http://localhost:3000</code></strong> — it redirects to the setup wizard. Because the database and MinIO storage are already wired up, the connection and media-storage steps are skipped: the only thing left is creating your administrator (confirmed instantly, no email required). You land in the CMS dashboard.</li>\\n</ol>\\n<div class='rounded-3xl border border-amber-200 bg-amber-50/80 p-6 my-8 dark:border-amber-500/20 dark:bg-amber-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Day-2 commands</p>\\n <pre class='mt-4 mb-0'><code># rebuild and restart the stack\\nnpm run docker:up\\n\\n# stop the stack (your data persists in Docker volumes)\\nnpm run docker:down\\n\\n# follow the application logs\\nnpm run docker:logs</code></pre>\\n <p class='mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200'>Docker mode is offered only in the interactive prompt — don&rsquo;t pass <code>--yes</code>, which forces the managed-cloud path below.</p>\\n</div>\\n\\n<h2 id='npm-cloud'>Option 3: npm create nextblock &rarr; Your Own Supabase + Cloudflare</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-violet-200 bg-violet-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-violet-700 dark:border-violet-500/20 dark:bg-violet-500/10 dark:text-violet-200'>Step 3 &middot; Intermediate</span><span class='text-slate-500 dark:text-slate-400'>Best for building your own site on managed cloud. Needs a Supabase project and an R2 bucket.</span></p>\\n<p>The best starting point for building your own site on managed cloud. The CLI scaffolds a standalone Next.js application with NextBlock already wired in — no monorepo, no workspace tooling — pointed at a Supabase project and Cloudflare R2 bucket you control, and it deploys anywhere Next.js runs.</p>\\n<p>Before you start, install <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20 or newer</a>, create a free project at <a href='https://supabase.com' target='_blank' rel='noopener'>supabase.com</a>, and set up a <a href='https://developers.cloudflare.com/r2/' target='_blank' rel='noopener'>Cloudflare R2</a> bucket for your images and files.</p>\\n<pre><code>npm create nextblock@latest my-site\\ncd my-site\\nnpm run dev</code></pre>\\n<p>At the first prompt, choose <em>Managed Cloud Mode (Vercel + Supabase Cloud)</em> and name your project. Then open <code>http://localhost:3000/setup</code> and let the wizard take over:</p>\\n<ol class='space-y-2'>\\n <li><strong>Connect Supabase</strong> — paste your project URL, publishable (anon) key, secret (service role) key, and a personal access token so the wizard can apply the database schema for you.</li>\\n <li><strong>Add Cloudflare R2</strong> — enter your R2 account ID, bucket name, access key ID, secret access key, and the bucket&rsquo;s public URL for serving images and files.</li>\\n <li><strong>Create your administrator</strong> — the wizard applies every migration, generates the app secrets, writes <code>.env.local</code>, creates your confirmed admin account, and signs you in. Restart <code>npm run dev</code> once afterwards so the fresh environment is baked into the app.</li>\\n</ol>\\n<div class='rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Premium modules</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Need a store? One command adds products, checkout, orders, and coupons — license-gated and ready when you are: <code>npx create-nextblock activate ecommerce</code></p>\\n</div>\\n\\n<h2 id='git-clone'>Option 4: Clone the Repository</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-200'>Step 4 &middot; Advanced</span><span class='text-slate-500 dark:text-slate-400'>Best for contributors and teams who want to customize the platform itself. Needs Node.js and git.</span></p>\\n<p>Run the full Nx monorepo: the CMS application, every shared package, the CLI source, and the documentation. This is the path for people who want to take part in the project — contributors, plugin authors, and teams that customize the platform itself.</p>\\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\\ncd nextblock\\nnpm install\\nnpx nx serve nextblock</code></pre>\\n<p>Open <code>http://localhost:4200</code> — a fresh install redirects every page to <code>/setup</code>, where the same three-step wizard connects Supabase, configures storage, and creates your admin. It validates your keys, writes <code>.env.local</code> with generated secrets, and applies all migrations over the Supabase Management API — no Supabase CLI required.</p>\\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Prefer to stay local? Use Docker here too</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>You don&rsquo;t need cloud accounts to develop against the monorepo. With Docker Desktop running, one command boots the same self-hosted stack as Option 2 — Postgres, auth, storage, and the CMS — straight from your clone:</p>\\n <pre class='mt-4 mb-0'><code>npm run docker:setup</code></pre>\\n <p class='mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200'>When it finishes, open <code>http://localhost:3000</code> and create your administrator. Note the monorepo has no <code>npm run dev</code> — use <code>npx nx serve nextblock</code> (port 4200) for the cloud path.</p>\\n</div>\\n\\n<h2 id='after-install'>After You Install: Your First 10 Minutes</h2>\\n<p>Every path drops you at <code>/cms/dashboard</code>, signed in as the first administrator. A built-in onboarding checklist walks you through the rest:</p>\\n<ul class='space-y-2'>\\n <li><strong>Add your branding</strong> — upload your logo and set the site title.</li>\\n <li><strong>Set your footer</strong> — copyright line and footer navigation.</li>\\n <li><strong>Configure email (SMTP)</strong> — under Settings, so password resets and invitations can send.</li>\\n <li><strong>Optional extras</strong> — connect analytics, enable bot protection, and (on Vercel) turn on automatic updates.</li>\\n</ul>\\n<p>From there, see how the platform fits together in <a href='/article/how-nextblock-works'>How NextBlock Works</a>, add a storefront with the <a href='/article/nextblock-commerce-guide'>Commerce guide</a>, or meet your AI copilot in the <a href='/article/nextblock-cortex-ai-guide'>Cortex AI guide</a>.</p>\\n\\n<h2 id='faq'>Installation FAQ</h2>\\n<h3>What do I need installed?</h3>\\n<p>Nothing for the Vercel path — it runs entirely in the browser. For <code>npm create nextblock</code> in Docker mode: <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20+</a> and Docker Desktop. For the managed-cloud path: Node.js 20+ plus a Supabase project and a Cloudflare R2 bucket. For the cloned repository: Node.js 20+ and git (add Docker Desktop if you want to run the local stack).</p>\\n<h3>Is NextBlock free?</h3>\\n<p>Yes — the core of NextBlock is a 100% free, open-source CMS (AGPL). Premium packages such as e-commerce and Cortex AI are optional and activate with a license key. Both Vercel and Supabase offer free tiers, so a starter site can run at no cost.</p>\\n<h3>Do I need a Supabase account?</h3>\\n<p>On Vercel, the database is created for you during the deploy. For the managed-cloud <code>npm create nextblock</code> path and the cloned repository you bring a free Supabase project. With Docker — whether through the CLI&rsquo;s Docker mode or <code>npm run docker:setup</code> in the clone — you need no cloud accounts at all.</p>\\n<h3>Do I have to run migrations or SQL by hand?</h3>\\n<p>No. The setup wizard, the Vercel build, and the Docker stack all apply the database schema automatically — and re-running is always safe.</p>\\n<h3>Can I switch paths later?</h3>\\n<p>Yes. Every path runs the same application and the same database schema, so you can prototype locally with Docker today and deploy to Vercel tomorrow. NextBlock deploys like any standard Next.js app.</p>\\n<h3>How do I update NextBlock?</h3>\\n<p>On Vercel, the Connect GitHub onboarding step enables a daily automatic sync with upstream (this needs your repository to be public). On a cloned repository, <code>git pull</code>, run <code>npm run db:migrate</code>, then restart (on production builds, pending migrations apply automatically). With Docker, pull the latest code and run <code>npm run docker:up</code>.</p>\\n\\n<div class='rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5'>\\n <p class='mt-0 text-2xl font-semibold text-slate-900 dark:text-white'>Ready to launch?</p>\\n <p class='text-sm text-slate-600 dark:text-slate-300'>Pick your path above, or jump straight to the fastest one.</p>\\n <div class='mt-5 flex flex-wrap justify-center gap-3'>\\n <a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' target='_blank' rel='noopener' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Deploy on Vercel</a>\\n <a href='https://github.com/nextblock-cms/nextblock' target='_blank' rel='noopener' class='inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50'>View on GitHub</a>\\n </div>\\n</div>\"}\r\n$nben$::jsonb,\r\n updated_at = now()\r\n WHERE post_id = 3\r\n AND block_type = 'text';\r\n\r\nUPDATE public.blocks\r\n SET content = $nbfr$\r\n{\"html_content\":\"<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock est un CMS open source et natif IA, construit sur Next.js et Supabase — et son installation ne passe plus par des fichiers de configuration, des assistants en ligne de commande ou du SQL manuel. Voici quatre façons de démarrer, <strong>classées de la plus simple à la plus technique</strong> : la première tient en un clic, la dernière donne le code source complet pour celles et ceux qui veulent participer à NextBlock. Elles aboutissent toutes au même endroit — un <strong>assistant de configuration</strong> dans le navigateur qui connecte votre base de données, configure le stockage des médias et crée votre compte administrateur.</p>\\n\\n<div class='mt-8 mb-6 flex items-center gap-4 text-[0.7rem] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>\\n <span class='flex-shrink-0'>Le plus simple</span>\\n <span class='h-1.5 flex-1 rounded-full bg-gradient-to-r from-blue-400 via-violet-400 to-emerald-400'></span>\\n <span class='flex-shrink-0'>Le plus de contrôle</span>\\n</div>\\n\\n<div class='grid gap-5 md:grid-cols-2 my-6'>\\n <a href='#one-click-vercel' class='block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 text-sm font-bold text-white'>1</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-blue-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Le plus simple &middot; un clic</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Déployer sur Vercel</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Un site de production en ligne avec une base de données gérée. Pas de terminal, aucun compte à configurer, rien à copier.</p>\\n </a>\\n <a href='#npm-docker' class='block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-amber-500 text-sm font-bold text-white'>2</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-amber-500'></span><span class='h-1.5 w-5 rounded-full bg-amber-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Facile &middot; 100 % local</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Docker</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Un seul prompt génère un projet et démarre toute la pile — base de données, auth, stockage, CMS — sur votre machine. Aucun compte cloud.</p>\\n </a>\\n <a href='#npm-cloud' class='block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-violet-600 text-sm font-bold text-white'>3</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Intermédiaire &middot; votre propre cloud</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Supabase + R2</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Générez une app autonome sur votre propre projet Supabase géré et Cloudflare R2, prête à déployer partout.</p>\\n </a>\\n <a href='#git-clone' class='block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-emerald-600 text-sm font-bold text-white'>4</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Avancé &middot; pour les contributeurs</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Cloner le dépôt</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Faites tourner le monorepo complet — le CMS, tous les packages et la documentation. Pour celles et ceux qui veulent aider à construire NextBlock (Docker fonctionne ici aussi).</p>\\n </a>\\n</div>\\n\\n<p class='text-sm text-slate-500 dark:text-slate-400'>Vous ne savez pas par où commencer ? Descendez la liste — choisissez la première option dont vous avez déjà les prérequis. La plupart des gens devraient commencer par <a href='#one-click-vercel'>Vercel</a>.</p>\\n\\n<figure class='my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10'>\\n <img src='/images/included.webp' alt='Aperçu de la plateforme NextBlock : éditeur de blocs, tableau de bord CMS et intégrations incluses dans chaque installation' class='w-full h-auto object-cover' />\\n <figcaption class='border-t border-white/10 px-6 py-4 text-sm text-slate-300'>Quel que soit le chemin choisi, vous obtenez le même éditeur de blocs, le même CMS et le même schéma de base de données.</figcaption>\\n</figure>\\n\\n<h2 id='one-click-vercel'>Option 1 : Déploiement Vercel en un clic</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-blue-700 dark:border-blue-500/20 dark:bg-blue-500/10 dark:text-blue-200'>Étape 1 &middot; Le plus simple</span><span class='text-slate-500 dark:text-slate-400'>Idéal pour un site en ligne avec le minimum d'effort — pas de terminal, aucun compte à configurer.</span></p>\\n<p>Le moyen le plus rapide d'obtenir un site NextBlock en production. Un seul bouton crée votre propre copie de NextBlock sur GitHub, provisionne une base de données Supabase gérée et déploie le site — sans jamais ouvrir un terminal ni copier la moindre clé.</p>\\n<ol class='space-y-2'>\\n <li><strong>Cliquez sur Deploy to Vercel</strong> et connectez-vous — Vercel clone NextBlock dans un nouveau dépôt qui vous appartient.</li>\\n <li><strong>Nommez le dépôt — et rendez-le Public.</strong> À la première étape sur Vercel, vous choisissez le nom du dépôt ; réglez sa visibilité sur <strong>Public</strong> plutôt que Privé. Un dépôt public est ce qui débloque les mises à jour automatiques en un clic plus tard — c'est donc le choix recommandé.</li>\\n <li><strong>Créez la base de données Supabase</strong> quand on vous le demande : choisissez un nom et une région. Vercel la connecte au projet et injecte les clés avant le premier build.</li>\\n <li><strong>Ouvrez votre nouveau site</strong> une fois le build terminé. Toute nouvelle instance vous amène directement à l'assistant de configuration.</li>\\n <li><strong>Créez votre compte administrateur.</strong> Il est confirmé instantanément — aucun email de vérification — et vous arrivez dans le tableau de bord du CMS.</li>\\n</ol>\\n<div class='my-8'>\\n <a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' target='_blank' rel='noopener' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Déployer sur Vercel &rarr;</a>\\n</div>\\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Recommandé &middot; rendez le dépôt public</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Quand Vercel vous demande de nommer le nouveau dépôt, choisissez <strong>Public</strong>. C'est gratuit, et c'est ce qui permet à l'étape <strong>Connect GitHub</strong> du tableau de bord d'installer un workflow quotidien qui garde votre site synchronisé avec la dernière version de NextBlock. Vous pourrez toujours passer un dépôt privé en public plus tard dans GitHub — mais démarrer en public est le chemin le plus simple vers les mises à jour automatiques.</p>\\n</div>\\n<div class='rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Zéro configuration</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Aucune variable d'environnement à remplir. Le stockage des médias utilise automatiquement votre projet Supabase connecté, les secrets de sécurité sont dérivés pour vous, et les migrations de base de données s'exécutent automatiquement à chaque build de production. Un domaine personnalisé plus tard ? Définissez <code>NEXT_PUBLIC_URL</code> dans votre projet Vercel et redéployez.</p>\\n</div>\\n\\n<h2 id='npm-docker'>Option 2 : npm create nextblock &rarr; Docker (100 % local)</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-amber-700 dark:border-amber-500/20 dark:bg-amber-500/10 dark:text-amber-200'>Étape 2 &middot; Facile</span><span class='text-slate-500 dark:text-slate-400'>Idéal pour tout essayer sur votre machine sans aucun compte cloud. Nécessite Docker Desktop.</span></p>\\n<p>Faites tourner NextBlock entièrement sur votre machine, sans aucun compte cloud. Le CLI génère une application Next.js autonome puis démarre toute la pile dans Docker pour vous : les moteurs Postgres et auth de Supabase, une API PostgREST derrière une passerelle Kong, un stockage MinIO compatible S3 et le CMS lui-même — idéal pour les évaluations, les environnements isolés et la pleine propriété de vos données.</p>\\n<p>Avant de commencer, installez <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20 ou plus récent</a> (npm inclus) et <a href='https://www.docker.com/products/docker-desktop/' target='_blank' rel='noopener'>Docker Desktop</a>, et assurez-vous que Docker Desktop est démarré.</p>\\n<pre><code>npm create nextblock@latest mon-site</code></pre>\\n<ol class='space-y-2'>\\n <li><strong>Choisissez le profil d'hébergement.</strong> Au premier prompt, choisissez <em>Local Self-Hosted Docker Mode (One-Click Local Sandbox)</em>, puis confirmez que Docker Desktop est installé et démarré.</li>\\n <li><strong>Laissez faire.</strong> Le CLI copie le template, installe les dépendances, génère des clés sécurisées et démarre toute la pile avec une seule commande Docker — sans aucune question. Le premier lancement télécharge les images et construit l'app, comptez donc quelques minutes ; chaque migration de base de données est appliquée automatiquement au démarrage de la pile.</li>\\n <li><strong>Ouvrez <code>http://localhost:3000</code></strong> — vous êtes redirigé vers l'assistant de configuration. Comme la base de données et le stockage MinIO sont déjà connectés, les étapes de connexion et de stockage sont ignorées : il ne reste qu'à créer votre administrateur (confirmé instantanément, sans email). Vous arrivez dans le tableau de bord du CMS.</li>\\n</ol>\\n<div class='rounded-3xl border border-amber-200 bg-amber-50/80 p-6 my-8 dark:border-amber-500/20 dark:bg-amber-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Commandes du quotidien</p>\\n <pre class='mt-4 mb-0'><code># reconstruire et redémarrer la pile\\nnpm run docker:up\\n\\n# arrêter la pile (vos données persistent dans les volumes Docker)\\nnpm run docker:down\\n\\n# suivre les logs de l'application\\nnpm run docker:logs</code></pre>\\n <p class='mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200'>Le mode Docker n'est proposé que dans le prompt interactif — ne passez pas <code>--yes</code>, qui force le mode cloud géré ci-dessous.</p>\\n</div>\\n\\n<h2 id='npm-cloud'>Option 3 : npm create nextblock &rarr; votre propre Supabase + Cloudflare</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-violet-200 bg-violet-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-violet-700 dark:border-violet-500/20 dark:bg-violet-500/10 dark:text-violet-200'>Étape 3 &middot; Intermédiaire</span><span class='text-slate-500 dark:text-slate-400'>Idéal pour construire votre propre site sur du cloud géré. Nécessite un projet Supabase et un bucket R2.</span></p>\\n<p>Le meilleur point de départ pour construire votre propre site sur du cloud géré. Le CLI génère une application Next.js autonome avec NextBlock déjà intégré — sans monorepo ni outillage de workspace — reliée à un projet Supabase et un bucket Cloudflare R2 que vous contrôlez, et elle se déploie partout où Next.js tourne.</p>\\n<p>Avant de commencer, installez <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20 ou plus récent</a>, créez un projet gratuit sur <a href='https://supabase.com' target='_blank' rel='noopener'>supabase.com</a>, et configurez un bucket <a href='https://developers.cloudflare.com/r2/' target='_blank' rel='noopener'>Cloudflare R2</a> pour vos images et fichiers.</p>\\n<pre><code>npm create nextblock@latest mon-site\\ncd mon-site\\nnpm run dev</code></pre>\\n<p>Au premier prompt, choisissez <em>Managed Cloud Mode (Vercel + Supabase Cloud)</em> et nommez votre projet. Ouvrez ensuite <code>http://localhost:3000/setup</code> et laissez l'assistant faire le travail :</p>\\n<ol class='space-y-2'>\\n <li><strong>Connectez Supabase</strong> — collez l'URL du projet, la clé publiable (anon), la clé secrète (service role) et un jeton d'accès personnel pour que l'assistant applique le schéma de base de données à votre place.</li>\\n <li><strong>Ajoutez Cloudflare R2</strong> — saisissez votre identifiant de compte R2, le nom du bucket, la clé d'accès (access key ID), la clé secrète et l'URL publique du bucket pour servir vos images et fichiers.</li>\\n <li><strong>Créez votre administrateur</strong> — l'assistant applique toutes les migrations, génère les secrets de l'application, écrit <code>.env.local</code>, crée votre compte admin confirmé et vous connecte. Redémarrez ensuite <code>npm run dev</code> une fois pour que le nouvel environnement soit intégré à l'application.</li>\\n</ol>\\n<div class='rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Modules premium</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Besoin d'une boutique ? Une seule commande ajoute produits, paiement, commandes et coupons — activés par clé de licence, prêts quand vous l'êtes : <code>npx create-nextblock activate ecommerce</code></p>\\n</div>\\n\\n<h2 id='git-clone'>Option 4 : Cloner le dépôt</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-200'>Étape 4 &middot; Avancé</span><span class='text-slate-500 dark:text-slate-400'>Idéal pour les contributeurs et les équipes qui veulent personnaliser la plateforme. Nécessite Node.js et git.</span></p>\\n<p>Faites tourner le monorepo Nx complet : l'application CMS, tous les packages partagés, le code du CLI et la documentation. C'est le chemin de celles et ceux qui veulent participer au projet — contributeurs, auteurs de plugins et équipes qui personnalisent la plateforme elle-même.</p>\\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\\ncd nextblock\\nnpm install\\nnpx nx serve nextblock</code></pre>\\n<p>Ouvrez <code>http://localhost:4200</code> — une nouvelle installation redirige chaque page vers <code>/setup</code>, où le même assistant en trois étapes connecte Supabase, configure le stockage et crée votre admin. Il valide vos clés, écrit <code>.env.local</code> avec des secrets générés, et applique toutes les migrations via l'API de management Supabase — sans CLI Supabase.</p>\\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Vous préférez rester local ? Docker fonctionne ici aussi</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Pas besoin de comptes cloud pour développer sur le monorepo. Avec Docker Desktop démarré, une seule commande lance la même pile auto-hébergée que l'option 2 — Postgres, auth, stockage et le CMS — directement depuis votre clone :</p>\\n <pre class='mt-4 mb-0'><code>npm run docker:setup</code></pre>\\n <p class='mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200'>Quand c'est terminé, ouvrez <code>http://localhost:3000</code> et créez votre administrateur. Notez que le monorepo n'a pas de <code>npm run dev</code> — utilisez <code>npx nx serve nextblock</code> (port 4200) pour le chemin cloud.</p>\\n</div>\\n\\n<h2 id='after-install'>Après l'installation : vos 10 premières minutes</h2>\\n<p>Chaque chemin vous dépose sur <code>/cms/dashboard</code>, connecté en tant que premier administrateur. Une checklist de démarrage intégrée vous guide pour la suite :</p>\\n<ul class='space-y-2'>\\n <li><strong>Ajoutez votre identité visuelle</strong> — téléversez votre logo et définissez le titre du site.</li>\\n <li><strong>Réglez votre pied de page</strong> — mention de copyright et navigation du pied de page.</li>\\n <li><strong>Configurez l'email (SMTP)</strong> — dans les réglages, pour que les réinitialisations de mot de passe et les invitations partent bien.</li>\\n <li><strong>Extras optionnels</strong> — connectez vos outils d'analytics, activez la protection anti-bots et (sur Vercel) les mises à jour automatiques.</li>\\n</ul>\\n<p>Ensuite, découvrez comment la plateforme s'articule dans <a href='/article/comment-nextblock-fonctionne'>Comment NextBlock fonctionne</a>, ou ajoutez une boutique avec le <a href='/article/guide-commerce-nextblock'>guide Commerce</a>.</p>\\n\\n<h2 id='faq'>FAQ d'installation</h2>\\n<h3>Que dois-je installer ?</h3>\\n<p>Rien pour le chemin Vercel — tout se passe dans le navigateur. Pour <code>npm create nextblock</code> en mode Docker : <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20+</a> et Docker Desktop. Pour le mode cloud géré : Node.js 20+ ainsi qu'un projet Supabase et un bucket Cloudflare R2. Pour le dépôt cloné : Node.js 20+ et git (ajoutez Docker Desktop si vous voulez faire tourner la pile locale).</p>\\n<h3>NextBlock est-il gratuit ?</h3>\\n<p>Oui — le cœur du CMS est 100 % gratuit et open source (AGPL). Les packages premium comme l'e-commerce et Cortex AI sont optionnels et s'activent avec une clé de licence. Vercel et Supabase proposent chacun une offre gratuite : un site de départ peut donc tourner sans frais.</p>\\n<h3>Ai-je besoin d'un compte Supabase ?</h3>\\n<p>Sur Vercel, la base de données est créée pour vous pendant le déploiement. Pour le chemin cloud géré <code>npm create nextblock</code> et le dépôt cloné, il vous faut un projet Supabase gratuit. Avec Docker — via le mode Docker du CLI ou <code>npm run docker:setup</code> dans le clone — aucun compte cloud n'est nécessaire.</p>\\n<h3>Dois-je exécuter des migrations ou du SQL à la main ?</h3>\\n<p>Non. L'assistant de configuration, le build Vercel et la pile Docker appliquent tous le schéma de base de données automatiquement — et relancer l'opération est toujours sans risque.</p>\\n<h3>Puis-je changer de chemin plus tard ?</h3>\\n<p>Oui. Chaque chemin exécute la même application et le même schéma de base de données : vous pouvez prototyper en local avec Docker aujourd'hui et déployer sur Vercel demain. NextBlock se déploie comme n'importe quelle app Next.js.</p>\\n<h3>Comment mettre à jour NextBlock ?</h3>\\n<p>Sur Vercel, l'étape Connect GitHub de la checklist active une synchronisation quotidienne automatique (cela nécessite un dépôt public). Sur un dépôt cloné, <code>git pull</code>, lancez <code>npm run db:migrate</code>, puis redémarrez (lors des builds de production, les migrations en attente s'appliquent automatiquement). Avec Docker, récupérez le dernier code et lancez <code>npm run docker:up</code>.</p>\\n\\n<div class='rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5'>\\n <p class='mt-0 text-2xl font-semibold text-slate-900 dark:text-white'>Prêt à vous lancer ?</p>\\n <p class='text-sm text-slate-600 dark:text-slate-300'>Choisissez votre chemin ci-dessus, ou passez directement au plus rapide.</p>\\n <div class='mt-5 flex flex-wrap justify-center gap-3'>\\n <a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' target='_blank' rel='noopener' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Déployer sur Vercel</a>\\n <a href='https://github.com/nextblock-cms/nextblock' target='_blank' rel='noopener' class='inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50'>Voir sur GitHub</a>\\n </div>\\n</div>\"}\r\n$nbfr$::jsonb,\r\n updated_at = now()\r\n WHERE post_id = 4\r\n AND block_type = 'text';\r\n"
61
- },
62
- {
63
- "version": "00000000000009",
64
- "name": "00000000000009_home_live_demo_promo_contrast.sql",
65
- "sql": "-- 00000000000009_home_live_demo_promo_contrast.sql\n-- Restyles the \"Live Demo\" promo that migrations 006/007 seeded on the EN (slug 'home')\n-- and FR (slug 'accueil') home pages. 006/007 gave it a fixed dark-navy GRADIENT section\n-- background -- and a gradient background is an inline style, so it renders identically\n-- in light and dark theme. Sitting directly under the (also dark) hero, that made the\n-- first two sections both dark in light mode and killed the old dark-hero -> white-second\n-- contrast rhythm.\n--\n-- Fix (append-only, do NOT edit 006/007 -- both already applied to prod + sandbox):\n-- switch the section background to \"none\" (transparent -> inherits the theme-aware page\n-- background: light in light theme, dark in dark theme) and render the promo as a single\n-- self-contained ELEVATED PANEL. The panel is a soft light card in light theme and keeps\n-- the rich navy look in dark theme (dark: gradient), with the browser mockup deliberately\n-- kept dark in both themes as a \"product screenshot\" focal point. The <!--nb-sandbox-promo-->\n-- sentinel is preserved so the reset-sandbox route still strips it on cms.nextblock.dev.\n--\n-- Idempotent: re-running sets identical content. Targets exactly the promo blocks via the\n-- nb-sandbox-promo sentinel, scoped by language (mirrors 00000000000007). blocks.content is\n-- JSONB, written with dollar-quoting.\n--\n-- NOTE: keep the word \"sandbox\" OUT of this filename -- generate-sandbox-reset.ts excludes\n-- any migration whose filename contains \"sandbox\" from the sandbox reset bundle.\n\nDO $body$\nDECLARE\n v_en_lang integer;\n v_fr_lang integer;\nBEGIN\n SELECT id INTO v_en_lang FROM public.languages WHERE code = 'en' LIMIT 1;\n SELECT id INTO v_fr_lang FROM public.languages WHERE code = 'fr' LIMIT 1;\n\n UPDATE public.blocks\n SET content = $en$\n{\"container_type\":\"container\",\"background\":{\"type\":\"none\"},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"column_gap\":\"xl\",\"vertical_alignment\":\"center\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<!--nb-sandbox-promo--><div class='relative overflow-hidden rounded-[2.5rem] border border-slate-200 bg-gradient-to-br from-white via-slate-50 to-blue-50 shadow-2xl shadow-slate-900/10 dark:border-white/10 dark:from-slate-900 dark:via-slate-900 dark:to-blue-950 dark:shadow-black/40'><div class='grid gap-10 p-8 sm:p-12 lg:grid-cols-2 lg:items-center'><div><p class='text-xs uppercase tracking-[0.25em] text-cyan-600 dark:text-cyan-400 font-bold mb-4'>Live Demo &middot; No Signup</p><h2 class='text-4xl md:text-5xl font-bold text-slate-900 dark:text-white mb-6 leading-tight'>Try the full CMS,<br/>live right now.</h2><p class='text-lg text-slate-600 dark:text-slate-300 max-w-xl leading-relaxed mb-8'>Don't just read about NextBlock&trade; &mdash; take the wheel. Our public sandbox is a complete, pre-loaded install where you can build pages with the block editor, manage bilingual content, and browse a working demo store. It resets daily, so explore freely and break whatever you like.</p><div class='flex flex-col sm:flex-row gap-4'><a href='https://cms.nextblock.dev' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 shadow-lg shadow-blue-500/25 transition-all no-underline'>Explore the Live Demo &rarr;</a><a href='https://cms.nextblock.dev/sign-in' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-slate-700 dark:text-white border border-slate-300 dark:border-white/20 hover:bg-slate-100 dark:hover:bg-white/10 transition-all no-underline'>Open the CMS Dashboard &rarr;</a></div><p class='text-xs text-slate-500 dark:text-slate-400 mt-4'>Demo login <span class='font-mono text-slate-700 dark:text-slate-300'>demo@nextblock.dev</span> / <span class='font-mono text-slate-700 dark:text-slate-300'>password</span> &middot; Resets daily</p></div><div class='rounded-3xl overflow-hidden border border-slate-800 bg-slate-900 shadow-xl shadow-slate-900/20 dark:border-white/10 dark:bg-white/5 dark:backdrop-blur-xl'><div class='flex items-center gap-2 px-4 py-3 border-b border-white/10 bg-white/5'><span class='w-3 h-3 rounded-full bg-red-400/70'></span><span class='w-3 h-3 rounded-full bg-yellow-400/70'></span><span class='w-3 h-3 rounded-full bg-green-400/70'></span><span class='ml-3 flex-1 truncate rounded-md bg-black/30 px-3 py-1 text-xs font-mono text-slate-300'>cms.nextblock.dev</span></div><div class='p-6 sm:p-8'><p class='text-sm text-slate-300 mb-5'>A living NextBlock&trade; install &mdash; everything here is real and clickable:</p><ul class='space-y-4 text-sm text-slate-200'><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Full block editor</strong> with slash commands &amp; drag-and-drop.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Bilingual</strong> English / French content management.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>A complete demo storefront</strong> with a full checkout flow.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Wipes clean daily</strong> &mdash; always a fresh start.</span></li></ul></div></div></div></div>\"}}]]}\n$en$::jsonb,\n updated_at = now()\n WHERE block_type = 'section'\n AND page_id IS NOT NULL\n AND language_id = v_en_lang\n AND content::text LIKE '%nb-sandbox-promo%';\n\n UPDATE public.blocks\n SET content = $fr$\n{\"container_type\":\"container\",\"background\":{\"type\":\"none\"},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"column_gap\":\"xl\",\"vertical_alignment\":\"center\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<!--nb-sandbox-promo--><div class='relative overflow-hidden rounded-[2.5rem] border border-slate-200 bg-gradient-to-br from-white via-slate-50 to-blue-50 shadow-2xl shadow-slate-900/10 dark:border-white/10 dark:from-slate-900 dark:via-slate-900 dark:to-blue-950 dark:shadow-black/40'><div class='grid gap-10 p-8 sm:p-12 lg:grid-cols-2 lg:items-center'><div><p class='text-xs uppercase tracking-[0.25em] text-cyan-600 dark:text-cyan-400 font-bold mb-4'>D&eacute;mo en direct &middot; Sans inscription</p><h2 class='text-4xl md:text-5xl font-bold text-slate-900 dark:text-white mb-6 leading-tight'>Essayez le CMS complet,<br/>en direct d&egrave;s maintenant.</h2><p class='text-lg text-slate-600 dark:text-slate-300 max-w-xl leading-relaxed mb-8'>Ne vous contentez pas de lire des articles sur NextBlock&trade; &mdash; prenez les commandes. Notre bac &agrave; sable public est une installation compl&egrave;te et pr&eacute;charg&eacute;e o&ugrave; vous pouvez cr&eacute;er des pages avec l'&eacute;diteur de blocs, g&eacute;rer du contenu bilingue et parcourir une vraie boutique de d&eacute;monstration. Il se r&eacute;initialise chaque jour : explorez librement et cassez tout ce que vous voulez.</p><div class='flex flex-col sm:flex-row gap-4'><a href='https://cms.nextblock.dev' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 shadow-lg shadow-blue-500/25 transition-all no-underline'>D&eacute;couvrir la d&eacute;mo &rarr;</a><a href='https://cms.nextblock.dev/sign-in' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-slate-700 dark:text-white border border-slate-300 dark:border-white/20 hover:bg-slate-100 dark:hover:bg-white/10 transition-all no-underline'>Ouvrir le tableau de bord &rarr;</a></div><p class='text-xs text-slate-500 dark:text-slate-400 mt-4'>Identifiants d&eacute;mo <span class='font-mono text-slate-700 dark:text-slate-300'>demo@nextblock.dev</span> / <span class='font-mono text-slate-700 dark:text-slate-300'>password</span> &middot; R&eacute;initialis&eacute; chaque jour</p></div><div class='rounded-3xl overflow-hidden border border-slate-800 bg-slate-900 shadow-xl shadow-slate-900/20 dark:border-white/10 dark:bg-white/5 dark:backdrop-blur-xl'><div class='flex items-center gap-2 px-4 py-3 border-b border-white/10 bg-white/5'><span class='w-3 h-3 rounded-full bg-red-400/70'></span><span class='w-3 h-3 rounded-full bg-yellow-400/70'></span><span class='w-3 h-3 rounded-full bg-green-400/70'></span><span class='ml-3 flex-1 truncate rounded-md bg-black/30 px-3 py-1 text-xs font-mono text-slate-300'>cms.nextblock.dev</span></div><div class='p-6 sm:p-8'><p class='text-sm text-slate-300 mb-5'>Une installation NextBlock&trade; bien vivante &mdash; tout ici est r&eacute;el et cliquable :</p><ul class='space-y-4 text-sm text-slate-200'><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>&Eacute;diteur de blocs complet</strong> : commandes slash et glisser-d&eacute;poser.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Gestion bilingue</strong> du contenu fran&ccedil;ais / anglais.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Une boutique de d&eacute;monstration compl&egrave;te</strong> avec un parcours de paiement complet.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Remis &agrave; z&eacute;ro chaque jour</strong> &mdash; toujours un nouveau d&eacute;part.</span></li></ul></div></div></div></div>\"}}]]}\n$fr$::jsonb,\n updated_at = now()\n WHERE block_type = 'section'\n AND page_id IS NOT NULL\n AND language_id = v_fr_lang\n AND content::text LIKE '%nb-sandbox-promo%';\nEND\n$body$;\n"
66
- },
67
- {
68
- "version": "00000000000010",
69
- "name": "00000000000010_drop_github_username.sql",
70
- "sql": "-- Drop the github_username profile column and remove the GitHub-auth artifacts.\r\n--\r\n-- GitHub OAuth sign-in / account-linking was removed from the app: it cannot be\r\n-- provisioned as part of a one-click deployment (each install needs its own GitHub\r\n-- OAuth app + client secret + Supabase's manual-linking toggle, none of which can\r\n-- ship in a migration). github_username was only ever populated from that OAuth\r\n-- flow, so the column and its seed strings are now dead.\r\n--\r\n-- NOTE: the self-update \"Connect GitHub\" flow (lib/updates/*, ConnectGitHubButton)\r\n-- is a separate feature and is intentionally left untouched — the connect_github\r\n-- translation is kept for it.\r\n\r\n-- 1. Rewrite handle_new_user() so new signups no longer read/populate github_username.\r\nCREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS trigger\r\n LANGUAGE plpgsql SECURITY DEFINER\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n admin_flag_set boolean := false;\r\n user_role public.user_role;\r\n v_full_name text;\r\n v_avatar_url text;\r\nBEGIN\r\n INSERT INTO public.site_settings (key, value)\r\n VALUES ('is_admin_created', 'false'::jsonb)\r\n ON CONFLICT (key) DO NOTHING;\r\n\r\n SELECT COALESCE(value::jsonb::boolean, false)\r\n INTO admin_flag_set\r\n FROM public.site_settings\r\n WHERE key = 'is_admin_created'\r\n FOR UPDATE;\r\n\r\n IF admin_flag_set = false THEN\r\n user_role := 'ADMIN'::public.user_role;\r\n\r\n UPDATE public.site_settings\r\n SET value = 'true'::jsonb\r\n WHERE key = 'is_admin_created';\r\n ELSE\r\n user_role := 'USER'::public.user_role;\r\n END IF;\r\n\r\n v_full_name := NEW.raw_user_meta_data->>'full_name';\r\n v_avatar_url := NEW.raw_user_meta_data->>'avatar_url';\r\n\r\n INSERT INTO public.profiles (\r\n id,\r\n role,\r\n full_name,\r\n avatar_url\r\n )\r\n VALUES (\r\n NEW.id,\r\n user_role,\r\n v_full_name,\r\n v_avatar_url\r\n )\r\n ON CONFLICT (id) DO UPDATE SET\r\n full_name = EXCLUDED.full_name,\r\n avatar_url = EXCLUDED.avatar_url;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\n-- 2. Drop the column now that nothing writes it.\r\nALTER TABLE public.profiles DROP COLUMN IF EXISTS github_username;\r\n\r\n-- 3. Remove the GitHub-auth-only translation strings. connect_github is intentionally\r\n-- kept — it is still used by the self-update \"Connect GitHub\" button.\r\nDELETE FROM public.translations\r\nWHERE key IN (\r\n 'continue_with_github',\r\n 'github_username',\r\n 'github_username_help',\r\n 'github_link_failed',\r\n 'github_connected'\r\n);\r\n"
71
- },
72
- {
73
- "version": "00000000000011",
74
- "name": "00000000000011_language_detection_admin_only.sql",
75
- "sql": "-- Restrict writes to the language-detection settings row to ADMIN only.\r\n--\r\n-- `site_settings.language_detection_settings` is a non-sensitive, anon-READABLE\r\n-- key (the request proxy reads it with the anon client to pick a visitor's first\r\n-- language). The CMS surfaces it under /cms/settings (ADMIN-only) and the server\r\n-- action guards with an ADMIN check, but the baseline write policies let ADMIN\r\n-- *or* WRITER write any non-sensitive key — so a WRITER could change site-wide\r\n-- detection directly via PostgREST. This migration adds the key to the ADMIN-only\r\n-- write group (INSERT/UPDATE/DELETE) to match the UI boundary, while leaving the\r\n-- SELECT policy untouched so the proxy's anon read keeps working.\r\n--\r\n-- Forward-only; recreates the three write policies idempotently.\r\n\r\nDROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;\r\nCREATE POLICY site_settings_insert_policy ON public.site_settings FOR INSERT TO authenticated WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nDROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;\r\nCREATE POLICY site_settings_update_policy ON public.site_settings FOR UPDATE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)))) WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nDROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;\r\nCREATE POLICY site_settings_delete_policy ON public.site_settings FOR DELETE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n"
76
- },
77
- {
78
- "version": "00000000000012",
79
- "name": "00000000000012_cortex_ai_stock_photo_settings.sql",
80
- "sql": "-- Protect the Cortex AI stock-photo provider API keys stored in site_settings.\r\n--\r\n-- The stock-photo search tool (search_stock_photos) can read a Pexels or Unsplash\r\n-- API key from site_settings so the key lives in the database instead of an env var.\r\n-- Those two rows are SECRETS and must never be publicly readable: like the OpenRouter\r\n-- BYOK key, they belong in the sensitive-keys group that only authenticated ADMINs can\r\n-- read or write. The baseline site_settings SELECT policy makes any key NOT in that\r\n-- group anon-readable, so we add the two stock-photo keys to every policy's sensitive\r\n-- array. The value stored is an encrypted envelope, but we protect the row regardless.\r\n--\r\n-- Forward-only; recreates all four site_settings policies idempotently, preserving the\r\n-- existing sensitive keys (including language_detection_settings on the write policies,\r\n-- which stays anon-READABLE and is therefore not added to the SELECT policy).\r\n\r\nDROP POLICY IF EXISTS site_settings_read_policy ON public.site_settings;\r\nCREATE POLICY site_settings_read_policy ON public.site_settings FOR SELECT USING (((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) AND (( SELECT auth.role() AS role) = 'authenticated'::text) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nDROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;\r\nCREATE POLICY site_settings_insert_policy ON public.site_settings FOR INSERT TO authenticated WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nDROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;\r\nCREATE POLICY site_settings_update_policy ON public.site_settings FOR UPDATE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)))) WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nDROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;\r\nCREATE POLICY site_settings_delete_policy ON public.site_settings FOR DELETE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n"
81
- },
82
- {
83
- "version": "00000000000013",
84
- "name": "00000000000013_youtube_nocookie_embeds.sql",
85
- "sql": "-- 00000000000013_youtube_nocookie_embeds.sql\r\n--\r\n-- Lighthouse Best Practices scored 96 instead of 100: the `inspector-issues` audit\r\n-- (binary, weight 1 of 27 -> 26/27 = 96) failed with a DevTools \"Cookie\" issue\r\n-- attributed to www.youtube.com/embed/71MyfoL4YVM. The seeded home-page hero and the\r\n-- seeded articles embed raw <iframe> markup pointing at www.youtube.com, whose player\r\n-- writes cookies on load.\r\n--\r\n-- The real fix is the render-time click-to-play facade added in\r\n-- apps/nextblock/components/media/YouTubeFacade.tsx (the nocookie host does NOT stop\r\n-- the player's document.cookie writes -- only not loading the player does). This\r\n-- migration is the data-hygiene half: it rewrites every stored embed to the\r\n-- privacy-enhanced host so any surface that still renders a raw iframe (published-lib\r\n-- renderers, CMS previews, exports, downstream installs on an older app build) is at\r\n-- least cookie-reduced, and so the stored content matches what we ship.\r\n--\r\n-- It also fixes the bogus Permissions Policy feature in the seeded allow attribute:\r\n-- 'accelerated-motion' is not a real feature (Chrome logs \"Unrecognized feature\");\r\n-- the canonical YouTube embed uses 'accelerometer'.\r\n--\r\n-- Append-only: 00000000000003_baseline_seed.sql is applied everywhere and is NOT edited.\r\n-- Scoped by CONTENT MATCH, never by block id -- ids drift on installs where the demo\r\n-- content was re-created (see the convention documented in 00000000000006:11-13 and\r\n-- used by 007/009). This is a pure host-substring swap, so it never clobbers a user's\r\n-- edited copy, it also repairs author-pasted embeds, and it is naturally idempotent\r\n-- (a second run matches zero rows) which matters because the nightly sandbox reset\r\n-- replays the whole migration chain from a truncated history.\r\n--\r\n-- No dollar-quoting is needed here: neither search nor replacement literal contains a\r\n-- single quote, and neither contains a JSON metacharacter, so the ::text round-trip\r\n-- always re-parses as valid JSON. Query strings (?si=...) are preserved.\r\n--\r\n-- NOTE: keep the word \"sandbox\" OUT of this filename -- generate-sandbox-reset.ts\r\n-- excludes any migration whose filename contains \"sandbox\".\r\n\r\n-- 1. Live page/post block content (the 4 seeded rows + any author-added embed).\r\nUPDATE public.blocks\r\n SET content = replace(\r\n replace(content::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE content IS NOT NULL\r\n AND (content::text LIKE '%www.youtube.com/embed/%'\r\n OR content::text LIKE '%accelerated-motion%');\r\n\r\n-- 2. Live Draft Mode snapshots. Publishing a pre-existing draft would otherwise write\r\n-- the cookie host straight back into public.blocks.\r\nUPDATE public.content_drafts\r\n SET blocks = replace(\r\n replace(blocks::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE blocks::text LIKE '%www.youtube.com/embed/%'\r\n OR blocks::text LIKE '%accelerated-motion%';\r\n\r\nUPDATE public.content_drafts\r\n SET meta = replace(\r\n replace(meta::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE meta::text LIKE '%www.youtube.com/embed/%'\r\n OR meta::text LIKE '%accelerated-motion%';\r\n\r\nUPDATE public.product_drafts\r\n SET blocks = replace(\r\n replace(blocks::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE blocks::text LIKE '%www.youtube.com/embed/%'\r\n OR blocks::text LIKE '%accelerated-motion%';\r\n\r\n-- product_drafts.meta carries short_description / description_json.\r\nUPDATE public.product_drafts\r\n SET meta = replace(\r\n replace(meta::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE meta::text LIKE '%www.youtube.com/embed/%'\r\n OR meta::text LIKE '%accelerated-motion%';\r\n\r\n-- 3. Revision history. apps/nextblock/app/cms/revisions/service.ts replays a snapshot\r\n-- verbatim, so one \"Restore version\" click would otherwise reintroduce the issue.\r\n-- URL-only rewrite; nothing else about the historical snapshot is touched.\r\nUPDATE public.page_revisions\r\n SET content = replace(\r\n replace(content::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE content::text LIKE '%www.youtube.com/embed/%'\r\n OR content::text LIKE '%accelerated-motion%';\r\n\r\nUPDATE public.post_revisions\r\n SET content = replace(\r\n replace(content::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE content::text LIKE '%www.youtube.com/embed/%'\r\n OR content::text LIKE '%accelerated-motion%';\r\n\r\n-- 4. Product rich text rendered on public /product/* routes.\r\nUPDATE public.products\r\n SET short_description = replace(\r\n replace(short_description, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )\r\n WHERE short_description IS NOT NULL\r\n AND (short_description LIKE '%www.youtube.com/embed/%'\r\n OR short_description LIKE '%accelerated-motion%');\r\n\r\nUPDATE public.products\r\n SET description_json = replace(\r\n replace(description_json::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE description_json IS NOT NULL\r\n AND (description_json::text LIKE '%www.youtube.com/embed/%'\r\n OR description_json::text LIKE '%accelerated-motion%');\r\n\r\n-- 5. Custom block definitions (rich-text defaults / emptyFallback markup rendered by\r\n-- DynamicLayoutEngine). Host-only swap, so the is_valid_custom_block_* CHECK\r\n-- constraints still hold. No-op on a fresh install.\r\nUPDATE public.custom_block_definitions\r\n SET fields = replace(\r\n replace(fields::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE fields::text LIKE '%www.youtube.com/embed/%'\r\n OR fields::text LIKE '%accelerated-motion%';\r\n\r\nUPDATE public.custom_block_definitions\r\n SET layout_schema = replace(\r\n replace(layout_schema::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE layout_schema::text LIKE '%www.youtube.com/embed/%'\r\n OR layout_schema::text LIKE '%accelerated-motion%';\r\n"
86
- },
87
- {
88
- "version": "00000000000014",
89
- "name": "00000000000014_site_themes.sql",
90
- "sql": "-- Editable site themes.\n--\n-- Themes used to be hardcoded CSS classes in libs/ui/src/styles/theme.css\n-- (:root / .dark / .vibrant) with the switcher list duplicated in\n-- apps/nextblock/app/providers.tsx and components/theme-switcher.tsx. This moves\n-- the palette into the database so an ADMIN can retint the site, add themes and\n-- remove them from /cms/settings/global-css without a redeploy.\n--\n-- Rendering: apps/nextblock/lib/themes/buildThemeCss.ts turns each row into a\n-- `:root.<slug> { --token: value; ... }` rule injected into <head> by\n-- app/layout.tsx. The `:root.x` form is two-class specificity, so generated\n-- themes always beat the (0,1,0) fallback rules still shipped in theme.css for\n-- consumers of the published @nextblock-cms/ui package.\n--\n-- Forward-only and idempotent.\n\nCREATE TABLE IF NOT EXISTS public.site_themes (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n slug text NOT NULL,\n name text NOT NULL,\n description text,\n -- lucide-react icon name rendered by the theme switcher.\n icon text NOT NULL DEFAULT 'Palette',\n -- Drives the CSS `color-scheme` property and decides whether the theme also\n -- carries Tailwind's `.dark` class so `dark:` variants resolve correctly.\n color_scheme text NOT NULL DEFAULT 'light',\n -- Flat map of design token -> raw CSS value, keys WITHOUT the leading `--`,\n -- e.g. {\"background\": \"0 0% 100%\", \"radius\": \"0.75rem\"}.\n tokens jsonb NOT NULL DEFAULT '{}'::jsonb,\n -- Optional per-theme CSS, emitted nested inside the theme rule so it is\n -- automatically scoped. Authors use the `&` nesting selector,\n -- e.g. `& h1 { text-shadow: 0 0 5px hsl(var(--primary)); }`.\n extra_css text,\n -- System themes cannot be deleted: next-themes' `enableSystem` resolves to\n -- 'light' or 'dark', so those two slugs must always exist.\n is_system boolean DEFAULT false NOT NULL,\n is_default boolean DEFAULT false NOT NULL,\n is_active boolean DEFAULT true NOT NULL,\n sort_order integer DEFAULT 0 NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT site_themes_pkey PRIMARY KEY (id),\n CONSTRAINT site_themes_slug_key UNIQUE (slug),\n CONSTRAINT site_themes_color_scheme_check CHECK ((color_scheme = ANY (ARRAY['light'::text, 'dark'::text]))),\n -- The slug becomes a CSS class and a next-themes value; keep it safe for both.\n CONSTRAINT site_themes_slug_format_check CHECK ((slug ~ '^[a-z][a-z0-9-]{0,38}[a-z0-9]$')),\n CONSTRAINT site_themes_tokens_is_object_check CHECK ((jsonb_typeof(tokens) = 'object'))\n);\n\nCOMMENT ON TABLE public.site_themes IS 'Editable colour themes. Each row renders to a `:root.<slug>` CSS rule injected by the root layout. Publicly readable (anonymous visitors need the palette); only ADMIN may write.';\n\nCREATE INDEX IF NOT EXISTS site_themes_active_sort_idx ON public.site_themes USING btree (is_active, sort_order);\n\n-- Exactly one default theme.\nCREATE UNIQUE INDEX IF NOT EXISTS site_themes_single_default_idx ON public.site_themes USING btree (is_default) WHERE (is_default = true);\n\nDROP TRIGGER IF EXISTS set_site_themes_updated_at ON public.site_themes;\nCREATE TRIGGER set_site_themes_updated_at\n BEFORE UPDATE ON public.site_themes\n FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\n\n-- A system theme must never be deleted, whoever asks.\nCREATE OR REPLACE FUNCTION public.prevent_system_theme_delete() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path = ''\n AS $$\nBEGIN\n IF OLD.is_system THEN\n RAISE EXCEPTION 'Theme \"%\" is a system theme and cannot be deleted', OLD.slug\n USING ERRCODE = 'restrict_violation';\n END IF;\n RETURN OLD;\nEND;\n$$;\n\nDROP TRIGGER IF EXISTS trg_prevent_system_theme_delete ON public.site_themes;\nCREATE TRIGGER trg_prevent_system_theme_delete\n BEFORE DELETE ON public.site_themes\n FOR EACH ROW EXECUTE FUNCTION public.prevent_system_theme_delete();\n\n-- Promoting a theme to default demotes the previous one, so the unique partial\n-- index above can never trip on a normal \"make this the default\" write.\nCREATE OR REPLACE FUNCTION public.handle_default_theme_change() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path = ''\n AS $$\nBEGIN\n IF NEW.is_default THEN\n UPDATE public.site_themes\n SET is_default = false\n WHERE id <> NEW.id AND is_default;\n END IF;\n RETURN NEW;\nEND;\n$$;\n\nDROP TRIGGER IF EXISTS trg_handle_default_theme_change ON public.site_themes;\nCREATE TRIGGER trg_handle_default_theme_change\n AFTER INSERT OR UPDATE OF is_default ON public.site_themes\n FOR EACH ROW WHEN (NEW.is_default) EXECUTE FUNCTION public.handle_default_theme_change();\n\nALTER TABLE public.site_themes ENABLE ROW LEVEL SECURITY;\n\nGRANT ALL ON TABLE public.site_themes TO anon;\nGRANT ALL ON TABLE public.site_themes TO authenticated;\nGRANT ALL ON TABLE public.site_themes TO service_role;\n\nDROP POLICY IF EXISTS \"Public read active themes\" ON public.site_themes;\nCREATE POLICY \"Public read active themes\" ON public.site_themes\n FOR SELECT TO authenticated, anon USING (true);\n\nDROP POLICY IF EXISTS \"Admins insert themes\" ON public.site_themes;\nCREATE POLICY \"Admins insert themes\" ON public.site_themes\n FOR INSERT TO authenticated\n WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins update themes\" ON public.site_themes;\nCREATE POLICY \"Admins update themes\" ON public.site_themes\n FOR UPDATE TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role))\n WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins delete themes\" ON public.site_themes;\nCREATE POLICY \"Admins delete themes\" ON public.site_themes\n FOR DELETE TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Service role manages themes\" ON public.site_themes;\nCREATE POLICY \"Service role manages themes\" ON public.site_themes\n TO service_role USING (true) WITH CHECK (true);\n\n-- Seed the three shipped themes from libs/ui/src/styles/theme.css.\n-- `--warning` / `--warning-foreground` are declared in the Tailwind theme\n-- (libs/ui/tailwind.config.js) but were never defined in CSS, so bg-warning and\n-- text-warning resolved to an invalid colour. They are given real values here.\nINSERT INTO public.site_themes (slug, name, description, icon, color_scheme, is_system, is_default, sort_order, tokens, extra_css)\nVALUES\n (\n 'light', 'Light', 'Clean, technical, stark.', 'Sun', 'light', true, true, 10,\n '{\n \"background\": \"0 0% 100%\",\n \"foreground\": \"222 47% 11%\",\n \"card\": \"0 0% 100%\",\n \"card-foreground\": \"222 47% 11%\",\n \"popover\": \"0 0% 100%\",\n \"popover-foreground\": \"222 47% 11%\",\n \"primary\": \"211.55 50.26% 37.84%\",\n \"primary-foreground\": \"210 40% 98%\",\n \"secondary\": \"210 40% 96.1%\",\n \"secondary-foreground\": \"222 47% 11%\",\n \"muted\": \"210 40% 96.1%\",\n \"muted-foreground\": \"215 16% 47%\",\n \"accent\": \"210 40% 96.1%\",\n \"accent-foreground\": \"222 47% 11%\",\n \"destructive\": \"0 84.2% 60.2%\",\n \"destructive-foreground\": \"210 40% 98%\",\n \"warning\": \"38 92% 50%\",\n \"warning-foreground\": \"222 47% 11%\",\n \"border\": \"214.3 31.8% 91.4%\",\n \"input\": \"214.3 31.8% 91.4%\",\n \"ring\": \"211.55 50.26% 37.84%\",\n \"radius\": \"0.75rem\",\n \"chart-1\": \"211.55 50.26% 37.84%\",\n \"chart-2\": \"215 16% 47%\",\n \"chart-3\": \"215 25% 27%\",\n \"chart-4\": \"210 40% 96%\",\n \"chart-5\": \"214 32% 91%\"\n }'::jsonb,\n NULL\n ),\n (\n 'dark', 'Dark', 'Midnight / neon tech.', 'Moon', 'dark', true, false, 20,\n '{\n \"background\": \"222 47% 2%\",\n \"foreground\": \"210 40% 98%\",\n \"card\": \"222 47% 11%\",\n \"card-foreground\": \"210 40% 98%\",\n \"popover\": \"222 47% 11%\",\n \"popover-foreground\": \"210 40% 98%\",\n \"primary\": \"217 91% 60%\",\n \"primary-foreground\": \"222 47% 11%\",\n \"secondary\": \"217.2 32.6% 17.5%\",\n \"secondary-foreground\": \"210 40% 98%\",\n \"muted\": \"217.2 32.6% 17.5%\",\n \"muted-foreground\": \"215 20.2% 65.1%\",\n \"accent\": \"217.2 32.6% 17.5%\",\n \"accent-foreground\": \"210 40% 98%\",\n \"destructive\": \"0 62.8% 30.6%\",\n \"destructive-foreground\": \"210 40% 98%\",\n \"warning\": \"38 92% 50%\",\n \"warning-foreground\": \"222 47% 11%\",\n \"border\": \"217.2 32.6% 17.5%\",\n \"input\": \"217.2 32.6% 17.5%\",\n \"ring\": \"224 76% 48%\",\n \"radius\": \"0.75rem\",\n \"chart-1\": \"220 70% 50%\",\n \"chart-2\": \"160 60% 45%\",\n \"chart-3\": \"30 80% 55%\",\n \"chart-4\": \"280 65% 60%\",\n \"chart-5\": \"340 75% 55%\"\n }'::jsonb,\n NULL\n ),\n (\n 'vibrant', 'Vibrant', 'Cyberpunk neon.', 'Zap', 'dark', false, false, 30,\n '{\n \"background\": \"260 50% 5%\",\n \"foreground\": \"180 100% 90%\",\n \"card\": \"260 50% 8%\",\n \"card-foreground\": \"180 100% 90%\",\n \"popover\": \"260 50% 8%\",\n \"popover-foreground\": \"180 100% 90%\",\n \"primary\": \"320 100% 55%\",\n \"primary-foreground\": \"0 0% 100%\",\n \"secondary\": \"180 100% 50%\",\n \"secondary-foreground\": \"260 50% 5%\",\n \"muted\": \"260 30% 15%\",\n \"muted-foreground\": \"260 20% 65%\",\n \"accent\": \"280 100% 50%\",\n \"accent-foreground\": \"0 0% 100%\",\n \"destructive\": \"0 100% 50%\",\n \"destructive-foreground\": \"0 0% 100%\",\n \"warning\": \"60 100% 50%\",\n \"warning-foreground\": \"260 50% 5%\",\n \"border\": \"320 100% 55%\",\n \"input\": \"260 30% 15%\",\n \"ring\": \"320 100% 55%\",\n \"radius\": \"0px\",\n \"chart-1\": \"320 100% 55%\",\n \"chart-2\": \"180 100% 50%\",\n \"chart-3\": \"280 100% 50%\",\n \"chart-4\": \"60 100% 50%\",\n \"chart-5\": \"120 100% 50%\"\n }'::jsonb,\n '& h1, & h2, & h3, & h4, & h5, & h6 {\n text-shadow: 0 0 5px hsl(var(--primary)), 0 0 10px hsl(var(--secondary));\n}\n& button, & [role=\"button\"] {\n box-shadow: 0 0 5px hsl(var(--primary) / 0.5);\n transition: box-shadow 0.3s ease;\n}\n& button:hover, & [role=\"button\"]:hover {\n box-shadow: 0 0 15px hsl(var(--primary));\n}\n& .card, & [class*=\"card\"] {\n border: 1px solid hsl(var(--primary));\n box-shadow: 0 0 10px hsl(var(--primary) / 0.2);\n}\n& .border {\n border-color: hsl(var(--border));\n box-shadow: 0 0 5px hsl(var(--border) / 0.3);\n}'\n )\nON CONFLICT (slug) DO NOTHING;\n"
91
- },
92
- {
93
- "version": "00000000000015",
94
- "name": "00000000000015_scheduled_publishing.sql",
95
- "sql": "-- Scheduled publishing for pages and products.\n--\n-- Posts already support scheduling: `posts.published_at` exists and every public\n-- read gates on `published_at IS NULL OR published_at <= now()`, so a row with\n-- status='published' and a future date is withheld until the date passes. Pages\n-- and products had no equivalent column, so \"go live on Tuesday\" was impossible\n-- for them. This adds the same column with the same semantics.\n--\n-- Visibility is derived from the (status, published_at) PAIR — no new enum value:\n--\n-- status = draft/archived -> not public, whatever the date\n-- status = published|active, published_at NULL -> public now\n-- status = published|active, date <= now() -> public now\n-- status = published|active, date > now() -> SCHEDULED (withheld)\n--\n-- Deriving \"scheduled\" instead of storing it keeps `page_status` unchanged (adding\n-- an enum value can't be done inside a transaction with other DDL in Postgres) and\n-- matches what posts have always done, so one code path covers all three types.\n--\n-- NULL is the safe default: every existing published row keeps rendering exactly as\n-- before, so this migration needs no backfill and changes no current behavior.\n--\n-- Forward-only and idempotent.\n\nALTER TABLE public.pages\n ADD COLUMN IF NOT EXISTS published_at timestamp with time zone;\n\nCOMMENT ON COLUMN public.pages.published_at IS\n 'Optional go-live moment. NULL = live as soon as status is published. A future value withholds the page from public reads until it passes (status stays \"published\"; the CMS renders that pair as \"Scheduled\").';\n\nALTER TABLE public.products\n ADD COLUMN IF NOT EXISTS published_at timestamp with time zone;\n\nCOMMENT ON COLUMN public.products.published_at IS\n 'Optional go-live moment. NULL = live as soon as status is active. A future value withholds the product from public reads until it passes (status stays \"active\"; the CMS renders that pair as \"Scheduled\").';\n\n-- Public listing/index queries filter on the (status, published_at) pair together\n-- (catalog, sitemap, page lookups), so a composite index serves them in one pass.\nCREATE INDEX IF NOT EXISTS pages_status_published_at_idx\n ON public.pages (status, published_at);\n\nCREATE INDEX IF NOT EXISTS products_status_published_at_idx\n ON public.products (status, published_at);\n"
96
- },
97
- {
98
- "version": "00000000000016",
99
- "name": "00000000000016_product_revisions_and_revision_baseline.sql",
100
- "sql": "-- 00000000000016_product_revisions_and_revision_baseline.sql\n--\n-- Revision History, part 1 of 2 (schema). The application-side rewrite lives in\n-- apps/nextblock/app/cms/revisions/**.\n--\n-- Three things happen here:\n--\n-- 1. products.version — the monotonic counter the hybrid revision engine drives,\n-- mirroring pages.version / posts.version.\n--\n-- 2. product_revisions — a structural mirror of page_revisions / post_revisions.\n-- product_id is uuid (products.id is uuid, not bigint), and\n-- writes are gated on is_admin() to match products_*_policy\n-- rather than the ADMIN|WRITER pattern the page/post revision\n-- tables use. A WRITER who could insert a revision but not\n-- apply a restore would get a silent no-op restore, because\n-- PostgREST returns no error for an UPDATE matching zero rows.\n--\n-- 3. Revision baseline — every page, post and product gets a real `snapshot` row to\n-- restore to. Until now the CMS synthesised a fake \"Initial\n-- Version\" entry in the UI whose Restore button resolved to\n-- \"current metadata + zero blocks\" and wiped the content.\n-- There is now an actual stored baseline instead.\n--\n-- Case A (version = 1, no revisions at all): the live row IS\n-- version 1. This covers seeded content — 00000000000003\n-- inserts every page and post at version 1 and writes no\n-- revision rows — and everything authored since the CMS save\n-- path stopped recording revisions. Snapshotting it at\n-- version 1 is what makes \"restore the original seeded page\"\n-- real for the first time.\n--\n-- Case B (version > 1 but no snapshot at or below it): the\n-- true v1 is unrecoverable and is NOT fabricated. A snapshot\n-- of the current state is stored at the current version so the\n-- diff chain has a valid base and future restores resolve.\n--\n-- Forward-only, idempotent, and it modifies no existing row: every backfill is an\n-- INSERT ... WHERE NOT EXISTS ... ON CONFLICT DO NOTHING.\n\n-- ---------------------------------------------------------------------------\n-- 1. products.version\n-- ---------------------------------------------------------------------------\n\nALTER TABLE public.products\n ADD COLUMN IF NOT EXISTS version integer DEFAULT 1 NOT NULL;\n\nCOMMENT ON COLUMN public.products.version IS 'Monotonic version number for hybrid revisions.';\n\n-- ---------------------------------------------------------------------------\n-- 2. product_revisions\n-- ---------------------------------------------------------------------------\n\nCREATE TABLE IF NOT EXISTS public.product_revisions (\n id bigint NOT NULL,\n product_id uuid NOT NULL,\n author_id uuid,\n version integer NOT NULL,\n revision_type public.revision_type NOT NULL,\n content jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.product_revisions IS 'Hybrid (snapshot/diff) revisions for products.';\nCOMMENT ON COLUMN public.product_revisions.content IS 'If snapshot: full content; if diff: JSON Patch array.';\n\nDO $rb$\nBEGIN\n IF NOT EXISTS (\n SELECT 1 FROM pg_attribute\n WHERE attrelid = 'public.product_revisions'::regclass\n AND attname = 'id'\n AND attidentity <> ''\n ) THEN\n ALTER TABLE public.product_revisions\n ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\n SEQUENCE NAME public.product_revisions_id_seq\n START WITH 1\n INCREMENT BY 1\n NO MINVALUE\n NO MAXVALUE\n CACHE 1\n );\n END IF;\nEND $rb$;\n\nDO $rb$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint\n WHERE conname = 'product_revisions_pkey'\n AND conrelid = 'public.product_revisions'::regclass) THEN\n ALTER TABLE ONLY public.product_revisions\n ADD CONSTRAINT product_revisions_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint\n WHERE conname = 'product_revisions_product_version_key'\n AND conrelid = 'public.product_revisions'::regclass) THEN\n ALTER TABLE ONLY public.product_revisions\n ADD CONSTRAINT product_revisions_product_version_key UNIQUE (product_id, version);\n END IF;\nEND $rb$;\n\nDO $rb$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint\n WHERE conname = 'product_revisions_author_id_fkey'\n AND conrelid = 'public.product_revisions'::regclass) THEN\n ALTER TABLE ONLY public.product_revisions\n ADD CONSTRAINT product_revisions_author_id_fkey\n FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint\n WHERE conname = 'product_revisions_product_id_fkey'\n AND conrelid = 'public.product_revisions'::regclass) THEN\n ALTER TABLE ONLY public.product_revisions\n ADD CONSTRAINT product_revisions_product_id_fkey\n FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nCREATE INDEX IF NOT EXISTS idx_product_revisions_author_id\n ON public.product_revisions USING btree (author_id);\n\nCREATE INDEX IF NOT EXISTS idx_product_revisions_product_id_version\n ON public.product_revisions USING btree (product_id, version);\n\nALTER TABLE public.product_revisions ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS product_revisions_read_policy ON public.product_revisions;\nCREATE POLICY product_revisions_read_policy ON public.product_revisions\n FOR SELECT TO authenticated USING (true);\n\nDROP POLICY IF EXISTS product_revisions_insert_policy ON public.product_revisions;\nCREATE POLICY product_revisions_insert_policy ON public.product_revisions\n FOR INSERT TO authenticated\n WITH CHECK (((SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS product_revisions_update_policy ON public.product_revisions;\nCREATE POLICY product_revisions_update_policy ON public.product_revisions\n FOR UPDATE TO authenticated\n USING (((SELECT public.is_admin() AS is_admin) IS TRUE))\n WITH CHECK (((SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS product_revisions_delete_policy ON public.product_revisions;\nCREATE POLICY product_revisions_delete_policy ON public.product_revisions\n FOR DELETE TO authenticated\n USING (((SELECT public.is_admin() AS is_admin) IS TRUE));\n\nGRANT ALL ON TABLE public.product_revisions TO anon;\nGRANT ALL ON TABLE public.product_revisions TO authenticated;\nGRANT ALL ON TABLE public.product_revisions TO service_role;\nGRANT ALL ON SEQUENCE public.product_revisions_id_seq TO anon;\nGRANT ALL ON SEQUENCE public.product_revisions_id_seq TO authenticated;\nGRANT ALL ON SEQUENCE public.product_revisions_id_seq TO service_role;\n\n-- ---------------------------------------------------------------------------\n-- 3. Revision baseline backfill\n--\n-- The JSON shape must match FullPageContent / FullPostContent / FullProductContent\n-- in apps/nextblock/app/cms/revisions/utils.ts exactly, or the first diff taken\n-- against a baseline row will be full of phantom operations.\n--\n-- Timestamps are rendered with an explicit millisecond-precision UTC format so they\n-- match JavaScript's Date#toISOString() (\"2026-07-03T17:52:15.643Z\"). Postgres'\n-- default jsonb rendering of timestamptz (\"2026-07-03T17:52:15.643901+00:00\") would\n-- differ from the value the application writes and produce a spurious diff on the\n-- very next save.\n-- ---------------------------------------------------------------------------\n\n-- 3a. Pages\nINSERT INTO public.page_revisions (page_id, author_id, version, revision_type, content)\nSELECT\n p.id,\n NULL::uuid,\n p.version,\n 'snapshot'::public.revision_type,\n jsonb_build_object(\n 'meta', jsonb_build_object(\n 'title', p.title,\n 'slug', p.slug,\n 'language_id', p.language_id,\n 'status', p.status,\n 'meta_title', p.meta_title,\n 'meta_description', p.meta_description,\n 'custom_canonical', p.custom_canonical,\n 'published_at', to_char(p.published_at AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'),\n 'feature_image_id', p.feature_image_id\n ),\n 'blocks', COALESCE((\n SELECT jsonb_agg(\n jsonb_build_object(\n 'language_id', b.language_id,\n 'block_type', b.block_type,\n 'content', b.content,\n 'order', b.\"order\"\n ) ORDER BY b.\"order\" ASC, b.id ASC\n )\n FROM public.blocks b\n WHERE b.page_id = p.id\n ), '[]'::jsonb)\n )\n FROM public.pages p\n WHERE NOT EXISTS (\n SELECT 1 FROM public.page_revisions r\n WHERE r.page_id = p.id\n AND r.revision_type = 'snapshot'\n AND r.version <= p.version\n )\nON CONFLICT (page_id, version) DO NOTHING;\n\n-- 3b. Posts\nINSERT INTO public.post_revisions (post_id, author_id, version, revision_type, content)\nSELECT\n po.id,\n NULL::uuid,\n po.version,\n 'snapshot'::public.revision_type,\n jsonb_build_object(\n 'meta', jsonb_build_object(\n 'title', po.title,\n 'slug', po.slug,\n 'language_id', po.language_id,\n 'status', po.status,\n 'meta_title', po.meta_title,\n 'meta_description', po.meta_description,\n 'custom_canonical', po.custom_canonical,\n 'published_at', to_char(po.published_at AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'),\n 'feature_image_id', po.feature_image_id,\n 'label', po.label,\n 'excerpt', po.excerpt,\n 'subtitle', po.subtitle\n ),\n 'blocks', COALESCE((\n SELECT jsonb_agg(\n jsonb_build_object(\n 'language_id', b.language_id,\n 'block_type', b.block_type,\n 'content', b.content,\n 'order', b.\"order\"\n ) ORDER BY b.\"order\" ASC, b.id ASC\n )\n FROM public.blocks b\n WHERE b.post_id = po.id\n ), '[]'::jsonb)\n )\n FROM public.posts po\n WHERE NOT EXISTS (\n SELECT 1 FROM public.post_revisions r\n WHERE r.post_id = po.id\n AND r.revision_type = 'snapshot'\n AND r.version <= po.version\n )\nON CONFLICT (post_id, version) DO NOTHING;\n\n-- 3c. Products.\n--\n-- Content only. price/prices/sale_*/scheduled_*/stock/sku/average_rating/total_reviews\n-- are deliberately excluded from the snapshot: pricing and inventory are mutated from\n-- outside the editor (promotions, Freemius sync, order fulfilment), ratings are derived\n-- aggregates, and inventory_items is keyed by bare SKU text with no FK to products — so\n-- replaying commerce state on restore would reach rows the editor never touched.\n-- Restoring a product restores its content, not its commerce state.\nINSERT INTO public.product_revisions (product_id, author_id, version, revision_type, content)\nSELECT\n pr.id,\n NULL::uuid,\n pr.version,\n 'snapshot'::public.revision_type,\n jsonb_build_object(\n 'meta', jsonb_build_object(\n 'title', pr.title,\n 'slug', pr.slug,\n 'language_id', pr.language_id,\n 'status', pr.status,\n 'meta_title', pr.meta_title,\n 'meta_description', pr.meta_description,\n 'custom_canonical', pr.custom_canonical,\n 'published_at', to_char(pr.published_at AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'),\n 'short_description', pr.short_description,\n 'description_json', pr.description_json\n ),\n 'blocks', COALESCE((\n SELECT jsonb_agg(\n jsonb_build_object(\n 'language_id', b.language_id,\n 'block_type', b.block_type,\n 'content', b.content,\n 'order', b.\"order\"\n ) ORDER BY b.\"order\" ASC, b.id ASC\n )\n FROM public.blocks b\n WHERE b.product_id = pr.id\n ), '[]'::jsonb)\n )\n FROM public.products pr\n WHERE NOT EXISTS (\n SELECT 1 FROM public.product_revisions r\n WHERE r.product_id = pr.id\n AND r.revision_type = 'snapshot'\n AND r.version <= pr.version\n )\nON CONFLICT (product_id, version) DO NOTHING;\n"
101
- },
102
- {
103
- "version": "00000000000017",
104
- "name": "00000000000017_cortex_ai_mcp_server.sql",
105
- "sql": "-- Cortex AI MCP (Model Context Protocol) server access.\n--\n-- Adds the bearer-token store that gates /api/mcp, the endpoint that exposes the\n-- Cortex AI tool registry to external MCP clients (Claude Code, Claude Desktop,\n-- Cursor, VS Code). Two pieces:\n--\n-- 1. public.mcp_access_tokens — one row per issued token. We store ONLY the\n-- SHA-256 hash of the token, never the token itself: the plaintext is shown\n-- to the admin exactly once at mint time and is unrecoverable afterwards, so\n-- a database leak cannot be replayed against the MCP endpoint. `token_prefix`\n-- is the non-secret leading fragment kept purely so the UI can tell two tokens\n-- apart in a list.\n--\n-- 2. cortex_ai_mcp_settings — a non-secret JSON site_settings row holding the\n-- server on/off switch and the localhost-trust flag. It is added to all four\n-- site_settings policies so only authenticated ADMINs can read or write it;\n-- the MCP route itself reads it through the service-role client, which\n-- bypasses RLS.\n--\n-- Forward-only. Recreates the four site_settings policies idempotently, preserving\n-- every key already in each policy's sensitive array (note that\n-- language_detection_settings stays anon-READABLE and so is absent from the SELECT\n-- policy, exactly as migration 00000000000012 left it).\n\nCREATE TABLE IF NOT EXISTS public.mcp_access_tokens (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n name text NOT NULL,\n -- Lowercase hex SHA-256 of the plaintext token. Unique so a lookup is a single\n -- indexed equality probe and duplicate mints are impossible.\n token_hash text NOT NULL UNIQUE,\n -- Non-secret display fragment, e.g. \"nbmcp_a1b2c3d4\". Never enough to authenticate.\n token_prefix text NOT NULL,\n -- 'read' grants the read-only tools; 'write' additionally grants the mutating ones.\n scopes text[] NOT NULL DEFAULT ARRAY['read', 'write']::text[],\n created_by uuid REFERENCES auth.users (id) ON DELETE SET NULL,\n created_at timestamptz NOT NULL DEFAULT now(),\n last_used_at timestamptz,\n expires_at timestamptz,\n revoked_at timestamptz\n);\n\nCOMMENT ON TABLE public.mcp_access_tokens IS\n 'Bearer tokens for the Cortex AI MCP server at /api/mcp. Stores SHA-256 hashes only; plaintext is displayed once at mint time.';\n\nCREATE INDEX IF NOT EXISTS mcp_access_tokens_token_hash_idx\n ON public.mcp_access_tokens (token_hash);\n\n-- Orders the admin token list newest-first without a sort.\nCREATE INDEX IF NOT EXISTS mcp_access_tokens_created_at_idx\n ON public.mcp_access_tokens (created_at DESC);\n\nALTER TABLE public.mcp_access_tokens ENABLE ROW LEVEL SECURITY;\n\n-- Tokens are credentials: admin-only, with no anon or WRITER access at all. The\n-- MCP route verifies them with the service-role client, which bypasses RLS.\nDROP POLICY IF EXISTS mcp_access_tokens_admin_select ON public.mcp_access_tokens;\nCREATE POLICY mcp_access_tokens_admin_select ON public.mcp_access_tokens\n FOR SELECT TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);\n\nDROP POLICY IF EXISTS mcp_access_tokens_admin_insert ON public.mcp_access_tokens;\nCREATE POLICY mcp_access_tokens_admin_insert ON public.mcp_access_tokens\n FOR INSERT TO authenticated\n WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);\n\nDROP POLICY IF EXISTS mcp_access_tokens_admin_update ON public.mcp_access_tokens;\nCREATE POLICY mcp_access_tokens_admin_update ON public.mcp_access_tokens\n FOR UPDATE TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role)\n WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);\n\nDROP POLICY IF EXISTS mcp_access_tokens_admin_delete ON public.mcp_access_tokens;\nCREATE POLICY mcp_access_tokens_admin_delete ON public.mcp_access_tokens\n FOR DELETE TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);\n\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.mcp_access_tokens TO authenticated;\nGRANT ALL ON public.mcp_access_tokens TO service_role;\n\n-- Add cortex_ai_mcp_settings to the admin-only site_settings group (all four policies).\nDROP POLICY IF EXISTS site_settings_read_policy ON public.site_settings;\nCREATE POLICY site_settings_read_policy ON public.site_settings FOR SELECT USING (((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT auth.role() AS role) = 'authenticated'::text) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n\nDROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;\nCREATE POLICY site_settings_insert_policy ON public.site_settings FOR INSERT TO authenticated WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n\nDROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;\nCREATE POLICY site_settings_update_policy ON public.site_settings FOR UPDATE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)))) WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n\nDROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;\nCREATE POLICY site_settings_delete_policy ON public.site_settings FOR DELETE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n"
106
- },
107
- {
108
- "version": "00000000000018",
109
- "name": "00000000000018_site_scripts.sql",
110
- "sql": "-- Site scripts: admin-authored JavaScript injected into every page of the public site.\n--\n-- Rich-text blocks can already carry an inline <script>, but that script belongs to\n-- one block on one page. This table is for behaviour that spans the site: chat\n-- widgets, third-party embeds, and the scroll/animation helpers that page classes\n-- rely on. Each row gets a name, an on/off switch, and a defined injection point.\n--\n-- NOT the same thing as `site_settings.privacy_settings -> custom_scripts`, which is\n-- a single consent-gated blob for marketing tags and only fires once a visitor\n-- accepts cookies. Rows here are functional site code and run unconditionally, so\n-- anything requiring consent belongs in that setting instead, not this table.\n--\n-- Scripts are emitted with the request's CSP nonce by the root layout, so they run\n-- under the site's existing Content-Security-Policy rather than forcing it open.\n--\n-- Security posture: this is arbitrary JavaScript on every page, so writes are\n-- ADMIN-only (WRITER is deliberately excluded, unlike most content tables) and the\n-- public may read only rows that are switched on, so a half-written draft is never\n-- served to a visitor.\n\nCREATE TABLE IF NOT EXISTS public.site_scripts (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n name text NOT NULL,\n description text,\n -- Raw JavaScript, stored WITHOUT the surrounding <script> tag. The layout adds\n -- the tag so the nonce and attributes are always applied by us, never by the\n -- author. Ignored when `src` is set.\n code text DEFAULT ''::text NOT NULL,\n -- When set, an external script is loaded from this URL and `code` is ignored.\n src text,\n -- Where the tag is emitted. 'head' runs before first paint (blocking, use\n -- sparingly); 'body_end' runs once the markup exists and is the right default\n -- for anything that queries the DOM.\n placement text DEFAULT 'body_end'::text NOT NULL,\n -- Applies to external `src` scripts; inline code ignores it.\n load_strategy text DEFAULT 'default'::text NOT NULL,\n is_active boolean DEFAULT false NOT NULL,\n sort_order integer DEFAULT 0 NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT site_scripts_pkey PRIMARY KEY (id),\n CONSTRAINT site_scripts_placement_check\n CHECK ((placement = ANY (ARRAY['head'::text, 'body_start'::text, 'body_end'::text]))),\n CONSTRAINT site_scripts_load_strategy_check\n CHECK ((load_strategy = ANY (ARRAY['default'::text, 'defer'::text, 'async'::text]))),\n -- An external script must be https so it cannot be downgraded in transit.\n CONSTRAINT site_scripts_src_scheme_check\n CHECK ((src IS NULL OR src ~ '^https://')),\n -- A row has to actually do something: inline code or an external src.\n CONSTRAINT site_scripts_has_payload_check\n CHECK ((src IS NOT NULL OR length(btrim(code)) > 0))\n);\n\nCOMMENT ON TABLE public.site_scripts IS\n 'Admin-authored JavaScript injected into the public site by the root layout, with the request CSP nonce applied. Only is_active rows are publicly readable; only ADMIN may write. Distinct from privacy_settings.custom_scripts, which is consent-gated marketing tags.';\n\nCREATE INDEX IF NOT EXISTS site_scripts_active_placement_sort_idx\n ON public.site_scripts USING btree (is_active, placement, sort_order);\n\nDROP TRIGGER IF EXISTS set_site_scripts_updated_at ON public.site_scripts;\nCREATE TRIGGER set_site_scripts_updated_at\n BEFORE UPDATE ON public.site_scripts\n FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\n\nALTER TABLE public.site_scripts ENABLE ROW LEVEL SECURITY;\n\nGRANT ALL ON TABLE public.site_scripts TO anon;\nGRANT ALL ON TABLE public.site_scripts TO authenticated;\nGRANT ALL ON TABLE public.site_scripts TO service_role;\n\n-- Anonymous visitors need the active scripts to render the page. Inactive rows stay\n-- private so a half-written script is never exposed before it is switched on.\nDROP POLICY IF EXISTS \"Public read active site scripts\" ON public.site_scripts;\nCREATE POLICY \"Public read active site scripts\" ON public.site_scripts\n FOR SELECT TO authenticated, anon USING (is_active);\n\nDROP POLICY IF EXISTS \"Admins read all site scripts\" ON public.site_scripts;\nCREATE POLICY \"Admins read all site scripts\" ON public.site_scripts\n FOR SELECT TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins insert site scripts\" ON public.site_scripts;\nCREATE POLICY \"Admins insert site scripts\" ON public.site_scripts\n FOR INSERT TO authenticated\n WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins update site scripts\" ON public.site_scripts;\nCREATE POLICY \"Admins update site scripts\" ON public.site_scripts\n FOR UPDATE TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role))\n WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins delete site scripts\" ON public.site_scripts;\nCREATE POLICY \"Admins delete site scripts\" ON public.site_scripts\n FOR DELETE TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n"
111
- },
112
- {
113
- "version": "00000000000019",
114
- "name": "00000000000019_site_script_revisions.sql",
115
- "sql": "-- Audit trail and undo for site scripts.\n--\n-- `site_scripts` ships arbitrary JavaScript to every visitor, which makes it the\n-- highest-privilege write in the CMS: a bad or malicious snippet can read cookies,\n-- watch checkout forms, or phone home. Content has Revision History for exactly this\n-- reason; code needs it more, not less. Every create/update/delete writes one row\n-- here, and every row is a complete, restorable snapshot — so this table is both the\n-- log (\"who shipped what, when, from where\") and the undo.\n--\n-- APPEND-ONLY BY CONSTRUCTION. There are no UPDATE or DELETE policies, and the\n-- trigger below rejects both even for the service role, which otherwise bypasses\n-- RLS. An audit trail that the compromised credential can rewrite is not an audit\n-- trail. Reverting therefore writes a NEW 'revert' row rather than removing history.\n--\n-- `script_id` and `actor_user_id` are deliberately PLAIN uuids with no foreign keys:\n-- an FK with ON DELETE SET NULL would have to UPDATE this table when a script or a\n-- profile is deleted, which the append-only trigger forbids. `script_name` is\n-- denormalised so a deleted script is still identifiable in the log.\n\nCREATE TABLE IF NOT EXISTS public.site_script_revisions (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n script_id uuid,\n script_name text NOT NULL,\n revision_type text NOT NULL,\n -- Null when the actor could not be resolved (e.g. a localhost dev connection).\n actor_user_id uuid,\n -- Which surface made the change, so an unexpected edit can be traced back to\n -- the dashboard or to an MCP token.\n source text DEFAULT 'cms'::text NOT NULL,\n summary text,\n -- Full restorable state of the script at this revision. For 'delete' it is the\n -- state immediately BEFORE removal, so restoring it brings the script back.\n snapshot jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT site_script_revisions_pkey PRIMARY KEY (id),\n CONSTRAINT site_script_revisions_type_check\n CHECK ((revision_type = ANY (ARRAY['create'::text, 'update'::text, 'delete'::text, 'revert'::text]))),\n CONSTRAINT site_script_revisions_source_check\n CHECK ((source = ANY (ARRAY['cms'::text, 'mcp'::text]))),\n CONSTRAINT site_script_revisions_snapshot_is_object_check\n CHECK ((jsonb_typeof(snapshot) = 'object'))\n);\n\nCOMMENT ON TABLE public.site_script_revisions IS\n 'Append-only audit log and undo history for site_scripts. Each row is a restorable snapshot. UPDATE and DELETE are blocked by trigger, including for the service role.';\n\nCREATE INDEX IF NOT EXISTS site_script_revisions_script_created_idx\n ON public.site_script_revisions USING btree (script_id, created_at DESC);\n\nCREATE INDEX IF NOT EXISTS site_script_revisions_created_idx\n ON public.site_script_revisions USING btree (created_at DESC);\n\n-- Enforced in the database rather than the application so it holds for every\n-- caller, including the service-role client the MCP server uses.\nCREATE OR REPLACE FUNCTION public.prevent_site_script_revision_rewrite() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path = ''\n AS $$\nBEGIN\n RAISE EXCEPTION 'site_script_revisions is append-only; % is not permitted', TG_OP\n USING ERRCODE = 'restrict_violation';\nEND;\n$$;\n\nDROP TRIGGER IF EXISTS trg_site_script_revisions_append_only ON public.site_script_revisions;\nCREATE TRIGGER trg_site_script_revisions_append_only\n BEFORE UPDATE OR DELETE ON public.site_script_revisions\n FOR EACH ROW EXECUTE FUNCTION public.prevent_site_script_revision_rewrite();\n\nALTER TABLE public.site_script_revisions ENABLE ROW LEVEL SECURITY;\n\nGRANT SELECT, INSERT ON TABLE public.site_script_revisions TO authenticated;\nGRANT ALL ON TABLE public.site_script_revisions TO service_role;\n\n-- Read is ADMIN-only: snapshots contain the full source of scripts that may not be\n-- active yet, and the log itself reveals operational history.\nDROP POLICY IF EXISTS \"Admins read site script revisions\" ON public.site_script_revisions;\nCREATE POLICY \"Admins read site script revisions\" ON public.site_script_revisions\n FOR SELECT TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins insert site script revisions\" ON public.site_script_revisions;\nCREATE POLICY \"Admins insert site script revisions\" ON public.site_script_revisions\n FOR INSERT TO authenticated\n WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n"
116
- },
117
- {
118
- "version": "00000000000020",
119
- "name": "00000000000020_updating_article.sql",
120
- "sql": "-- 00000000000020_updating_article.sql\n-- Seeds the \"How Updating NextBlock Works\" guide as an EN/FR twin pair, the companion\n-- to the install guide seeded in the baseline (00000000000003, slugs 'how-to-setup-nextblock'\n-- / 'comment-configurer-nextblock'). It documents the single `npm run update` command\n-- across all four install paths — one-click Vercel, npm create → Docker, npm create →\n-- managed cloud, and the cloned monorepo.\n--\n-- Forward-only and idempotent by construction:\n-- * posts carries UNIQUE (language_id, slug), so the inserts use ON CONFLICT DO NOTHING;\n-- * blocks has no natural unique key, so the body insert is guarded on NOT EXISTS;\n-- * ids are never hardcoded — posts.id and blocks.id are identity columns and a live\n-- database has real editor-created rows occupying the low ids the baseline used.\n-- * the feature image is looked up rather than asserted, so a site that deleted the\n-- seeded media row still gets the article (with no cover) instead of a failed migration.\n--\n-- Because the sandbox reset payload and the /setup wizard's embedded bundle are both\n-- generated FROM this directory, the article reaches a fresh install and every hourly\n-- sandbox reset with no extra wiring — regenerate them with `npm run generate:sandbox`\n-- and `npm run generate:migrations-bundle`.\n--\n-- blocks.content is JSONB written with PostgreSQL dollar-quoting (same style as 006/008)\n-- so the HTML can use ordinary single-quoted class attributes without quote doubling.\n\nDO $body$\nDECLARE\n v_group uuid := 'c0d3f1a2-8b47-4e19-9a52-7f6b1d4e8c30';\n v_image uuid;\n v_en_post integer;\n v_fr_post integer;\nBEGIN\n SELECT id INTO v_image\n FROM public.media\n WHERE id = '641ddf75-5c90-41df-8b83-e7c298f30a6a'::uuid;\n\n ---------------------------------------------------------------------------\n -- English\n ---------------------------------------------------------------------------\n INSERT INTO public.posts (\n language_id, author_id, title, slug, label, excerpt, subtitle, status,\n published_at, meta_title, meta_description, feature_image_id, version,\n translation_group_id\n )\n SELECT\n 1, NULL,\n 'How Updating NextBlock Works: One Command for Every Install',\n 'how-updating-works',\n 'Maintenance',\n 'However you installed NextBlock — one-click Vercel, the CLI, Docker or a git clone — a single npm run update brings the code, the dependencies and the database schema forward together.',\n 'Automatic upstream syncing on Vercel, and one command everywhere else: what npm run update does, what it never touches, and how to roll it back.',\n 'published', now(),\n 'How to Update NextBlock — One Command for Every Install',\n 'Update NextBlock in one step. npm run update pulls new code, installs dependencies and applies pending database migrations on Vercel, Docker, CLI and git-clone installs alike.',\n v_image, 1, v_group\n WHERE NOT EXISTS (\n SELECT 1 FROM public.posts WHERE language_id = 1 AND slug = 'how-updating-works'\n );\n\n SELECT id INTO v_en_post\n FROM public.posts\n WHERE language_id = 1 AND slug = 'how-updating-works'\n ORDER BY id\n LIMIT 1;\n\n IF v_en_post IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM public.blocks WHERE post_id = v_en_post\n ) THEN\n INSERT INTO public.blocks (post_id, language_id, block_type, content, \"order\")\n VALUES (v_en_post, 1, 'text', $nben$\n{\"html_content\":\"<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock ships improvements continuously — new blocks, editor fixes, security patches, and occasionally a database change that the new code depends on. Keeping up with all of that used to mean knowing which of the four install paths you were on. It no longer does. Every NextBlock project, however it was created, understands one command:</p>\\n\\n<div class='my-10 overflow-hidden rounded-[2rem] border border-slate-800 bg-slate-950 shadow-2xl'>\\n <div class='flex items-center gap-2 border-b border-white/10 px-5 py-3'>\\n <span class='h-3 w-3 rounded-full bg-red-400/70'></span>\\n <span class='h-3 w-3 rounded-full bg-yellow-400/70'></span>\\n <span class='h-3 w-3 rounded-full bg-green-400/70'></span>\\n <span class='ml-3 text-xs font-mono text-slate-400'>your project</span>\\n </div>\\n <div class='px-6 py-8 text-center'>\\n <p class='mt-0 mb-2 font-mono text-2xl sm:text-3xl font-semibold text-emerald-300'>npm run update</p>\\n <p class='mb-0 text-sm text-slate-400'>Code &middot; dependencies &middot; database schema &mdash; in that order, in one step.</p>\\n </div>\\n</div>\\n\\n<p>It figures out which kind of install it is running inside, picks the right source for new code, installs the matching dependencies, and then applies any database migrations the new version needs. If you would rather look before you leap, <code>npm run update -- --check</code> reports exactly what would change and touches nothing.</p>\\n\\n<h2 id='the-four-paths'>The four install paths, and how each one gets updates</h2>\\n<p class='text-slate-600 dark:text-slate-400'>These map one-to-one onto the four options in <a href='/article/how-to-setup-nextblock'>the install guide</a>. The command is the same everywhere; what differs is where the new code comes from.</p>\\n\\n<div class='grid gap-5 md:grid-cols-2 my-8'>\\n <a href='#vercel' class='block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 text-sm font-bold text-white'>1</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Fully automatic</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>One-click Vercel &amp; GitHub forks</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>A daily workflow merges upstream into your repository and Vercel redeploys. You do nothing.</p>\\n </a>\\n <a href='#docker' class='block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-amber-500 text-sm font-bold text-white'>2</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>One command</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Docker</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Update, then rebuild the local stack. Your Postgres and media volumes survive untouched.</p>\\n </a>\\n <a href='#cloud' class='block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-violet-600 text-sm font-bold text-white'>3</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>One command</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Supabase</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>New framework files come from npm; your own pages, routes and content are left alone.</p>\\n </a>\\n <a href='#clone' class='block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-emerald-600 text-sm font-bold text-white'>4</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>One command</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>git clone the monorepo</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>A guarded git pull or upstream merge, then dependencies and migrations. No manual steps.</p>\\n </a>\\n</div>\\n\\n<h2 id='vercel'>1. One-click Vercel and GitHub forks &mdash; hands-off</h2>\\n<p>This path updates itself. When you deployed, NextBlock created a repository you own; the dashboard&rsquo;s <strong>Connect GitHub</strong> onboarding step installs a workflow into it that runs <strong>every day at midnight UTC</strong> and can also be triggered by hand from your repository&rsquo;s <strong>Actions</strong> tab.</p>\\n<ol class='space-y-2'>\\n <li>The workflow merges the latest upstream NextBlock into your deploy branch.</li>\\n <li>A clean merge is pushed to your branch, which triggers an ordinary Vercel deployment.</li>\\n <li>During that production build, NextBlock applies any pending database migrations <em>before</em> the app is built &mdash; so new code never runs against an old schema.</li>\\n <li>If the merge conflicts, nothing is pushed. The workflow opens a GitHub issue instead, and your CMS dashboard shows an amber banner linking straight to it. Resolve it, close the issue, and the banner clears itself.</li>\\n</ol>\\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Make the repository public</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>A public repository is completely zero-config. On a private one, add a <code>NEXTBLOCK_GITHUB_TOKEN</code> environment variable with read access to issues so the conflict banner still works &mdash; and note that Vercel&rsquo;s free Hobby plan refuses to auto-deploy automated commits on private repositories, so the merge would land without deploying.</p>\\n</div>\\n<p>Working on a local clone of that fork? <code>npm run update</code> does the same merge on your machine, adding an <code>upstream</code> remote if it is missing, then installs dependencies and applies migrations.</p>\\n\\n<h2 id='docker'>2. npm create nextblock &rarr; Docker &mdash; update, then rebuild</h2>\\n<p>From your project directory:</p>\\n<pre><code>npm run update\\nnpm run docker:up</code></pre>\\n<p>The first command refreshes the application, its dependencies and the schema; the second rebuilds and restarts the containers. Your database and media live in Docker volumes and are never touched by either step &mdash; <code>docker:up</code> rebuilds images, not data.</p>\\n\\n<h2 id='cloud'>3. npm create nextblock &rarr; managed Supabase &mdash; one command</h2>\\n<pre><code>npm run update\\nnpm run build\\nnpm start</code></pre>\\n<p>Your project is a standalone Next.js app, so new framework code is fetched from the published <code>create-nextblock</code> package on npm &mdash; the exact artifact your project was scaffolded from, versioned in lockstep with the release. NextBlock refreshes the files it owns, merges the new dependency versions into your <code>package.json</code>, runs <code>npm install</code>, and then applies migrations.</p>\\n<div class='rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Deploying to Vercel from this project</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Run <code>npm run update</code> locally, commit the result, and push. Your production build applies any pending migrations on the way up, exactly as it does for one-click installs.</p>\\n</div>\\n\\n<h2 id='clone'>4. The cloned monorepo &mdash; one command</h2>\\n<pre><code>npm run update</code></pre>\\n<p>In a clone of the NextBlock repository this fast-forwards your checkout, reinstalls workspace dependencies and applies pending migrations. It refuses to run over uncommitted changes and tells you how to stash them first, so an update can never silently eat work in progress. If you have local commits, it stops and points you at <code>git pull --rebase</code> rather than guessing.</p>\\n\\n<h2 id='what-it-does'>What <code>npm run update</code> actually does</h2>\\n<ol class='space-y-2'>\\n <li><strong>Identifies the install.</strong> Monorepo or standalone app; git-backed or npm-backed; Docker or not.</li>\\n <li><strong>Updates the code</strong> from the right source &mdash; an upstream git merge, a fast-forward pull, or the published <code>create-nextblock</code> package.</li>\\n <li><strong>Installs dependencies</strong> with <code>npm install</code>, so the code and the packages it imports move together.</li>\\n <li><strong>Refreshes the migration files</strong> shipped inside <code>@nextblock-cms/db</code>, so the newest schema changes are on disk before anything is applied.</li>\\n <li><strong>Applies pending migrations</strong>, listing them first and asking before it writes.</li>\\n <li><strong>Clears the dashboard&rsquo;s update banner</strong> once the new version is really in place.</li>\\n</ol>\\n\\n<h3>Options</h3>\\n<div class='overflow-x-auto my-6'>\\n<table class='w-full text-left text-sm'>\\n <thead><tr class='border-b border-slate-200 dark:border-white/10'><th class='py-3 pr-4 font-semibold'>Command</th><th class='py-3 font-semibold'>What it does</th></tr></thead>\\n <tbody class='align-top'>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update</code></td><td class='py-3'>Code, dependencies and schema.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --check</code></td><td class='py-3'>Report what would change. Writes nothing.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --yes</code></td><td class='py-3'>Skip the confirmation prompts. Useful in CI.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --db-only</code></td><td class='py-3'>Apply pending migrations and nothing else.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --skip-db</code></td><td class='py-3'>Update code and dependencies, leave the database alone.</td></tr>\\n <tr><td class='py-3 pr-4'><code>npm run update -- --force</code></td><td class='py-3'>Run even when you are already on the latest version.</td></tr>\\n </tbody>\\n</table>\\n</div>\\n\\n<h2 id='database'>What happens to your database</h2>\\n<p>Schema changes are <strong>forward-only</strong>. NextBlock never rewrites or replays a migration that has already run: each one is applied and recorded in the same transaction, so a failure rolls back cleanly and leaves the database exactly as it was. Already-applied migrations are skipped by version, which makes re-running an update completely safe.</p>\\n<p>Migrations change <em>structure</em> &mdash; tables, columns, indexes, permissions. Your pages, posts, products, media and users are yours; the update never deletes or rewrites them.</p>\\n<div class='rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Belt and braces</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Before a big jump on a production site, take a database snapshot &mdash; Supabase does daily backups on paid plans, and you can trigger one on demand from the Supabase dashboard. Then run <code>npm run update -- --check</code> to see the pending list before you commit to it.</p>\\n</div>\\n\\n<h2 id='safety'>If something goes wrong</h2>\\n<ul class='space-y-2'>\\n <li><strong>Standalone projects:</strong> every framework file the update replaces is copied first into a timestamped folder under <code>.nextblock-backup/</code> in your project. Nothing is deleted, so files you added yourself are never removed.</li>\\n <li><strong>Git-backed installs:</strong> the update is an ordinary commit. <code>git log</code> shows it and <code>git revert</code> undoes it.</li>\\n <li><strong>A conflicted merge</strong> is aborted automatically &mdash; your working tree is left exactly as it was, with instructions printed for resolving it by hand.</li>\\n <li><strong>A failed migration</strong> rolls back. Fix the cause and re-run; nothing half-applied is left behind.</li>\\n</ul>\\n<p>If you have customised a file that NextBlock owns &mdash; something under <code>app/</code>, <code>components/</code> or <code>lib/</code> &mdash; the update will replace it and back up your version. Diff the backup afterwards to bring your change forward. Customisations that live in your own new files, in the CMS, or in <code>.env</code> are never affected.</p>\\n\\n<h2 id='knowing'>Knowing when there is something to update</h2>\\n<p>You do not have to poll. NextBlock checks in the background while you use the CMS and raises a dashboard banner when a newer version is published, telling you which version you are on and what is available. Administrators can also just run <code>npm run update -- --check</code> at any time.</p>\\n\\n<h2 id='faq'>Update FAQ</h2>\\n<h3>Will updating overwrite my content or settings?</h3>\\n<p>No. Content, media, users and settings live in your database; site configuration lives in your environment variables. The update touches application code, dependencies and schema structure only.</p>\\n<h3>Do I have to update every release?</h3>\\n<p>No, though staying close to the latest release keeps you on security fixes and makes each jump smaller. Updates apply in sequence, so skipping several versions still lands correctly.</p>\\n<h3>Can I run it in CI?</h3>\\n<p>Yes &mdash; <code>npm run update -- --yes</code> never prompts, and it exits non-zero if the schema step fails so a pipeline can catch it.</p>\\n<h3>What if my project has no database connection configured?</h3>\\n<p>Code and dependencies still update; the schema step is skipped with a warning telling you which environment variable to set. Re-run <code>npm run update -- --db-only</code> once it is configured.</p>\\n<h3>I am on the one-click Vercel deploy &mdash; do I need to run anything?</h3>\\n<p>No. That path is fully automatic. The command exists for when you want an update <em>now</em> rather than at midnight, or when you are working on a local clone.</p>\\n\\n<div class='rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5'>\\n <p class='mt-0 text-2xl font-semibold text-slate-900 dark:text-white'>One command, every install.</p>\\n <p class='text-sm text-slate-600 dark:text-slate-300'>New to NextBlock? Start with the install guide &mdash; then never think about upgrades again.</p>\\n <div class='mt-5 flex flex-wrap justify-center gap-3'>\\n <a href='/article/how-to-setup-nextblock' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Read the install guide</a>\\n <a href='https://github.com/nextblock-cms/nextblock' target='_blank' rel='noopener' class='inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50'>View on GitHub</a>\\n </div>\\n</div>\"}\n$nben$::jsonb, 0);\n END IF;\n\n ---------------------------------------------------------------------------\n -- French\n ---------------------------------------------------------------------------\n INSERT INTO public.posts (\n language_id, author_id, title, slug, label, excerpt, subtitle, status,\n published_at, meta_title, meta_description, feature_image_id, version,\n translation_group_id\n )\n SELECT\n 2, NULL,\n 'Les mises à jour de NextBlock : une seule commande, quelle que soit l''installation',\n 'comment-fonctionnent-les-mises-a-jour',\n 'Maintenance',\n 'Quelle que soit votre installation — Vercel en un clic, le CLI, Docker ou un git clone — une seule commande npm run update fait avancer ensemble le code, les dépendances et le schéma de base de données.',\n 'Synchronisation automatique sur Vercel, et une seule commande partout ailleurs : ce que fait npm run update, ce qu''il ne touche jamais, et comment revenir en arrière.',\n 'published', now(),\n 'Mettre à jour NextBlock — une commande pour toutes les installations',\n 'Mettez NextBlock à jour en une étape. npm run update récupère le nouveau code, installe les dépendances et applique les migrations en attente, sur Vercel, Docker, CLI et git clone.',\n v_image, 1, v_group\n WHERE NOT EXISTS (\n SELECT 1 FROM public.posts\n WHERE language_id = 2 AND slug = 'comment-fonctionnent-les-mises-a-jour'\n );\n\n SELECT id INTO v_fr_post\n FROM public.posts\n WHERE language_id = 2 AND slug = 'comment-fonctionnent-les-mises-a-jour'\n ORDER BY id\n LIMIT 1;\n\n IF v_fr_post IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM public.blocks WHERE post_id = v_fr_post\n ) THEN\n INSERT INTO public.blocks (post_id, language_id, block_type, content, \"order\")\n VALUES (v_fr_post, 2, 'text', $nbfr$\n{\"html_content\":\"<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock &eacute;volue en continu &mdash; nouveaux blocs, corrections de l'&eacute;diteur, correctifs de s&eacute;curit&eacute;, et parfois une modification de la base de donn&eacute;es dont le nouveau code d&eacute;pend. Suivre tout cela supposait autrefois de savoir laquelle des quatre m&eacute;thodes d'installation vous aviez utilis&eacute;e. Ce n'est plus le cas. Tout projet NextBlock, quelle que soit sa cr&eacute;ation, comprend une seule commande :</p>\\n\\n<div class='my-10 overflow-hidden rounded-[2rem] border border-slate-800 bg-slate-950 shadow-2xl'>\\n <div class='flex items-center gap-2 border-b border-white/10 px-5 py-3'>\\n <span class='h-3 w-3 rounded-full bg-red-400/70'></span>\\n <span class='h-3 w-3 rounded-full bg-yellow-400/70'></span>\\n <span class='h-3 w-3 rounded-full bg-green-400/70'></span>\\n <span class='ml-3 text-xs font-mono text-slate-400'>votre projet</span>\\n </div>\\n <div class='px-6 py-8 text-center'>\\n <p class='mt-0 mb-2 font-mono text-2xl sm:text-3xl font-semibold text-emerald-300'>npm run update</p>\\n <p class='mb-0 text-sm text-slate-400'>Code &middot; d&eacute;pendances &middot; sch&eacute;ma de base de donn&eacute;es &mdash; dans cet ordre, en une seule &eacute;tape.</p>\\n </div>\\n</div>\\n\\n<p>La commande d&eacute;termine dans quel type d'installation elle s'ex&eacute;cute, choisit la bonne source pour le nouveau code, installe les d&eacute;pendances correspondantes, puis applique les migrations dont la nouvelle version a besoin. Si vous pr&eacute;f&eacute;rez regarder avant de sauter, <code>npm run update -- --check</code> indique exactement ce qui changerait sans rien modifier.</p>\\n\\n<h2 id='the-four-paths'>Les quatre installations et leurs mises &agrave; jour</h2>\\n<p class='text-slate-600 dark:text-slate-400'>Elles correspondent une &agrave; une aux quatre options du <a href='/article/comment-configurer-nextblock'>guide d'installation</a>. La commande est la m&ecirc;me partout ; seule la provenance du nouveau code change.</p>\\n\\n<div class='grid gap-5 md:grid-cols-2 my-8'>\\n <a href='#vercel' class='block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 text-sm font-bold text-white'>1</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Enti&egrave;rement automatique</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Vercel en un clic et forks GitHub</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Un workflow quotidien fusionne les nouveaut&eacute;s dans votre d&eacute;p&ocirc;t et Vercel red&eacute;ploie. Vous n'avez rien &agrave; faire.</p>\\n </a>\\n <a href='#docker' class='block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-amber-500 text-sm font-bold text-white'>2</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Une commande</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Docker</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Mettez &agrave; jour, puis reconstruisez la pile locale. Vos volumes Postgres et m&eacute;dias restent intacts.</p>\\n </a>\\n <a href='#cloud' class='block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-violet-600 text-sm font-bold text-white'>3</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Une commande</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Supabase</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Les nouveaux fichiers viennent de npm ; vos pages, routes et contenus ne sont pas touch&eacute;s.</p>\\n </a>\\n <a href='#clone' class='block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-emerald-600 text-sm font-bold text-white'>4</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Une commande</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>git clone du monorepo</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Un pull ou une fusion prot&eacute;g&eacute;e, puis les d&eacute;pendances et les migrations. Aucune &eacute;tape manuelle.</p>\\n </a>\\n</div>\\n\\n<h2 id='vercel'>1. Vercel en un clic et forks GitHub &mdash; sans intervention</h2>\\n<p>Ce chemin se met &agrave; jour tout seul. Lors du d&eacute;ploiement, NextBlock a cr&eacute;&eacute; un d&eacute;p&ocirc;t qui vous appartient ; l'&eacute;tape <strong>Connect GitHub</strong> du tableau de bord y installe un workflow qui s'ex&eacute;cute <strong>chaque jour &agrave; minuit UTC</strong> et peut aussi &ecirc;tre lanc&eacute; &agrave; la demande depuis l'onglet <strong>Actions</strong> de votre d&eacute;p&ocirc;t.</p>\\n<ol class='space-y-2'>\\n <li>Le workflow fusionne la derni&egrave;re version de NextBlock dans votre branche de d&eacute;ploiement.</li>\\n <li>Une fusion propre est pouss&eacute;e sur votre branche, ce qui d&eacute;clenche un d&eacute;ploiement Vercel normal.</li>\\n <li>Pendant ce build de production, NextBlock applique les migrations en attente <em>avant</em> de construire l'application &mdash; le nouveau code ne tourne donc jamais sur un ancien sch&eacute;ma.</li>\\n <li>En cas de conflit, rien n'est pouss&eacute;. Le workflow ouvre une issue GitHub et votre tableau de bord affiche une banni&egrave;re ambre qui pointe dessus. R&eacute;solvez, fermez l'issue, et la banni&egrave;re dispara&icirc;t d'elle-m&ecirc;me.</li>\\n</ol>\\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Rendez le d&eacute;p&ocirc;t public</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Un d&eacute;p&ocirc;t public ne demande aucune configuration. Sur un d&eacute;p&ocirc;t priv&eacute;, ajoutez une variable d'environnement <code>NEXTBLOCK_GITHUB_TOKEN</code> avec un acc&egrave;s en lecture aux issues pour que la banni&egrave;re de conflit fonctionne &mdash; et sachez que l'offre gratuite Hobby de Vercel refuse de d&eacute;ployer automatiquement les commits automatis&eacute;s sur un d&eacute;p&ocirc;t priv&eacute;.</p>\\n</div>\\n<p>Vous travaillez sur un clone local de ce fork ? <code>npm run update</code> effectue la m&ecirc;me fusion sur votre machine, en ajoutant le d&eacute;p&ocirc;t <code>upstream</code> s'il manque, puis installe les d&eacute;pendances et applique les migrations.</p>\\n\\n<h2 id='docker'>2. npm create nextblock &rarr; Docker &mdash; mettre &agrave; jour puis reconstruire</h2>\\n<p>Depuis le dossier de votre projet :</p>\\n<pre><code>npm run update\\nnpm run docker:up</code></pre>\\n<p>La premi&egrave;re commande met &agrave; jour l'application, ses d&eacute;pendances et le sch&eacute;ma ; la seconde reconstruit et red&eacute;marre les conteneurs. Votre base de donn&eacute;es et vos m&eacute;dias vivent dans des volumes Docker et ne sont touch&eacute;s ni par l'une ni par l'autre &mdash; <code>docker:up</code> reconstruit des images, pas des donn&eacute;es.</p>\\n\\n<h2 id='cloud'>3. npm create nextblock &rarr; Supabase g&eacute;r&eacute; &mdash; une commande</h2>\\n<pre><code>npm run update\\nnpm run build\\nnpm start</code></pre>\\n<p>Votre projet est une application Next.js autonome : le nouveau code provient donc du paquet <code>create-nextblock</code> publi&eacute; sur npm &mdash; exactement l'artefact &agrave; partir duquel votre projet a &eacute;t&eacute; g&eacute;n&eacute;r&eacute;, versionn&eacute; en phase avec la release. NextBlock rafra&icirc;chit les fichiers qui lui appartiennent, fusionne les nouvelles versions de d&eacute;pendances dans votre <code>package.json</code>, lance <code>npm install</code>, puis applique les migrations.</p>\\n<div class='rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Vous d&eacute;ployez ce projet sur Vercel ?</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Lancez <code>npm run update</code> en local, validez le r&eacute;sultat et poussez. Votre build de production applique les migrations en attente au passage, exactement comme pour les installations en un clic.</p>\\n</div>\\n\\n<h2 id='clone'>4. Le monorepo clon&eacute; &mdash; une commande</h2>\\n<pre><code>npm run update</code></pre>\\n<p>Dans un clone du d&eacute;p&ocirc;t NextBlock, la commande met votre copie &agrave; jour, r&eacute;installe les d&eacute;pendances du workspace et applique les migrations en attente. Elle refuse de s'ex&eacute;cuter par-dessus des modifications non valid&eacute;es et vous explique comment les mettre de c&ocirc;t&eacute; : une mise &agrave; jour ne peut donc jamais faire dispara&icirc;tre du travail en cours. Si vous avez des commits locaux, elle s'arr&ecirc;te et vous oriente vers <code>git pull --rebase</code> plut&ocirc;t que de deviner.</p>\\n\\n<h2 id='what-it-does'>Ce que fait r&eacute;ellement <code>npm run update</code></h2>\\n<ol class='space-y-2'>\\n <li><strong>Identifie l'installation.</strong> Monorepo ou application autonome ; bas&eacute;e sur git ou sur npm ; Docker ou non.</li>\\n <li><strong>Met &agrave; jour le code</strong> depuis la bonne source &mdash; fusion git, pull en avance rapide, ou le paquet <code>create-nextblock</code> publi&eacute;.</li>\\n <li><strong>Installe les d&eacute;pendances</strong> avec <code>npm install</code>, pour que le code et les paquets qu'il importe avancent ensemble.</li>\\n <li><strong>Rafra&icirc;chit les fichiers de migration</strong> livr&eacute;s dans <code>@nextblock-cms/db</code>, afin que les derni&egrave;res &eacute;volutions du sch&eacute;ma soient sur le disque avant toute application.</li>\\n <li><strong>Applique les migrations en attente</strong>, en les listant d'abord et en demandant confirmation.</li>\\n <li><strong>Efface la banni&egrave;re de mise &agrave; jour</strong> du tableau de bord une fois la nouvelle version r&eacute;ellement en place.</li>\\n</ol>\\n\\n<h3>Options</h3>\\n<div class='overflow-x-auto my-6'>\\n<table class='w-full text-left text-sm'>\\n <thead><tr class='border-b border-slate-200 dark:border-white/10'><th class='py-3 pr-4 font-semibold'>Commande</th><th class='py-3 font-semibold'>Effet</th></tr></thead>\\n <tbody class='align-top'>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update</code></td><td class='py-3'>Code, d&eacute;pendances et sch&eacute;ma.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --check</code></td><td class='py-3'>Indique ce qui changerait. N'&eacute;crit rien.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --yes</code></td><td class='py-3'>Sans confirmation. Pratique en CI.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --db-only</code></td><td class='py-3'>Applique uniquement les migrations en attente.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --skip-db</code></td><td class='py-3'>Met &agrave; jour le code et les d&eacute;pendances, sans toucher &agrave; la base.</td></tr>\\n <tr><td class='py-3 pr-4'><code>npm run update -- --force</code></td><td class='py-3'>S'ex&eacute;cute m&ecirc;me si vous &ecirc;tes d&eacute;j&agrave; &agrave; jour.</td></tr>\\n </tbody>\\n</table>\\n</div>\\n\\n<h2 id='database'>Ce qui arrive &agrave; votre base de donn&eacute;es</h2>\\n<p>Les &eacute;volutions du sch&eacute;ma sont <strong>uniquement additives</strong>. NextBlock ne r&eacute;&eacute;crit ni ne rejoue jamais une migration d&eacute;j&agrave; appliqu&eacute;e : chacune est appliqu&eacute;e et enregistr&eacute;e dans la m&ecirc;me transaction, si bien qu'un &eacute;chec est annul&eacute; proprement et laisse la base exactement dans son &eacute;tat initial. Les migrations d&eacute;j&agrave; appliqu&eacute;es sont ignor&eacute;es par num&eacute;ro de version, ce qui rend une nouvelle ex&eacute;cution totalement s&ucirc;re.</p>\\n<p>Les migrations modifient la <em>structure</em> &mdash; tables, colonnes, index, permissions. Vos pages, articles, produits, m&eacute;dias et utilisateurs vous appartiennent : la mise &agrave; jour ne les supprime ni ne les r&eacute;&eacute;crit.</p>\\n<div class='rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Par pr&eacute;caution</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Avant un grand saut sur un site en production, prenez une sauvegarde de la base &mdash; Supabase en r&eacute;alise quotidiennement sur les offres payantes, et vous pouvez en d&eacute;clencher une &agrave; la demande depuis son tableau de bord. Lancez ensuite <code>npm run update -- --check</code> pour voir la liste des migrations en attente.</p>\\n</div>\\n\\n<h2 id='safety'>En cas de probl&egrave;me</h2>\\n<ul class='space-y-2'>\\n <li><strong>Projets autonomes :</strong> chaque fichier remplac&eacute; est d'abord copi&eacute; dans un dossier horodat&eacute; sous <code>.nextblock-backup/</code> dans votre projet. Rien n'est supprim&eacute; : les fichiers que vous avez ajout&eacute;s ne disparaissent jamais.</li>\\n <li><strong>Installations bas&eacute;es sur git :</strong> la mise &agrave; jour est un commit ordinaire. <code>git log</code> l'affiche et <code>git revert</code> l'annule.</li>\\n <li><strong>Une fusion en conflit</strong> est annul&eacute;e automatiquement &mdash; votre copie de travail reste intacte, avec les instructions pour r&eacute;soudre &agrave; la main.</li>\\n <li><strong>Une migration en &eacute;chec</strong> est annul&eacute;e. Corrigez la cause et relancez : rien ne reste &agrave; moiti&eacute; appliqu&eacute;.</li>\\n</ul>\\n<p>Si vous avez personnalis&eacute; un fichier appartenant &agrave; NextBlock &mdash; sous <code>app/</code>, <code>components/</code> ou <code>lib/</code> &mdash; la mise &agrave; jour le remplacera en sauvegardant votre version. Comparez ensuite la sauvegarde pour reporter votre modification. Les personnalisations qui vivent dans vos propres fichiers, dans le CMS ou dans <code>.env</code> ne sont jamais affect&eacute;es.</p>\\n\\n<h2 id='knowing'>Savoir qu'une mise &agrave; jour est disponible</h2>\\n<p>Inutile de surveiller. NextBlock v&eacute;rifie en arri&egrave;re-plan pendant que vous utilisez le CMS et affiche une banni&egrave;re sur le tableau de bord d&egrave;s qu'une version plus r&eacute;cente est publi&eacute;e, en indiquant votre version actuelle et celle disponible. Les administrateurs peuvent aussi lancer <code>npm run update -- --check</code> &agrave; tout moment.</p>\\n\\n<h2 id='faq'>FAQ des mises &agrave; jour</h2>\\n<h3>La mise &agrave; jour va-t-elle &eacute;craser mon contenu ou mes r&eacute;glages ?</h3>\\n<p>Non. Contenus, m&eacute;dias, utilisateurs et r&eacute;glages vivent dans votre base de donn&eacute;es ; la configuration du site vit dans vos variables d'environnement. La mise &agrave; jour ne touche que le code, les d&eacute;pendances et la structure du sch&eacute;ma.</p>\\n<h3>Dois-je installer chaque version ?</h3>\\n<p>Non, mais rester proche de la derni&egrave;re version vous garantit les correctifs de s&eacute;curit&eacute; et rend chaque saut plus petit. Les migrations s'appliquent dans l'ordre : sauter plusieurs versions fonctionne malgr&eacute; tout.</p>\\n<h3>Puis-je l'ex&eacute;cuter en CI ?</h3>\\n<p>Oui &mdash; <code>npm run update -- --yes</code> ne pose aucune question et renvoie un code d'erreur si l'&eacute;tape sch&eacute;ma &eacute;choue, pour qu'un pipeline puisse le d&eacute;tecter.</p>\\n<h3>Et si aucune connexion &agrave; la base n'est configur&eacute;e ?</h3>\\n<p>Le code et les d&eacute;pendances sont tout de m&ecirc;me mis &agrave; jour ; l'&eacute;tape sch&eacute;ma est ignor&eacute;e avec un avertissement indiquant la variable d'environnement &agrave; d&eacute;finir. Relancez ensuite <code>npm run update -- --db-only</code>.</p>\\n<h3>Je suis sur le d&eacute;ploiement Vercel en un clic &mdash; dois-je lancer quelque chose ?</h3>\\n<p>Non. Ce chemin est enti&egrave;rement automatique. La commande existe pour mettre &agrave; jour <em>tout de suite</em> plut&ocirc;t qu'&agrave; minuit, ou lorsque vous travaillez sur un clone local.</p>\\n\\n<div class='rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5'>\\n <p class='mt-0 text-2xl font-semibold text-slate-900 dark:text-white'>Une commande, toutes les installations.</p>\\n <p class='text-sm text-slate-600 dark:text-slate-300'>Vous d&eacute;butez avec NextBlock ? Commencez par le guide d'installation &mdash; puis oubliez les mises &agrave; jour.</p>\\n <div class='mt-5 flex flex-wrap justify-center gap-3'>\\n <a href='/article/comment-configurer-nextblock' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Lire le guide d'installation</a>\\n <a href='https://github.com/nextblock-cms/nextblock' target='_blank' rel='noopener' class='inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50'>Voir sur GitHub</a>\\n </div>\\n</div>\"}\n$nbfr$::jsonb, 0);\n END IF;\nEND\n$body$;\n\n-- Revision baseline for the two new posts.\n--\n-- 00000000000016 back-fills a 'snapshot' revision for every post that existed WHEN IT\n-- RAN. On a fresh install migrations run in order, so 016 executes before this file and\n-- these two posts would be the only ones in the CMS without a restore point. This is the\n-- same projection as 016 section 3b, scoped to the two slugs.\nINSERT INTO public.post_revisions (post_id, author_id, version, revision_type, content)\nSELECT\n po.id,\n NULL::uuid,\n po.version,\n 'snapshot'::public.revision_type,\n jsonb_build_object(\n 'meta', jsonb_build_object(\n 'title', po.title,\n 'slug', po.slug,\n 'language_id', po.language_id,\n 'status', po.status,\n 'meta_title', po.meta_title,\n 'meta_description', po.meta_description,\n 'custom_canonical', po.custom_canonical,\n 'published_at', to_char(po.published_at AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'),\n 'feature_image_id', po.feature_image_id,\n 'label', po.label,\n 'excerpt', po.excerpt,\n 'subtitle', po.subtitle\n ),\n 'blocks', COALESCE((\n SELECT jsonb_agg(\n jsonb_build_object(\n 'language_id', b.language_id,\n 'block_type', b.block_type,\n 'content', b.content,\n 'order', b.\"order\"\n ) ORDER BY b.\"order\" ASC, b.id ASC\n )\n FROM public.blocks b\n WHERE b.post_id = po.id\n ), '[]'::jsonb)\n )\n FROM public.posts po\n WHERE po.slug IN ('how-updating-works', 'comment-fonctionnent-les-mises-a-jour')\n AND NOT EXISTS (\n SELECT 1 FROM public.post_revisions r\n WHERE r.post_id = po.id\n AND r.revision_type = 'snapshot'\n AND r.version <= po.version\n )\nON CONFLICT (post_id, version) DO NOTHING;\n"
121
- },
122
- {
123
- "version": "00000000000021",
124
- "name": "00000000000021_updating_article_git_merge.sql",
125
- "sql": "-- 00000000000021_updating_article_git_merge.sql\n-- Corrects the \"How Updating NextBlock Works\" article seeded in 00000000000020.\n--\n-- That version described the standalone update as \"replace the files and keep a backup\n-- under .nextblock-backup/\". The updater now performs a real git 3-way merge instead:\n-- both template versions are committed into the project's own object database under\n-- refs/nextblock/{base,head}, and the diff between them is applied with `git apply --3way`.\n-- A developer's edits to framework files are preserved and only genuine overlaps conflict.\n-- The copy-and-back-up path survives only as the fallback for a project with no git repo,\n-- no commits, or a dirty tree.\n--\n-- 020 is already applied everywhere and never replays, so the copy is corrected here as\n-- targeted, idempotent string replacements rather than a full re-write of the body: each\n-- replace() is a no-op once the old sentence is gone, so re-running changes nothing.\n-- Data-only; no schema change.\n\nDO $body$\nDECLARE\n v_en_post integer;\n v_fr_post integer;\nBEGIN\n SELECT id INTO v_en_post\n FROM public.posts WHERE language_id = 1 AND slug = 'how-updating-works'\n ORDER BY id LIMIT 1;\n\n SELECT id INTO v_fr_post\n FROM public.posts WHERE language_id = 2 AND slug = 'comment-fonctionnent-les-mises-a-jour'\n ORDER BY id LIMIT 1;\n\n ---------------------------------------------------------------------------\n -- English\n ---------------------------------------------------------------------------\n IF v_en_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(\n replace(\n content->>'html_content',\n -- 1. the \"if something goes wrong\" bullet\n '<li><strong>Standalone projects:</strong> every framework file the update replaces is copied first into a timestamped folder under <code>.nextblock-backup/</code> in your project. Nothing is deleted, so files you added yourself are never removed.</li>',\n '<li><strong>Standalone projects:</strong> the update is applied as a <strong>git 3-way merge</strong> into your working tree &mdash; nothing is committed for you. Review it with <code>git diff</code>, and undo the whole thing with <code>git reset --hard HEAD</code>. Nothing is ever deleted, so files you added yourself are never removed.</li>'\n ),\n -- 2. the \"you customised a framework file\" paragraph\n '<p>If you have customised a file that NextBlock owns &mdash; something under <code>app/</code>, <code>components/</code> or <code>lib/</code> &mdash; the update will replace it and back up your version. Diff the backup afterwards to bring your change forward. Customisations that live in your own new files, in the CMS, or in <code>.env</code> are never affected.</p>',\n '<p>If you have customised a file that NextBlock owns &mdash; something under <code>app/</code>, <code>components/</code> or <code>lib/</code> &mdash; <strong>your edit is kept</strong>. The update merges the upstream change into your version, and only a change that genuinely overlaps yours conflicts, with ordinary <code>&lt;&lt;&lt;&lt;&lt;&lt;&lt; ours</code> / <code>&gt;&gt;&gt;&gt;&gt;&gt;&gt; theirs</code> markers. List them with <code>git diff --name-only --diff-filter=U</code> and resolve with <code>git checkout --theirs</code> or <code>--ours</code>. Customisations in your own files, in the CMS, or in <code>.env</code> are never touched at all.</p>'\n ),\n -- 3. the managed-cloud section\n '<p>Your project is a standalone Next.js app, so new framework code is fetched from the published <code>create-nextblock</code> package on npm &mdash; the exact artifact your project was scaffolded from, versioned in lockstep with the release. NextBlock refreshes the files it owns, merges the new dependency versions into your <code>package.json</code>, runs <code>npm install</code>, and then applies migrations.</p>',\n '<p>Your project is a standalone Next.js app with no upstream to pull from, so new framework code comes from the published <code>create-nextblock</code> package on npm &mdash; the exact artifact your project was scaffolded from, versioned in lockstep with the release. NextBlock fetches both your current version and the new one, and applies the difference between them as a <strong>git 3-way merge</strong>, so the update behaves exactly like a <code>git pull</code>: files you never touched update silently, files you customised keep your changes. It then merges the new dependency versions into your <code>package.json</code>, runs <code>npm install</code>, and applies migrations.</p><p>This needs a git repository with at least one commit and a clean working tree &mdash; commit your work before updating. Without that there is nothing to merge against, so the files are copied instead and anything replaced is kept under <code>.nextblock-backup/</code>.</p>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_en_post\n AND block_type = 'text';\n END IF;\n\n ---------------------------------------------------------------------------\n -- French\n ---------------------------------------------------------------------------\n IF v_fr_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(\n replace(\n content->>'html_content',\n '<li><strong>Projets autonomes :</strong> chaque fichier remplac&eacute; est d''abord copi&eacute; dans un dossier horodat&eacute; sous <code>.nextblock-backup/</code> dans votre projet. Rien n''est supprim&eacute; : les fichiers que vous avez ajout&eacute;s ne disparaissent jamais.</li>',\n '<li><strong>Projets autonomes :</strong> la mise &agrave; jour est appliqu&eacute;e comme une <strong>fusion git &agrave; trois voies</strong> dans votre copie de travail &mdash; rien n''est valid&eacute; &agrave; votre place. Examinez-la avec <code>git diff</code>, et annulez tout avec <code>git reset --hard HEAD</code>. Rien n''est jamais supprim&eacute; : les fichiers que vous avez ajout&eacute;s ne disparaissent jamais.</li>'\n ),\n '<p>Si vous avez personnalis&eacute; un fichier appartenant &agrave; NextBlock &mdash; sous <code>app/</code>, <code>components/</code> ou <code>lib/</code> &mdash; la mise &agrave; jour le remplacera en sauvegardant votre version. Comparez ensuite la sauvegarde pour reporter votre modification. Les personnalisations qui vivent dans vos propres fichiers, dans le CMS ou dans <code>.env</code> ne sont jamais affect&eacute;es.</p>',\n '<p>Si vous avez personnalis&eacute; un fichier appartenant &agrave; NextBlock &mdash; sous <code>app/</code>, <code>components/</code> ou <code>lib/</code> &mdash; <strong>votre modification est conserv&eacute;e</strong>. La mise &agrave; jour fusionne le changement amont dans votre version, et seul un changement qui chevauche r&eacute;ellement le v&ocirc;tre entre en conflit, avec les marqueurs habituels <code>&lt;&lt;&lt;&lt;&lt;&lt;&lt; ours</code> / <code>&gt;&gt;&gt;&gt;&gt;&gt;&gt; theirs</code>. Listez-les avec <code>git diff --name-only --diff-filter=U</code> et r&eacute;solvez avec <code>git checkout --theirs</code> ou <code>--ours</code>. Les personnalisations dans vos propres fichiers, dans le CMS ou dans <code>.env</code> ne sont jamais touch&eacute;es.</p>'\n ),\n '<p>Votre projet est une application Next.js autonome : le nouveau code provient donc du paquet <code>create-nextblock</code> publi&eacute; sur npm &mdash; exactement l''artefact &agrave; partir duquel votre projet a &eacute;t&eacute; g&eacute;n&eacute;r&eacute;, versionn&eacute; en phase avec la release. NextBlock rafra&icirc;chit les fichiers qui lui appartiennent, fusionne les nouvelles versions de d&eacute;pendances dans votre <code>package.json</code>, lance <code>npm install</code>, puis applique les migrations.</p>',\n '<p>Votre projet est une application Next.js autonome sans d&eacute;p&ocirc;t amont &agrave; tirer : le nouveau code provient donc du paquet <code>create-nextblock</code> publi&eacute; sur npm &mdash; exactement l''artefact &agrave; partir duquel votre projet a &eacute;t&eacute; g&eacute;n&eacute;r&eacute;, versionn&eacute; en phase avec la release. NextBlock r&eacute;cup&egrave;re votre version actuelle et la nouvelle, puis applique la diff&eacute;rence entre les deux comme une <strong>fusion git &agrave; trois voies</strong> : la mise &agrave; jour se comporte donc exactement comme un <code>git pull</code> &mdash; les fichiers que vous n''avez jamais touch&eacute;s se mettent &agrave; jour silencieusement, ceux que vous avez personnalis&eacute;s conservent vos changements. Elle fusionne ensuite les nouvelles versions de d&eacute;pendances dans votre <code>package.json</code>, lance <code>npm install</code> et applique les migrations.</p><p>Cela n&eacute;cessite un d&eacute;p&ocirc;t git avec au moins un commit et une copie de travail propre &mdash; validez votre travail avant de mettre &agrave; jour. Sans cela il n''y a rien contre quoi fusionner : les fichiers sont alors copi&eacute;s et tout ce qui est remplac&eacute; est conserv&eacute; sous <code>.nextblock-backup/</code>.</p>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_fr_post\n AND block_type = 'text';\n END IF;\nEND\n$body$;\n"
126
- },
127
- {
128
- "version": "00000000000022",
129
- "name": "00000000000022_updating_article_accuracy.sql",
130
- "sql": "-- 00000000000022_updating_article_accuracy.sql\n-- Three accuracy fixes to the \"How Updating NextBlock Works\" article\n-- (seeded in 00000000000020, first corrected in 00000000000021).\n--\n-- 0. The merge is performed with `git merge-file`, not `git apply --3way`. The latter\n-- implies --index: it stages its result (so `git diff` shows the developer nothing),\n-- requires every path to be tracked (one gitignored framework path aborted the whole\n-- update), and requires the worktree to match the index. merge-file touches no git\n-- state at all. Consequently the resolution commands 021 shipped are wrong: there are\n-- no index stages, so `git checkout --theirs/--ours` does not apply. The conflict\n-- markers are ordinary text; `git checkout -- <file>` discards one file's merge.\n--\n-- 1. \"A conflicted merge is aborted automatically\" was true of only ONE path. The\n-- monorepo/fork path does abort and restore the tree, because the merge belongs to\n-- upstream. The standalone path deliberately LEAVES the conflict in the working tree,\n-- because it is the developer's own repository and resolving it is the whole point.\n-- The bullet now states both.\n-- 2. The Docker section claimed `npm run update` refreshes the schema. It does not, by\n-- design: the self-hosted stack ships its own migration runner (the `migrate` service\n-- in docker-compose.yml, which tracks applied versions in a different table), so the\n-- updater stages the SQL and hands off to `npm run docker:up` rather than applying it\n-- twice through two different trackers.\n--\n-- Same targeted, idempotent replace() approach as 021 — a no-op once the old sentence is\n-- gone. Data-only; no schema change.\n\nDO $body$\nDECLARE\n v_en_post integer;\n v_fr_post integer;\nBEGIN\n SELECT id INTO v_en_post\n FROM public.posts WHERE language_id = 1 AND slug = 'how-updating-works'\n ORDER BY id LIMIT 1;\n\n SELECT id INTO v_fr_post\n FROM public.posts WHERE language_id = 2 AND slug = 'comment-fonctionnent-les-mises-a-jour'\n ORDER BY id LIMIT 1;\n\n IF v_en_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(\n replace(\n content->>'html_content',\n -- 021 shipped index-based resolution commands; the merge no longer\n -- uses the git index, so they do not apply.\n '<strong>your edit is kept</strong>. The update merges the upstream change into your version, and only a change that genuinely overlaps yours conflicts, with ordinary <code>&lt;&lt;&lt;&lt;&lt;&lt;&lt; ours</code> / <code>&gt;&gt;&gt;&gt;&gt;&gt;&gt; theirs</code> markers. List them with <code>git diff --name-only --diff-filter=U</code> and resolve with <code>git checkout --theirs</code> or <code>--ours</code>.',\n '<strong>your edit is kept</strong>. The update merges the upstream change into your version, and only a change that genuinely overlaps yours conflicts &mdash; the updater lists those files, and each one carries ordinary <code>&lt;&lt;&lt;&lt;&lt;&lt;&lt; your version</code> / <code>&gt;&gt;&gt;&gt;&gt;&gt;&gt; NextBlock</code> markers. Edit them as you would any conflict, or run <code>git checkout -- &lt;file&gt;</code> to discard the merge for that one file.'\n ),\n '<li><strong>A conflicted merge</strong> is aborted automatically &mdash; your working tree is left exactly as it was, with instructions printed for resolving it by hand.</li>',\n '<li><strong>A conflict</strong> behaves differently by install, on purpose. On a fork or clone the upstream merge is <em>aborted</em> and your working tree is left exactly as it was. On a standalone project the conflict is <em>left in place</em> for you to resolve &mdash; it is your own repository, and that is the point &mdash; and <code>git reset --hard HEAD</code> backs the whole update out.</li>'\n ),\n '<p>The first command refreshes the application, its dependencies and the schema; the second rebuilds and restarts the containers. Your database and media live in Docker volumes and are never touched by either step &mdash; <code>docker:up</code> rebuilds images, not data.</p>',\n '<p>The first command updates the application and its dependencies and stages the new migrations; the second rebuilds the containers <em>and applies those migrations</em>. The self-hosted stack runs its own migration service, so the updater hands the schema step to it rather than applying the same SQL through two different trackers. Your database and media live in Docker volumes and are never touched by either command &mdash; <code>docker:up</code> rebuilds images, not data.</p>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_en_post\n AND block_type = 'text';\n END IF;\n\n IF v_fr_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(\n replace(\n content->>'html_content',\n '<strong>votre modification est conserv&eacute;e</strong>. La mise &agrave; jour fusionne le changement amont dans votre version, et seul un changement qui chevauche r&eacute;ellement le v&ocirc;tre entre en conflit, avec les marqueurs habituels <code>&lt;&lt;&lt;&lt;&lt;&lt;&lt; ours</code> / <code>&gt;&gt;&gt;&gt;&gt;&gt;&gt; theirs</code>. Listez-les avec <code>git diff --name-only --diff-filter=U</code> et r&eacute;solvez avec <code>git checkout --theirs</code> ou <code>--ours</code>.',\n '<strong>votre modification est conserv&eacute;e</strong>. La mise &agrave; jour fusionne le changement amont dans votre version, et seul un changement qui chevauche r&eacute;ellement le v&ocirc;tre entre en conflit &mdash; la commande liste ces fichiers, et chacun porte les marqueurs habituels <code>&lt;&lt;&lt;&lt;&lt;&lt;&lt; your version</code> / <code>&gt;&gt;&gt;&gt;&gt;&gt;&gt; NextBlock</code>. Modifiez-les comme n''importe quel conflit, ou lancez <code>git checkout -- &lt;fichier&gt;</code> pour abandonner la fusion sur ce seul fichier.'\n ),\n '<li><strong>Une fusion en conflit</strong> est annul&eacute;e automatiquement &mdash; votre copie de travail reste intacte, avec les instructions pour r&eacute;soudre &agrave; la main.</li>',\n '<li><strong>Un conflit</strong> se comporte diff&eacute;remment selon l''installation, volontairement. Sur un fork ou un clone, la fusion amont est <em>annul&eacute;e</em> et votre copie de travail reste intacte. Sur un projet autonome, le conflit est <em>laiss&eacute; en place</em> pour que vous le r&eacute;solviez &mdash; c''est votre d&eacute;p&ocirc;t, et c''est tout l''int&eacute;r&ecirc;t &mdash; et <code>git reset --hard HEAD</code> annule toute la mise &agrave; jour.</li>'\n ),\n '<p>La premi&egrave;re commande met &agrave; jour l''application, ses d&eacute;pendances et le sch&eacute;ma ; la seconde reconstruit et red&eacute;marre les conteneurs. Votre base de donn&eacute;es et vos m&eacute;dias vivent dans des volumes Docker et ne sont touch&eacute;s ni par l''une ni par l''autre &mdash; <code>docker:up</code> reconstruit des images, pas des donn&eacute;es.</p>',\n '<p>La premi&egrave;re commande met &agrave; jour l''application et ses d&eacute;pendances et pr&eacute;pare les nouvelles migrations ; la seconde reconstruit les conteneurs <em>et applique ces migrations</em>. La pile auto-h&eacute;berg&eacute;e dispose de son propre service de migration : la mise &agrave; jour lui confie donc l''&eacute;tape sch&eacute;ma plut&ocirc;t que d''appliquer le m&ecirc;me SQL via deux suivis diff&eacute;rents. Votre base de donn&eacute;es et vos m&eacute;dias vivent dans des volumes Docker et ne sont touch&eacute;s par aucune des deux commandes &mdash; <code>docker:up</code> reconstruit des images, pas des donn&eacute;es.</p>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_fr_post\n AND block_type = 'text';\n END IF;\nEND\n$body$;\n"
131
- },
132
- {
133
- "version": "00000000000023",
134
- "name": "00000000000023_updating_article_layout_not_host.sql",
135
- "sql": "-- 00000000000023_updating_article_layout_not_host.sql\n-- Corrects the most misleading claim in the \"How Updating NextBlock Works\" article\n-- (seeded 00000000000020, corrected in 021 and 022): that the automatic GitHub Action is\n-- about being deployed on Vercel.\n--\n-- It is not. The upstream-sync Action merges the NextBlock MONOREPO into the repository,\n-- so it only works where the repository IS the monorepo — a Vercel 1-click deploy, a\n-- GitHub fork, or a clone. A project scaffolded by `npm create nextblock` is the flattened\n-- standalone app (app/, components/, lib/ at the root); merging apps/ + libs/ + nx.json\n-- into it would wreck it. Pushing that project to GitHub and deploying it on Vercel does\n-- not change its layout, so it is still a `npm run update` install. Docker is orthogonal:\n-- it is how you RUN a project, not what shape the repository is.\n--\n-- Also documents that a merge conflict now holds the migration step back until the\n-- conflict is resolved, so the schema never moves ahead of undecided code.\n--\n-- Targeted, idempotent replace() as in 021/022. Data-only; no schema change.\n\nDO $body$\nDECLARE\n v_en_post integer;\n v_fr_post integer;\nBEGIN\n SELECT id INTO v_en_post\n FROM public.posts WHERE language_id = 1 AND slug = 'how-updating-works'\n ORDER BY id LIMIT 1;\n\n SELECT id INTO v_fr_post\n FROM public.posts WHERE language_id = 2 AND slug = 'comment-fonctionnent-les-mises-a-jour'\n ORDER BY id LIMIT 1;\n\n IF v_en_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(\n replace(\n content->>'html_content',\n -- 1. The section intro: say what actually qualifies.\n '<p>This path updates itself. When you deployed, NextBlock created a repository you own; the dashboard&rsquo;s <strong>Connect GitHub</strong> onboarding step installs a workflow into it that runs <strong>every day at midnight UTC</strong> and can also be triggered by hand from your repository&rsquo;s <strong>Actions</strong> tab.</p>',\n '<p>This path updates itself. When you deployed, NextBlock created a repository you own; the dashboard&rsquo;s <strong>Connect GitHub</strong> onboarding step installs a workflow into it that runs <strong>every day at midnight UTC</strong> and can also be triggered by hand from your repository&rsquo;s <strong>Actions</strong> tab.</p>\\n<div class=''rounded-3xl border border-slate-200 bg-slate-50 p-6 my-8 dark:border-white/10 dark:bg-white/5''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-slate-600 dark:text-slate-300''>What qualifies &mdash; it is the repository, not the host</p>\\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>The workflow merges the <strong>NextBlock monorepo</strong> into your repository, so it only works where your repository <em>is</em> that monorepo: a one-click deploy, a GitHub fork, or a clone. A project created with <code>npm create nextblock</code> is the flattened standalone app &mdash; <code>app/</code>, <code>components/</code> and <code>lib/</code> at the root &mdash; and merging <code>apps/</code>, <code>libs/</code> and <code>nx.json</code> into it would wreck it. Pushing that project to GitHub and deploying it on Vercel does not change its shape: it is still an <code>npm run update</code> install, and NextBlock will not offer it this workflow. Docker is a separate question entirely &mdash; that is how you <em>run</em> a project, not what shape its repository is.</p>\\n</div>'\n ),\n -- 2. The FAQ answer, which asked exactly the question this clarifies.\n '<h3>I am on the one-click Vercel deploy &mdash; do I need to run anything?</h3>\\n<p>No. That path is fully automatic. The command exists for when you want an update <em>now</em> rather than at midnight, or when you are working on a local clone.</p>',\n '<h3>I am on the one-click Vercel deploy &mdash; do I need to run anything?</h3>\\n<p>No. That path is fully automatic. The command exists for when you want an update <em>now</em> rather than at midnight, or when you are working on a local clone.</p>\\n<h3>I deployed to Vercel, but from <code>npm create nextblock</code>. Is that automatic too?</h3>\\n<p>No &mdash; and this is the distinction that catches people out. Automatic updates depend on your repository being the NextBlock <strong>monorepo</strong>, not on where the site is hosted. A project scaffolded by the CLI is the flattened standalone app whatever you deploy it to, so it updates with <code>npm run update</code>. You will not see the <strong>Connect GitHub</strong> step on that kind of install, because the workflow it installs would merge a completely different source tree into yours.</p>'\n ),\n -- 3. Conflicts hold the schema step.\n '<li><strong>A failed migration</strong> rolls back. Fix the cause and re-run; nothing half-applied is left behind.</li>',\n '<li><strong>A failed migration</strong> rolls back. Fix the cause and re-run; nothing half-applied is left behind.</li>\\n <li><strong>Unresolved conflicts hold the database back.</strong> If a merge left conflicts, the update finishes the code and dependency work but <em>stops before migrating</em> &mdash; your schema never moves ahead of code you have not finished deciding on. Resolve them and run <code>npm run update</code> again to apply the migrations, or walk away with <code>git reset --hard HEAD</code>; either way the database was never touched.</li>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_en_post\n AND block_type = 'text';\n END IF;\n\n IF v_fr_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(\n replace(\n content->>'html_content',\n '<p>Ce chemin se met &agrave; jour tout seul. Lors du d&eacute;ploiement, NextBlock a cr&eacute;&eacute; un d&eacute;p&ocirc;t qui vous appartient ; l''&eacute;tape <strong>Connect GitHub</strong> du tableau de bord y installe un workflow qui s''ex&eacute;cute <strong>chaque jour &agrave; minuit UTC</strong> et peut aussi &ecirc;tre lanc&eacute; &agrave; la demande depuis l''onglet <strong>Actions</strong> de votre d&eacute;p&ocirc;t.</p>',\n '<p>Ce chemin se met &agrave; jour tout seul. Lors du d&eacute;ploiement, NextBlock a cr&eacute;&eacute; un d&eacute;p&ocirc;t qui vous appartient ; l''&eacute;tape <strong>Connect GitHub</strong> du tableau de bord y installe un workflow qui s''ex&eacute;cute <strong>chaque jour &agrave; minuit UTC</strong> et peut aussi &ecirc;tre lanc&eacute; &agrave; la demande depuis l''onglet <strong>Actions</strong> de votre d&eacute;p&ocirc;t.</p>\\n<div class=''rounded-3xl border border-slate-200 bg-slate-50 p-6 my-8 dark:border-white/10 dark:bg-white/5''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-slate-600 dark:text-slate-300''>Ce qui compte : le d&eacute;p&ocirc;t, pas l''h&eacute;bergeur</p>\\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Le workflow fusionne le <strong>monorepo NextBlock</strong> dans votre d&eacute;p&ocirc;t : il ne fonctionne donc que si votre d&eacute;p&ocirc;t <em>est</em> ce monorepo &mdash; d&eacute;ploiement en un clic, fork GitHub ou clone. Un projet cr&eacute;&eacute; avec <code>npm create nextblock</code> est l''application autonome aplatie &mdash; <code>app/</code>, <code>components/</code> et <code>lib/</code> &agrave; la racine &mdash; et y fusionner <code>apps/</code>, <code>libs/</code> et <code>nx.json</code> le casserait. Pousser ce projet sur GitHub et le d&eacute;ployer sur Vercel ne change pas sa forme : il se met toujours &agrave; jour avec <code>npm run update</code>, et NextBlock ne lui proposera pas ce workflow. Docker est une tout autre question &mdash; c''est la fa&ccedil;on d''<em>ex&eacute;cuter</em> un projet, pas la forme de son d&eacute;p&ocirc;t.</p>\\n</div>'\n ),\n '<h3>Je suis sur le d&eacute;ploiement Vercel en un clic &mdash; dois-je lancer quelque chose ?</h3>\\n<p>Non. Ce chemin est enti&egrave;rement automatique. La commande existe pour mettre &agrave; jour <em>tout de suite</em> plut&ocirc;t qu''&agrave; minuit, ou lorsque vous travaillez sur un clone local.</p>',\n '<h3>Je suis sur le d&eacute;ploiement Vercel en un clic &mdash; dois-je lancer quelque chose ?</h3>\\n<p>Non. Ce chemin est enti&egrave;rement automatique. La commande existe pour mettre &agrave; jour <em>tout de suite</em> plut&ocirc;t qu''&agrave; minuit, ou lorsque vous travaillez sur un clone local.</p>\\n<h3>J''ai d&eacute;ploy&eacute; sur Vercel, mais depuis <code>npm create nextblock</code>. Est-ce automatique aussi ?</h3>\\n<p>Non &mdash; et c''est la distinction qui pi&egrave;ge le plus. Les mises &agrave; jour automatiques d&eacute;pendent du fait que votre d&eacute;p&ocirc;t soit le <strong>monorepo</strong> NextBlock, pas de l''endroit o&ugrave; le site est h&eacute;berg&eacute;. Un projet g&eacute;n&eacute;r&eacute; par le CLI reste l''application autonome aplatie, quel que soit l''h&eacute;bergeur : il se met &agrave; jour avec <code>npm run update</code>. L''&eacute;tape <strong>Connect GitHub</strong> ne s''affiche pas sur ce type d''installation, car le workflow qu''elle installe fusionnerait une arborescence totalement diff&eacute;rente dans la v&ocirc;tre.</p>'\n ),\n '<li><strong>Une migration en &eacute;chec</strong> est annul&eacute;e. Corrigez la cause et relancez : rien ne reste &agrave; moiti&eacute; appliqu&eacute;.</li>',\n '<li><strong>Une migration en &eacute;chec</strong> est annul&eacute;e. Corrigez la cause et relancez : rien ne reste &agrave; moiti&eacute; appliqu&eacute;.</li>\\n <li><strong>Les conflits non r&eacute;solus bloquent la base.</strong> Si une fusion a laiss&eacute; des conflits, la mise &agrave; jour termine le code et les d&eacute;pendances mais <em>s''arr&ecirc;te avant les migrations</em> &mdash; votre sch&eacute;ma ne prend jamais de l''avance sur un code que vous n''avez pas fini d''arbitrer. R&eacute;solvez-les puis relancez <code>npm run update</code> pour appliquer les migrations, ou abandonnez avec <code>git reset --hard HEAD</code> : dans les deux cas la base n''a jamais &eacute;t&eacute; touch&eacute;e.</li>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_fr_post\n AND block_type = 'text';\n END IF;\nEND\n$body$;\n"
136
- },
137
- {
138
- "version": "00000000000024",
139
- "name": "00000000000024_updating_article_newline_fix.sql",
140
- "sql": "-- 00000000000024_updating_article_newline_fix.sql\n-- Repairs two mistakes made by 00000000000023 in the \"How Updating NextBlock Works\"\n-- article, and lands the FAQ entry that migration failed to insert.\n--\n-- 1. RENDERING BUG. 023 wrote `\\n` inside ordinary single-quoted SQL literals. With\n-- standard_conforming_strings on (the default), that is a literal backslash followed\n-- by 'n' — not a newline — so five visible \"\\n\" sequences were stored in the article\n-- body. Replaced here with real newlines via chr(10). Idempotent: once none remain,\n-- replace() is a no-op.\n--\n-- 2. SILENT NO-MATCH. 023's FAQ replacement targeted a string spanning `</h3>\\n<p>`, and\n-- for the same reason the literal never matched the real newline in the stored HTML, so\n-- the replacement quietly did nothing. Redone here by anchoring on the single-line <h3>\n-- alone and prepending the new entry — no newline in either the search or the\n-- replacement, which is the rule this file establishes for editing the article.\n--\n-- Data-only; no schema change.\n\nDO $body$\nDECLARE\n v_en_post integer;\n v_fr_post integer;\nBEGIN\n SELECT id INTO v_en_post\n FROM public.posts WHERE language_id = 1 AND slug = 'how-updating-works'\n ORDER BY id LIMIT 1;\n\n SELECT id INTO v_fr_post\n FROM public.posts WHERE language_id = 2 AND slug = 'comment-fonctionnent-les-mises-a-jour'\n ORDER BY id LIMIT 1;\n\n IF v_en_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n -- (1) literal backslash-n -> real newline\n replace(content->>'html_content', E'\\\\n', chr(10)),\n -- (2) the FAQ entry 023 failed to insert\n '<h3>I am on the one-click Vercel deploy &mdash; do I need to run anything?</h3>',\n '<h3>I deployed to Vercel, but from <code>npm create nextblock</code>. Is that automatic too?</h3><p>No &mdash; and this is the distinction that catches people out. Automatic updates depend on your repository being the NextBlock <strong>monorepo</strong>, not on where the site is hosted. A project scaffolded by the CLI is the flattened standalone app whatever you deploy it to, so it updates with <code>npm run update</code>. You will not see the <strong>Connect GitHub</strong> step on that kind of install, because the workflow it installs would merge a completely different source tree into yours.</p><h3>I am on the one-click Vercel deploy &mdash; do I need to run anything?</h3>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_en_post\n AND block_type = 'text';\n END IF;\n\n IF v_fr_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(content->>'html_content', E'\\\\n', chr(10)),\n '<h3>Je suis sur le d&eacute;ploiement Vercel en un clic &mdash; dois-je lancer quelque chose ?</h3>',\n '<h3>J''ai d&eacute;ploy&eacute; sur Vercel, mais depuis <code>npm create nextblock</code>. Est-ce automatique aussi ?</h3><p>Non &mdash; et c''est la distinction qui pi&egrave;ge le plus. Les mises &agrave; jour automatiques d&eacute;pendent du fait que votre d&eacute;p&ocirc;t soit le <strong>monorepo</strong> NextBlock, pas de l''endroit o&ugrave; le site est h&eacute;berg&eacute;. Un projet g&eacute;n&eacute;r&eacute; par le CLI reste l''application autonome aplatie, quel que soit l''h&eacute;bergeur : il se met &agrave; jour avec <code>npm run update</code>. L''&eacute;tape <strong>Connect GitHub</strong> ne s''affiche pas sur ce type d''installation, car le workflow qu''elle installe fusionnerait une arborescence totalement diff&eacute;rente dans la v&ocirc;tre.</p><h3>Je suis sur le d&eacute;ploiement Vercel en un clic &mdash; dois-je lancer quelque chose ?</h3>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_fr_post\n AND block_type = 'text';\n END IF;\nEND\n$body$;\n"
141
- },
142
- {
143
- "version": "00000000000025",
144
- "name": "00000000000025_rebrand_nextblock_dev.sql",
145
- "sql": "-- 00000000000025_rebrand_nextblock_dev.sql\n-- Domain rebrand: the retired .ca domain -> nextblock.dev.\n--\n-- Two halves to this change, because two populations need different treatment:\n--\n-- Fresh installs -- migrations 00000000000003_baseline_seed and\n-- 00000000000006/007/009_home_live_demo_promo* were rewritten in place\n-- to seed nextblock.dev directly. Safe to edit despite the append-only\n-- rule: Supabase tracks migration history by version string with no\n-- checksum, so an already-applied file is never re-read or replayed.\n-- Those four are seed/content only -- no schema, no constraints.\n--\n-- Existing installs -- prod, sandbox, and any deployed fork already seeded the old domain\n-- from the pre-rewrite versions of those files. They never replay, so\n-- this migration corrects their data forward.\n--\n-- Both paths converge on nextblock.dev. On a fresh install this migration matches nothing\n-- and is a no-op, which is also what makes it safe to re-run.\n--\n-- Also flips the sandbox demo account address. The account itself is created by\n-- apps/nextblock/app/api/cron/reset-sandbox/route.ts (now 'demo@nextblock.dev'); the legacy\n-- demo account is NOT removed here -- the reset route never deletes auth users and never\n-- truncates public.profiles, so it must be deleted by hand in the Supabase Auth dashboard\n-- or it survives as a working ADMIN.\n--\n-- NOTE: keep the word \"sandbox\" OUT of this filename -- generate-sandbox-reset.ts excludes any\n-- migration whose filename contains \"sandbox\" from the sandbox reset bundle.\n\nDO $body$\nDECLARE\n -- Assembled from two halves on purpose: it is the one string this rebrand is meant to\n -- erase, and spelling it out here would leave the repo-wide grep with a permanent hit\n -- inside the very migration that removes it. Resolves to the retired domain at runtime.\n legacy_domain constant text := 'nextblock' || '.ca';\nBEGIN\n -- 1. Seeded UI strings (e.g. the \"Purchase at ...\" link on the sandbox checkout panel).\n UPDATE public.translations\n SET translations = replace(translations::text, legacy_domain, 'nextblock.dev')::jsonb,\n updated_at = now()\n WHERE translations::text LIKE '%' || legacy_domain || '%';\n\n -- 2. Page/post content -- the home-page \"Live Demo\" promo carries the demo login address.\n UPDATE public.blocks\n SET content = replace(content::text, legacy_domain, 'nextblock.dev')::jsonb,\n updated_at = now()\n WHERE content::text LIKE '%' || legacy_domain || '%';\n\n -- 3. Site settings -- invoice branding on the sandbox carries the billing address.\n UPDATE public.site_settings\n SET value = replace(value::text, legacy_domain, 'nextblock.dev')::jsonb\n WHERE value::text LIKE '%' || legacy_domain || '%';\nEND\n$body$;\n"
146
- },
147
- {
148
- "version": "00000000000026",
149
- "name": "00000000000026_product_inquiries.sql",
150
- "sql": "-- Purchase enquiries raised when the store cannot take payment.\n--\n-- A NextBlock store can be fully built — products, prices, images, pages — while its\n-- Stripe or Freemius credentials are still missing. Until this migration the only\n-- symptom was a shopper reaching checkout and receiving the payment provider's raw\n-- rejection (\"Invalid API Key provided: sk_test_*ummy\"), which tells them nothing and\n-- loses the sale outright.\n--\n-- The storefront now offers those shoppers an enquiry form in place of Add-to-Cart,\n-- and every submission lands here. The row is the product, not the email: SMTP is a\n-- separate piece of setup that the same half-configured store has usually also not\n-- done, so a notification that cannot be sent must never be the only record. The\n-- `email_delivered` flag records whether the owner was successfully notified; false\n-- means \"read this in the CMS, nobody got a mail about it\".\n--\n-- SECURITY POSTURE. Rows hold visitor-supplied PII (name, email, free text) plus a\n-- masked IP. There is deliberately NO anon policy of any kind: anonymous visitors\n-- neither read nor write this table directly. The public server action inserts with\n-- the service-role client after bot-protection and throttle checks, exactly as\n-- `privacy_consent_logs` does — that keeps the insert shape server-controlled and\n-- means a leaked anon key cannot enumerate or seed enquiries. ADMINs read; nobody\n-- else does, because these are sales leads and personal data.\n--\n-- `product_id` is a PLAIN uuid with NO foreign key, following site_script_revisions:\n-- an enquiry is evidence that someone wanted a product, and deleting the product\n-- should not delete or rewrite that evidence. `product_slug` and `product_title` are\n-- denormalised so a deleted product is still identifiable in the list.\n--\n-- Forward-only and idempotent.\n\nCREATE TABLE IF NOT EXISTS public.product_inquiries (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n -- No FK by design (see header). Null when the product was deleted after the fact.\n product_id uuid,\n product_slug text,\n product_title text,\n sender_name text NOT NULL,\n sender_email text NOT NULL,\n message text NOT NULL,\n -- Which language the visitor was browsing in, so the owner can reply in kind.\n locale text,\n ip_masked text,\n user_agent text,\n -- False when SMTP was unconfigured or the send failed: the CMS list is then the\n -- only place this enquiry exists.\n email_delivered boolean DEFAULT false NOT NULL,\n is_resolved boolean DEFAULT false NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT product_inquiries_pkey PRIMARY KEY (id),\n CONSTRAINT product_inquiries_sender_name_not_blank CHECK ((char_length(btrim(sender_name)) > 0)),\n CONSTRAINT product_inquiries_sender_email_not_blank CHECK ((char_length(btrim(sender_email)) > 0)),\n CONSTRAINT product_inquiries_message_not_blank CHECK ((char_length(btrim(message)) > 0))\n);\n\nCOMMENT ON TABLE public.product_inquiries IS 'Visitor purchase enquiries raised when the store cannot take payment. Written by the service role from a public server action; read by ADMINs only.';\n\nCOMMENT ON COLUMN public.product_inquiries.product_id IS 'Plain uuid, no FK: an enquiry outlives the product it was about.';\n\nCOMMENT ON COLUMN public.product_inquiries.ip_masked IS 'Partially masked IP (e.g. 203.0.113.x) - never store a full address. Also backs the per-IP submission throttle.';\n\nCOMMENT ON COLUMN public.product_inquiries.email_delivered IS 'False when the owner notification could not be sent (e.g. SMTP unconfigured); the stored row is then the only record.';\n\n-- Newest-first is the only listing order the CMS needs.\nCREATE INDEX IF NOT EXISTS product_inquiries_created_idx\n ON public.product_inquiries USING btree (created_at DESC);\n\n-- Backs the throttle lookup in the public server action: count recent rows per IP.\nCREATE INDEX IF NOT EXISTS product_inquiries_ip_created_idx\n ON public.product_inquiries USING btree (ip_masked, created_at DESC);\n\n-- Lets the CMS count outstanding enquiries without scanning resolved history.\nCREATE INDEX IF NOT EXISTS product_inquiries_unresolved_idx\n ON public.product_inquiries USING btree (created_at DESC)\n WHERE (is_resolved = false);\n\nALTER TABLE public.product_inquiries ENABLE ROW LEVEL SECURITY;\n\n-- anon is deliberately absent: the public path goes through the service role.\nGRANT SELECT, UPDATE ON TABLE public.product_inquiries TO authenticated;\nGRANT ALL ON TABLE public.product_inquiries TO service_role;\n\nDROP POLICY IF EXISTS product_inquiries_admin_read_policy ON public.product_inquiries;\nCREATE POLICY product_inquiries_admin_read_policy ON public.product_inquiries FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\n-- ADMINs may only flip the resolved flag; the enquiry content itself is a record.\nDROP POLICY IF EXISTS product_inquiries_admin_update_policy ON public.product_inquiries;\nCREATE POLICY product_inquiries_admin_update_policy ON public.product_inquiries FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS product_inquiries_service_role_policy ON public.product_inquiries;\nCREATE POLICY product_inquiries_service_role_policy ON public.product_inquiries TO service_role USING (true) WITH CHECK (true);\n\n-- Where enquiry notifications are sent. Kept in the PUBLIC settings row rather than the\n-- secret one because it is an address, not a credential — but it is never rendered to\n-- the storefront: the public form posts a product id and the server resolves the\n-- recipient. Empty string means \"fall back\", see resolveSellerContactEmail().\nINSERT INTO public.site_settings (key, value)\nVALUES ('store_contact', '{\"contactEmail\": \"\"}'::jsonb)\nON CONFLICT (key) DO NOTHING;\n\n-- Storefront copy for the enquiry flow. Public-facing, so it must be translatable;\n-- the components pass an English literal as the fallback, so an install that never\n-- runs this seed still renders correctly.\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES\n ('ecommerce.contact_seller', '{\"en\": \"Contact the seller\", \"fr\": \"Contacter le vendeur\"}', now(), now()),\n ('ecommerce.contact_seller_heading', '{\"en\": \"Interested in this product?\", \"fr\": \"Ce produit vous intéresse ?\"}', now(), now()),\n ('ecommerce.contact_seller_intro', '{\"en\": \"Online ordering isn''t available for this item yet. Send the seller a message and they''ll get back to you about buying it.\", \"fr\": \"La commande en ligne n''est pas encore disponible pour cet article. Envoyez un message au vendeur et il vous répondra au sujet de son achat.\"}', now(), now()),\n ('ecommerce.contact_seller_name', '{\"en\": \"Your name\", \"fr\": \"Votre nom\"}', now(), now()),\n ('ecommerce.contact_seller_email', '{\"en\": \"Your email\", \"fr\": \"Votre courriel\"}', now(), now()),\n ('ecommerce.contact_seller_message', '{\"en\": \"Message\", \"fr\": \"Message\"}', now(), now()),\n ('ecommerce.contact_seller_send', '{\"en\": \"Send message\", \"fr\": \"Envoyer le message\"}', now(), now()),\n ('ecommerce.contact_seller_sending', '{\"en\": \"Sending...\", \"fr\": \"Envoi...\"}', now(), now()),\n ('ecommerce.contact_seller_sent', '{\"en\": \"Thanks - your message has been sent to the seller. They''ll reply to the email address you gave.\", \"fr\": \"Merci - votre message a été envoyé au vendeur. Il répondra à l''adresse courriel que vous avez indiquée.\"}', now(), now()),\n ('ecommerce.contact_seller_error', '{\"en\": \"Sorry, your message couldn''t be sent. Please try again in a moment.\", \"fr\": \"Désolé, votre message n''a pas pu être envoyé. Veuillez réessayer dans un instant.\"}', now(), now()),\n ('ecommerce.contact_seller_invalid', '{\"en\": \"Please check your name, email address and message, then try again.\", \"fr\": \"Veuillez vérifier votre nom, votre adresse courriel et votre message, puis réessayer.\"}', now(), now()),\n ('ecommerce.contact_seller_throttled', '{\"en\": \"You''ve sent several messages already. Please wait a few minutes before sending another.\", \"fr\": \"Vous avez déjà envoyé plusieurs messages. Veuillez patienter quelques minutes avant d''en envoyer un autre.\"}', now(), now()),\n ('ecommerce.not_available_for_purchase', '{\"en\": \"Not available for online purchase\", \"fr\": \"Non disponible à l''achat en ligne\"}', now(), now()),\n ('ecommerce.checkout_payments_unavailable', '{\"en\": \"This store is not able to take payments right now. Please contact the seller to complete your purchase.\", \"fr\": \"Cette boutique ne peut pas accepter de paiements pour le moment. Veuillez contacter le vendeur pour finaliser votre achat.\"}', now(), now())\nON CONFLICT (key) DO NOTHING;\n"
151
- },
152
- {
153
- "version": "00000000000027",
154
- "name": "00000000000027_message_threads.sql",
155
- "sql": "-- Private conversations with anonymous visitors, and the end of the exposed form address.\n--\n-- Two problems, one shape.\n--\n-- FIRST: a contact-form block stores its destination address in the block's own\n-- content, and `FormBlockRenderer` is a \"use client\" component that receives that\n-- content wholesale. Everything crossing that boundary is serialized into the RSC\n-- payload, so the shop owner's inbox is published in the markup of every page\n-- carrying a form. `form_endpoints` moves the address server-side and leaves behind a\n-- `form_key` — an opaque handle that grants nothing and is safe to serialize.\n--\n-- SECOND: a visitor who sends a message has nowhere to receive an answer. They are\n-- anonymous — `cms_interactions` cannot hold them, because its `user_id` is NOT NULL\n-- with a foreign key to `profiles`. So this is a separate lane: `message_threads` plus\n-- `thread_messages`, reached by a tokenised link rather than an account.\n--\n-- WHY REVIEWS AND COMMENTS ARE NOT HERE. They are already public, already tied to a\n-- registered account, and a staff answer to them is published content, not a private\n-- reply. That lane stays in `cms_interactions` and is handled by migration 28. One\n-- inbox reads both; the storage stays honest about the difference.\n--\n-- SECURITY POSTURE. Rows hold visitor PII (name, email, free text) and a masked IP.\n-- There is NO anon grant and NO anon policy: the tokenised visitor page verifies the\n-- token in application code and then reads with the service role, the same posture the\n-- MCP token route takes against `mcp_access_tokens`. ADMINs read; nobody else does.\n--\n-- `token_hash` stores only a SHA-256 of the visitor's token. The plaintext exists long\n-- enough to be placed in one outbound email and is never written down. A token is\n-- minted on the FIRST ADMIN REPLY, never at submission — a store that receives a\n-- hundred enquiries and answers three has three live credentials, not a hundred.\n--\n-- `source` is a text column with a CHECK rather than an enum, deliberately.\n-- `interaction_type` in this same schema is the cautionary tale: a two-value enum\n-- guarded by an exhaustive CHECK, where adding a third value needs ALTER TYPE ... ADD\n-- VALUE — and PostgreSQL forbids using a value added in the transaction that added it,\n-- which is exactly one migration file. A CHECK is replaceable in one statement.\n--\n-- Forward-only and idempotent.\n\n-- ---------------------------------------------------------------------------\n-- 1. form_endpoints — where a contact-form block's mail goes, and what its fields\n-- are called. `fields` is a SERVER-SIDE snapshot of the field manifest so the\n-- notification email renders labels the browser did not supply; the same rule\n-- the product-enquiry action follows when it looks the product title up itself.\n-- ---------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS public.form_endpoints (\n form_key uuid NOT NULL,\n label text DEFAULT 'Contact form' NOT NULL,\n -- NULL/empty means \"fall back to the resolver ladder\", like store_contact.\n recipient_email text,\n fields jsonb DEFAULT '[]'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT form_endpoints_pkey PRIMARY KEY (form_key),\n CONSTRAINT form_endpoints_label_not_blank CHECK ((char_length(btrim(label)) > 0)),\n CONSTRAINT form_endpoints_fields_array CHECK ((jsonb_typeof(fields) = 'array'))\n);\n\nCOMMENT ON TABLE public.form_endpoints IS 'Server-side destination and field manifest for a contact-form block, keyed by the non-secret form_key carried in block content. The address never reaches the browser.';\n\nCOMMENT ON COLUMN public.form_endpoints.form_key IS 'Non-secret opaque handle. Safe in the RSC payload: it grants nothing, it is only a lookup key.';\n\nCOMMENT ON COLUMN public.form_endpoints.fields IS 'Snapshot of [{temp_id,label,field_type}] written by the CMS editor, so labels in emails and the inbox are server-trusted.';\n\nDROP TRIGGER IF EXISTS set_form_endpoints_updated_at ON public.form_endpoints;\nCREATE TRIGGER set_form_endpoints_updated_at BEFORE UPDATE ON public.form_endpoints\n FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\n\nALTER TABLE public.form_endpoints ENABLE ROW LEVEL SECURITY;\n\n-- anon deliberately absent: the public submit path goes through the service role.\nGRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.form_endpoints TO authenticated;\nGRANT ALL ON TABLE public.form_endpoints TO service_role;\n\nDROP POLICY IF EXISTS form_endpoints_editor_read_policy ON public.form_endpoints;\nCREATE POLICY form_endpoints_editor_read_policy ON public.form_endpoints FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS form_endpoints_admin_write_policy ON public.form_endpoints;\nCREATE POLICY form_endpoints_admin_write_policy ON public.form_endpoints FOR ALL TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS form_endpoints_service_role_policy ON public.form_endpoints;\nCREATE POLICY form_endpoints_service_role_policy ON public.form_endpoints TO service_role USING (true) WITH CHECK (true);\n\n-- ---------------------------------------------------------------------------\n-- 2. message_threads — the private-lane spine and the only home of a thread token.\n-- `subject_id` and `form_key` are PLAIN uuids with NO foreign key: a conversation\n-- outlives the product or the form block it started from.\n-- ---------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS public.message_threads (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n source text NOT NULL,\n -- product_inquiries.id when source = 'product_inquiry'.\n subject_id uuid,\n -- form_endpoints.form_key when source = 'contact_form'.\n form_key uuid,\n -- Denormalised so a deleted product or removed form block stays identifiable.\n subject_label text DEFAULT 'Message' NOT NULL,\n sender_name text,\n -- NULL when a contact form collected no email field. Such a thread can never be\n -- answered, and the CMS says so rather than failing silently at reply time.\n sender_email text,\n locale text,\n -- Submitted field map for a contact form: {temp_id: value}.\n fields jsonb DEFAULT '{}'::jsonb NOT NULL,\n status text DEFAULT 'open' NOT NULL,\n unread_for_admin boolean DEFAULT true NOT NULL,\n unread_for_visitor boolean DEFAULT false NOT NULL,\n -- SHA-256 hex of the visitor's token; NULL until the first admin reply.\n token_hash text,\n token_expires_at timestamp with time zone,\n token_revoked_at timestamp with time zone,\n token_last_used_at timestamp with time zone,\n last_message_at timestamp with time zone DEFAULT now() NOT NULL,\n ip_masked text,\n user_agent text,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT message_threads_pkey PRIMARY KEY (id),\n CONSTRAINT message_threads_token_hash_key UNIQUE (token_hash),\n CONSTRAINT message_threads_source_check\n CHECK ((source = ANY (ARRAY['product_inquiry'::text, 'contact_form'::text]))),\n CONSTRAINT message_threads_status_check\n CHECK ((status = ANY (ARRAY['open'::text, 'closed'::text]))),\n CONSTRAINT message_threads_subject_check CHECK (\n (((source = 'product_inquiry'::text) AND (subject_id IS NOT NULL)) OR\n ((source = 'contact_form'::text) AND (form_key IS NOT NULL)))),\n CONSTRAINT message_threads_fields_object CHECK ((jsonb_typeof(fields) = 'object'))\n);\n\nCOMMENT ON TABLE public.message_threads IS 'Private conversations with anonymous visitors (product enquiries and contact-form submissions). Written by the service role from public server actions; read by ADMINs only. Public reviews and comments are NOT here - they live in cms_interactions.';\n\nCOMMENT ON COLUMN public.message_threads.token_hash IS 'SHA-256 hex of the visitor thread token. The raw token is never stored; it is minted on the first admin reply and mailed once.';\n\nCOMMENT ON COLUMN public.message_threads.subject_id IS 'product_inquiries.id. Plain uuid, no FK: a conversation outlives the enquiry record it grew from.';\n\nCREATE INDEX IF NOT EXISTS message_threads_last_message_idx\n ON public.message_threads USING btree (last_message_at DESC);\n\nCREATE INDEX IF NOT EXISTS message_threads_source_last_idx\n ON public.message_threads USING btree (source, last_message_at DESC);\n\n-- Backs the CMS nav unread badge without scanning read history.\nCREATE INDEX IF NOT EXISTS message_threads_unread_idx\n ON public.message_threads USING btree (last_message_at DESC)\n WHERE (unread_for_admin = true);\n\n-- Idempotent backfill / dedupe of enquiry threads.\nCREATE INDEX IF NOT EXISTS message_threads_subject_idx\n ON public.message_threads USING btree (subject_id) WHERE (subject_id IS NOT NULL);\n\n-- No separate token index: message_threads_token_hash_key is already the unique\n-- btree the /thread lookup probes.\n\nDROP TRIGGER IF EXISTS set_message_threads_updated_at ON public.message_threads;\nCREATE TRIGGER set_message_threads_updated_at BEFORE UPDATE ON public.message_threads\n FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\n\nALTER TABLE public.message_threads ENABLE ROW LEVEL SECURITY;\n\nGRANT SELECT, UPDATE ON TABLE public.message_threads TO authenticated;\nGRANT ALL ON TABLE public.message_threads TO service_role;\n\nDROP POLICY IF EXISTS message_threads_admin_read_policy ON public.message_threads;\nCREATE POLICY message_threads_admin_read_policy ON public.message_threads FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS message_threads_admin_update_policy ON public.message_threads;\nCREATE POLICY message_threads_admin_update_policy ON public.message_threads FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS message_threads_service_role_policy ON public.message_threads;\nCREATE POLICY message_threads_service_role_policy ON public.message_threads TO service_role USING (true) WITH CHECK (true);\n\n-- ---------------------------------------------------------------------------\n-- 3. thread_messages — one turn of a private conversation.\n-- ---------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS public.thread_messages (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n thread_id uuid NOT NULL,\n direction text NOT NULL,\n body text NOT NULL,\n -- profiles.id for an outbound reply. No FK: a departing admin must not take the\n -- conversation with them.\n author_id uuid,\n author_name text,\n -- False when SMTP was unconfigured or the send failed. sendEmail() throws when\n -- unconfigured, so this row - not the mail - is the record.\n email_delivered boolean DEFAULT false NOT NULL,\n email_error text,\n ip_masked text,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT thread_messages_pkey PRIMARY KEY (id),\n CONSTRAINT thread_messages_direction_check\n CHECK ((direction = ANY (ARRAY['inbound'::text, 'outbound'::text]))),\n CONSTRAINT thread_messages_body_not_blank CHECK ((char_length(btrim(body)) > 0))\n);\n\nCOMMENT ON TABLE public.thread_messages IS 'Turns of a private conversation. Content is append-only; only the delivery flags may change after insert.';\n\n-- A real foreign key here, unlike the outward pointers above: a message has no\n-- meaning without its thread.\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint\n WHERE conname = 'thread_messages_thread_id_fkey'\n AND conrelid = 'public.thread_messages'::regclass) THEN\n ALTER TABLE ONLY public.thread_messages\n ADD CONSTRAINT thread_messages_thread_id_fkey FOREIGN KEY (thread_id)\n REFERENCES public.message_threads(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nCREATE INDEX IF NOT EXISTS thread_messages_thread_created_idx\n ON public.thread_messages USING btree (thread_id, created_at);\n\n-- Backs the per-IP reply throttle on the public thread page.\nCREATE INDEX IF NOT EXISTS thread_messages_ip_created_idx\n ON public.thread_messages USING btree (ip_masked, created_at DESC);\n\n-- Message TEXT is append-only — not the whole row, because email_delivered has to be\n-- settable after the send completes. The trigger blocks rewrites of the load-bearing\n-- columns only, and it binds the service role too, which otherwise bypasses RLS.\nCREATE OR REPLACE FUNCTION public.prevent_thread_message_rewrite() RETURNS trigger\n LANGUAGE plpgsql SET search_path = '' AS $$\nBEGIN\n IF TG_OP = 'DELETE' THEN\n RAISE EXCEPTION 'thread_messages is append-only; DELETE is not permitted'\n USING ERRCODE = 'restrict_violation';\n END IF;\n IF NEW.body IS DISTINCT FROM OLD.body\n OR NEW.direction IS DISTINCT FROM OLD.direction\n OR NEW.thread_id IS DISTINCT FROM OLD.thread_id\n OR NEW.created_at IS DISTINCT FROM OLD.created_at THEN\n RAISE EXCEPTION 'thread_messages content is append-only; only delivery flags may change'\n USING ERRCODE = 'restrict_violation';\n END IF;\n RETURN NEW;\nEND;\n$$;\n\nDROP TRIGGER IF EXISTS trg_thread_messages_append_only ON public.thread_messages;\nCREATE TRIGGER trg_thread_messages_append_only BEFORE UPDATE OR DELETE ON public.thread_messages\n FOR EACH ROW EXECUTE FUNCTION public.prevent_thread_message_rewrite();\n\n-- NB: the ON DELETE CASCADE above fires this trigger, so deleting a thread is\n-- impossible by design. \"Delete\" in the CMS means status='closed' plus a revoked\n-- token, and the UI says so.\n\nALTER TABLE public.thread_messages ENABLE ROW LEVEL SECURITY;\n\nGRANT SELECT ON TABLE public.thread_messages TO authenticated;\nGRANT ALL ON TABLE public.thread_messages TO service_role;\n\nDROP POLICY IF EXISTS thread_messages_admin_read_policy ON public.thread_messages;\nCREATE POLICY thread_messages_admin_read_policy ON public.thread_messages FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS thread_messages_service_role_policy ON public.thread_messages;\nCREATE POLICY thread_messages_service_role_policy ON public.thread_messages TO service_role USING (true) WITH CHECK (true);\n\n-- ---------------------------------------------------------------------------\n-- 4. Backfill: every existing enquiry becomes a thread whose first inbound turn is\n-- the original message. product_inquiries is KEPT and kept written — it is the\n-- enquiry's own record and owns the ip_masked the submission throttle counts.\n-- The thread is a conversation ABOUT the enquiry, not a replacement for it.\n-- ---------------------------------------------------------------------------\nINSERT INTO public.message_threads\n (source, subject_id, subject_label, sender_name, sender_email, locale,\n ip_masked, user_agent, unread_for_admin, status, last_message_at, created_at)\nSELECT 'product_inquiry', i.id, COALESCE(i.product_title, 'Product enquiry'),\n i.sender_name, i.sender_email, i.locale, i.ip_masked, i.user_agent,\n NOT i.is_resolved, CASE WHEN i.is_resolved THEN 'closed' ELSE 'open' END,\n i.created_at, i.created_at\nFROM public.product_inquiries i\nWHERE NOT EXISTS (SELECT 1 FROM public.message_threads t WHERE t.subject_id = i.id);\n\nINSERT INTO public.thread_messages (thread_id, direction, body, email_delivered, ip_masked, created_at)\nSELECT t.id, 'inbound', i.message, i.email_delivered, i.ip_masked, i.created_at\nFROM public.product_inquiries i\nJOIN public.message_threads t ON t.subject_id = i.id\nWHERE NOT EXISTS (SELECT 1 FROM public.thread_messages m WHERE m.thread_id = t.id);\n\n-- ---------------------------------------------------------------------------\n-- 5. The form-block data migration.\n-- Rewrites every stored form block: mints a form_key, moves recipient_email into\n-- form_endpoints, snapshots the field manifest, and DELETES recipient_email from\n-- the jsonb. Three shapes have to be handled, because a form nested inside a\n-- section has no blocks row of its own — it lives in the section's content:\n-- a) blocks.content where block_type = 'form'\n-- b) blocks.content where block_type = 'section' (column_blocks, slides)\n-- c) content_drafts.blocks / product_drafts.blocks (jsonb arrays of snapshots)\n-- ---------------------------------------------------------------------------\nCREATE OR REPLACE FUNCTION public.nb_migrate_form_columns(p_cols jsonb)\nRETURNS jsonb LANGUAGE plpgsql VOLATILE SET search_path = '' AS $fn$\nDECLARE v_col jsonb; v_blk jsonb; v_new_cols jsonb := '[]'::jsonb; v_new_col jsonb;\nBEGIN\n FOR v_col IN SELECT * FROM jsonb_array_elements(p_cols) LOOP\n v_new_col := '[]'::jsonb;\n FOR v_blk IN SELECT * FROM jsonb_array_elements(coalesce(v_col, '[]'::jsonb)) LOOP\n v_new_col := v_new_col || jsonb_build_array(\n v_blk || jsonb_build_object('content', public.nb_migrate_form_content(v_blk->'content')));\n END LOOP;\n v_new_cols := v_new_cols || jsonb_build_array(v_new_col);\n END LOOP;\n RETURN v_new_cols;\nEND;\n$fn$;\n\nCREATE OR REPLACE FUNCTION public.nb_migrate_form_content(p_content jsonb)\nRETURNS jsonb LANGUAGE plpgsql VOLATILE SET search_path = '' AS $fn$\nDECLARE\n v_key uuid; v_email text; v_fields jsonb; v_slide jsonb; v_new_slides jsonb;\nBEGIN\n IF p_content IS NULL OR jsonb_typeof(p_content) <> 'object' THEN\n RETURN p_content;\n END IF;\n\n -- Leaf: this object IS a form block's content.\n IF (p_content ? 'recipient_email') AND (p_content ? 'fields') THEN\n v_key := gen_random_uuid();\n v_email := nullif(btrim(coalesce(p_content->>'recipient_email','')), '');\n SELECT coalesce(jsonb_agg(jsonb_build_object(\n 'temp_id', f->>'temp_id', 'label', f->>'label', 'field_type', f->>'field_type')), '[]'::jsonb)\n INTO v_fields\n FROM jsonb_array_elements(coalesce(p_content->'fields', '[]'::jsonb)) f;\n INSERT INTO public.form_endpoints (form_key, label, recipient_email, fields)\n VALUES (v_key, 'Contact form', v_email, v_fields)\n ON CONFLICT (form_key) DO NOTHING;\n RETURN (p_content - 'recipient_email') || jsonb_build_object('form_key', v_key::text);\n END IF;\n\n -- Container: a section's slides, each with column_blocks.\n IF (p_content ? 'slides') AND jsonb_typeof(p_content->'slides') = 'array' THEN\n v_new_slides := '[]'::jsonb;\n FOR v_slide IN SELECT * FROM jsonb_array_elements(p_content->'slides') LOOP\n v_new_slides := v_new_slides || jsonb_build_array(\n v_slide || jsonb_build_object('column_blocks',\n public.nb_migrate_form_columns(coalesce(v_slide->'column_blocks','[]'::jsonb))));\n END LOOP;\n p_content := p_content || jsonb_build_object('slides', v_new_slides);\n END IF;\n\n -- Container: a standard section's column_blocks.\n IF (p_content ? 'column_blocks') AND jsonb_typeof(p_content->'column_blocks') = 'array' THEN\n p_content := p_content || jsonb_build_object('column_blocks',\n public.nb_migrate_form_columns(p_content->'column_blocks'));\n END IF;\n\n RETURN p_content;\nEND;\n$fn$;\n\n-- The LIKE guard keeps this from rewriting (and bumping updated_at on) every block.\nUPDATE public.blocks\n SET content = public.nb_migrate_form_content(content)\n WHERE content::text LIKE '%recipient_email%';\n\nUPDATE public.content_drafts d\n SET blocks = (SELECT coalesce(jsonb_agg(b || jsonb_build_object(\n 'content', public.nb_migrate_form_content(b->'content'))), '[]'::jsonb)\n FROM jsonb_array_elements(d.blocks) b)\n WHERE d.blocks::text LIKE '%recipient_email%';\n\nUPDATE public.product_drafts d\n SET blocks = (SELECT coalesce(jsonb_agg(b || jsonb_build_object(\n 'content', public.nb_migrate_form_content(b->'content'))), '[]'::jsonb)\n FROM jsonb_array_elements(d.blocks) b)\n WHERE d.blocks::text LIKE '%recipient_email%';\n\n-- One-shot helpers. Dropping them keeps the public schema (and db:types) clean.\nDROP FUNCTION IF EXISTS public.nb_migrate_form_content(jsonb);\nDROP FUNCTION IF EXISTS public.nb_migrate_form_columns(jsonb);\n\n-- ---------------------------------------------------------------------------\n-- 6. Seeds. Thread-page copy is public-facing so it must be translatable; the\n-- components pass an English literal as the fallback, so an install that never\n-- runs this seed still renders correctly.\n-- ---------------------------------------------------------------------------\nINSERT INTO public.site_settings (key, value)\nVALUES ('forms_contact', '{\"contactEmail\": \"\"}'::jsonb)\nON CONFLICT (key) DO NOTHING;\n\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES\n ('thread.heading', '{\"en\": \"Your conversation\", \"fr\": \"Votre conversation\"}', now(), now()),\n ('thread.reply_label', '{\"en\": \"Write a reply\", \"fr\": \"Écrire une réponse\"}', now(), now()),\n ('thread.send', '{\"en\": \"Send reply\", \"fr\": \"Envoyer la réponse\"}', now(), now()),\n ('thread.sending', '{\"en\": \"Sending...\", \"fr\": \"Envoi...\"}', now(), now()),\n ('thread.sent', '{\"en\": \"Thanks - your reply has been sent.\", \"fr\": \"Merci - votre réponse a été envoyée.\"}', now(), now()),\n ('thread.error', '{\"en\": \"Sorry, your reply couldn''t be sent. Please try again in a moment.\", \"fr\": \"Désolé, votre réponse n''a pas pu être envoyée. Veuillez réessayer dans un instant.\"}', now(), now()),\n ('thread.throttled', '{\"en\": \"You''ve sent several replies already. Please wait a few minutes.\", \"fr\": \"Vous avez déjà envoyé plusieurs réponses. Veuillez patienter quelques minutes.\"}', now(), now()),\n ('thread.closed', '{\"en\": \"This conversation has been closed.\", \"fr\": \"Cette conversation est fermée.\"}', now(), now()),\n ('thread.invalid', '{\"en\": \"This link has expired or is no longer valid. If you still need help, please contact us again from our website.\", \"fr\": \"Ce lien a expiré ou n''est plus valide. Si vous avez encore besoin d''aide, contactez-nous de nouveau depuis notre site.\"}', now(), now()),\n ('thread.you', '{\"en\": \"You\", \"fr\": \"Vous\"}', now(), now()),\n ('forms.submission_stored', '{\"en\": \"Thanks - your message has been received.\", \"fr\": \"Merci - votre message a bien été reçu.\"}', now(), now())\nON CONFLICT (key) DO NOTHING;\n"
156
- },
157
- {
158
- "version": "00000000000028",
159
- "name": "00000000000028_interaction_replies.sql",
160
- "sql": "-- Staff replies on reviews and post comments, and the indexes cms_interactions never had.\n--\n-- WHY THIS IS A DIFFERENT SHAPE FROM MIGRATION 27, AND WHY IT NEEDS NO TOKEN.\n-- `cms_interactions.user_id` is NOT NULL with a foreign key to `profiles`: every review\n-- and comment already comes from a signed-in account with a reachable address, and the\n-- content already renders publicly on /product/{slug} and /article/{slug}. A staff\n-- answer there is PUBLISHED CONTENT plus moderation, not a private conversation.\n-- Routing it through the tokenised thread page would hide a public reply behind a\n-- secret link. So: one inbox in the CMS, two storage models underneath.\n--\n-- THE ONE CONSTRAINT THAT DICTATES THE MODELLING.\n-- `check_rating_only_for_review` is an exhaustive OR over ('review','comment'):\n-- (type='review' AND rating IS NOT NULL AND rating BETWEEN 1 AND 5)\n-- OR (type='comment' AND rating IS NULL)\n-- A type='review' row with a NULL rating is rejected, so a reply to a review cannot be\n-- a 'review' row. Adding a 'reply' value to interaction_type would ALSO violate it —\n-- the OR covers no third value — and PostgreSQL forbids using an enum value in the\n-- transaction that added it, which is exactly one migration file.\n--\n-- THEREFORE a reply is a `type='comment'` row carrying the PARENT's target and a NULL\n-- rating: a combination the existing constraints already accept, unchanged. The bonus\n-- is decisive — `update_product_ratings()` aggregates only\n-- WHERE product_id = ? AND type='review' AND status='approved'\n-- so a reply can never move products.average_rating or products.total_reviews. The new\n-- CHECK below pins that invariant in the schema instead of trusting the action to\n-- remember it.\n--\n-- No new policies are needed. `cms_interactions_insert_policy` already admits an\n-- ADMIN/WRITER self-insert with status='approved'\n-- (auth.uid() = user_id AND (status='pending' OR role IN ('ADMIN','WRITER')))\n-- which is precisely a published staff reply.\n--\n-- Forward-only and idempotent.\n\nALTER TABLE public.cms_interactions ADD COLUMN IF NOT EXISTS parent_id uuid;\n\nCOMMENT ON COLUMN public.cms_interactions.parent_id IS 'Set on a staff reply: points at the review or comment being answered. Replies are always type=comment with a NULL rating, and carry the parent''s product_id/post_id so check_product_or_post still holds.';\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint\n WHERE conname = 'cms_interactions_parent_id_fkey'\n AND conrelid = 'public.cms_interactions'::regclass) THEN\n ALTER TABLE ONLY public.cms_interactions\n ADD CONSTRAINT cms_interactions_parent_id_fkey FOREIGN KEY (parent_id)\n REFERENCES public.cms_interactions(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\n-- One level only, never itself, no rating, always a comment. Keeps the reply\n-- invisible to the ratings trigger and to the existing storefront review query.\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint\n WHERE conname = 'cms_interactions_reply_check'\n AND conrelid = 'public.cms_interactions'::regclass) THEN\n ALTER TABLE public.cms_interactions\n ADD CONSTRAINT cms_interactions_reply_check CHECK (\n ((parent_id IS NULL) OR\n ((parent_id <> id) AND (rating IS NULL) AND (type = 'comment'::public.interaction_type))));\n END IF;\nEND $rb$;\n\n-- Fetch every reply for a page of parents in one probe.\nCREATE INDEX IF NOT EXISTS cms_interactions_parent_idx\n ON public.cms_interactions USING btree (parent_id, created_at)\n WHERE (parent_id IS NOT NULL);\n\n-- cms_interactions has had ZERO secondary indexes since the baseline, while both\n-- public renderers filter on exactly these predicates on every product and article\n-- page. The inbox multiplies read volume on this table, so they go in now.\nCREATE INDEX IF NOT EXISTS cms_interactions_product_type_status_idx\n ON public.cms_interactions USING btree (product_id, type, status, created_at DESC)\n WHERE (product_id IS NOT NULL);\n\nCREATE INDEX IF NOT EXISTS cms_interactions_post_type_status_idx\n ON public.cms_interactions USING btree (post_id, type, status, created_at DESC)\n WHERE (post_id IS NOT NULL);\n\n-- Inbox listing: newest top-level items, replies excluded.\nCREATE INDEX IF NOT EXISTS cms_interactions_inbox_idx\n ON public.cms_interactions USING btree (created_at DESC)\n WHERE (parent_id IS NULL);\n\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES\n ('interactions.staff_reply', '{\"en\": \"Reply from the team\", \"fr\": \"Réponse de l''équipe\"}', now(), now()),\n ('interactions.staff_badge', '{\"en\": \"Staff\", \"fr\": \"Équipe\"}', now(), now())\nON CONFLICT (key) DO NOTHING;\n"
161
- },
162
- {
163
- "version": "00000000000029",
164
- "name": "00000000000029_form_endpoints_default_empty.sql",
165
- "sql": "-- A contact form has no address of its own by default.\n--\n-- The starter content ships a contact page addressed to `contact@example.com`, and\n-- migration 27 faithfully carried that placeholder into `form_endpoints`. Faithful was\n-- the wrong call. `example.com` is reserved by RFC 2606 precisely so it can never be\n-- registered, so the address is not merely unhelpful — it is guaranteed undeliverable,\n-- while looking like a real setting to every layer downstream. The visitor is thanked,\n-- the relay accepts the message, and nobody is ever notified. Nothing errors, so an\n-- install can run that way indefinitely.\n--\n-- Since the messaging system arrived, a per-form address is not something an operator\n-- needs to think about at all. Submissions are stored as threads and answered in\n-- CMS → Messages; the notification address is a single site-wide setting, and a form\n-- only carries its own address when someone deliberately wants that form routed\n-- elsewhere (a careers form to HR, say).\n--\n-- So the default becomes NULL, meaning \"use the site contact address\", which resolves\n-- to the address set in CMS → Messages and finally to the first admin's own login. An\n-- install therefore reaches a real human out of the box without configuring anything.\n--\n-- The sandbox is unaffected: `resolveFormRecipient` overrides everything with\n-- SANDBOX_CONTACT_EMAIL when NEXT_PUBLIC_IS_SANDBOX is set, so the hosted demo keeps\n-- routing to the operator's own inbox without storing an address here.\n--\n-- Forward-only and idempotent.\n\n-- Reserved domains from RFC 2606 / RFC 6761. Matching on the domain rather than the\n-- exact seeded string also catches an operator who typed their own placeholder.\nUPDATE public.form_endpoints\n SET recipient_email = NULL\n WHERE recipient_email IS NOT NULL\n AND (\n lower(recipient_email) LIKE '%@example.com'\n OR lower(recipient_email) LIKE '%@example.org'\n OR lower(recipient_email) LIKE '%@example.net'\n OR lower(recipient_email) LIKE '%@example.edu'\n OR lower(recipient_email) LIKE '%.example'\n OR lower(recipient_email) LIKE '%.invalid'\n OR lower(recipient_email) LIKE '%.test'\n OR lower(recipient_email) LIKE '%.localhost'\n OR lower(recipient_email) LIKE '%.local'\n );\n\nCOMMENT ON COLUMN public.form_endpoints.recipient_email IS 'Per-form override. NULL means \"use the site contact address\" (CMS -> Messages, falling back to the first admin), which is the default and the usual case.';\n\n-- Same reasoning for the site-wide rows: a placeholder there is worse than an empty\n-- value, because an empty one falls through to the first admin and actually arrives.\nUPDATE public.site_settings\n SET value = jsonb_set(value, '{contactEmail}', '\"\"'::jsonb)\n WHERE key IN ('forms_contact', 'store_contact')\n AND value ? 'contactEmail'\n AND lower(value->>'contactEmail') LIKE '%@example.%';\n"
166
- },
167
- {
168
- "version": "00000000000030",
169
- "name": "00000000000030_seo_redirects_and_robots.sql",
170
- "sql": "-- SEO engine: managed 301/302 redirects and operator-configurable robots directives.\n--\n-- Two unrelated-looking things ship in one migration because they are the same\n-- feature from an operator's point of view: the `/cms/settings/seo` screen is where\n-- someone goes to say \"this URL moved\" and \"do not crawl that\". Splitting them\n-- across two migrations would only mean two files that must always be applied\n-- together.\n--\n-- WHY A TABLE AND NOT next.config.js redirects(). Redirects are content, not\n-- configuration. An editor who renames a page's slug needs the old URL to keep\n-- working immediately, without a redeploy and without touching source control.\n-- That rules out the build-time array; it has to be data.\n--\n-- WHY THE PROXY READS THIS WITH THE ANON KEY. apps/nextblock/proxy.ts (Next 16's\n-- renamed middleware) resolves redirects before rendering, and it holds a Supabase\n-- client built from the anon key. So the public SELECT policy below is load-bearing:\n-- without an explicit `TO authenticated, anon` grant the proxy's lookup would return\n-- zero rows for every anonymous visitor -- silently, with no error -- and no redirect\n-- would ever fire. Only is_active rows are exposed, so a half-written rule is never\n-- live.\n--\n-- WHY status_code IS AN integer AND NOT AN ENUM. The same reasoning migration 27\n-- recorded for `source`: a Postgres enum cannot be extended and used inside the same\n-- transaction, which is exactly the scope of one migration file. A CHECK constraint\n-- is replaceable in a single statement. 301 and 302 are the only two values the\n-- admin UI offers; 307/308 are deliberately not exposed, because their\n-- method-preserving semantics surprise operators who just want \"this page moved\".\n--\n-- LOOP SAFETY is enforced in application code (wouldCreateLoop in\n-- @nextblock-cms/utils, called by the admin server actions) rather than by a\n-- constraint, because detecting a cycle requires walking the whole table and a CHECK\n-- constraint can only see one row. The self-redirect case IS cheap to check per row,\n-- so that one is a constraint -- it is the cycle operators actually hit.\n--\n-- SECURITY POSTURE. A redirect can send every visitor of a path to an arbitrary\n-- external origin, which makes this table an open-redirect surface and a phishing\n-- lever. Writes are therefore ADMIN-only -- WRITER is deliberately excluded, matching\n-- site_scripts rather than the content tables. Reads are public but limited to active\n-- rows. The robots settings live in site_settings, whose existing read policy is\n-- already public for non-secret keys; nothing here is secret.\n--\n-- Forward-only and idempotent.\n\nCREATE TABLE IF NOT EXISTS public.cms_redirects (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n -- The incoming site-relative path to match, normalized by the application to a\n -- leading slash with no trailing slash (except root). Matching is exact: prefix\n -- and wildcard rules are deliberately not supported, because they are the usual\n -- way an operator builds an accidental loop.\n source_path text NOT NULL,\n -- Where to send the visitor. Either another site-relative path or a fully\n -- qualified https URL for an off-site move.\n destination_path text NOT NULL,\n -- 301 permanent (the SEO-meaningful one: search engines transfer ranking signals\n -- and browsers cache it aggressively) or 302 temporary.\n status_code integer DEFAULT 301 NOT NULL,\n is_active boolean DEFAULT true NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT cms_redirects_pkey PRIMARY KEY (id),\n -- One rule per source. This UNIQUE constraint also provides the index the proxy\n -- lookup relies on, so no separate plain index on source_path is needed.\n CONSTRAINT cms_redirects_source_path_key UNIQUE (source_path),\n CONSTRAINT cms_redirects_status_code_check\n CHECK ((status_code = ANY (ARRAY[301, 302]))),\n -- A source is always a path on this site; accepting an absolute URL here would\n -- silently never match, since the proxy only ever compares pathnames.\n CONSTRAINT cms_redirects_source_path_check\n CHECK ((source_path ~ '^/')),\n -- A destination is either a site-relative path or an https URL. Plain http is\n -- refused so a redirect can never downgrade a visitor to cleartext.\n CONSTRAINT cms_redirects_destination_path_check\n CHECK (((destination_path ~ '^/') OR (destination_path ~ '^https://'))),\n -- The one cycle a single row can express, and the one operators actually create.\n CONSTRAINT cms_redirects_no_self_redirect_check\n CHECK ((source_path <> destination_path))\n);\n\nCOMMENT ON TABLE public.cms_redirects IS\n 'Operator-managed 301/302 redirects resolved by apps/nextblock/proxy.ts before rendering. Only is_active rows are publicly readable; only ADMIN may write, because a redirect rule is an open-redirect surface.';\nCOMMENT ON COLUMN public.cms_redirects.source_path IS\n 'Exact site-relative path to match, normalized to a leading slash and no trailing slash (except root). No wildcards, by design.';\nCOMMENT ON COLUMN public.cms_redirects.destination_path IS\n 'Site-relative path or absolute https URL to send the visitor to.';\nCOMMENT ON COLUMN public.cms_redirects.status_code IS\n '301 permanent or 302 temporary. 307/308 are not offered by the admin UI.';\nCOMMENT ON COLUMN public.cms_redirects.is_active IS\n 'Only active rows are readable by anon, and only active rows are matched by the proxy.';\n\n-- The proxy loads the whole active set once per cache window rather than querying per\n-- request, so the hot query is \"all active rows\" and this partial index is what serves\n-- it. Carrying source_path in the index keeps that read index-only.\nCREATE INDEX IF NOT EXISTS cms_redirects_active_source_idx\n ON public.cms_redirects USING btree (source_path) WHERE (is_active);\n\nDROP TRIGGER IF EXISTS set_cms_redirects_updated_at ON public.cms_redirects;\nCREATE TRIGGER set_cms_redirects_updated_at\n BEFORE UPDATE ON public.cms_redirects\n FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\n\nALTER TABLE public.cms_redirects ENABLE ROW LEVEL SECURITY;\n\nGRANT ALL ON TABLE public.cms_redirects TO anon;\nGRANT ALL ON TABLE public.cms_redirects TO authenticated;\nGRANT ALL ON TABLE public.cms_redirects TO service_role;\n\n-- The proxy runs as anon for a logged-out visitor, which is the overwhelming majority\n-- of traffic and the only traffic redirects really matter for. Without this policy the\n-- lookup returns zero rows and the feature is silently dead.\nDROP POLICY IF EXISTS \"Public read active redirects\" ON public.cms_redirects;\nCREATE POLICY \"Public read active redirects\" ON public.cms_redirects\n FOR SELECT TO authenticated, anon USING (is_active);\n\nDROP POLICY IF EXISTS \"Admins read all redirects\" ON public.cms_redirects;\nCREATE POLICY \"Admins read all redirects\" ON public.cms_redirects\n FOR SELECT TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins insert redirects\" ON public.cms_redirects;\nCREATE POLICY \"Admins insert redirects\" ON public.cms_redirects\n FOR INSERT TO authenticated\n WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins update redirects\" ON public.cms_redirects;\nCREATE POLICY \"Admins update redirects\" ON public.cms_redirects\n FOR UPDATE TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role))\n WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins delete redirects\" ON public.cms_redirects;\nCREATE POLICY \"Admins delete redirects\" ON public.cms_redirects\n FOR DELETE TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\n-- Robots directives live in site_settings rather than a table of their own: there is\n-- exactly one robots.txt per install, so a dedicated table would only ever hold one\n-- row, and site_settings already carries the public-read / staff-write policy this\n-- needs. The stored shape matches `RobotsSettings` in @nextblock-cms/utils, and\n-- `normalizeRobotsSettings` tolerates a missing or malformed value -- so this seed is\n-- a convenience for the settings screen, not a correctness requirement for rendering.\n--\n-- ON CONFLICT DO NOTHING because an install that has already configured robots must\n-- not have its rules reset by a replay of this migration.\nINSERT INTO public.site_settings (key, value)\nVALUES (\n 'seo_robots_settings',\n '{\"customRules\": \"\", \"isIndexingEnabled\": true, \"sitemapEnabled\": true, \"userAgentRules\": [{\"allow\": [\"/\"], \"disallow\": [], \"userAgent\": \"*\"}]}'::jsonb\n)\nON CONFLICT (key) DO NOTHING;\n"
171
- },
172
- {
173
- "version": "00000000000031",
174
- "name": "00000000000031_seo_robots_settings_admin_only.sql",
175
- "sql": "-- Restrict writes to the robots.txt settings row to ADMIN only.\n--\n-- `site_settings.seo_robots_settings` is the row that decides whether the whole site\n-- is crawlable: app/robots.ts reads it on every /robots.txt hit and serves a blanket\n-- `Disallow: /` when `isIndexingEnabled` is false. The CMS only offers that switch\n-- under /cms/settings/seo, and `saveRobotsSettings` re-checks for ADMIN before it\n-- writes — but RLS is the independent boundary, and it did not agree. The baseline\n-- write policies let ADMIN *or* WRITER write any key outside the sensitive array, so\n-- a WRITER holding a normal session could PATCH this row through PostgREST and take\n-- the entire site out of Google. That failure is quiet (nothing in the CMS shows it),\n-- slow to notice (search traffic decays over weeks) and hard to attribute after the\n-- fact, which is why the database has to refuse it rather than trusting the one\n-- server action that happens to guard it today.\n--\n-- The SELECT policy is deliberately NOT touched. This key must stay anon-READABLE:\n-- app/robots.ts reads it with the anon (SSG) client on every crawl, and adding the key\n-- to the read policy's sensitive array would make robots.txt fall back to its\n-- permissive defaults for every crawler. Only INSERT/UPDATE/DELETE move to ADMIN-only,\n-- matching the UI boundary — exactly the shape migration 00000000000011 used for\n-- language_detection_settings, which is anon-readable for the same reason.\n--\n-- Every key already present in each policy's array is preserved (dropping one would\n-- silently widen write access back to WRITER for that key); `seo_robots_settings` is\n-- appended to the three write policies only.\n\nDROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;\nCREATE POLICY site_settings_insert_policy ON public.site_settings FOR INSERT TO authenticated WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text, 'seo_robots_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text, 'seo_robots_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n\nDROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;\nCREATE POLICY site_settings_update_policy ON public.site_settings FOR UPDATE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text, 'seo_robots_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text, 'seo_robots_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)))) WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text, 'seo_robots_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text, 'seo_robots_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n\nDROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;\nCREATE POLICY site_settings_delete_policy ON public.site_settings FOR DELETE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text, 'seo_robots_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text, 'seo_robots_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n\n-- Forward-only and idempotent.\n"
176
- }
177
- ];
1
+ // AUTO-GENERATED by tools/scripts/generate-migrations-bundle.ts — do not edit by hand.
2
+ // Regenerate after adding a migration: npm run generate:migrations-bundle
3
+ //
4
+ // Embedded forward-only migrations so the /setup wizard can apply the schema on
5
+ // serverless hosts (Vercel) where libs/db isn't on the function filesystem.
6
+
7
+ export interface BundledMigration {
8
+ /** Numeric version prefix, e.g. "00000000000030". */
9
+ version: string;
10
+ /** Original migration filename. */
11
+ name: string;
12
+ /** Raw SQL contents. */
13
+ sql: string;
14
+ }
15
+
16
+ export const MIGRATIONS_BUNDLE: BundledMigration[] = [
17
+ {
18
+ "version": "00000000000000",
19
+ "name": "00000000000000_baseline_schema.sql",
20
+ "sql": "-- AUTO-GENERATED baseline (re-baseline of migrations 000..044). Idempotent; safe to replay.\r\n-- 00 · schema: enums, functions, tables, sequences, defaults\r\n-- Regenerate via tools/scripts/rebaseline-transform.mjs. Do not hand-edit.\r\n\r\nSET check_function_bodies = false;\r\n\r\nCREATE SCHEMA IF NOT EXISTS public;\r\n\r\nCOMMENT ON SCHEMA public IS 'standard public schema';\r\n\r\nDO $rb$ BEGIN\r\nCREATE TYPE public.approval_status AS ENUM (\r\n 'pending',\r\n 'approved',\r\n 'denied'\r\n);\r\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\r\n\r\nDO $rb$ BEGIN\r\nCREATE TYPE public.interaction_type AS ENUM (\r\n 'review',\r\n 'comment'\r\n);\r\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\r\n\r\nDO $rb$ BEGIN\r\nCREATE TYPE public.menu_location AS ENUM (\r\n 'HEADER',\r\n 'FOOTER',\r\n 'SIDEBAR'\r\n);\r\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\r\n\r\nDO $rb$ BEGIN\r\nCREATE TYPE public.page_status AS ENUM (\r\n 'draft',\r\n 'published',\r\n 'archived'\r\n);\r\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\r\n\r\nDO $rb$ BEGIN\r\nCREATE TYPE public.revision_type AS ENUM (\r\n 'snapshot',\r\n 'diff'\r\n);\r\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\r\n\r\nDO $rb$ BEGIN\r\nCREATE TYPE public.user_role AS ENUM (\r\n 'ADMIN',\r\n 'WRITER',\r\n 'USER'\r\n);\r\nEXCEPTION WHEN duplicate_object THEN null; END $rb$;\r\n\r\nCREATE OR REPLACE FUNCTION public.apply_order_inventory_deduction(p_order_id uuid) RETURNS void\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_track_quantities boolean := public.get_ecommerce_track_quantities();\r\n v_item record;\r\n v_inventory_deducted_at timestamptz;\r\n v_sku text;\r\n v_current_quantity integer;\r\nBEGIN\r\n SELECT inventory_deducted_at\r\n INTO v_inventory_deducted_at\r\n FROM public.orders\r\n WHERE id = p_order_id\r\n FOR UPDATE;\r\n\r\n IF NOT FOUND OR v_inventory_deducted_at IS NOT NULL THEN\r\n RETURN;\r\n END IF;\r\n\r\n IF NOT v_track_quantities THEN\r\n UPDATE public.orders\r\n SET inventory_deducted_at = now()\r\n WHERE id = p_order_id;\r\n\r\n RETURN;\r\n END IF;\r\n\r\n FOR v_item IN\r\n SELECT\r\n product_id,\r\n variant_id,\r\n SUM(quantity)::integer AS quantity\r\n FROM public.order_items\r\n WHERE order_id = p_order_id\r\n GROUP BY product_id, variant_id\r\n LOOP\r\n v_sku := NULL;\r\n v_current_quantity := 0;\r\n\r\n IF v_item.variant_id IS NOT NULL THEN\r\n SELECT\r\n sku,\r\n GREATEST(COALESCE(stock_quantity, 0), 0)\r\n INTO v_sku,\r\n v_current_quantity\r\n FROM public.product_variants\r\n WHERE id = v_item.variant_id\r\n LIMIT 1;\r\n ELSIF v_item.product_id IS NOT NULL THEN\r\n SELECT\r\n sku,\r\n GREATEST(COALESCE(stock, 0), 0)\r\n INTO v_sku,\r\n v_current_quantity\r\n FROM public.products\r\n WHERE id = v_item.product_id\r\n LIMIT 1;\r\n END IF;\r\n\r\n IF NULLIF(trim(v_sku), '') IS NULL THEN\r\n CONTINUE;\r\n END IF;\r\n\r\n INSERT INTO public.inventory_items (sku, quantity)\r\n VALUES (v_sku, v_current_quantity)\r\n ON CONFLICT (sku) DO NOTHING;\r\n\r\n UPDATE public.inventory_items\r\n SET\r\n quantity = GREATEST(COALESCE(quantity, 0) - v_item.quantity, 0),\r\n updated_at = now()\r\n WHERE sku = v_sku;\r\n END LOOP;\r\n\r\n UPDATE public.orders\r\n SET inventory_deducted_at = now()\r\n WHERE id = p_order_id;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.assign_order_invoice_metadata(p_order_id uuid, p_paid_at timestamp with time zone DEFAULT now()) RETURNS TABLE(invoice_number text, paid_at timestamp with time zone)\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_order public.orders%ROWTYPE;\r\n v_effective_paid_at timestamptz;\r\nBEGIN\r\n SELECT *\r\n INTO v_order\r\n FROM public.orders\r\n WHERE id = p_order_id\r\n FOR UPDATE;\r\n\r\n IF NOT FOUND THEN\r\n RAISE EXCEPTION 'Order % not found', p_order_id;\r\n END IF;\r\n\r\n v_effective_paid_at := COALESCE(v_order.paid_at, p_paid_at, now(), v_order.created_at);\r\n\r\n UPDATE public.orders\r\n SET\r\n invoice_number = COALESCE(v_order.invoice_number, public.generate_order_invoice_number()),\r\n paid_at = v_effective_paid_at\r\n WHERE id = p_order_id\r\n RETURNING orders.invoice_number, orders.paid_at\r\n INTO invoice_number, paid_at;\r\n\r\n RETURN NEXT;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.clear_currency_price_overrides(target_currency text) RETURNS void\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nDECLARE\r\n v_target_currency text := upper(trim(target_currency));\r\nBEGIN\r\n IF v_target_currency = '' THEN\r\n RETURN;\r\n END IF;\r\n\r\n UPDATE public.products\r\n SET\r\n prices = COALESCE(prices, '{}'::jsonb) - v_target_currency,\r\n sale_prices = CASE\r\n WHEN sale_prices IS NULL THEN NULL\r\n WHEN sale_prices - v_target_currency = '{}'::jsonb THEN NULL\r\n ELSE sale_prices - v_target_currency\r\n END,\r\n updated_at = now()\r\n WHERE COALESCE(prices, '{}'::jsonb) ? v_target_currency\r\n OR COALESCE(sale_prices, '{}'::jsonb) ? v_target_currency;\r\n\r\n UPDATE public.product_variants\r\n SET\r\n prices = COALESCE(prices, '{}'::jsonb) - v_target_currency,\r\n sale_prices = CASE\r\n WHEN sale_prices IS NULL THEN NULL\r\n WHEN sale_prices - v_target_currency = '{}'::jsonb THEN NULL\r\n ELSE sale_prices - v_target_currency\r\n END,\r\n updated_at = now()\r\n WHERE COALESCE(prices, '{}'::jsonb) ? v_target_currency\r\n OR COALESCE(sale_prices, '{}'::jsonb) ? v_target_currency;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.is_valid_custom_block_fields(candidate jsonb) RETURNS boolean\r\n LANGUAGE sql IMMUTABLE\r\n SET search_path TO ''\r\n AS $_$\r\n SELECT CASE\r\n WHEN jsonb_typeof(candidate) <> 'array' THEN false\r\n ELSE\r\n NOT EXISTS (\r\n SELECT 1\r\n FROM jsonb_array_elements(candidate) AS field(value)\r\n WHERE jsonb_typeof(field.value) <> 'object'\r\n OR jsonb_typeof(field.value -> 'key') IS DISTINCT FROM 'string'\r\n OR jsonb_typeof(field.value -> 'label') IS DISTINCT FROM 'string'\r\n OR jsonb_typeof(field.value -> 'type') IS DISTINCT FROM 'string'\r\n OR field.value ->> 'key' !~ '^[a-z][a-z0-9_]*$'\r\n OR field.value ->> 'type' NOT IN ('text', 'rich-text', 'image_r2', 'db_relation')\r\n )\r\n AND (\r\n SELECT COUNT(*) = COUNT(DISTINCT field.value ->> 'key')\r\n FROM jsonb_array_elements(candidate) AS field(value)\r\n )\r\n END;\r\n$_$;\r\n\r\nCREATE OR REPLACE FUNCTION public.is_valid_custom_block_layout_schema(candidate jsonb) RETURNS boolean\r\n LANGUAGE sql IMMUTABLE\r\n SET search_path TO ''\r\n AS $$\r\n SELECT CASE\r\n WHEN jsonb_typeof(candidate) <> 'object' THEN false\r\n ELSE candidate ->> 'type' IN ('container', 'field_render')\r\n END;\r\n$$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.custom_block_definitions (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n slug text NOT NULL,\r\n name text NOT NULL,\r\n description text DEFAULT ''::text NOT NULL,\r\n fields jsonb DEFAULT '[]'::jsonb NOT NULL,\r\n layout_schema jsonb NOT NULL,\r\n is_original boolean DEFAULT true NOT NULL,\r\n CONSTRAINT custom_block_definitions_fields_check CHECK (public.is_valid_custom_block_fields(fields)),\r\n CONSTRAINT custom_block_definitions_layout_schema_check CHECK (public.is_valid_custom_block_layout_schema(layout_schema)),\r\n CONSTRAINT custom_block_definitions_name_check CHECK ((length(TRIM(BOTH FROM name)) > 0)),\r\n CONSTRAINT custom_block_definitions_slug_check CHECK ((slug ~ '^[a-z][a-z0-9-]*$'::text))\r\n);\r\n\r\nCOMMENT ON TABLE public.custom_block_definitions IS 'Registry for user-created block definitions rendered from database JSONB without runtime code compilation.';\r\n\r\nCOMMENT ON COLUMN public.custom_block_definitions.fields IS 'Strict JSONB field declarations for data-rendered custom blocks.';\r\n\r\nCOMMENT ON COLUMN public.custom_block_definitions.layout_schema IS 'Open-ended recursive layout schema consumed by the dynamic layout renderer.';\r\n\r\nCOMMENT ON COLUMN public.custom_block_definitions.is_original IS 'False when a definition was created by duplicating an existing registry row.';\r\n\r\nCREATE OR REPLACE FUNCTION public.duplicate_block_definition(target_id uuid) RETURNS public.custom_block_definitions\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $_$\r\nDECLARE\r\n source_definition public.custom_block_definitions%ROWTYPE;\r\n copied_definition public.custom_block_definitions%ROWTYPE;\r\n base_slug text;\r\n copy_slug text;\r\n copy_index integer := 1;\r\nBEGIN\r\n IF auth.role() <> 'service_role'\r\n AND COALESCE((SELECT public.get_current_user_role())::text, '') NOT IN ('ADMIN', 'WRITER') THEN\r\n RAISE EXCEPTION 'Not authorized to duplicate custom block definitions.'\r\n USING ERRCODE = '42501';\r\n END IF;\r\n\r\n SELECT *\r\n INTO source_definition\r\n FROM public.custom_block_definitions\r\n WHERE id = target_id;\r\n\r\n IF NOT FOUND THEN\r\n RAISE EXCEPTION 'Custom block definition % not found.', target_id\r\n USING ERRCODE = 'P0002';\r\n END IF;\r\n\r\n base_slug := regexp_replace(source_definition.slug, '-copy(-[0-9]+)?$', '');\r\n copy_slug := base_slug || '-copy';\r\n\r\n WHILE EXISTS (\r\n SELECT 1\r\n FROM public.custom_block_definitions\r\n WHERE slug = copy_slug\r\n ) LOOP\r\n copy_index := copy_index + 1;\r\n copy_slug := base_slug || '-copy-' || copy_index;\r\n END LOOP;\r\n\r\n INSERT INTO public.custom_block_definitions (\r\n id,\r\n slug,\r\n name,\r\n description,\r\n fields,\r\n layout_schema,\r\n is_original\r\n )\r\n VALUES (\r\n gen_random_uuid(),\r\n copy_slug,\r\n source_definition.name || ' Copy',\r\n source_definition.description,\r\n source_definition.fields,\r\n source_definition.layout_schema,\r\n false\r\n )\r\n RETURNING *\r\n INTO copied_definition;\r\n\r\n RETURN copied_definition;\r\nEND;\r\n$_$;\r\n\r\nCREATE OR REPLACE FUNCTION public.format_order_invoice_number(p_value bigint) RETURNS text\r\n LANGUAGE sql IMMUTABLE\r\n SET search_path TO ''\r\n AS $$\r\n SELECT 'INV-' || lpad(p_value::text, 6, '0');\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.generate_order_invoice_number() RETURNS text\r\n LANGUAGE sql\r\n SET search_path TO ''\r\n AS $$\r\n SELECT public.format_order_invoice_number(nextval('public.order_invoice_number_seq'));\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.get_current_user_role() RETURNS public.user_role\r\n LANGUAGE sql STABLE\r\n SET search_path TO 'public'\r\n AS $$\r\n SELECT role FROM public.profiles WHERE id = auth.uid();\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.get_default_currency_code() RETURNS text\r\n LANGUAGE sql STABLE\r\n SET search_path TO ''\r\n AS $$\r\n SELECT COALESCE(\r\n (\r\n SELECT upper(code)\r\n FROM public.currencies\r\n WHERE is_default = true\r\n ORDER BY updated_at DESC, created_at DESC, code ASC\r\n LIMIT 1\r\n ),\r\n 'USD'\r\n );\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.get_ecommerce_track_quantities() RETURNS boolean\r\n LANGUAGE plpgsql STABLE\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_value jsonb;\r\n v_raw text;\r\nBEGIN\r\n SELECT value\r\n INTO v_value\r\n FROM public.site_settings\r\n WHERE key = 'ecommerce_inventory_settings';\r\n\r\n IF v_value IS NULL THEN\r\n RETURN true;\r\n END IF;\r\n\r\n IF jsonb_typeof(v_value) = 'object' THEN\r\n v_raw := NULLIF(v_value->>'track_quantities', '');\r\n ELSE\r\n v_raw := NULLIF(trim(BOTH '\"' FROM v_value::text), '');\r\n END IF;\r\n\r\n IF v_raw IS NULL THEN\r\n RETURN true;\r\n END IF;\r\n\r\n IF lower(v_raw) IN ('false', 'f', '0', 'no', 'off') THEN\r\n RETURN false;\r\n END IF;\r\n\r\n RETURN true;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.get_my_claim(claim text) RETURNS jsonb\r\n LANGUAGE sql STABLE\r\n SET search_path TO ''\r\n AS $$\r\n SELECT COALESCE(current_setting('request.jwt.claims', true)::jsonb ->> claim, NULL)::jsonb\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_blocks_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_coupon_freemius_mappings_write() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.freemius_product_id := btrim(NEW.freemius_product_id);\r\n NEW.freemius_coupon_code := upper(regexp_replace(btrim(NEW.freemius_coupon_code), '\\s+', '', 'g'));\r\n NEW.sync_status := lower(btrim(COALESCE(NEW.sync_status, 'pending')));\r\n NEW.updated_at := now();\r\n\r\n IF NEW.created_at IS NULL THEN\r\n NEW.created_at := now();\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_coupons_write() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.code := upper(regexp_replace(btrim(NEW.code), '\\s+', '', 'g'));\r\n NEW.name := btrim(NEW.name);\r\n NEW.provider_scope := lower(btrim(NEW.provider_scope));\r\n NEW.discount_type := lower(btrim(NEW.discount_type));\r\n NEW.freemius_sync_status := lower(btrim(COALESCE(NEW.freemius_sync_status, 'not_synced')));\r\n NEW.updated_at := now();\r\n\r\n IF NEW.created_at IS NULL THEN\r\n NEW.created_at := now();\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_default_currency_change() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n IF TG_OP = 'INSERT' THEN\r\n IF NEW.is_default THEN\r\n PERFORM public.sync_legacy_price_columns_for_currency(NEW.code);\r\n END IF;\r\n ELSIF NEW.is_default\r\n AND (\r\n OLD.is_default IS DISTINCT FROM NEW.is_default\r\n OR OLD.code IS DISTINCT FROM NEW.code\r\n ) THEN\r\n PERFORM public.sync_legacy_price_columns_for_currency(NEW.code);\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_inventory_item_change() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_sku text := COALESCE(NEW.sku, OLD.sku);\r\nBEGIN\r\n PERFORM public.sync_inventory_cache_for_sku(v_sku);\r\n RETURN COALESCE(NEW, OLD);\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_inventory_items_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_languages_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_media_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_navigation_items_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS trigger\r\n LANGUAGE plpgsql SECURITY DEFINER\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n admin_flag_set boolean := false;\r\n user_role public.user_role;\r\n v_full_name text;\r\n v_avatar_url text;\r\n v_github_username text;\r\n v_provider text;\r\nBEGIN\r\n INSERT INTO public.site_settings (key, value)\r\n VALUES ('is_admin_created', 'false'::jsonb)\r\n ON CONFLICT (key) DO NOTHING;\r\n\r\n SELECT COALESCE(value::jsonb::boolean, false)\r\n INTO admin_flag_set\r\n FROM public.site_settings\r\n WHERE key = 'is_admin_created'\r\n FOR UPDATE;\r\n\r\n IF admin_flag_set = false THEN\r\n user_role := 'ADMIN'::public.user_role;\r\n\r\n UPDATE public.site_settings\r\n SET value = 'true'::jsonb\r\n WHERE key = 'is_admin_created';\r\n ELSE\r\n user_role := 'USER'::public.user_role;\r\n END IF;\r\n\r\n v_full_name := NEW.raw_user_meta_data->>'full_name';\r\n v_avatar_url := NEW.raw_user_meta_data->>'avatar_url';\r\n v_provider := NEW.raw_app_meta_data->>'provider';\r\n\r\n IF v_provider = 'github' OR (NEW.raw_user_meta_data->>'iss') LIKE '%github%' THEN\r\n v_github_username := COALESCE(\r\n NEW.raw_user_meta_data->>'user_name',\r\n NEW.raw_user_meta_data->>'preferred_username'\r\n );\r\n ELSE\r\n v_github_username := NULL;\r\n END IF;\r\n\r\n INSERT INTO public.profiles (\r\n id,\r\n role,\r\n full_name,\r\n avatar_url,\r\n github_username\r\n )\r\n VALUES (\r\n NEW.id,\r\n user_role,\r\n v_full_name,\r\n v_avatar_url,\r\n v_github_username\r\n )\r\n ON CONFLICT (id) DO UPDATE SET\r\n full_name = EXCLUDED.full_name,\r\n avatar_url = EXCLUDED.avatar_url,\r\n github_username = EXCLUDED.github_username;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_pages_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_posts_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_product_freemius_sale_coupons_write() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.freemius_product_id := btrim(NEW.freemius_product_id);\r\n NEW.freemius_coupon_code := upper(regexp_replace(btrim(NEW.freemius_coupon_code), '\\s+', '', 'g'));\r\n NEW.sync_status := lower(btrim(COALESCE(NEW.sync_status, 'pending')));\r\n NEW.updated_at := now();\r\n\r\n IF NEW.created_at IS NULL THEN\r\n NEW.created_at := now();\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_shipping_zone_locations_write() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.country_code = upper(btrim(NEW.country_code));\r\n NEW.state_code = CASE\r\n WHEN NEW.state_code IS NULL OR btrim(NEW.state_code) = '' THEN NULL\r\n ELSE upper(btrim(NEW.state_code))\r\n END;\r\n NEW.postal_code = CASE\r\n WHEN NEW.postal_code IS NULL OR btrim(NEW.postal_code) = '' THEN NULL\r\n ELSE upper(btrim(NEW.postal_code))\r\n END;\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_tax_rates_write() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.country_code = upper(btrim(NEW.country_code));\r\n NEW.state_code = CASE\r\n WHEN NEW.state_code IS NULL OR btrim(NEW.state_code) = '' THEN NULL\r\n ELSE upper(btrim(NEW.state_code))\r\n END;\r\n NEW.tax_name = btrim(NEW.tax_name);\r\n NEW.updated_at = now();\r\n\r\n IF NEW.created_at IS NULL THEN\r\n NEW.created_at = now();\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.handle_ucp_cart_sessions_update() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.updated_at = now();\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.is_admin() RETURNS boolean\r\n LANGUAGE sql STABLE\r\n SET search_path TO 'public'\r\n AS $$\r\n SELECT role = 'ADMIN' FROM public.profiles WHERE id = auth.uid();\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.is_valid_currency_amount_map(amounts jsonb) RETURNS boolean\r\n LANGUAGE sql IMMUTABLE\r\n SET search_path TO ''\r\n AS $_$\r\n SELECT CASE\r\n WHEN amounts IS NULL THEN false\r\n WHEN jsonb_typeof(amounts) <> 'object' THEN false\r\n WHEN amounts = '{}'::jsonb THEN false\r\n ELSE NOT EXISTS (\r\n SELECT 1\r\n FROM jsonb_each(amounts) AS entry\r\n WHERE entry.key !~ '^[A-Z]{3}$'\r\n OR jsonb_typeof(entry.value) <> 'number'\r\n OR entry.value::text !~ '^[0-9]+$'\r\n )\r\n END;\r\n$_$;\r\n\r\nCREATE OR REPLACE FUNCTION public.is_valid_sale_price_map(prices jsonb, sale_prices jsonb) RETURNS boolean\r\n LANGUAGE sql IMMUTABLE\r\n SET search_path TO ''\r\n AS $_$\r\n SELECT CASE\r\n WHEN sale_prices IS NULL THEN true\r\n WHEN jsonb_typeof(sale_prices) <> 'object' THEN false\r\n WHEN sale_prices = '{}'::jsonb THEN true\r\n WHEN prices IS NULL OR jsonb_typeof(prices) <> 'object' THEN false\r\n ELSE NOT EXISTS (\r\n SELECT 1\r\n FROM jsonb_each(sale_prices) AS entry\r\n WHERE entry.key !~ '^[A-Z]{3}$'\r\n OR NOT (prices ? entry.key)\r\n OR jsonb_typeof(entry.value) <> 'number'\r\n OR entry.value::text !~ '^[0-9]+$'\r\n OR entry.value::text::numeric > (prices ->> entry.key)::numeric\r\n )\r\n END;\r\n$_$;\r\n\r\nCREATE OR REPLACE FUNCTION public.normalize_currency_amount_map(amounts jsonb) RETURNS jsonb\r\n LANGUAGE sql IMMUTABLE\r\n SET search_path TO ''\r\n AS $_$\r\n SELECT CASE\r\n WHEN amounts IS NULL THEN '{}'::jsonb\r\n WHEN jsonb_typeof(amounts) <> 'object' THEN amounts\r\n ELSE COALESCE(\r\n (\r\n SELECT jsonb_object_agg(\r\n upper(trim(entry.key)),\r\n CASE\r\n WHEN jsonb_typeof(entry.value) = 'number'\r\n AND entry.value::text ~ '^[0-9]+$' THEN\r\n to_jsonb((entry.value::text)::bigint)\r\n ELSE\r\n entry.value\r\n END\r\n )\r\n FROM jsonb_each(amounts) AS entry\r\n ),\r\n '{}'::jsonb\r\n )\r\n END;\r\n$_$;\r\n\r\nCREATE OR REPLACE FUNCTION public.set_currency_defaults() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nBEGIN\r\n NEW.code := upper(trim(NEW.code));\r\n NEW.updated_at := now();\r\n\r\n IF NEW.is_default THEN\r\n NEW.is_active := true;\r\n NEW.exchange_rate := 1;\r\n NEW.auto_update_exchange_rate := false;\r\n NEW.auto_sync_product_prices := false;\r\n NEW.exchange_rate_source := COALESCE(NULLIF(NEW.exchange_rate_source, ''), 'store-default');\r\n NEW.exchange_rate_updated_at := COALESCE(NEW.exchange_rate_updated_at, now());\r\n\r\n UPDATE public.currencies\r\n SET is_default = false,\r\n updated_at = now()\r\n WHERE id IS DISTINCT FROM NEW.id\r\n AND is_default = true;\r\n ELSIF NULLIF(NEW.exchange_rate_source, '') IS NULL THEN\r\n NEW.exchange_rate_source := NULL;\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.set_current_timestamp_updated_at() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nDECLARE\r\n _new record;\r\nBEGIN\r\n _new := NEW;\r\n _new.updated_at = now();\r\n RETURN _new;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.sync_currency_price_maps() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nDECLARE\r\n v_default_currency text := public.get_default_currency_code();\r\n v_price_map_changed boolean := false;\r\n v_legacy_changed boolean := false;\r\nBEGIN\r\n NEW.prices := public.normalize_currency_amount_map(COALESCE(NEW.prices, '{}'::jsonb));\r\n NEW.sale_prices := public.normalize_currency_amount_map(COALESCE(NEW.sale_prices, '{}'::jsonb));\r\n\r\n IF NEW.sale_prices = '{}'::jsonb THEN\r\n NEW.sale_prices := NULL;\r\n END IF;\r\n\r\n IF TG_OP = 'UPDATE' THEN\r\n v_price_map_changed :=\r\n NEW.prices IS DISTINCT FROM OLD.prices\r\n OR NEW.sale_prices IS DISTINCT FROM OLD.sale_prices;\r\n v_legacy_changed :=\r\n NEW.price IS DISTINCT FROM OLD.price\r\n OR NEW.sale_price IS DISTINCT FROM OLD.sale_price;\r\n END IF;\r\n\r\n IF TG_OP = 'INSERT' THEN\r\n IF NEW.prices ? v_default_currency THEN\r\n NEW.price := (NEW.prices ->> v_default_currency)::integer;\r\n ELSE\r\n NEW.prices := NEW.prices || jsonb_build_object(\r\n v_default_currency,\r\n GREATEST(COALESCE(NEW.price, 0), 0)\r\n );\r\n END IF;\r\n\r\n IF NEW.sale_prices IS NOT NULL AND NEW.sale_prices ? v_default_currency THEN\r\n NEW.sale_price := (NEW.sale_prices ->> v_default_currency)::integer;\r\n ELSIF NEW.sale_price IS NOT NULL THEN\r\n NEW.sale_prices := COALESCE(NEW.sale_prices, '{}'::jsonb)\r\n || jsonb_build_object(v_default_currency, GREATEST(NEW.sale_price, 0));\r\n END IF;\r\n\r\n RETURN NEW;\r\n END IF;\r\n\r\n IF v_price_map_changed AND NOT v_legacy_changed THEN\r\n IF NOT (NEW.prices ? v_default_currency) THEN\r\n NEW.prices := NEW.prices || jsonb_build_object(\r\n v_default_currency,\r\n GREATEST(COALESCE(OLD.price, NEW.price, 0), 0)\r\n );\r\n END IF;\r\n\r\n NEW.price := (NEW.prices ->> v_default_currency)::integer;\r\n NEW.sale_price := CASE\r\n WHEN NEW.sale_prices IS NOT NULL AND NEW.sale_prices ? v_default_currency THEN\r\n (NEW.sale_prices ->> v_default_currency)::integer\r\n ELSE\r\n NULL\r\n END;\r\n\r\n RETURN NEW;\r\n END IF;\r\n\r\n NEW.prices := NEW.prices || jsonb_build_object(\r\n v_default_currency,\r\n GREATEST(COALESCE(NEW.price, 0), 0)\r\n );\r\n\r\n IF NEW.sale_price IS NULL THEN\r\n IF NEW.sale_prices IS NOT NULL THEN\r\n NEW.sale_prices := NEW.sale_prices - v_default_currency;\r\n\r\n IF NEW.sale_prices = '{}'::jsonb THEN\r\n NEW.sale_prices := NULL;\r\n END IF;\r\n END IF;\r\n ELSE\r\n NEW.sale_prices := COALESCE(NEW.sale_prices, '{}'::jsonb)\r\n || jsonb_build_object(v_default_currency, GREATEST(NEW.sale_price, 0));\r\n END IF;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.sync_inventory_cache_for_sku(p_sku text) RETURNS void\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_quantity integer := 0;\r\nBEGIN\r\n IF NULLIF(trim(p_sku), '') IS NULL THEN\r\n RETURN;\r\n END IF;\r\n\r\n SELECT quantity\r\n INTO v_quantity\r\n FROM public.inventory_items\r\n WHERE sku = p_sku\r\n LIMIT 1;\r\n\r\n v_quantity := COALESCE(v_quantity, 0);\r\n\r\n UPDATE public.product_variants\r\n SET\r\n stock_quantity = v_quantity,\r\n updated_at = now()\r\n WHERE sku = p_sku;\r\n\r\n UPDATE public.products AS products\r\n SET\r\n stock = v_quantity,\r\n updated_at = now()\r\n WHERE products.sku = p_sku\r\n AND NOT EXISTS (\r\n SELECT 1\r\n FROM public.product_variants\r\n WHERE product_id = products.id\r\n );\r\n\r\n UPDATE public.products AS products\r\n SET\r\n stock = COALESCE((\r\n SELECT SUM(COALESCE(inventory.quantity, 0))\r\n FROM public.product_variants AS variants\r\n LEFT JOIN public.inventory_items AS inventory\r\n ON inventory.sku = variants.sku\r\n WHERE variants.product_id = products.id\r\n ), 0),\r\n updated_at = now()\r\n WHERE EXISTS (\r\n SELECT 1\r\n FROM public.product_variants AS variants\r\n WHERE variants.product_id = products.id\r\n AND variants.sku = p_sku\r\n );\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.sync_legacy_price_columns_for_currency(target_currency text) RETURNS void\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nDECLARE\r\n v_target_currency text := upper(trim(target_currency));\r\nBEGIN\r\n UPDATE public.products\r\n SET\r\n prices = CASE\r\n WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN prices\r\n ELSE COALESCE(prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, price)\r\n END,\r\n sale_prices = CASE\r\n WHEN sale_price IS NULL THEN sale_prices\r\n WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN sale_prices\r\n ELSE COALESCE(sale_prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, sale_price)\r\n END,\r\n price = CASE\r\n WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN\r\n (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer\r\n ELSE\r\n price\r\n END,\r\n sale_price = CASE\r\n WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN\r\n (sale_prices ->> v_target_currency)::integer\r\n ELSE\r\n sale_price\r\n END,\r\n updated_at = now()\r\n WHERE\r\n NOT (COALESCE(prices, '{}'::jsonb) ? v_target_currency)\r\n OR (\r\n sale_price IS NOT NULL\r\n AND (sale_prices IS NULL OR NOT (sale_prices ? v_target_currency))\r\n )\r\n OR (\r\n COALESCE(prices, '{}'::jsonb) ? v_target_currency\r\n AND price IS DISTINCT FROM (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer\r\n )\r\n OR (\r\n sale_prices IS NOT NULL\r\n AND sale_prices ? v_target_currency\r\n AND sale_price IS DISTINCT FROM (sale_prices ->> v_target_currency)::integer\r\n );\r\n\r\n UPDATE public.product_variants\r\n SET\r\n prices = CASE\r\n WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN prices\r\n ELSE COALESCE(prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, price)\r\n END,\r\n sale_prices = CASE\r\n WHEN sale_price IS NULL THEN sale_prices\r\n WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN sale_prices\r\n ELSE COALESCE(sale_prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, sale_price)\r\n END,\r\n price = CASE\r\n WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN\r\n (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer\r\n ELSE\r\n price\r\n END,\r\n sale_price = CASE\r\n WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN\r\n (sale_prices ->> v_target_currency)::integer\r\n ELSE\r\n sale_price\r\n END,\r\n updated_at = now()\r\n WHERE\r\n NOT (COALESCE(prices, '{}'::jsonb) ? v_target_currency)\r\n OR (\r\n sale_price IS NOT NULL\r\n AND (sale_prices IS NULL OR NOT (sale_prices ? v_target_currency))\r\n )\r\n OR (\r\n COALESCE(prices, '{}'::jsonb) ? v_target_currency\r\n AND price IS DISTINCT FROM (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer\r\n )\r\n OR (\r\n sale_prices IS NOT NULL\r\n AND sale_prices ? v_target_currency\r\n AND sale_price IS DISTINCT FROM (sale_prices ->> v_target_currency)::integer\r\n );\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.sync_shipping_method_currency_maps() RETURNS trigger\r\n LANGUAGE plpgsql\r\n SET search_path TO ''\r\n AS $$\r\nDECLARE\r\n v_source_currency text;\r\nBEGIN\r\n v_source_currency := upper(trim(COALESCE(NULLIF(NEW.cost_currency, ''), public.get_default_currency_code())));\r\n\r\n NEW.cost_currency := v_source_currency;\r\n NEW.currency_pricing_mode := COALESCE(NULLIF(lower(trim(NEW.currency_pricing_mode)), ''), 'auto');\r\n NEW.cost_amounts := public.normalize_currency_amount_map(COALESCE(NEW.cost_amounts, '{}'::jsonb));\r\n NEW.min_order_amounts := public.normalize_currency_amount_map(COALESCE(NEW.min_order_amounts, '{}'::jsonb));\r\n\r\n IF NEW.currency_pricing_mode NOT IN ('auto', 'manual') THEN\r\n RAISE EXCEPTION 'Unsupported shipping currency pricing mode: %', NEW.currency_pricing_mode;\r\n END IF;\r\n\r\n IF NEW.cost_amounts = '{}'::jsonb THEN\r\n NEW.cost_amounts := jsonb_build_object(v_source_currency, GREATEST(COALESCE(NEW.cost_amount, 0), 0));\r\n ELSIF NOT (NEW.cost_amounts ? v_source_currency) THEN\r\n NEW.cost_amounts := NEW.cost_amounts || jsonb_build_object(\r\n v_source_currency,\r\n GREATEST(COALESCE(NEW.cost_amount, 0), 0)\r\n );\r\n END IF;\r\n\r\n IF NEW.min_order_amounts = '{}'::jsonb THEN\r\n NEW.min_order_amounts := jsonb_build_object(\r\n v_source_currency,\r\n GREATEST(COALESCE(NEW.min_order_amount, 0), 0)\r\n );\r\n ELSIF NOT (NEW.min_order_amounts ? v_source_currency) THEN\r\n NEW.min_order_amounts := NEW.min_order_amounts || jsonb_build_object(\r\n v_source_currency,\r\n GREATEST(COALESCE(NEW.min_order_amount, 0), 0)\r\n );\r\n END IF;\r\n\r\n IF NEW.currency_pricing_mode = 'auto' THEN\r\n NEW.cost_amounts := jsonb_build_object(\r\n v_source_currency,\r\n GREATEST((NEW.cost_amounts ->> v_source_currency)::integer, 0)\r\n );\r\n NEW.min_order_amounts := jsonb_build_object(\r\n v_source_currency,\r\n GREATEST((NEW.min_order_amounts ->> v_source_currency)::integer, 0)\r\n );\r\n END IF;\r\n\r\n NEW.cost_amount := GREATEST((NEW.cost_amounts ->> v_source_currency)::integer, 0);\r\n NEW.min_order_amount := GREATEST((NEW.min_order_amounts ->> v_source_currency)::integer, 0);\r\n NEW.updated_at := now();\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.update_product_ratings() RETURNS trigger\r\n LANGUAGE plpgsql SECURITY DEFINER\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_product_id uuid;\r\n v_avg numeric(3,2);\r\n v_count integer;\r\nBEGIN\r\n -- Determine which product_id we need to update\r\n IF TG_OP = 'DELETE' THEN\r\n v_product_id := OLD.product_id;\r\n ELSE\r\n v_product_id := NEW.product_id;\r\n END IF;\r\n\r\n IF v_product_id IS NOT NULL THEN\r\n -- Calculate average and count of approved reviews for this product\r\n SELECT COALESCE(avg(rating), 0.00), count(*)\r\n INTO v_avg, v_count\r\n FROM public.cms_interactions\r\n WHERE product_id = v_product_id\r\n AND type = 'review'\r\n AND status = 'approved';\r\n\r\n -- Update products table\r\n UPDATE public.products\r\n SET average_rating = ROUND(COALESCE(v_avg, 0.00), 2),\r\n total_reviews = v_count\r\n WHERE id = v_product_id;\r\n END IF;\r\n\r\n -- Handle old product_id if it changed on UPDATE (e.g. transfer of reviews)\r\n IF TG_OP = 'UPDATE' AND OLD.product_id IS NOT NULL AND OLD.product_id <> NEW.product_id THEN\r\n SELECT COALESCE(avg(rating), 0.00), count(*)\r\n INTO v_avg, v_count\r\n FROM public.cms_interactions\r\n WHERE product_id = OLD.product_id\r\n AND type = 'review'\r\n AND status = 'approved';\r\n\r\n UPDATE public.products\r\n SET average_rating = ROUND(COALESCE(v_avg, 0.00), 2),\r\n total_reviews = v_count\r\n WHERE id = OLD.product_id;\r\n END IF;\r\n\r\n RETURN NULL;\r\nEND;\r\n$$;\r\n\r\nCREATE OR REPLACE FUNCTION public.upsert_product_with_variants(product_payload jsonb) RETURNS uuid\r\n LANGUAGE plpgsql\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n v_product_id uuid := NULLIF(product_payload->>'id', '')::uuid;\r\n v_translation_group_id uuid := NULLIF(product_payload->>'translation_group_id', '')::uuid;\r\n v_product_type text := CASE\r\n WHEN product_payload->>'product_type' IN ('physical', 'digital') THEN\r\n product_payload->>'product_type'\r\n WHEN NULLIF(product_payload->>'freemius_product_id', '') IS NOT NULL\r\n OR NULLIF(product_payload->>'freemius_plan_id', '') IS NOT NULL THEN\r\n 'digital'\r\n ELSE\r\n 'physical'\r\n END;\r\n v_payment_provider text := CASE\r\n WHEN v_product_type = 'digital' THEN 'freemius'\r\n ELSE 'stripe'\r\n END;\r\n v_variants jsonb := COALESCE(product_payload->'variants', '[]'::jsonb);\r\n v_variant jsonb;\r\n v_variant_id uuid;\r\n v_term_id text;\r\n v_has_variants boolean := jsonb_typeof(v_variants) = 'array' AND jsonb_array_length(v_variants) > 0;\r\n v_total_variant_stock integer := 0;\r\nBEGIN\r\n IF NOT public.is_admin() THEN\r\n RAISE EXCEPTION 'Admin access required';\r\n END IF;\r\n\r\n IF v_has_variants THEN\r\n SELECT COALESCE(SUM(COALESCE((value->>'stock_quantity')::integer, 0)), 0)\r\n INTO v_total_variant_stock\r\n FROM jsonb_array_elements(v_variants);\r\n END IF;\r\n\r\n IF v_product_id IS NULL THEN\r\n INSERT INTO public.products (\r\n title,\r\n slug,\r\n sku,\r\n product_type,\r\n payment_provider,\r\n upc,\r\n stock,\r\n status,\r\n short_description,\r\n description_json,\r\n metadata,\r\n price,\r\n prices,\r\n sale_price,\r\n sale_prices,\r\n sale_start_at,\r\n sale_end_at,\r\n freemius_plan_id,\r\n freemius_product_id,\r\n trial_period_days,\r\n trial_requires_payment_method,\r\n language_id,\r\n translation_group_id\r\n )\r\n VALUES (\r\n product_payload->>'title',\r\n product_payload->>'slug',\r\n product_payload->>'sku',\r\n v_product_type,\r\n v_payment_provider,\r\n NULLIF(product_payload->>'upc', ''),\r\n CASE\r\n WHEN v_has_variants THEN v_total_variant_stock\r\n ELSE COALESCE((product_payload->>'stock')::integer, 0)\r\n END,\r\n COALESCE(product_payload->>'status', 'draft'),\r\n NULLIF(product_payload->>'short_description', ''),\r\n product_payload->'description_json',\r\n COALESCE(product_payload->'metadata', '{}'::jsonb),\r\n COALESCE((product_payload->>'price')::integer, 0),\r\n COALESCE(product_payload->'prices', '{}'::jsonb),\r\n CASE\r\n WHEN product_payload ? 'sale_price' AND product_payload->>'sale_price' <> '' THEN\r\n (product_payload->>'sale_price')::integer\r\n ELSE\r\n NULL\r\n END,\r\n CASE\r\n WHEN product_payload ? 'sale_prices' THEN COALESCE(product_payload->'sale_prices', '{}'::jsonb)\r\n ELSE NULL\r\n END,\r\n CASE\r\n WHEN product_payload ? 'sale_start_at' AND product_payload->>'sale_start_at' <> '' THEN\r\n (product_payload->>'sale_start_at')::timestamptz\r\n ELSE\r\n NULL\r\n END,\r\n CASE\r\n WHEN product_payload ? 'sale_end_at' AND product_payload->>'sale_end_at' <> '' THEN\r\n (product_payload->>'sale_end_at')::timestamptz\r\n ELSE\r\n NULL\r\n END,\r\n NULLIF(product_payload->>'freemius_plan_id', ''),\r\n NULLIF(product_payload->>'freemius_product_id', ''),\r\n COALESCE((product_payload->>'trial_period_days')::integer, 0),\r\n COALESCE((product_payload->>'trial_requires_payment_method')::boolean, false),\r\n (product_payload->>'language_id')::bigint,\r\n COALESCE(v_translation_group_id, gen_random_uuid())\r\n )\r\n RETURNING id INTO v_product_id;\r\n ELSE\r\n UPDATE public.products\r\n SET\r\n title = product_payload->>'title',\r\n slug = product_payload->>'slug',\r\n sku = product_payload->>'sku',\r\n product_type = v_product_type,\r\n payment_provider = v_payment_provider,\r\n upc = NULLIF(product_payload->>'upc', ''),\r\n stock = CASE\r\n WHEN v_has_variants THEN v_total_variant_stock\r\n ELSE COALESCE((product_payload->>'stock')::integer, 0)\r\n END,\r\n status = COALESCE(product_payload->>'status', status),\r\n short_description = NULLIF(product_payload->>'short_description', ''),\r\n description_json = product_payload->'description_json',\r\n metadata = COALESCE(product_payload->'metadata', '{}'::jsonb),\r\n price = COALESCE((product_payload->>'price')::integer, 0),\r\n prices = COALESCE(product_payload->'prices', '{}'::jsonb),\r\n sale_price = CASE\r\n WHEN product_payload ? 'sale_price' AND product_payload->>'sale_price' <> '' THEN\r\n (product_payload->>'sale_price')::integer\r\n ELSE\r\n NULL\r\n END,\r\n sale_prices = CASE\r\n WHEN product_payload ? 'sale_prices' THEN COALESCE(product_payload->'sale_prices', '{}'::jsonb)\r\n ELSE NULL\r\n END,\r\n sale_start_at = CASE\r\n WHEN product_payload ? 'sale_start_at' AND product_payload->>'sale_start_at' <> '' THEN\r\n (product_payload->>'sale_start_at')::timestamptz\r\n ELSE\r\n NULL\r\n END,\r\n sale_end_at = CASE\r\n WHEN product_payload ? 'sale_end_at' AND product_payload->>'sale_end_at' <> '' THEN\r\n (product_payload->>'sale_end_at')::timestamptz\r\n ELSE\r\n NULL\r\n END,\r\n freemius_plan_id = NULLIF(product_payload->>'freemius_plan_id', ''),\r\n freemius_product_id = NULLIF(product_payload->>'freemius_product_id', ''),\r\n trial_period_days = COALESCE((product_payload->>'trial_period_days')::integer, 0),\r\n trial_requires_payment_method = COALESCE((product_payload->>'trial_requires_payment_method')::boolean, false),\r\n language_id = COALESCE((product_payload->>'language_id')::bigint, language_id),\r\n translation_group_id = COALESCE(v_translation_group_id, translation_group_id),\r\n updated_at = now()\r\n WHERE id = v_product_id;\r\n\r\n IF NOT FOUND THEN\r\n RAISE EXCEPTION 'Product not found';\r\n END IF;\r\n END IF;\r\n\r\n DELETE FROM public.variant_attribute_mapping\r\n WHERE variant_id IN (\r\n SELECT id\r\n FROM public.product_variants\r\n WHERE product_id = v_product_id\r\n );\r\n\r\n DELETE FROM public.product_variants\r\n WHERE product_id = v_product_id;\r\n\r\n IF v_has_variants THEN\r\n FOR v_variant IN\r\n SELECT value FROM jsonb_array_elements(v_variants)\r\n LOOP\r\n INSERT INTO public.product_variants (\r\n product_id,\r\n sku,\r\n upc,\r\n price,\r\n prices,\r\n sale_price,\r\n sale_prices,\r\n sale_start_at,\r\n sale_end_at,\r\n stock_quantity,\r\n main_media_id\r\n )\r\n VALUES (\r\n v_product_id,\r\n v_variant->>'sku',\r\n NULLIF(v_variant->>'upc', ''),\r\n COALESCE((v_variant->>'price')::integer, 0),\r\n COALESCE(v_variant->'prices', '{}'::jsonb),\r\n CASE\r\n WHEN v_variant ? 'sale_price' AND v_variant->>'sale_price' <> '' THEN\r\n (v_variant->>'sale_price')::integer\r\n ELSE\r\n NULL\r\n END,\r\n CASE\r\n WHEN v_variant ? 'sale_prices' THEN COALESCE(v_variant->'sale_prices', '{}'::jsonb)\r\n ELSE NULL\r\n END,\r\n CASE\r\n WHEN v_variant ? 'sale_start_at' AND v_variant->>'sale_start_at' <> '' THEN\r\n (v_variant->>'sale_start_at')::timestamptz\r\n ELSE\r\n NULL\r\n END,\r\n CASE\r\n WHEN v_variant ? 'sale_end_at' AND v_variant->>'sale_end_at' <> '' THEN\r\n (v_variant->>'sale_end_at')::timestamptz\r\n ELSE\r\n NULL\r\n END,\r\n COALESCE((v_variant->>'stock_quantity')::integer, 0),\r\n NULLIF(v_variant->>'main_media_id', '')::uuid\r\n )\r\n RETURNING id INTO v_variant_id;\r\n\r\n FOR v_term_id IN\r\n SELECT jsonb_array_elements_text(COALESCE(v_variant->'attribute_term_ids', '[]'::jsonb))\r\n LOOP\r\n INSERT INTO public.variant_attribute_mapping (variant_id, attribute_term_id)\r\n VALUES (v_variant_id, v_term_id::uuid);\r\n END LOOP;\r\n END LOOP;\r\n END IF;\r\n\r\n RETURN v_product_id;\r\nEND;\r\n$$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.blocks (\r\n id bigint NOT NULL,\r\n page_id bigint,\r\n post_id bigint,\r\n language_id bigint NOT NULL,\r\n block_type text NOT NULL,\r\n content jsonb,\r\n \"order\" integer DEFAULT 0 NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n product_id uuid,\r\n CONSTRAINT check_exactly_one_parent CHECK ((((page_id IS NOT NULL) AND (post_id IS NULL) AND (product_id IS NULL)) OR ((post_id IS NOT NULL) AND (page_id IS NULL) AND (product_id IS NULL)) OR ((product_id IS NOT NULL) AND (page_id IS NULL) AND (post_id IS NULL))))\r\n);\r\n\r\nCOMMENT ON TABLE public.blocks IS 'Stores content blocks for pages and posts.';\r\n\r\nCOMMENT ON COLUMN public.blocks.block_type IS 'Type of the block, e.g., \"text\", \"image\".';\r\n\r\nCOMMENT ON COLUMN public.blocks.content IS 'JSONB content specific to the block_type.';\r\n\r\nCOMMENT ON COLUMN public.blocks.\"order\" IS 'Sort order of the block.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.blocks'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.blocks ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.blocks_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.categories (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n name text NOT NULL,\r\n slug text NOT NULL,\r\n description text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n name_translations jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n description_translations jsonb DEFAULT '{}'::jsonb NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.categories IS 'Product categories for organizing catalog items.';\r\n\r\nCOMMENT ON COLUMN public.categories.name_translations IS 'Translated category names (e.g. {\"fr\": \"Numérique\"}).';\r\n\r\nCOMMENT ON COLUMN public.categories.description_translations IS 'Translated category descriptions.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.cms_interactions (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n type public.interaction_type NOT NULL,\r\n status public.approval_status DEFAULT 'pending'::public.approval_status NOT NULL,\r\n content text NOT NULL,\r\n rating integer,\r\n user_id uuid NOT NULL,\r\n product_id uuid,\r\n post_id bigint,\r\n reactions jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT check_product_or_post CHECK ((((product_id IS NOT NULL) AND (post_id IS NULL)) OR ((post_id IS NOT NULL) AND (product_id IS NULL)))),\r\n CONSTRAINT check_rating_only_for_review CHECK ((((type = 'review'::public.interaction_type) AND (rating IS NOT NULL) AND (rating >= 1) AND (rating <= 5)) OR ((type = 'comment'::public.interaction_type) AND (rating IS NULL))))\r\n);\r\n\r\nCOMMENT ON TABLE public.cms_interactions IS 'Stores user-submitted product reviews and blog post comments.';\r\n\r\nCOMMENT ON COLUMN public.cms_interactions.rating IS 'Star rating 1-5, only populated for product reviews.';\r\n\r\nCOMMENT ON COLUMN public.cms_interactions.user_id IS 'References public.profiles.id';\r\n\r\nCOMMENT ON COLUMN public.cms_interactions.reactions IS 'JSONB structure tracking counts of reactions (likes, etc.).';\r\n\r\nCREATE TABLE IF NOT EXISTS public.content_drafts (\r\n id bigint NOT NULL,\r\n parent_type text NOT NULL,\r\n parent_id bigint NOT NULL,\r\n author_id uuid,\r\n base_version integer DEFAULT 1 NOT NULL,\r\n meta jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n blocks jsonb DEFAULT '[]'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT content_drafts_blocks_array CHECK ((jsonb_typeof(blocks) = 'array'::text)),\r\n CONSTRAINT content_drafts_meta_object CHECK ((jsonb_typeof(meta) = 'object'::text)),\r\n CONSTRAINT content_drafts_parent_type_check CHECK ((parent_type = ANY (ARRAY['page'::text, 'post'::text])))\r\n);\r\n\r\nCOMMENT ON TABLE public.content_drafts IS 'Draft snapshots used by Draft Mode and front-end visual editing before publishing to live page/post rows.';\r\n\r\nCOMMENT ON COLUMN public.content_drafts.parent_type IS 'The content table this draft belongs to: page or post.';\r\n\r\nCOMMENT ON COLUMN public.content_drafts.parent_id IS 'ID of the page or post being drafted.';\r\n\r\nCOMMENT ON COLUMN public.content_drafts.base_version IS 'Published page/post version the draft was created from.';\r\n\r\nCOMMENT ON COLUMN public.content_drafts.meta IS 'Draft page/post metadata snapshot.';\r\n\r\nCOMMENT ON COLUMN public.content_drafts.blocks IS 'Ordered draft block snapshot, including block ids, block types, content, language ids, and order values.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.content_drafts'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.content_drafts ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.content_drafts_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.cortex_ai_db_mutation_audit (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n actor_user_id uuid,\r\n tool_name text NOT NULL,\r\n action_name text NOT NULL,\r\n target_tables text[] DEFAULT '{}'::text[] NOT NULL,\r\n operation_summary text NOT NULL,\r\n payload_hash text NOT NULL,\r\n payload jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n preview jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n status text NOT NULL,\r\n error_message text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT cortex_ai_db_mutation_audit_status_check CHECK ((status = ANY (ARRAY['success'::text, 'failure'::text])))\r\n);\r\n\r\nCOMMENT ON TABLE public.cortex_ai_db_mutation_audit IS 'Audit trail for confirmed Cortex AI database mutation attempts.';\r\n\r\nCOMMENT ON COLUMN public.cortex_ai_db_mutation_audit.payload IS 'Redacted tool input payload for the confirmed mutation attempt.';\r\n\r\nCOMMENT ON COLUMN public.cortex_ai_db_mutation_audit.preview IS 'Redacted confirmation preview shown before the mutation was confirmed.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.coupon_freemius_mappings (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n coupon_id uuid NOT NULL,\r\n product_id uuid,\r\n freemius_product_id text NOT NULL,\r\n freemius_coupon_id text,\r\n freemius_coupon_code text NOT NULL,\r\n sync_status text DEFAULT 'pending'::text NOT NULL,\r\n sync_error text,\r\n remote_payload jsonb,\r\n last_synced_at timestamp with time zone,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT coupon_freemius_mappings_code_not_blank CHECK ((char_length(btrim(freemius_coupon_code)) > 0)),\r\n CONSTRAINT coupon_freemius_mappings_product_not_blank CHECK ((char_length(btrim(freemius_product_id)) > 0)),\r\n CONSTRAINT coupon_freemius_mappings_sync_status_valid CHECK ((sync_status = ANY (ARRAY['pending'::text, 'synced'::text, 'failed'::text, 'deleted'::text])))\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.coupon_products (\r\n coupon_id uuid NOT NULL,\r\n product_id uuid NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.coupon_products IS 'Optional product allow-list for coupons. No rows means all products in the provider scope are eligible.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.coupon_redemptions (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n coupon_id uuid,\r\n order_id uuid,\r\n coupon_code text NOT NULL,\r\n provider text NOT NULL,\r\n discount_total integer DEFAULT 0 NOT NULL,\r\n user_id uuid,\r\n customer_email text,\r\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n redeemed_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT coupon_redemptions_code_not_blank CHECK ((char_length(btrim(coupon_code)) > 0)),\r\n CONSTRAINT coupon_redemptions_discount_total_check CHECK ((discount_total >= 0)),\r\n CONSTRAINT coupon_redemptions_provider_check CHECK ((provider = ANY (ARRAY['stripe'::text, 'freemius'::text])))\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.coupons (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n code text NOT NULL,\r\n name text NOT NULL,\r\n internal_note text,\r\n provider_scope text DEFAULT 'all'::text NOT NULL,\r\n discount_type text NOT NULL,\r\n discount_amount integer NOT NULL,\r\n is_active boolean DEFAULT true NOT NULL,\r\n starts_at timestamp with time zone,\r\n ends_at timestamp with time zone,\r\n redemption_limit integer,\r\n redemptions_count integer DEFAULT 0 NOT NULL,\r\n freemius_sync_status text DEFAULT 'not_synced'::text NOT NULL,\r\n freemius_sync_error text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT coupons_code_not_blank CHECK ((char_length(btrim(code)) > 0)),\r\n CONSTRAINT coupons_date_window_valid CHECK (((starts_at IS NULL) OR (ends_at IS NULL) OR (starts_at < ends_at))),\r\n CONSTRAINT coupons_discount_amount_positive CHECK ((discount_amount > 0)),\r\n CONSTRAINT coupons_discount_type_valid CHECK ((discount_type = ANY (ARRAY['percent'::text, 'fixed'::text]))),\r\n CONSTRAINT coupons_freemius_sync_status_valid CHECK ((freemius_sync_status = ANY (ARRAY['not_synced'::text, 'pending'::text, 'synced'::text, 'failed'::text, 'not_required'::text]))),\r\n CONSTRAINT coupons_name_not_blank CHECK ((char_length(btrim(name)) > 0)),\r\n CONSTRAINT coupons_percent_amount_valid CHECK (((discount_type <> 'percent'::text) OR (discount_amount <= 100))),\r\n CONSTRAINT coupons_provider_scope_valid CHECK ((provider_scope = ANY (ARRAY['all'::text, 'stripe'::text, 'freemius'::text]))),\r\n CONSTRAINT coupons_redemption_limit_positive CHECK (((redemption_limit IS NULL) OR (redemption_limit > 0))),\r\n CONSTRAINT coupons_redemptions_count_nonnegative CHECK ((redemptions_count >= 0))\r\n);\r\n\r\nCOMMENT ON TABLE public.coupons IS 'Unified commerce coupons managed by NextBlock CMS and applied through provider-aware checkout.';\r\n\r\nCOMMENT ON COLUMN public.coupons.provider_scope IS 'Provider eligibility: all, stripe, or freemius.';\r\n\r\nCOMMENT ON COLUMN public.coupons.discount_amount IS 'Percent value for percent coupons, or fixed minor-unit amount for fixed coupons.';\r\n\r\nCOMMENT ON COLUMN public.coupons.freemius_sync_status IS 'Aggregate status for syncing this coupon to Freemius product coupon records.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.currencies (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n code text NOT NULL,\r\n symbol text NOT NULL,\r\n exchange_rate numeric(20,10) NOT NULL,\r\n is_default boolean DEFAULT false NOT NULL,\r\n is_active boolean DEFAULT true NOT NULL,\r\n rounding_mode text DEFAULT 'none'::text NOT NULL,\r\n rounding_increment integer DEFAULT 1 NOT NULL,\r\n rounding_charm_amount integer,\r\n auto_update_exchange_rate boolean DEFAULT true NOT NULL,\r\n exchange_rate_updated_at timestamp with time zone,\r\n exchange_rate_source text,\r\n auto_sync_product_prices boolean DEFAULT false NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT currencies_charm_requires_amount CHECK (((rounding_mode <> 'charm'::text) OR (rounding_charm_amount IS NOT NULL))),\r\n CONSTRAINT currencies_code_check CHECK ((code ~ '^[A-Z]{3}$'::text)),\r\n CONSTRAINT currencies_default_auto_update_disabled CHECK (((NOT is_default) OR (auto_update_exchange_rate = false))),\r\n CONSTRAINT currencies_default_exchange_rate_is_one CHECK (((NOT is_default) OR (exchange_rate = (1)::numeric))),\r\n CONSTRAINT currencies_default_must_be_active CHECK (((NOT is_default) OR is_active)),\r\n CONSTRAINT currencies_default_product_price_sync_disabled CHECK (((NOT is_default) OR (auto_sync_product_prices = false))),\r\n CONSTRAINT currencies_exchange_rate_check CHECK ((exchange_rate > (0)::numeric)),\r\n CONSTRAINT currencies_rounding_charm_nonnegative CHECK (((rounding_charm_amount IS NULL) OR (rounding_charm_amount >= 0))),\r\n CONSTRAINT currencies_rounding_increment_positive CHECK ((rounding_increment > 0)),\r\n CONSTRAINT currencies_rounding_mode_valid CHECK ((rounding_mode = ANY (ARRAY['none'::text, 'nearest'::text, 'up'::text, 'down'::text, 'charm'::text])))\r\n);\r\n\r\nCOMMENT ON TABLE public.currencies IS 'Store currencies available for storefront display and conversion.';\r\n\r\nCOMMENT ON COLUMN public.currencies.exchange_rate IS 'Relative to the current store default currency. The default currency should have exchange_rate = 1.';\r\n\r\nCOMMENT ON COLUMN public.currencies.rounding_mode IS 'Rounding strategy applied when prices are auto-converted into this currency.';\r\n\r\nCOMMENT ON COLUMN public.currencies.rounding_increment IS 'Rounding step in the currency smallest unit. Example: 5 means 0.05 for USD/CAD.';\r\n\r\nCOMMENT ON COLUMN public.currencies.rounding_charm_amount IS 'Charm ending in the currency smallest unit. Example: 90 means prices like 29.90.';\r\n\r\nCOMMENT ON COLUMN public.currencies.auto_update_exchange_rate IS 'Whether scheduled FX sync jobs should refresh this currency.';\r\n\r\nCOMMENT ON COLUMN public.currencies.exchange_rate_updated_at IS 'When this currency exchange rate was last refreshed or manually set.';\r\n\r\nCOMMENT ON COLUMN public.currencies.exchange_rate_source IS 'Human-readable source for the current exchange rate, such as a provider host or manual override.';\r\n\r\nCOMMENT ON COLUMN public.currencies.auto_sync_product_prices IS 'Whether storefront product and variant prices in this currency are derived automatically from the store default currency using FX and rounding rules.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.email_2fa_challenges (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n user_id uuid NOT NULL,\r\n token_hash text NOT NULL,\r\n expires_at timestamp with time zone NOT NULL,\r\n consumed_at timestamp with time zone,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.email_2fa_challenges IS 'Short-lived SHA-256 hashes of 6-digit email verification codes. Readable/writable only by the service role.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.freemius_plans (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n product_id uuid NOT NULL,\r\n name text NOT NULL,\r\n title text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.freemius_pricing (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n plan_id uuid NOT NULL,\r\n api_monthly_price numeric,\r\n api_annual_price numeric,\r\n api_lifetime_price numeric,\r\n override_monthly_price numeric,\r\n override_annual_price numeric,\r\n override_lifetime_price numeric,\r\n license_quota integer,\r\n is_active boolean DEFAULT true NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.inventory_items (\r\n sku text NOT NULL,\r\n quantity integer DEFAULT 0 NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT inventory_items_quantity_check CHECK ((quantity >= 0))\r\n);\r\n\r\nCOMMENT ON TABLE public.inventory_items IS 'Source-of-truth inventory records keyed by sellable SKU.';\r\n\r\nCOMMENT ON COLUMN public.inventory_items.sku IS 'Global sellable SKU. Matching products or variants share inventory.';\r\n\r\nCOMMENT ON COLUMN public.inventory_items.quantity IS 'Available quantity for this SKU.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.languages (\r\n id bigint NOT NULL,\r\n code text NOT NULL,\r\n name text NOT NULL,\r\n is_default boolean DEFAULT false NOT NULL,\r\n is_active boolean DEFAULT true,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.languages IS 'Stores supported languages for the CMS.';\r\n\r\nCOMMENT ON COLUMN public.languages.code IS 'BCP 47 language code.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.languages'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.languages ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.languages_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.logos (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n name text NOT NULL,\r\n media_id uuid,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.logos IS 'Stores company and brand logos.';\r\n\r\nCOMMENT ON COLUMN public.logos.name IS 'The name of the brand or company for the logo.';\r\n\r\nCOMMENT ON COLUMN public.logos.media_id IS 'Foreign key to the media table for the logo image.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.media (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n uploader_id uuid,\r\n file_name text NOT NULL,\r\n object_key text NOT NULL,\r\n file_type text,\r\n size_bytes bigint,\r\n description text,\r\n width integer,\r\n height integer,\r\n blur_data_url text,\r\n variants jsonb,\r\n file_path text,\r\n folder text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.media IS 'Stores information about uploaded media assets.';\r\n\r\nCOMMENT ON COLUMN public.media.object_key IS 'Unique key (path) in Cloudflare R2.';\r\n\r\nCOMMENT ON COLUMN public.media.width IS 'Width of the image in pixels.';\r\n\r\nCOMMENT ON COLUMN public.media.height IS 'Height of the image in pixels.';\r\n\r\nCOMMENT ON COLUMN public.media.blur_data_url IS 'Base64 encoded string for image blur placeholders.';\r\n\r\nCOMMENT ON COLUMN public.media.variants IS 'Array of image variant objects.';\r\n\r\nCOMMENT ON COLUMN public.media.file_path IS 'Full path to the file in the storage bucket.';\r\n\r\nCOMMENT ON COLUMN public.media.folder IS 'Folder path prefix for the R2 object.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.navigation_items (\r\n id bigint NOT NULL,\r\n language_id bigint NOT NULL,\r\n menu_key public.menu_location NOT NULL,\r\n label text NOT NULL,\r\n url text NOT NULL,\r\n parent_id bigint,\r\n \"order\" integer DEFAULT 0 NOT NULL,\r\n page_id bigint,\r\n translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.navigation_items IS 'Stores navigation menu items.';\r\n\r\nCOMMENT ON COLUMN public.navigation_items.menu_key IS 'Identifies the menu this item belongs to.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.navigation_items'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.navigation_items ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.navigation_items_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE SEQUENCE IF NOT EXISTS public.order_invoice_number_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1;\r\n\r\nCREATE TABLE IF NOT EXISTS public.order_items (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n order_id uuid NOT NULL,\r\n product_id uuid,\r\n variant_id uuid,\r\n quantity integer NOT NULL,\r\n price_at_purchase integer NOT NULL\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.orders (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n user_id uuid,\r\n status text DEFAULT 'pending'::text NOT NULL,\r\n total integer NOT NULL,\r\n stripe_session_id text,\r\n payment_intent_id text,\r\n customer_details jsonb,\r\n provider text DEFAULT 'stripe'::text,\r\n freemius_product_id text,\r\n freemius_plan_id text,\r\n freemius_license_id text,\r\n freemius_subscription_id text,\r\n freemius_trial_id text,\r\n freemius_user_id text,\r\n freemius_trial_ends_at timestamp with time zone,\r\n freemius_last_event_type text,\r\n freemius_last_synced_at timestamp with time zone,\r\n currency text DEFAULT 'USD'::text NOT NULL,\r\n subtotal integer,\r\n shipping_total integer,\r\n tax_total integer DEFAULT 0 NOT NULL,\r\n tax_details jsonb,\r\n exchange_rate_at_purchase numeric(20,10) DEFAULT 1 NOT NULL,\r\n inventory_deducted_at timestamp with time zone,\r\n invoice_number text,\r\n paid_at timestamp with time zone,\r\n created_at timestamp with time zone DEFAULT now(),\r\n coupon_id uuid,\r\n coupon_code text,\r\n discount_total integer DEFAULT 0 NOT NULL,\r\n discount_details jsonb,\r\n CONSTRAINT orders_discount_total_check CHECK ((discount_total >= 0)),\r\n CONSTRAINT orders_exchange_rate_at_purchase_positive CHECK ((exchange_rate_at_purchase > (0)::numeric)),\r\n CONSTRAINT orders_provider_check CHECK ((provider = ANY (ARRAY['stripe'::text, 'freemius'::text]))),\r\n CONSTRAINT orders_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'trial'::text, 'paid'::text, 'shipped'::text, 'cancelled'::text, 'refunded'::text])))\r\n);\r\n\r\nCOMMENT ON COLUMN public.orders.freemius_license_id IS 'Freemius license ID used to reconcile checkout callbacks and webhooks.';\r\n\r\nCOMMENT ON COLUMN public.orders.freemius_subscription_id IS 'Freemius subscription ID when the order is associated with recurring billing.';\r\n\r\nCOMMENT ON COLUMN public.orders.freemius_trial_id IS 'Freemius trial ID when checkout starts in trial mode.';\r\n\r\nCOMMENT ON COLUMN public.orders.freemius_trial_ends_at IS 'Freemius trial expiration timestamp when supplied by checkout or webhook data.';\r\n\r\nCOMMENT ON COLUMN public.orders.freemius_last_event_type IS 'Last Freemius checkout callback or webhook event applied to the order.';\r\n\r\nCOMMENT ON COLUMN public.orders.freemius_last_synced_at IS 'Timestamp when Freemius metadata was last reconciled locally.';\r\n\r\nCOMMENT ON COLUMN public.orders.currency IS 'ISO currency code used for the order totals.';\r\n\r\nCOMMENT ON COLUMN public.orders.subtotal IS 'Subtotal before shipping and tax, in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.orders.shipping_total IS 'Shipping amount before tax, in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.orders.tax_total IS 'Total tax amount collected for the order, in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.orders.tax_details IS 'Normalized tax breakdown payload sourced from manual rates or finalized Stripe tax data.';\r\n\r\nCOMMENT ON COLUMN public.orders.exchange_rate_at_purchase IS 'Exchange rate locked at purchase time relative to the store default currency.';\r\n\r\nCOMMENT ON COLUMN public.orders.invoice_number IS 'Stable printable invoice number assigned once when the order first becomes paid.';\r\n\r\nCOMMENT ON COLUMN public.orders.paid_at IS 'Timestamp when the order was first marked as paid.';\r\n\r\nCOMMENT ON COLUMN public.orders.coupon_code IS 'Coupon code applied to the order at checkout time.';\r\n\r\nCOMMENT ON COLUMN public.orders.discount_total IS 'Total discount applied to this order in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.orders.discount_details IS 'Provider-aware coupon quote details captured at checkout.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.package_activations (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n license_key text NOT NULL,\r\n instance_name text NOT NULL,\r\n package_id text NOT NULL,\r\n status text DEFAULT 'active'::text NOT NULL,\r\n meta jsonb DEFAULT '{}'::jsonb,\r\n last_validated_at timestamp with time zone DEFAULT now(),\r\n created_at timestamp with time zone DEFAULT now()\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.page_revisions (\r\n id bigint NOT NULL,\r\n page_id bigint NOT NULL,\r\n author_id uuid,\r\n version integer NOT NULL,\r\n revision_type public.revision_type NOT NULL,\r\n content jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.page_revisions IS 'Hybrid (snapshot/diff) revisions for pages.';\r\n\r\nCOMMENT ON COLUMN public.page_revisions.content IS 'If snapshot: full content; if diff: JSON Patch array.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.page_revisions'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.page_revisions ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.page_revisions_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.pages (\r\n id bigint NOT NULL,\r\n language_id bigint NOT NULL,\r\n author_id uuid,\r\n title text NOT NULL,\r\n slug text NOT NULL,\r\n status public.page_status DEFAULT 'draft'::public.page_status NOT NULL,\r\n meta_title text,\r\n meta_description text,\r\n version integer DEFAULT 1 NOT NULL,\r\n translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n feature_image_id uuid\r\n);\r\n\r\nCOMMENT ON TABLE public.pages IS 'Stores static pages for the website.';\r\n\r\nCOMMENT ON COLUMN public.pages.slug IS 'URL-friendly identifier, unique per language.';\r\n\r\nCOMMENT ON COLUMN public.pages.version IS 'Monotonic version number for hybrid revisions.';\r\n\r\nCOMMENT ON COLUMN public.pages.translation_group_id IS 'Groups different language versions of the same conceptual page.';\r\n\r\nCOMMENT ON COLUMN public.pages.feature_image_id IS 'ID of the media item to be used as the page feature image.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.pages'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.pages ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.pages_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.post_revisions (\r\n id bigint NOT NULL,\r\n post_id bigint NOT NULL,\r\n author_id uuid,\r\n version integer NOT NULL,\r\n revision_type public.revision_type NOT NULL,\r\n content jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.post_revisions IS 'Hybrid (snapshot/diff) revisions for posts.';\r\n\r\nCOMMENT ON COLUMN public.post_revisions.content IS 'If snapshot: full content; if diff: JSON Patch array.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.post_revisions'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.post_revisions ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.post_revisions_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.posts (\r\n id bigint NOT NULL,\r\n language_id bigint NOT NULL,\r\n author_id uuid,\r\n title text NOT NULL,\r\n slug text NOT NULL,\r\n label text,\r\n excerpt text,\r\n subtitle text,\r\n status public.page_status DEFAULT 'draft'::public.page_status NOT NULL,\r\n published_at timestamp with time zone,\r\n meta_title text,\r\n meta_description text,\r\n feature_image_id uuid,\r\n version integer DEFAULT 1 NOT NULL,\r\n translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.posts IS 'Stores blog posts or news articles.';\r\n\r\nCOMMENT ON COLUMN public.posts.slug IS 'URL-friendly identifier, unique per language.';\r\n\r\nCOMMENT ON COLUMN public.posts.label IS 'Short editorial label rendered as a pill on post hero and article cards.';\r\n\r\nCOMMENT ON COLUMN public.posts.excerpt IS 'Short editorial summary used in post metadata rows and post cards.';\r\n\r\nCOMMENT ON COLUMN public.posts.subtitle IS 'Longer deck shown under the post title.';\r\n\r\nCOMMENT ON COLUMN public.posts.feature_image_id IS 'ID of the media item to be used as the feature image.';\r\n\r\nCOMMENT ON COLUMN public.posts.version IS 'Monotonic version number for hybrid revisions.';\r\n\r\nCOMMENT ON COLUMN public.posts.translation_group_id IS 'Groups different language versions of the same conceptual post.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.posts'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.posts ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.posts_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.privacy_consent_logs (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n consent_token text NOT NULL,\r\n categories jsonb DEFAULT '{\"analytics\": false, \"marketing\": false, \"necessary\": true}'::jsonb NOT NULL,\r\n ip_masked text,\r\n user_agent text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT privacy_consent_logs_categories_check CHECK ((jsonb_typeof(categories) = 'object'::text))\r\n);\r\n\r\nCOMMENT ON TABLE public.privacy_consent_logs IS 'Immutable audit log of visitor consent decisions for Quebec Law 25 / PIPEDA accountability.';\r\n\r\nCOMMENT ON COLUMN public.privacy_consent_logs.consent_token IS 'Opaque token also stored in the nb_consent_preference cookie to correlate a decision with its record.';\r\n\r\nCOMMENT ON COLUMN public.privacy_consent_logs.ip_masked IS 'Partially masked IP (e.g. 203.0.113.x) - never store a full address for an analytics/marketing consent log.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_attribute_terms (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n attribute_id uuid NOT NULL,\r\n value text NOT NULL,\r\n slug text NOT NULL,\r\n sort_order integer DEFAULT 0 NOT NULL,\r\n value_translations jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now(),\r\n updated_at timestamp with time zone DEFAULT now()\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_attributes (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n name text NOT NULL,\r\n slug text NOT NULL,\r\n name_translations jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now(),\r\n updated_at timestamp with time zone DEFAULT now()\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_categories (\r\n product_id uuid NOT NULL,\r\n category_id uuid NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.product_categories IS 'Junction table mapping products to multiple categories.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_drafts (\r\n id bigint NOT NULL,\r\n product_id uuid NOT NULL,\r\n author_id uuid,\r\n meta jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n blocks jsonb DEFAULT '[]'::jsonb NOT NULL,\r\n CONSTRAINT product_drafts_blocks_array CHECK ((jsonb_typeof(blocks) = 'array'::text)),\r\n CONSTRAINT product_drafts_meta_object CHECK ((jsonb_typeof(meta) = 'object'::text))\r\n);\r\n\r\nCOMMENT ON TABLE public.product_drafts IS 'Draft product metadata snapshots used by Draft Mode and front-end visual editing before publishing to live product rows.';\r\n\r\nCOMMENT ON COLUMN public.product_drafts.product_id IS 'ID of the product being drafted.';\r\n\r\nCOMMENT ON COLUMN public.product_drafts.meta IS 'Draft product metadata snapshot. Front-end visual editing currently mutates visible title, short_description, and description_json fields.';\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.product_drafts'::regclass AND attname = 'id' AND attidentity <> '') THEN\r\n ALTER TABLE public.product_drafts ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\r\n SEQUENCE NAME public.product_drafts_id_seq\r\n START WITH 1\r\n INCREMENT BY 1\r\n NO MINVALUE\r\n NO MAXVALUE\r\n CACHE 1\r\n );\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_freemius_sale_coupons (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n product_id uuid NOT NULL,\r\n freemius_product_id text NOT NULL,\r\n freemius_plan_id text,\r\n freemius_coupon_id text,\r\n freemius_coupon_code text NOT NULL,\r\n discount_percent integer,\r\n starts_at timestamp with time zone,\r\n ends_at timestamp with time zone,\r\n is_active boolean DEFAULT false NOT NULL,\r\n sync_status text DEFAULT 'pending'::text NOT NULL,\r\n sync_error text,\r\n remote_payload jsonb,\r\n last_synced_at timestamp with time zone,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT product_freemius_sale_coupons_code_not_blank CHECK ((char_length(btrim(freemius_coupon_code)) > 0)),\r\n CONSTRAINT product_freemius_sale_coupons_discount_valid CHECK (((discount_percent IS NULL) OR ((discount_percent > 0) AND (discount_percent <= 100)))),\r\n CONSTRAINT product_freemius_sale_coupons_fm_product_not_blank CHECK ((char_length(btrim(freemius_product_id)) > 0)),\r\n CONSTRAINT product_freemius_sale_coupons_sync_status_valid CHECK ((sync_status = ANY (ARRAY['pending'::text, 'synced'::text, 'failed'::text, 'deleted'::text]))),\r\n CONSTRAINT product_freemius_sale_coupons_window_valid CHECK (((starts_at IS NULL) OR (ends_at IS NULL) OR (starts_at < ends_at)))\r\n);\r\n\r\nCOMMENT ON TABLE public.product_freemius_sale_coupons IS 'Auto-generated, time-bounded Freemius coupons that enforce a scheduled sale on a Freemius product at Freemius-hosted checkout.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_media (\r\n product_id uuid NOT NULL,\r\n media_id uuid NOT NULL,\r\n sort_order integer DEFAULT 0\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.product_variants (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n product_id uuid NOT NULL,\r\n sku text NOT NULL,\r\n price_adjustment integer DEFAULT 0 NOT NULL,\r\n price integer DEFAULT 0 NOT NULL,\r\n prices jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n sale_price integer,\r\n sale_prices jsonb,\r\n stock_quantity integer DEFAULT 0 NOT NULL,\r\n upc text,\r\n main_media_id uuid,\r\n created_at timestamp with time zone DEFAULT now(),\r\n updated_at timestamp with time zone DEFAULT now(),\r\n sale_start_at timestamp with time zone,\r\n sale_end_at timestamp with time zone,\r\n scheduled_price integer,\r\n scheduled_prices jsonb,\r\n scheduled_price_at timestamp with time zone,\r\n CONSTRAINT product_variants_prices_is_valid CHECK (public.is_valid_currency_amount_map(prices)),\r\n CONSTRAINT product_variants_sale_prices_are_valid CHECK (public.is_valid_sale_price_map(prices, sale_prices)),\r\n CONSTRAINT product_variants_sale_window_valid CHECK (((sale_start_at IS NULL) OR (sale_end_at IS NULL) OR (sale_start_at < sale_end_at)))\r\n);\r\n\r\nCOMMENT ON COLUMN public.product_variants.prices IS 'Variant regular prices by ISO 4217 code in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.product_variants.sale_prices IS 'Variant sale prices by ISO 4217 code in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.product_variants.sale_start_at IS 'Inclusive start of the scheduled sale window (UTC) for this variant. NULL means no lower bound.';\r\n\r\nCOMMENT ON COLUMN public.product_variants.sale_end_at IS 'Exclusive end of the scheduled sale window (UTC) for this variant. NULL means no upper bound.';\r\n\r\nCOMMENT ON COLUMN public.product_variants.scheduled_price IS 'Pending regular price (smallest currency unit) applied once scheduled_price_at has passed.';\r\n\r\nCOMMENT ON COLUMN public.product_variants.scheduled_prices IS 'Pending multi-currency regular prices by ISO 4217 code, applied once scheduled_price_at has passed.';\r\n\r\nCOMMENT ON COLUMN public.product_variants.scheduled_price_at IS 'Effective timestamp (UTC) for the pending regular-price change.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.products (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n language_id bigint NOT NULL,\r\n translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n sku text NOT NULL,\r\n title text NOT NULL,\r\n slug text NOT NULL,\r\n product_type text NOT NULL,\r\n payment_provider text NOT NULL,\r\n price integer NOT NULL,\r\n prices jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n sale_price integer,\r\n sale_prices jsonb,\r\n stock integer DEFAULT 0,\r\n status text DEFAULT 'draft'::text NOT NULL,\r\n meta_title text,\r\n meta_description text,\r\n short_description text,\r\n description_json jsonb,\r\n metadata jsonb,\r\n freemius_plan_id text,\r\n freemius_product_id text,\r\n trial_period_days integer DEFAULT 0 NOT NULL,\r\n trial_requires_payment_method boolean DEFAULT false NOT NULL,\r\n upc text,\r\n is_taxable boolean DEFAULT true NOT NULL,\r\n created_at timestamp with time zone DEFAULT now(),\r\n updated_at timestamp with time zone DEFAULT now(),\r\n sale_start_at timestamp with time zone,\r\n sale_end_at timestamp with time zone,\r\n scheduled_price integer,\r\n scheduled_prices jsonb,\r\n scheduled_price_at timestamp with time zone,\r\n average_rating numeric(3,2) DEFAULT 0.00 NOT NULL,\r\n total_reviews integer DEFAULT 0 NOT NULL,\r\n CONSTRAINT products_payment_provider_check CHECK ((payment_provider = ANY (ARRAY['stripe'::text, 'freemius'::text]))),\r\n CONSTRAINT products_prices_is_valid CHECK (public.is_valid_currency_amount_map(prices)),\r\n CONSTRAINT products_product_type_check CHECK ((product_type = ANY (ARRAY['physical'::text, 'digital'::text]))),\r\n CONSTRAINT products_sale_prices_are_valid CHECK (public.is_valid_sale_price_map(prices, sale_prices)),\r\n CONSTRAINT products_sale_window_valid CHECK (((sale_start_at IS NULL) OR (sale_end_at IS NULL) OR (sale_start_at < sale_end_at))),\r\n CONSTRAINT products_status_check CHECK ((status = ANY (ARRAY['draft'::text, 'active'::text, 'archived'::text]))),\r\n CONSTRAINT products_trial_period_days_check CHECK ((trial_period_days >= 0)),\r\n CONSTRAINT products_type_provider_consistency_check CHECK ((((product_type = 'physical'::text) AND (payment_provider = 'stripe'::text)) OR ((product_type = 'digital'::text) AND (payment_provider = 'freemius'::text))))\r\n);\r\n\r\nCOMMENT ON COLUMN public.products.prices IS 'Regular prices by ISO 4217 code in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.products.sale_prices IS 'Sale prices by ISO 4217 code in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.products.is_taxable IS 'When true, this product participates in Stripe tax calculation.';\r\n\r\nCOMMENT ON COLUMN public.products.sale_start_at IS 'Inclusive start of the scheduled sale window (UTC). NULL means no lower bound.';\r\n\r\nCOMMENT ON COLUMN public.products.sale_end_at IS 'Exclusive end of the scheduled sale window (UTC). NULL means no upper bound. Both NULL = always-on sale.';\r\n\r\nCOMMENT ON COLUMN public.products.scheduled_price IS 'Pending regular price (smallest currency unit) applied once scheduled_price_at has passed.';\r\n\r\nCOMMENT ON COLUMN public.products.scheduled_prices IS 'Pending multi-currency regular prices by ISO 4217 code, applied once scheduled_price_at has passed.';\r\n\r\nCOMMENT ON COLUMN public.products.scheduled_price_at IS 'Effective timestamp (UTC) for the pending regular-price change.';\r\n\r\nCOMMENT ON COLUMN public.products.average_rating IS 'Aggregated average 1-5 rating of approved reviews.';\r\n\r\nCOMMENT ON COLUMN public.products.total_reviews IS 'Total count of approved reviews for this product.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.profiles (\r\n id uuid NOT NULL,\r\n updated_at timestamp with time zone,\r\n full_name text,\r\n avatar_url text,\r\n website text,\r\n github_username text,\r\n phone text,\r\n role public.user_role DEFAULT 'USER'::public.user_role NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.profiles IS 'Profile information for each user, extending auth.users.';\r\n\r\nCOMMENT ON COLUMN public.profiles.id IS 'References auth.users.id';\r\n\r\nCOMMENT ON COLUMN public.profiles.role IS 'User role for RBAC.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.shipping_zone_locations (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n zone_id uuid NOT NULL,\r\n country_code text NOT NULL,\r\n state_code text,\r\n postal_code text,\r\n created_at timestamp with time zone DEFAULT now()\r\n);\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_locations.country_code IS 'ISO 3166-1 alpha-2 country code.';\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_locations.state_code IS 'Optional state/province code within the selected country (for example CA, NY, ON, QC). NULL means the whole country.';\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_locations.postal_code IS 'Optional exact postal code or wildcard pattern. NULL means all postal codes in the matched country/state.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.shipping_zone_methods (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n zone_id uuid NOT NULL,\r\n method_type text NOT NULL,\r\n cost_amount integer DEFAULT 0 NOT NULL,\r\n cost_currency text DEFAULT 'USD'::text NOT NULL,\r\n min_order_amount integer DEFAULT 0 NOT NULL,\r\n name text NOT NULL,\r\n name_translations jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n currency_pricing_mode text DEFAULT 'auto'::text NOT NULL,\r\n cost_amounts jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n min_order_amounts jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now(),\r\n updated_at timestamp with time zone DEFAULT now(),\r\n CONSTRAINT shipping_zone_methods_cost_amounts_include_source CHECK ((cost_amounts ? upper(cost_currency))),\r\n CONSTRAINT shipping_zone_methods_cost_amounts_valid CHECK (public.is_valid_currency_amount_map(cost_amounts)),\r\n CONSTRAINT shipping_zone_methods_cost_currency_format CHECK ((cost_currency ~ '^[A-Z]{3}$'::text)),\r\n CONSTRAINT shipping_zone_methods_currency_pricing_mode_valid CHECK ((currency_pricing_mode = ANY (ARRAY['auto'::text, 'manual'::text]))),\r\n CONSTRAINT shipping_zone_methods_method_type_check CHECK ((method_type = ANY (ARRAY['flat_rate'::text, 'free_shipping'::text]))),\r\n CONSTRAINT shipping_zone_methods_min_order_amounts_include_source CHECK ((min_order_amounts ? upper(cost_currency))),\r\n CONSTRAINT shipping_zone_methods_min_order_amounts_valid CHECK (public.is_valid_currency_amount_map(min_order_amounts))\r\n);\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_methods.name_translations IS 'Localized shipping method labels keyed by language code. Example: {\"fr\": \"Livraison standard\"}.';\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_methods.currency_pricing_mode IS 'Whether this rate uses auto FX conversion from a single source currency or exact manual amounts per currency.';\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_methods.cost_amounts IS 'Shipping costs by ISO 4217 code in the smallest currency unit.';\r\n\r\nCOMMENT ON COLUMN public.shipping_zone_methods.min_order_amounts IS 'Minimum order thresholds by ISO 4217 code in the smallest currency unit.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.shipping_zones (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n name text NOT NULL,\r\n priority_order integer DEFAULT 0 NOT NULL,\r\n created_at timestamp with time zone DEFAULT now(),\r\n updated_at timestamp with time zone DEFAULT now()\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.site_settings (\r\n key text NOT NULL,\r\n value jsonb\r\n);\r\n\r\nCOMMENT ON TABLE public.site_settings IS 'Key-value store for global site settings. Sensitive keys (Cortex AI BYOK, Bot Protection Secret, Email secret, Payment secret) hold encrypted envelopes and are restricted to ADMIN via row-level policies.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.system_alerts (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n alert_type text NOT NULL,\r\n title text NOT NULL,\r\n message text NOT NULL,\r\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n is_resolved boolean DEFAULT false NOT NULL,\r\n resolved_at timestamp with time zone,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT system_alerts_alert_type_check CHECK ((alert_type = ANY (ARRAY['merge_conflict'::text, 'runtime_update_available'::text])))\r\n);\r\n\r\nCOMMENT ON TABLE public.system_alerts IS 'System notifications for the automated upstream-update architecture (merge conflicts, runtime updates available). Written by service-role; read by ADMINs.';\r\n\r\nCOMMENT ON COLUMN public.system_alerts.alert_type IS 'One of: merge_conflict, runtime_update_available.';\r\n\r\nCOMMENT ON COLUMN public.system_alerts.metadata IS 'Structured deep-link context for the dashboard banner CTA (repo/branch/action_url or latest_version/download_url).';\r\n\r\nCOMMENT ON COLUMN public.system_alerts.is_resolved IS 'When true the alert is hidden from the dashboard banner.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.system_configuration (\r\n id integer DEFAULT 1 NOT NULL,\r\n auto_accept_signups boolean DEFAULT false NOT NULL,\r\n settings jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT system_configuration_singleton CHECK ((id = 1))\r\n);\r\n\r\nCOMMENT ON TABLE public.system_configuration IS 'Singleton (id = 1) of global setup-wizard configuration. ADMIN-only via RLS; never store secrets in settings.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.tax_rates (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n country_code text NOT NULL,\r\n state_code text,\r\n tax_name text NOT NULL,\r\n tax_rate numeric(7,4) NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT tax_rates_tax_name_check CHECK ((char_length(btrim(tax_name)) > 0)),\r\n CONSTRAINT tax_rates_tax_rate_check CHECK (((tax_rate >= (0)::numeric) AND (tax_rate <= (100)::numeric)))\r\n);\r\n\r\nCOMMENT ON TABLE public.tax_rates IS 'Manual tax rates used for Stripe storefront orders. Multiple rows can exist per jurisdiction to support combined taxes such as GST + PST.';\r\n\r\nCOMMENT ON COLUMN public.tax_rates.country_code IS 'ISO 3166-1 alpha-2 country code.';\r\n\r\nCOMMENT ON COLUMN public.tax_rates.state_code IS 'Optional state/province code within country_code. NULL represents a country-wide or federal tax.';\r\n\r\nCOMMENT ON COLUMN public.tax_rates.tax_name IS 'Display name for the tax component, for example GST, PST, HST, or State Sales Tax.';\r\n\r\nCOMMENT ON COLUMN public.tax_rates.tax_rate IS 'Percent value, not decimal fraction. Example: 5.0000 means 5%.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.translations (\r\n key text NOT NULL,\r\n translations jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON COLUMN public.translations.key IS 'A unique, slugified identifier (e.g., \"sign_in_button_text\").';\r\n\r\nCOMMENT ON COLUMN public.translations.translations IS 'Stores translations as key-value pairs (e.g., {\"en\": \"Sign In\", \"fr\": \"S''inscrire\"}).';\r\n\r\nCREATE TABLE IF NOT EXISTS public.ucp_cart_sessions (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n status text DEFAULT 'active'::text NOT NULL,\r\n currency text DEFAULT 'USD'::text NOT NULL,\r\n locale text,\r\n buyer_identity jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n context jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n signals jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n attribution jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n line_items jsonb DEFAULT '[]'::jsonb NOT NULL,\r\n totals jsonb DEFAULT '[]'::jsonb NOT NULL,\r\n checkout_url text,\r\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n expires_at timestamp with time zone DEFAULT (now() + '7 days'::interval) NOT NULL,\r\n CONSTRAINT ucp_cart_sessions_attribution_object CHECK ((jsonb_typeof(attribution) = 'object'::text)),\r\n CONSTRAINT ucp_cart_sessions_buyer_identity_object CHECK ((jsonb_typeof(buyer_identity) = 'object'::text)),\r\n CONSTRAINT ucp_cart_sessions_context_object CHECK ((jsonb_typeof(context) = 'object'::text)),\r\n CONSTRAINT ucp_cart_sessions_line_items_array CHECK ((jsonb_typeof(line_items) = 'array'::text)),\r\n CONSTRAINT ucp_cart_sessions_metadata_object CHECK ((jsonb_typeof(metadata) = 'object'::text)),\r\n CONSTRAINT ucp_cart_sessions_signals_object CHECK ((jsonb_typeof(signals) = 'object'::text)),\r\n CONSTRAINT ucp_cart_sessions_status_check CHECK ((status = ANY (ARRAY['active'::text, 'cancelled'::text, 'completed'::text]))),\r\n CONSTRAINT ucp_cart_sessions_totals_array CHECK ((jsonb_typeof(totals) = 'array'::text))\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS public.user_addresses (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n user_id uuid NOT NULL,\r\n address_type text NOT NULL,\r\n is_default boolean DEFAULT false NOT NULL,\r\n recipient_name text,\r\n company_name text,\r\n line1 text,\r\n line2 text,\r\n city text,\r\n state text,\r\n postal_code text,\r\n country_code text,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT user_addresses_address_type_check CHECK ((address_type = ANY (ARRAY['billing'::text, 'shipping'::text])))\r\n);\r\n\r\nCOMMENT ON COLUMN public.user_addresses.company_name IS 'Optional company or organization name for the address.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.user_security_settings (\r\n user_id uuid NOT NULL,\r\n mfa_enabled boolean DEFAULT false NOT NULL,\r\n mfa_type text,\r\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\r\n CONSTRAINT user_security_settings_mfa_type_check CHECK ((mfa_type = ANY (ARRAY['totp'::text, 'email'::text])))\r\n);\r\n\r\nCOMMENT ON TABLE public.user_security_settings IS 'Per-user multi-factor configuration. mfa_type is NULL until a factor is enrolled.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.user_trusted_devices (\r\n id uuid DEFAULT gen_random_uuid() NOT NULL,\r\n user_id uuid NOT NULL,\r\n device_hash text NOT NULL,\r\n browser_metadata text,\r\n expires_at timestamp with time zone NOT NULL,\r\n created_at timestamp with time zone DEFAULT now() NOT NULL\r\n);\r\n\r\nCOMMENT ON TABLE public.user_trusted_devices IS 'SHA-256 hashes of trusted-device tokens. A 2FA bypass is only honoured when a non-expired row matches the cookie token, so deleting a row instantly revokes trust.';\r\n\r\nCOMMENT ON COLUMN public.user_trusted_devices.device_hash IS 'SHA-256 of the raw token held in the nb_trusted_device cookie. The raw token is never stored.';\r\n\r\nCREATE TABLE IF NOT EXISTS public.variant_attribute_mapping (\r\n variant_id uuid NOT NULL,\r\n attribute_term_id uuid NOT NULL\r\n);\r\n"
21
+ },
22
+ {
23
+ "version": "00000000000001",
24
+ "name": "00000000000001_baseline_constraints_and_indexes.sql",
25
+ "sql": "-- AUTO-GENERATED baseline (re-baseline of migrations 000..044). Idempotent; safe to replay.\r\n-- 01 · constraints (PK / unique / check / FK) + indexes\r\n-- Regenerate via tools/scripts/rebaseline-transform.mjs. Do not hand-edit.\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'blocks_pkey' AND conrelid = 'public.blocks'::regclass) THEN\r\n ALTER TABLE ONLY public.blocks\r\n ADD CONSTRAINT blocks_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'categories_pkey' AND conrelid = 'public.categories'::regclass) THEN\r\n ALTER TABLE ONLY public.categories\r\n ADD CONSTRAINT categories_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'categories_slug_key' AND conrelid = 'public.categories'::regclass) THEN\r\n ALTER TABLE ONLY public.categories\r\n ADD CONSTRAINT categories_slug_key UNIQUE (slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cms_interactions_pkey' AND conrelid = 'public.cms_interactions'::regclass) THEN\r\n ALTER TABLE ONLY public.cms_interactions\r\n ADD CONSTRAINT cms_interactions_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'content_drafts_parent_unique' AND conrelid = 'public.content_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.content_drafts\r\n ADD CONSTRAINT content_drafts_parent_unique UNIQUE (parent_type, parent_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'content_drafts_pkey' AND conrelid = 'public.content_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.content_drafts\r\n ADD CONSTRAINT content_drafts_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cortex_ai_db_mutation_audit_pkey' AND conrelid = 'public.cortex_ai_db_mutation_audit'::regclass) THEN\r\n ALTER TABLE ONLY public.cortex_ai_db_mutation_audit\r\n ADD CONSTRAINT cortex_ai_db_mutation_audit_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_freemius_mappings_pkey' AND conrelid = 'public.coupon_freemius_mappings'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_freemius_mappings\r\n ADD CONSTRAINT coupon_freemius_mappings_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_products_pkey' AND conrelid = 'public.coupon_products'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_products\r\n ADD CONSTRAINT coupon_products_pkey PRIMARY KEY (coupon_id, product_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_redemptions_pkey' AND conrelid = 'public.coupon_redemptions'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_redemptions\r\n ADD CONSTRAINT coupon_redemptions_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupons_pkey' AND conrelid = 'public.coupons'::regclass) THEN\r\n ALTER TABLE ONLY public.coupons\r\n ADD CONSTRAINT coupons_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'currencies_code_key' AND conrelid = 'public.currencies'::regclass) THEN\r\n ALTER TABLE ONLY public.currencies\r\n ADD CONSTRAINT currencies_code_key UNIQUE (code);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'currencies_pkey' AND conrelid = 'public.currencies'::regclass) THEN\r\n ALTER TABLE ONLY public.currencies\r\n ADD CONSTRAINT currencies_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'custom_block_definitions_pkey' AND conrelid = 'public.custom_block_definitions'::regclass) THEN\r\n ALTER TABLE ONLY public.custom_block_definitions\r\n ADD CONSTRAINT custom_block_definitions_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'custom_block_definitions_slug_key' AND conrelid = 'public.custom_block_definitions'::regclass) THEN\r\n ALTER TABLE ONLY public.custom_block_definitions\r\n ADD CONSTRAINT custom_block_definitions_slug_key UNIQUE (slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'email_2fa_challenges_pkey' AND conrelid = 'public.email_2fa_challenges'::regclass) THEN\r\n ALTER TABLE ONLY public.email_2fa_challenges\r\n ADD CONSTRAINT email_2fa_challenges_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'freemius_plans_pkey' AND conrelid = 'public.freemius_plans'::regclass) THEN\r\n ALTER TABLE ONLY public.freemius_plans\r\n ADD CONSTRAINT freemius_plans_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'freemius_pricing_pkey' AND conrelid = 'public.freemius_pricing'::regclass) THEN\r\n ALTER TABLE ONLY public.freemius_pricing\r\n ADD CONSTRAINT freemius_pricing_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'inventory_items_pkey' AND conrelid = 'public.inventory_items'::regclass) THEN\r\n ALTER TABLE ONLY public.inventory_items\r\n ADD CONSTRAINT inventory_items_pkey PRIMARY KEY (sku);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'languages_code_key' AND conrelid = 'public.languages'::regclass) THEN\r\n ALTER TABLE ONLY public.languages\r\n ADD CONSTRAINT languages_code_key UNIQUE (code);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'languages_pkey' AND conrelid = 'public.languages'::regclass) THEN\r\n ALTER TABLE ONLY public.languages\r\n ADD CONSTRAINT languages_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'logos_pkey' AND conrelid = 'public.logos'::regclass) THEN\r\n ALTER TABLE ONLY public.logos\r\n ADD CONSTRAINT logos_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'media_object_key_key' AND conrelid = 'public.media'::regclass) THEN\r\n ALTER TABLE ONLY public.media\r\n ADD CONSTRAINT media_object_key_key UNIQUE (object_key);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'media_pkey' AND conrelid = 'public.media'::regclass) THEN\r\n ALTER TABLE ONLY public.media\r\n ADD CONSTRAINT media_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'navigation_items_pkey' AND conrelid = 'public.navigation_items'::regclass) THEN\r\n ALTER TABLE ONLY public.navigation_items\r\n ADD CONSTRAINT navigation_items_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'order_items_pkey' AND conrelid = 'public.order_items'::regclass) THEN\r\n ALTER TABLE ONLY public.order_items\r\n ADD CONSTRAINT order_items_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'orders_pkey' AND conrelid = 'public.orders'::regclass) THEN\r\n ALTER TABLE ONLY public.orders\r\n ADD CONSTRAINT orders_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'orders_stripe_session_id_key' AND conrelid = 'public.orders'::regclass) THEN\r\n ALTER TABLE ONLY public.orders\r\n ADD CONSTRAINT orders_stripe_session_id_key UNIQUE (stripe_session_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'package_activations_license_key_package_id_key' AND conrelid = 'public.package_activations'::regclass) THEN\r\n ALTER TABLE ONLY public.package_activations\r\n ADD CONSTRAINT package_activations_license_key_package_id_key UNIQUE (license_key, package_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'package_activations_pkey' AND conrelid = 'public.package_activations'::regclass) THEN\r\n ALTER TABLE ONLY public.package_activations\r\n ADD CONSTRAINT package_activations_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'page_revisions_page_version_key' AND conrelid = 'public.page_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.page_revisions\r\n ADD CONSTRAINT page_revisions_page_version_key UNIQUE (page_id, version);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'page_revisions_pkey' AND conrelid = 'public.page_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.page_revisions\r\n ADD CONSTRAINT page_revisions_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pages_language_id_slug_key' AND conrelid = 'public.pages'::regclass) THEN\r\n ALTER TABLE ONLY public.pages\r\n ADD CONSTRAINT pages_language_id_slug_key UNIQUE (language_id, slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pages_pkey' AND conrelid = 'public.pages'::regclass) THEN\r\n ALTER TABLE ONLY public.pages\r\n ADD CONSTRAINT pages_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'post_revisions_pkey' AND conrelid = 'public.post_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.post_revisions\r\n ADD CONSTRAINT post_revisions_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'post_revisions_post_version_key' AND conrelid = 'public.post_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.post_revisions\r\n ADD CONSTRAINT post_revisions_post_version_key UNIQUE (post_id, version);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_language_id_slug_key' AND conrelid = 'public.posts'::regclass) THEN\r\n ALTER TABLE ONLY public.posts\r\n ADD CONSTRAINT posts_language_id_slug_key UNIQUE (language_id, slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_pkey' AND conrelid = 'public.posts'::regclass) THEN\r\n ALTER TABLE ONLY public.posts\r\n ADD CONSTRAINT posts_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'privacy_consent_logs_consent_token_key' AND conrelid = 'public.privacy_consent_logs'::regclass) THEN\r\n ALTER TABLE ONLY public.privacy_consent_logs\r\n ADD CONSTRAINT privacy_consent_logs_consent_token_key UNIQUE (consent_token);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'privacy_consent_logs_pkey' AND conrelid = 'public.privacy_consent_logs'::regclass) THEN\r\n ALTER TABLE ONLY public.privacy_consent_logs\r\n ADD CONSTRAINT privacy_consent_logs_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_attribute_terms_attribute_id_slug_key' AND conrelid = 'public.product_attribute_terms'::regclass) THEN\r\n ALTER TABLE ONLY public.product_attribute_terms\r\n ADD CONSTRAINT product_attribute_terms_attribute_id_slug_key UNIQUE (attribute_id, slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_attribute_terms_pkey' AND conrelid = 'public.product_attribute_terms'::regclass) THEN\r\n ALTER TABLE ONLY public.product_attribute_terms\r\n ADD CONSTRAINT product_attribute_terms_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_attributes_pkey' AND conrelid = 'public.product_attributes'::regclass) THEN\r\n ALTER TABLE ONLY public.product_attributes\r\n ADD CONSTRAINT product_attributes_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_attributes_slug_key' AND conrelid = 'public.product_attributes'::regclass) THEN\r\n ALTER TABLE ONLY public.product_attributes\r\n ADD CONSTRAINT product_attributes_slug_key UNIQUE (slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_categories_pkey' AND conrelid = 'public.product_categories'::regclass) THEN\r\n ALTER TABLE ONLY public.product_categories\r\n ADD CONSTRAINT product_categories_pkey PRIMARY KEY (product_id, category_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_drafts_pkey' AND conrelid = 'public.product_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.product_drafts\r\n ADD CONSTRAINT product_drafts_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_drafts_product_unique' AND conrelid = 'public.product_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.product_drafts\r\n ADD CONSTRAINT product_drafts_product_unique UNIQUE (product_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_freemius_sale_coupons_pkey' AND conrelid = 'public.product_freemius_sale_coupons'::regclass) THEN\r\n ALTER TABLE ONLY public.product_freemius_sale_coupons\r\n ADD CONSTRAINT product_freemius_sale_coupons_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_freemius_sale_coupons_product_unique' AND conrelid = 'public.product_freemius_sale_coupons'::regclass) THEN\r\n ALTER TABLE ONLY public.product_freemius_sale_coupons\r\n ADD CONSTRAINT product_freemius_sale_coupons_product_unique UNIQUE (product_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_media_pkey' AND conrelid = 'public.product_media'::regclass) THEN\r\n ALTER TABLE ONLY public.product_media\r\n ADD CONSTRAINT product_media_pkey PRIMARY KEY (product_id, media_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_variants_pkey' AND conrelid = 'public.product_variants'::regclass) THEN\r\n ALTER TABLE ONLY public.product_variants\r\n ADD CONSTRAINT product_variants_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_variants_product_id_sku_key' AND conrelid = 'public.product_variants'::regclass) THEN\r\n ALTER TABLE ONLY public.product_variants\r\n ADD CONSTRAINT product_variants_product_id_sku_key UNIQUE (product_id, sku);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'products_language_id_sku_key' AND conrelid = 'public.products'::regclass) THEN\r\n ALTER TABLE ONLY public.products\r\n ADD CONSTRAINT products_language_id_sku_key UNIQUE (language_id, sku);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'products_language_id_slug_key' AND conrelid = 'public.products'::regclass) THEN\r\n ALTER TABLE ONLY public.products\r\n ADD CONSTRAINT products_language_id_slug_key UNIQUE (language_id, slug);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'products_pkey' AND conrelid = 'public.products'::regclass) THEN\r\n ALTER TABLE ONLY public.products\r\n ADD CONSTRAINT products_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'profiles_pkey' AND conrelid = 'public.profiles'::regclass) THEN\r\n ALTER TABLE ONLY public.profiles\r\n ADD CONSTRAINT profiles_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'shipping_zone_locations_pkey' AND conrelid = 'public.shipping_zone_locations'::regclass) THEN\r\n ALTER TABLE ONLY public.shipping_zone_locations\r\n ADD CONSTRAINT shipping_zone_locations_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'shipping_zone_methods_pkey' AND conrelid = 'public.shipping_zone_methods'::regclass) THEN\r\n ALTER TABLE ONLY public.shipping_zone_methods\r\n ADD CONSTRAINT shipping_zone_methods_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'shipping_zones_pkey' AND conrelid = 'public.shipping_zones'::regclass) THEN\r\n ALTER TABLE ONLY public.shipping_zones\r\n ADD CONSTRAINT shipping_zones_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'site_settings_pkey' AND conrelid = 'public.site_settings'::regclass) THEN\r\n ALTER TABLE ONLY public.site_settings\r\n ADD CONSTRAINT site_settings_pkey PRIMARY KEY (key);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'system_alerts_pkey' AND conrelid = 'public.system_alerts'::regclass) THEN\r\n ALTER TABLE ONLY public.system_alerts\r\n ADD CONSTRAINT system_alerts_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'system_configuration_pkey' AND conrelid = 'public.system_configuration'::regclass) THEN\r\n ALTER TABLE ONLY public.system_configuration\r\n ADD CONSTRAINT system_configuration_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'tax_rates_pkey' AND conrelid = 'public.tax_rates'::regclass) THEN\r\n ALTER TABLE ONLY public.tax_rates\r\n ADD CONSTRAINT tax_rates_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'translations_pkey' AND conrelid = 'public.translations'::regclass) THEN\r\n ALTER TABLE ONLY public.translations\r\n ADD CONSTRAINT translations_pkey PRIMARY KEY (key);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'ucp_cart_sessions_pkey' AND conrelid = 'public.ucp_cart_sessions'::regclass) THEN\r\n ALTER TABLE ONLY public.ucp_cart_sessions\r\n ADD CONSTRAINT ucp_cart_sessions_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_addresses_pkey' AND conrelid = 'public.user_addresses'::regclass) THEN\r\n ALTER TABLE ONLY public.user_addresses\r\n ADD CONSTRAINT user_addresses_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_security_settings_pkey' AND conrelid = 'public.user_security_settings'::regclass) THEN\r\n ALTER TABLE ONLY public.user_security_settings\r\n ADD CONSTRAINT user_security_settings_pkey PRIMARY KEY (user_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_trusted_devices_device_hash_key' AND conrelid = 'public.user_trusted_devices'::regclass) THEN\r\n ALTER TABLE ONLY public.user_trusted_devices\r\n ADD CONSTRAINT user_trusted_devices_device_hash_key UNIQUE (device_hash);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_trusted_devices_pkey' AND conrelid = 'public.user_trusted_devices'::regclass) THEN\r\n ALTER TABLE ONLY public.user_trusted_devices\r\n ADD CONSTRAINT user_trusted_devices_pkey PRIMARY KEY (id);\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'variant_attribute_mapping_pkey' AND conrelid = 'public.variant_attribute_mapping'::regclass) THEN\r\n ALTER TABLE ONLY public.variant_attribute_mapping\r\n ADD CONSTRAINT variant_attribute_mapping_pkey PRIMARY KEY (variant_id, attribute_term_id);\r\n END IF;\r\nEND $rb$;\r\n\r\nCREATE INDEX IF NOT EXISTS content_drafts_author_id_idx ON public.content_drafts USING btree (author_id);\r\n\r\nCREATE INDEX IF NOT EXISTS content_drafts_parent_idx ON public.content_drafts USING btree (parent_type, parent_id);\r\n\r\nCREATE INDEX IF NOT EXISTS content_drafts_updated_at_idx ON public.content_drafts USING btree (updated_at DESC);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS coupon_freemius_mappings_coupon_product_unique ON public.coupon_freemius_mappings USING btree (coupon_id, freemius_product_id);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS coupon_redemptions_order_unique ON public.coupon_redemptions USING btree (order_id) WHERE (order_id IS NOT NULL);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS coupons_code_unique ON public.coupons USING btree (upper(code));\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS ensure_single_default_language_idx ON public.languages USING btree (is_default) WHERE (is_default = true);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_blocks_language_id ON public.blocks USING btree (language_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_blocks_page_id ON public.blocks USING btree (page_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_blocks_post_id ON public.blocks USING btree (post_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_coupon_freemius_mappings_freemius_product_id ON public.coupon_freemius_mappings USING btree (freemius_product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_coupon_freemius_mappings_product_id ON public.coupon_freemius_mappings USING btree (product_id) WHERE (product_id IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_coupon_products_product_id ON public.coupon_products USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_coupon_redemptions_coupon_id ON public.coupon_redemptions USING btree (coupon_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_coupon_redemptions_user_id ON public.coupon_redemptions USING btree (user_id) WHERE (user_id IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_coupons_active_dates ON public.coupons USING btree (is_active, starts_at, ends_at);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS idx_currencies_single_default ON public.currencies USING btree (is_default) WHERE (is_default = true);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_custom_block_definitions_is_original ON public.custom_block_definitions USING btree (is_original);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_email_2fa_challenges_expires_at ON public.email_2fa_challenges USING btree (expires_at);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_email_2fa_challenges_user_id ON public.email_2fa_challenges USING btree (user_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_freemius_plans_product_id ON public.freemius_plans USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_freemius_pricing_plan_id ON public.freemius_pricing USING btree (plan_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_inventory_items_updated_at ON public.inventory_items USING btree (updated_at DESC);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_logos_media_id ON public.logos USING btree (media_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_media_uploader_id ON public.media USING btree (uploader_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_navigation_items_language_id ON public.navigation_items USING btree (language_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_navigation_items_menu_lang_order ON public.navigation_items USING btree (menu_key, language_id, \"order\");\r\n\r\nCREATE INDEX IF NOT EXISTS idx_navigation_items_page_id ON public.navigation_items USING btree (page_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_navigation_items_parent_id ON public.navigation_items USING btree (parent_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_order_items_order_id ON public.order_items USING btree (order_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_order_items_product_id ON public.order_items USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_order_items_variant_id ON public.order_items USING btree (variant_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_orders_coupon_id ON public.orders USING btree (coupon_id) WHERE (coupon_id IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_orders_freemius_license_id ON public.orders USING btree (freemius_license_id) WHERE (freemius_license_id IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_orders_freemius_subscription_id ON public.orders USING btree (freemius_subscription_id) WHERE (freemius_subscription_id IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_orders_freemius_trial_id ON public.orders USING btree (freemius_trial_id) WHERE (freemius_trial_id IS NOT NULL);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS idx_orders_invoice_number_unique ON public.orders USING btree (invoice_number) WHERE (invoice_number IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_orders_user_id ON public.orders USING btree (user_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_package_activations_license_key ON public.package_activations USING btree (license_key);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_package_activations_package_id ON public.package_activations USING btree (package_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_page_revisions_author_id ON public.page_revisions USING btree (author_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_page_revisions_page_id_version ON public.page_revisions USING btree (page_id, version);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_pages_author_id ON public.pages USING btree (author_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_pages_feature_image_id ON public.pages USING btree (feature_image_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_pages_translation_group_id ON public.pages USING btree (translation_group_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_post_revisions_author_id ON public.post_revisions USING btree (author_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_post_revisions_post_id_version ON public.post_revisions USING btree (post_id, version);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_posts_author_id ON public.posts USING btree (author_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_posts_feature_image_id ON public.posts USING btree (feature_image_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_posts_translation_group_id ON public.posts USING btree (translation_group_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_privacy_consent_logs_created_at ON public.privacy_consent_logs USING btree (created_at DESC);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_attribute_terms_attribute_id ON public.product_attribute_terms USING btree (attribute_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_categories_category_id ON public.product_categories USING btree (category_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_categories_product_id ON public.product_categories USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_freemius_sale_coupons_freemius_product_id ON public.product_freemius_sale_coupons USING btree (freemius_product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_media_media_id ON public.product_media USING btree (media_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_media_product_id ON public.product_media USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_variants_main_media_id ON public.product_variants USING btree (main_media_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_variants_prices_gin ON public.product_variants USING gin (prices jsonb_path_ops);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_product_variants_product_id ON public.product_variants USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_products_prices_gin ON public.products USING gin (prices jsonb_path_ops);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_products_slug ON public.products USING btree (slug);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_products_translation_group_id ON public.products USING btree (translation_group_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_shipping_zone_locations_country_state_postal ON public.shipping_zone_locations USING btree (country_code, state_code, postal_code);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_shipping_zone_locations_zone_id ON public.shipping_zone_locations USING btree (zone_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_shipping_zone_methods_name_translations ON public.shipping_zone_methods USING gin (name_translations);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_shipping_zone_methods_zone_id ON public.shipping_zone_methods USING btree (zone_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_system_alerts_unresolved ON public.system_alerts USING btree (is_resolved, created_at DESC);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_tax_rates_country_state ON public.tax_rates USING btree (country_code, state_code);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_ucp_cart_sessions_created_at ON public.ucp_cart_sessions USING btree (created_at DESC);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_ucp_cart_sessions_status_expires_at ON public.ucp_cart_sessions USING btree (status, expires_at);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS idx_user_addresses_one_default_per_type ON public.user_addresses USING btree (user_id, address_type) WHERE (is_default = true);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_user_addresses_type ON public.user_addresses USING btree (address_type);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_user_addresses_user_id ON public.user_addresses USING btree (user_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_user_trusted_devices_expires_at ON public.user_trusted_devices USING btree (expires_at);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_user_trusted_devices_user_id ON public.user_trusted_devices USING btree (user_id);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_variant_attribute_mapping_attribute_term_id ON public.variant_attribute_mapping USING btree (attribute_term_id);\r\n\r\nCREATE INDEX IF NOT EXISTS media_folder_idx ON public.media USING btree (folder);\r\n\r\nCREATE INDEX IF NOT EXISTS product_drafts_author_id_idx ON public.product_drafts USING btree (author_id);\r\n\r\nCREATE INDEX IF NOT EXISTS product_drafts_product_id_idx ON public.product_drafts USING btree (product_id);\r\n\r\nCREATE INDEX IF NOT EXISTS product_drafts_updated_at_idx ON public.product_drafts USING btree (updated_at DESC);\r\n\r\nCREATE INDEX IF NOT EXISTS product_variants_sale_window_idx ON public.product_variants USING btree (sale_start_at, sale_end_at) WHERE ((sale_start_at IS NOT NULL) OR (sale_end_at IS NOT NULL));\r\n\r\nCREATE INDEX IF NOT EXISTS product_variants_scheduled_price_idx ON public.product_variants USING btree (scheduled_price_at) WHERE (scheduled_price_at IS NOT NULL);\r\n\r\nCREATE INDEX IF NOT EXISTS products_sale_window_idx ON public.products USING btree (sale_start_at, sale_end_at) WHERE ((sale_start_at IS NOT NULL) OR (sale_end_at IS NOT NULL));\r\n\r\nCREATE INDEX IF NOT EXISTS products_scheduled_price_idx ON public.products USING btree (scheduled_price_at) WHERE (scheduled_price_at IS NOT NULL);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS tax_rates_country_state_name_key ON public.tax_rates USING btree (country_code, COALESCE(state_code, ''::text), lower(tax_name));\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'blocks_language_id_fkey' AND conrelid = 'public.blocks'::regclass) THEN\r\n ALTER TABLE ONLY public.blocks\r\n ADD CONSTRAINT blocks_language_id_fkey FOREIGN KEY (language_id) REFERENCES public.languages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'blocks_page_id_fkey' AND conrelid = 'public.blocks'::regclass) THEN\r\n ALTER TABLE ONLY public.blocks\r\n ADD CONSTRAINT blocks_page_id_fkey FOREIGN KEY (page_id) REFERENCES public.pages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'blocks_post_id_fkey' AND conrelid = 'public.blocks'::regclass) THEN\r\n ALTER TABLE ONLY public.blocks\r\n ADD CONSTRAINT blocks_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'blocks_product_id_fkey' AND conrelid = 'public.blocks'::regclass) THEN\r\n ALTER TABLE ONLY public.blocks\r\n ADD CONSTRAINT blocks_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cms_interactions_post_id_fkey' AND conrelid = 'public.cms_interactions'::regclass) THEN\r\n ALTER TABLE ONLY public.cms_interactions\r\n ADD CONSTRAINT cms_interactions_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cms_interactions_product_id_fkey' AND conrelid = 'public.cms_interactions'::regclass) THEN\r\n ALTER TABLE ONLY public.cms_interactions\r\n ADD CONSTRAINT cms_interactions_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cms_interactions_user_id_profiles_fkey' AND conrelid = 'public.cms_interactions'::regclass) THEN\r\n ALTER TABLE ONLY public.cms_interactions\r\n ADD CONSTRAINT cms_interactions_user_id_profiles_fkey FOREIGN KEY (user_id) REFERENCES public.profiles(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'content_drafts_author_id_fkey' AND conrelid = 'public.content_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.content_drafts\r\n ADD CONSTRAINT content_drafts_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cortex_ai_db_mutation_audit_actor_user_id_fkey' AND conrelid = 'public.cortex_ai_db_mutation_audit'::regclass) THEN\r\n ALTER TABLE ONLY public.cortex_ai_db_mutation_audit\r\n ADD CONSTRAINT cortex_ai_db_mutation_audit_actor_user_id_fkey FOREIGN KEY (actor_user_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_freemius_mappings_coupon_id_fkey' AND conrelid = 'public.coupon_freemius_mappings'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_freemius_mappings\r\n ADD CONSTRAINT coupon_freemius_mappings_coupon_id_fkey FOREIGN KEY (coupon_id) REFERENCES public.coupons(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_freemius_mappings_product_id_fkey' AND conrelid = 'public.coupon_freemius_mappings'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_freemius_mappings\r\n ADD CONSTRAINT coupon_freemius_mappings_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_products_coupon_id_fkey' AND conrelid = 'public.coupon_products'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_products\r\n ADD CONSTRAINT coupon_products_coupon_id_fkey FOREIGN KEY (coupon_id) REFERENCES public.coupons(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_products_product_id_fkey' AND conrelid = 'public.coupon_products'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_products\r\n ADD CONSTRAINT coupon_products_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_redemptions_coupon_id_fkey' AND conrelid = 'public.coupon_redemptions'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_redemptions\r\n ADD CONSTRAINT coupon_redemptions_coupon_id_fkey FOREIGN KEY (coupon_id) REFERENCES public.coupons(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_redemptions_order_id_fkey' AND conrelid = 'public.coupon_redemptions'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_redemptions\r\n ADD CONSTRAINT coupon_redemptions_order_id_fkey FOREIGN KEY (order_id) REFERENCES public.orders(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'coupon_redemptions_user_id_fkey' AND conrelid = 'public.coupon_redemptions'::regclass) THEN\r\n ALTER TABLE ONLY public.coupon_redemptions\r\n ADD CONSTRAINT coupon_redemptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'email_2fa_challenges_user_id_fkey' AND conrelid = 'public.email_2fa_challenges'::regclass) THEN\r\n ALTER TABLE ONLY public.email_2fa_challenges\r\n ADD CONSTRAINT email_2fa_challenges_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'freemius_plans_product_id_fkey' AND conrelid = 'public.freemius_plans'::regclass) THEN\r\n ALTER TABLE ONLY public.freemius_plans\r\n ADD CONSTRAINT freemius_plans_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'freemius_pricing_plan_id_fkey' AND conrelid = 'public.freemius_pricing'::regclass) THEN\r\n ALTER TABLE ONLY public.freemius_pricing\r\n ADD CONSTRAINT freemius_pricing_plan_id_fkey FOREIGN KEY (plan_id) REFERENCES public.freemius_plans(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'logos_media_id_fkey' AND conrelid = 'public.logos'::regclass) THEN\r\n ALTER TABLE ONLY public.logos\r\n ADD CONSTRAINT logos_media_id_fkey FOREIGN KEY (media_id) REFERENCES public.media(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'media_uploader_id_fkey' AND conrelid = 'public.media'::regclass) THEN\r\n ALTER TABLE ONLY public.media\r\n ADD CONSTRAINT media_uploader_id_fkey FOREIGN KEY (uploader_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'navigation_items_language_id_fkey' AND conrelid = 'public.navigation_items'::regclass) THEN\r\n ALTER TABLE ONLY public.navigation_items\r\n ADD CONSTRAINT navigation_items_language_id_fkey FOREIGN KEY (language_id) REFERENCES public.languages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'navigation_items_page_id_fkey' AND conrelid = 'public.navigation_items'::regclass) THEN\r\n ALTER TABLE ONLY public.navigation_items\r\n ADD CONSTRAINT navigation_items_page_id_fkey FOREIGN KEY (page_id) REFERENCES public.pages(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'navigation_items_parent_id_fkey' AND conrelid = 'public.navigation_items'::regclass) THEN\r\n ALTER TABLE ONLY public.navigation_items\r\n ADD CONSTRAINT navigation_items_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES public.navigation_items(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'order_items_order_id_fkey' AND conrelid = 'public.order_items'::regclass) THEN\r\n ALTER TABLE ONLY public.order_items\r\n ADD CONSTRAINT order_items_order_id_fkey FOREIGN KEY (order_id) REFERENCES public.orders(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'order_items_product_id_fkey' AND conrelid = 'public.order_items'::regclass) THEN\r\n ALTER TABLE ONLY public.order_items\r\n ADD CONSTRAINT order_items_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'order_items_variant_id_fkey' AND conrelid = 'public.order_items'::regclass) THEN\r\n ALTER TABLE ONLY public.order_items\r\n ADD CONSTRAINT order_items_variant_id_fkey FOREIGN KEY (variant_id) REFERENCES public.product_variants(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'orders_coupon_id_fkey' AND conrelid = 'public.orders'::regclass) THEN\r\n ALTER TABLE ONLY public.orders\r\n ADD CONSTRAINT orders_coupon_id_fkey FOREIGN KEY (coupon_id) REFERENCES public.coupons(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'orders_user_id_fkey' AND conrelid = 'public.orders'::regclass) THEN\r\n ALTER TABLE ONLY public.orders\r\n ADD CONSTRAINT orders_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'page_revisions_author_id_fkey' AND conrelid = 'public.page_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.page_revisions\r\n ADD CONSTRAINT page_revisions_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'page_revisions_page_id_fkey' AND conrelid = 'public.page_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.page_revisions\r\n ADD CONSTRAINT page_revisions_page_id_fkey FOREIGN KEY (page_id) REFERENCES public.pages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pages_author_id_fkey' AND conrelid = 'public.pages'::regclass) THEN\r\n ALTER TABLE ONLY public.pages\r\n ADD CONSTRAINT pages_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pages_feature_image_id_fkey' AND conrelid = 'public.pages'::regclass) THEN\r\n ALTER TABLE ONLY public.pages\r\n ADD CONSTRAINT pages_feature_image_id_fkey FOREIGN KEY (feature_image_id) REFERENCES public.media(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pages_language_id_fkey' AND conrelid = 'public.pages'::regclass) THEN\r\n ALTER TABLE ONLY public.pages\r\n ADD CONSTRAINT pages_language_id_fkey FOREIGN KEY (language_id) REFERENCES public.languages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'post_revisions_author_id_fkey' AND conrelid = 'public.post_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.post_revisions\r\n ADD CONSTRAINT post_revisions_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'post_revisions_post_id_fkey' AND conrelid = 'public.post_revisions'::regclass) THEN\r\n ALTER TABLE ONLY public.post_revisions\r\n ADD CONSTRAINT post_revisions_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_author_id_fkey' AND conrelid = 'public.posts'::regclass) THEN\r\n ALTER TABLE ONLY public.posts\r\n ADD CONSTRAINT posts_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_feature_image_id_fkey' AND conrelid = 'public.posts'::regclass) THEN\r\n ALTER TABLE ONLY public.posts\r\n ADD CONSTRAINT posts_feature_image_id_fkey FOREIGN KEY (feature_image_id) REFERENCES public.media(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_language_id_fkey' AND conrelid = 'public.posts'::regclass) THEN\r\n ALTER TABLE ONLY public.posts\r\n ADD CONSTRAINT posts_language_id_fkey FOREIGN KEY (language_id) REFERENCES public.languages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_attribute_terms_attribute_id_fkey' AND conrelid = 'public.product_attribute_terms'::regclass) THEN\r\n ALTER TABLE ONLY public.product_attribute_terms\r\n ADD CONSTRAINT product_attribute_terms_attribute_id_fkey FOREIGN KEY (attribute_id) REFERENCES public.product_attributes(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_categories_category_id_fkey' AND conrelid = 'public.product_categories'::regclass) THEN\r\n ALTER TABLE ONLY public.product_categories\r\n ADD CONSTRAINT product_categories_category_id_fkey FOREIGN KEY (category_id) REFERENCES public.categories(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_categories_product_id_fkey' AND conrelid = 'public.product_categories'::regclass) THEN\r\n ALTER TABLE ONLY public.product_categories\r\n ADD CONSTRAINT product_categories_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_drafts_author_id_fkey' AND conrelid = 'public.product_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.product_drafts\r\n ADD CONSTRAINT product_drafts_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_drafts_product_id_fkey' AND conrelid = 'public.product_drafts'::regclass) THEN\r\n ALTER TABLE ONLY public.product_drafts\r\n ADD CONSTRAINT product_drafts_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_freemius_sale_coupons_product_id_fkey' AND conrelid = 'public.product_freemius_sale_coupons'::regclass) THEN\r\n ALTER TABLE ONLY public.product_freemius_sale_coupons\r\n ADD CONSTRAINT product_freemius_sale_coupons_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_media_media_id_fkey' AND conrelid = 'public.product_media'::regclass) THEN\r\n ALTER TABLE ONLY public.product_media\r\n ADD CONSTRAINT product_media_media_id_fkey FOREIGN KEY (media_id) REFERENCES public.media(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_media_product_id_fkey' AND conrelid = 'public.product_media'::regclass) THEN\r\n ALTER TABLE ONLY public.product_media\r\n ADD CONSTRAINT product_media_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_variants_main_media_id_fkey' AND conrelid = 'public.product_variants'::regclass) THEN\r\n ALTER TABLE ONLY public.product_variants\r\n ADD CONSTRAINT product_variants_main_media_id_fkey FOREIGN KEY (main_media_id) REFERENCES public.media(id) ON DELETE SET NULL;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_variants_product_id_fkey' AND conrelid = 'public.product_variants'::regclass) THEN\r\n ALTER TABLE ONLY public.product_variants\r\n ADD CONSTRAINT product_variants_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'products_language_id_fkey' AND conrelid = 'public.products'::regclass) THEN\r\n ALTER TABLE ONLY public.products\r\n ADD CONSTRAINT products_language_id_fkey FOREIGN KEY (language_id) REFERENCES public.languages(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'profiles_id_fkey' AND conrelid = 'public.profiles'::regclass) THEN\r\n ALTER TABLE ONLY public.profiles\r\n ADD CONSTRAINT profiles_id_fkey FOREIGN KEY (id) REFERENCES auth.users(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'shipping_zone_locations_zone_id_fkey' AND conrelid = 'public.shipping_zone_locations'::regclass) THEN\r\n ALTER TABLE ONLY public.shipping_zone_locations\r\n ADD CONSTRAINT shipping_zone_locations_zone_id_fkey FOREIGN KEY (zone_id) REFERENCES public.shipping_zones(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'shipping_zone_methods_zone_id_fkey' AND conrelid = 'public.shipping_zone_methods'::regclass) THEN\r\n ALTER TABLE ONLY public.shipping_zone_methods\r\n ADD CONSTRAINT shipping_zone_methods_zone_id_fkey FOREIGN KEY (zone_id) REFERENCES public.shipping_zones(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_addresses_user_id_fkey' AND conrelid = 'public.user_addresses'::regclass) THEN\r\n ALTER TABLE ONLY public.user_addresses\r\n ADD CONSTRAINT user_addresses_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.profiles(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_security_settings_user_id_fkey' AND conrelid = 'public.user_security_settings'::regclass) THEN\r\n ALTER TABLE ONLY public.user_security_settings\r\n ADD CONSTRAINT user_security_settings_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_trusted_devices_user_id_fkey' AND conrelid = 'public.user_trusted_devices'::regclass) THEN\r\n ALTER TABLE ONLY public.user_trusted_devices\r\n ADD CONSTRAINT user_trusted_devices_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'variant_attribute_mapping_attribute_term_id_fkey' AND conrelid = 'public.variant_attribute_mapping'::regclass) THEN\r\n ALTER TABLE ONLY public.variant_attribute_mapping\r\n ADD CONSTRAINT variant_attribute_mapping_attribute_term_id_fkey FOREIGN KEY (attribute_term_id) REFERENCES public.product_attribute_terms(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n\r\nDO $rb$ BEGIN\r\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'variant_attribute_mapping_variant_id_fkey' AND conrelid = 'public.variant_attribute_mapping'::regclass) THEN\r\n ALTER TABLE ONLY public.variant_attribute_mapping\r\n ADD CONSTRAINT variant_attribute_mapping_variant_id_fkey FOREIGN KEY (variant_id) REFERENCES public.product_variants(id) ON DELETE CASCADE;\r\n END IF;\r\nEND $rb$;\r\n"
26
+ },
27
+ {
28
+ "version": "00000000000002",
29
+ "name": "00000000000002_baseline_security_and_grants.sql",
30
+ "sql": "-- AUTO-GENERATED baseline (re-baseline of migrations 000..044). Idempotent; safe to replay.\r\n-- 02 · row-level security, policies, triggers, grants\r\n-- Regenerate via tools/scripts/rebaseline-transform.mjs. Do not hand-edit.\r\n\r\nDROP TRIGGER IF EXISTS on_blocks_update ON public.blocks;\r\nCREATE TRIGGER on_blocks_update BEFORE UPDATE ON public.blocks FOR EACH ROW EXECUTE FUNCTION public.handle_blocks_update();\r\n\r\nDROP TRIGGER IF EXISTS on_content_drafts_update ON public.content_drafts;\r\nCREATE TRIGGER on_content_drafts_update BEFORE UPDATE ON public.content_drafts FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\r\n\r\nDROP TRIGGER IF EXISTS on_coupon_freemius_mappings_write ON public.coupon_freemius_mappings;\r\nCREATE TRIGGER on_coupon_freemius_mappings_write BEFORE INSERT OR UPDATE ON public.coupon_freemius_mappings FOR EACH ROW EXECUTE FUNCTION public.handle_coupon_freemius_mappings_write();\r\n\r\nDROP TRIGGER IF EXISTS on_coupons_write ON public.coupons;\r\nCREATE TRIGGER on_coupons_write BEFORE INSERT OR UPDATE ON public.coupons FOR EACH ROW EXECUTE FUNCTION public.handle_coupons_write();\r\n\r\nDROP TRIGGER IF EXISTS on_inventory_item_change ON public.inventory_items;\r\nCREATE TRIGGER on_inventory_item_change AFTER INSERT OR DELETE OR UPDATE OF quantity ON public.inventory_items FOR EACH ROW EXECUTE FUNCTION public.handle_inventory_item_change();\r\n\r\nDROP TRIGGER IF EXISTS on_inventory_items_update ON public.inventory_items;\r\nCREATE TRIGGER on_inventory_items_update BEFORE UPDATE ON public.inventory_items FOR EACH ROW EXECUTE FUNCTION public.handle_inventory_items_update();\r\n\r\nDROP TRIGGER IF EXISTS on_languages_update ON public.languages;\r\nCREATE TRIGGER on_languages_update BEFORE UPDATE ON public.languages FOR EACH ROW EXECUTE FUNCTION public.handle_languages_update();\r\n\r\nDROP TRIGGER IF EXISTS on_media_update ON public.media;\r\nCREATE TRIGGER on_media_update BEFORE UPDATE ON public.media FOR EACH ROW EXECUTE FUNCTION public.handle_media_update();\r\n\r\nDROP TRIGGER IF EXISTS on_navigation_items_update ON public.navigation_items;\r\nCREATE TRIGGER on_navigation_items_update BEFORE UPDATE ON public.navigation_items FOR EACH ROW EXECUTE FUNCTION public.handle_navigation_items_update();\r\n\r\nDROP TRIGGER IF EXISTS on_pages_update ON public.pages;\r\nCREATE TRIGGER on_pages_update BEFORE UPDATE ON public.pages FOR EACH ROW EXECUTE FUNCTION public.handle_pages_update();\r\n\r\nDROP TRIGGER IF EXISTS on_posts_update ON public.posts;\r\nCREATE TRIGGER on_posts_update BEFORE UPDATE ON public.posts FOR EACH ROW EXECUTE FUNCTION public.handle_posts_update();\r\n\r\nDROP TRIGGER IF EXISTS on_product_drafts_update ON public.product_drafts;\r\nCREATE TRIGGER on_product_drafts_update BEFORE UPDATE ON public.product_drafts FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\r\n\r\nDROP TRIGGER IF EXISTS on_product_freemius_sale_coupons_write ON public.product_freemius_sale_coupons;\r\nCREATE TRIGGER on_product_freemius_sale_coupons_write BEFORE INSERT OR UPDATE ON public.product_freemius_sale_coupons FOR EACH ROW EXECUTE FUNCTION public.handle_product_freemius_sale_coupons_write();\r\n\r\nDROP TRIGGER IF EXISTS on_shipping_zone_locations_write ON public.shipping_zone_locations;\r\nCREATE TRIGGER on_shipping_zone_locations_write BEFORE INSERT OR UPDATE ON public.shipping_zone_locations FOR EACH ROW EXECUTE FUNCTION public.handle_shipping_zone_locations_write();\r\n\r\nDROP TRIGGER IF EXISTS on_tax_rates_write ON public.tax_rates;\r\nCREATE TRIGGER on_tax_rates_write BEFORE INSERT OR UPDATE ON public.tax_rates FOR EACH ROW EXECUTE FUNCTION public.handle_tax_rates_write();\r\n\r\nDROP TRIGGER IF EXISTS set_updated_at ON public.cms_interactions;\r\nCREATE TRIGGER set_updated_at BEFORE UPDATE ON public.cms_interactions FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\r\n\r\nDROP TRIGGER IF EXISTS set_updated_at ON public.translations;\r\nCREATE TRIGGER set_updated_at BEFORE UPDATE ON public.translations FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\r\n\r\nDROP TRIGGER IF EXISTS trg_handle_default_currency_change ON public.currencies;\r\nCREATE TRIGGER trg_handle_default_currency_change AFTER INSERT OR UPDATE ON public.currencies FOR EACH ROW EXECUTE FUNCTION public.handle_default_currency_change();\r\n\r\nDROP TRIGGER IF EXISTS trg_handle_ucp_cart_sessions_update ON public.ucp_cart_sessions;\r\nCREATE TRIGGER trg_handle_ucp_cart_sessions_update BEFORE UPDATE ON public.ucp_cart_sessions FOR EACH ROW EXECUTE FUNCTION public.handle_ucp_cart_sessions_update();\r\n\r\nDROP TRIGGER IF EXISTS trg_set_currency_defaults ON public.currencies;\r\nCREATE TRIGGER trg_set_currency_defaults BEFORE INSERT OR UPDATE ON public.currencies FOR EACH ROW EXECUTE FUNCTION public.set_currency_defaults();\r\n\r\nDROP TRIGGER IF EXISTS trg_sync_product_variants_currency_prices ON public.product_variants;\r\nCREATE TRIGGER trg_sync_product_variants_currency_prices BEFORE INSERT OR UPDATE OF price, sale_price, prices, sale_prices ON public.product_variants FOR EACH ROW EXECUTE FUNCTION public.sync_currency_price_maps();\r\n\r\nDROP TRIGGER IF EXISTS trg_sync_products_currency_prices ON public.products;\r\nCREATE TRIGGER trg_sync_products_currency_prices BEFORE INSERT OR UPDATE OF price, sale_price, prices, sale_prices ON public.products FOR EACH ROW EXECUTE FUNCTION public.sync_currency_price_maps();\r\n\r\nDROP TRIGGER IF EXISTS trg_sync_shipping_method_currency_maps ON public.shipping_zone_methods;\r\nCREATE TRIGGER trg_sync_shipping_method_currency_maps BEFORE INSERT OR UPDATE OF cost_amount, cost_currency, min_order_amount, currency_pricing_mode, cost_amounts, min_order_amounts ON public.shipping_zone_methods FOR EACH ROW EXECUTE FUNCTION public.sync_shipping_method_currency_maps();\r\n\r\nDROP TRIGGER IF EXISTS trg_system_alerts_updated_at ON public.system_alerts;\r\nCREATE TRIGGER trg_system_alerts_updated_at BEFORE UPDATE ON public.system_alerts FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\r\n\r\nDROP TRIGGER IF EXISTS trigger_update_product_ratings ON public.cms_interactions;\r\nCREATE TRIGGER trigger_update_product_ratings AFTER INSERT OR DELETE OR UPDATE ON public.cms_interactions FOR EACH ROW EXECUTE FUNCTION public.update_product_ratings();\r\n\r\nDROP POLICY IF EXISTS \"Admin can delete categories\" ON public.categories;\r\nCREATE POLICY \"Admin can delete categories\" ON public.categories FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS \"Admin can delete product_categories\" ON public.product_categories;\r\nCREATE POLICY \"Admin can delete product_categories\" ON public.product_categories FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS \"Admin can insert categories\" ON public.categories;\r\nCREATE POLICY \"Admin can insert categories\" ON public.categories FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS \"Admin can insert product_categories\" ON public.product_categories;\r\nCREATE POLICY \"Admin can insert product_categories\" ON public.product_categories FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS \"Admin can update categories\" ON public.categories;\r\nCREATE POLICY \"Admin can update categories\" ON public.categories FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS \"Admin can update product_categories\" ON public.product_categories;\r\nCREATE POLICY \"Admin can update product_categories\" ON public.product_categories FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS \"Allow authenticated read access\" ON public.package_activations;\r\nCREATE POLICY \"Allow authenticated read access\" ON public.package_activations FOR SELECT TO authenticated USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Allow service role full access\" ON public.package_activations;\r\nCREATE POLICY \"Allow service role full access\" ON public.package_activations TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Public can view categories\" ON public.categories;\r\nCREATE POLICY \"Public can view categories\" ON public.categories FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public can view inventory items\" ON public.inventory_items;\r\nCREATE POLICY \"Public can view inventory items\" ON public.inventory_items FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public can view product media\" ON public.product_media;\r\nCREATE POLICY \"Public can view product media\" ON public.product_media FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public can view product_categories\" ON public.product_categories;\r\nCREATE POLICY \"Public can view product_categories\" ON public.product_categories FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public can view products\" ON public.products;\r\nCREATE POLICY \"Public can view products\" ON public.products FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read access for freemius_plans\" ON public.freemius_plans;\r\nCREATE POLICY \"Public read access for freemius_plans\" ON public.freemius_plans FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read access for freemius_pricing\" ON public.freemius_pricing;\r\nCREATE POLICY \"Public read access for freemius_pricing\" ON public.freemius_pricing FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read active currencies\" ON public.currencies;\r\nCREATE POLICY \"Public read active currencies\" ON public.currencies FOR SELECT TO authenticated, anon USING ((is_active = true));\r\n\r\nDROP POLICY IF EXISTS \"Public read product_attribute_terms\" ON public.product_attribute_terms;\r\nCREATE POLICY \"Public read product_attribute_terms\" ON public.product_attribute_terms FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read product_attributes\" ON public.product_attributes;\r\nCREATE POLICY \"Public read product_attributes\" ON public.product_attributes FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read product_variants\" ON public.product_variants;\r\nCREATE POLICY \"Public read product_variants\" ON public.product_variants FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read shipping_zone_locations\" ON public.shipping_zone_locations;\r\nCREATE POLICY \"Public read shipping_zone_locations\" ON public.shipping_zone_locations FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read shipping_zone_methods\" ON public.shipping_zone_methods;\r\nCREATE POLICY \"Public read shipping_zone_methods\" ON public.shipping_zone_methods FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read shipping_zones\" ON public.shipping_zones;\r\nCREATE POLICY \"Public read shipping_zones\" ON public.shipping_zones FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read tax_rates\" ON public.tax_rates;\r\nCREATE POLICY \"Public read tax_rates\" ON public.tax_rates FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Public read variant_attribute_mapping\" ON public.variant_attribute_mapping;\r\nCREATE POLICY \"Public read variant_attribute_mapping\" ON public.variant_attribute_mapping FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS \"Service Role manages inventory items\" ON public.inventory_items;\r\nCREATE POLICY \"Service Role manages inventory items\" ON public.inventory_items TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Service Role manages order items\" ON public.order_items;\r\nCREATE POLICY \"Service Role manages order items\" ON public.order_items TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Service Role manages orders\" ON public.orders;\r\nCREATE POLICY \"Service Role manages orders\" ON public.orders TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Service Role manages tax_rates\" ON public.tax_rates;\r\nCREATE POLICY \"Service Role manages tax_rates\" ON public.tax_rates TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Service role manages all addresses\" ON public.user_addresses;\r\nCREATE POLICY \"Service role manages all addresses\" ON public.user_addresses TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Service role manages currencies\" ON public.currencies;\r\nCREATE POLICY \"Service role manages currencies\" ON public.currencies TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS \"Users can manage own addresses\" ON public.user_addresses;\r\nCREATE POLICY \"Users can manage own addresses\" ON public.user_addresses TO authenticated USING ((user_id = ( SELECT auth.uid() AS uid))) WITH CHECK ((user_id = ( SELECT auth.uid() AS uid)));\r\n\r\nDROP POLICY IF EXISTS \"Users can view own order items\" ON public.order_items;\r\nCREATE POLICY \"Users can view own order items\" ON public.order_items FOR SELECT TO authenticated USING (((( SELECT public.is_admin() AS is_admin) IS TRUE) OR (EXISTS ( SELECT 1\r\n FROM public.orders\r\n WHERE ((orders.id = order_items.order_id) AND (orders.user_id = ( SELECT auth.uid() AS uid)))))));\r\n\r\nDROP POLICY IF EXISTS \"Users can view own orders\" ON public.orders;\r\nCREATE POLICY \"Users can view own orders\" ON public.orders FOR SELECT TO authenticated USING (((( SELECT public.is_admin() AS is_admin) IS TRUE) OR (user_id = ( SELECT auth.uid() AS uid))));\r\n\r\nALTER TABLE public.blocks ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS blocks_anon_read_policy ON public.blocks;\r\nCREATE POLICY blocks_anon_read_policy ON public.blocks FOR SELECT TO anon USING ((((page_id IS NOT NULL) AND (EXISTS ( SELECT 1\r\n FROM public.pages p\r\n WHERE ((p.id = blocks.page_id) AND (p.status = 'published'::public.page_status))))) OR ((post_id IS NOT NULL) AND (EXISTS ( SELECT 1\r\n FROM public.posts pt\r\n WHERE ((pt.id = blocks.post_id) AND (pt.status = 'published'::public.page_status) AND ((pt.published_at IS NULL) OR (pt.published_at <= now())))))) OR ((product_id IS NOT NULL) AND (EXISTS ( SELECT 1\r\n FROM public.products pr\r\n WHERE ((pr.id = blocks.product_id) AND (pr.status = 'active'::text)))))));\r\n\r\nDROP POLICY IF EXISTS blocks_delete_policy ON public.blocks;\r\nCREATE POLICY blocks_delete_policy ON public.blocks FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS blocks_insert_policy ON public.blocks;\r\nCREATE POLICY blocks_insert_policy ON public.blocks FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS blocks_read_policy ON public.blocks;\r\nCREATE POLICY blocks_read_policy ON public.blocks FOR SELECT TO authenticated USING (((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])) OR (((page_id IS NOT NULL) AND (EXISTS ( SELECT 1\r\n FROM public.pages p\r\n WHERE ((p.id = blocks.page_id) AND (p.status = 'published'::public.page_status))))) OR ((post_id IS NOT NULL) AND (EXISTS ( SELECT 1\r\n FROM public.posts pt\r\n WHERE ((pt.id = blocks.post_id) AND (pt.status = 'published'::public.page_status) AND ((pt.published_at IS NULL) OR (pt.published_at <= now())))))) OR ((product_id IS NOT NULL) AND (EXISTS ( SELECT 1\r\n FROM public.products pr\r\n WHERE ((pr.id = blocks.product_id) AND (pr.status = 'active'::text))))))));\r\n\r\nDROP POLICY IF EXISTS blocks_update_policy ON public.blocks;\r\nCREATE POLICY blocks_update_policy ON public.blocks FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.categories ENABLE ROW LEVEL SECURITY;\r\n\r\nALTER TABLE public.cms_interactions ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS cms_interactions_delete_policy ON public.cms_interactions;\r\nCREATE POLICY cms_interactions_delete_policy ON public.cms_interactions FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS cms_interactions_insert_policy ON public.cms_interactions;\r\nCREATE POLICY cms_interactions_insert_policy ON public.cms_interactions FOR INSERT TO authenticated WITH CHECK (((auth.uid() = user_id) AND ((status = 'pending'::public.approval_status) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])))));\r\n\r\nDROP POLICY IF EXISTS cms_interactions_read_policy ON public.cms_interactions;\r\nCREATE POLICY cms_interactions_read_policy ON public.cms_interactions FOR SELECT USING (((status = 'approved'::public.approval_status) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))));\r\n\r\nDROP POLICY IF EXISTS cms_interactions_update_policy ON public.cms_interactions;\r\nCREATE POLICY cms_interactions_update_policy ON public.cms_interactions FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.content_drafts ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS content_drafts_delete_policy ON public.content_drafts;\r\nCREATE POLICY content_drafts_delete_policy ON public.content_drafts FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS content_drafts_insert_policy ON public.content_drafts;\r\nCREATE POLICY content_drafts_insert_policy ON public.content_drafts FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS content_drafts_select_policy ON public.content_drafts;\r\nCREATE POLICY content_drafts_select_policy ON public.content_drafts FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS content_drafts_update_policy ON public.content_drafts;\r\nCREATE POLICY content_drafts_update_policy ON public.content_drafts FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.cortex_ai_db_mutation_audit ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS cortex_ai_db_mutation_audit_admin_read_policy ON public.cortex_ai_db_mutation_audit;\r\nCREATE POLICY cortex_ai_db_mutation_audit_admin_read_policy ON public.cortex_ai_db_mutation_audit FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS cortex_ai_db_mutation_audit_service_role_policy ON public.cortex_ai_db_mutation_audit;\r\nCREATE POLICY cortex_ai_db_mutation_audit_service_role_policy ON public.cortex_ai_db_mutation_audit TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.coupon_freemius_mappings ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS coupon_freemius_mappings_admin_policy ON public.coupon_freemius_mappings;\r\nCREATE POLICY coupon_freemius_mappings_admin_policy ON public.coupon_freemius_mappings TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupon_freemius_mappings_service_role_policy ON public.coupon_freemius_mappings;\r\nCREATE POLICY coupon_freemius_mappings_service_role_policy ON public.coupon_freemius_mappings TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.coupon_products ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS coupon_products_admin_policy ON public.coupon_products;\r\nCREATE POLICY coupon_products_admin_policy ON public.coupon_products TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupon_products_service_role_policy ON public.coupon_products;\r\nCREATE POLICY coupon_products_service_role_policy ON public.coupon_products TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.coupon_redemptions ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS coupon_redemptions_admin_select_policy ON public.coupon_redemptions;\r\nCREATE POLICY coupon_redemptions_admin_select_policy ON public.coupon_redemptions FOR SELECT TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupon_redemptions_service_role_policy ON public.coupon_redemptions;\r\nCREATE POLICY coupon_redemptions_service_role_policy ON public.coupon_redemptions TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.coupons ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS coupons_admin_delete_policy ON public.coupons;\r\nCREATE POLICY coupons_admin_delete_policy ON public.coupons FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupons_admin_insert_policy ON public.coupons;\r\nCREATE POLICY coupons_admin_insert_policy ON public.coupons FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupons_admin_select_policy ON public.coupons;\r\nCREATE POLICY coupons_admin_select_policy ON public.coupons FOR SELECT TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupons_admin_update_policy ON public.coupons;\r\nCREATE POLICY coupons_admin_update_policy ON public.coupons FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS coupons_service_role_policy ON public.coupons;\r\nCREATE POLICY coupons_service_role_policy ON public.coupons TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.currencies ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS currencies_delete_policy ON public.currencies;\r\nCREATE POLICY currencies_delete_policy ON public.currencies FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS currencies_insert_policy ON public.currencies;\r\nCREATE POLICY currencies_insert_policy ON public.currencies FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS currencies_update_policy ON public.currencies;\r\nCREATE POLICY currencies_update_policy ON public.currencies FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.custom_block_definitions ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS custom_block_definitions_delete_policy ON public.custom_block_definitions;\r\nCREATE POLICY custom_block_definitions_delete_policy ON public.custom_block_definitions FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS custom_block_definitions_insert_policy ON public.custom_block_definitions;\r\nCREATE POLICY custom_block_definitions_insert_policy ON public.custom_block_definitions FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS custom_block_definitions_public_read_policy ON public.custom_block_definitions;\r\nCREATE POLICY custom_block_definitions_public_read_policy ON public.custom_block_definitions FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS custom_block_definitions_service_role_policy ON public.custom_block_definitions;\r\nCREATE POLICY custom_block_definitions_service_role_policy ON public.custom_block_definitions TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS custom_block_definitions_update_policy ON public.custom_block_definitions;\r\nCREATE POLICY custom_block_definitions_update_policy ON public.custom_block_definitions FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.email_2fa_challenges ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS email_2fa_challenges_service_role_policy ON public.email_2fa_challenges;\r\nCREATE POLICY email_2fa_challenges_service_role_policy ON public.email_2fa_challenges TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.freemius_plans ENABLE ROW LEVEL SECURITY;\r\n\r\nALTER TABLE public.freemius_pricing ENABLE ROW LEVEL SECURITY;\r\n\r\nALTER TABLE public.inventory_items ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS inventory_items_delete_policy ON public.inventory_items;\r\nCREATE POLICY inventory_items_delete_policy ON public.inventory_items FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS inventory_items_insert_policy ON public.inventory_items;\r\nCREATE POLICY inventory_items_insert_policy ON public.inventory_items FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS inventory_items_update_policy ON public.inventory_items;\r\nCREATE POLICY inventory_items_update_policy ON public.inventory_items FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.languages ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS languages_delete_policy ON public.languages;\r\nCREATE POLICY languages_delete_policy ON public.languages FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS languages_insert_policy ON public.languages;\r\nCREATE POLICY languages_insert_policy ON public.languages FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS languages_read_policy ON public.languages;\r\nCREATE POLICY languages_read_policy ON public.languages FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS languages_update_policy ON public.languages;\r\nCREATE POLICY languages_update_policy ON public.languages FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nALTER TABLE public.logos ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS logos_delete_policy ON public.logos;\r\nCREATE POLICY logos_delete_policy ON public.logos FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS logos_insert_policy ON public.logos;\r\nCREATE POLICY logos_insert_policy ON public.logos FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS logos_read_policy ON public.logos;\r\nCREATE POLICY logos_read_policy ON public.logos FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS logos_update_policy ON public.logos;\r\nCREATE POLICY logos_update_policy ON public.logos FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nALTER TABLE public.media ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS media_delete_policy ON public.media;\r\nCREATE POLICY media_delete_policy ON public.media FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS media_insert_policy ON public.media;\r\nCREATE POLICY media_insert_policy ON public.media FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS media_read_policy ON public.media;\r\nCREATE POLICY media_read_policy ON public.media FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS media_service_role_policy ON public.media;\r\nCREATE POLICY media_service_role_policy ON public.media TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS media_update_policy ON public.media;\r\nCREATE POLICY media_update_policy ON public.media FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.navigation_items ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS navigation_items_delete_policy ON public.navigation_items;\r\nCREATE POLICY navigation_items_delete_policy ON public.navigation_items FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS navigation_items_insert_policy ON public.navigation_items;\r\nCREATE POLICY navigation_items_insert_policy ON public.navigation_items FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS navigation_items_update_policy ON public.navigation_items;\r\nCREATE POLICY navigation_items_update_policy ON public.navigation_items FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS navigation_read_policy ON public.navigation_items;\r\nCREATE POLICY navigation_read_policy ON public.navigation_items FOR SELECT USING (true);\r\n\r\nALTER TABLE public.order_items ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS order_items_delete_policy ON public.order_items;\r\nCREATE POLICY order_items_delete_policy ON public.order_items FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS order_items_insert_policy ON public.order_items;\r\nCREATE POLICY order_items_insert_policy ON public.order_items FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS order_items_update_policy ON public.order_items;\r\nCREATE POLICY order_items_update_policy ON public.order_items FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS orders_delete_policy ON public.orders;\r\nCREATE POLICY orders_delete_policy ON public.orders FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS orders_insert_policy ON public.orders;\r\nCREATE POLICY orders_insert_policy ON public.orders FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS orders_update_policy ON public.orders;\r\nCREATE POLICY orders_update_policy ON public.orders FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.package_activations ENABLE ROW LEVEL SECURITY;\r\n\r\nALTER TABLE public.page_revisions ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS page_revisions_delete_policy ON public.page_revisions;\r\nCREATE POLICY page_revisions_delete_policy ON public.page_revisions FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS page_revisions_insert_policy ON public.page_revisions;\r\nCREATE POLICY page_revisions_insert_policy ON public.page_revisions FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS page_revisions_read_policy ON public.page_revisions;\r\nCREATE POLICY page_revisions_read_policy ON public.page_revisions FOR SELECT TO authenticated USING (true);\r\n\r\nDROP POLICY IF EXISTS page_revisions_update_policy ON public.page_revisions;\r\nCREATE POLICY page_revisions_update_policy ON public.page_revisions FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.pages ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS pages_anon_read_policy ON public.pages;\r\nCREATE POLICY pages_anon_read_policy ON public.pages FOR SELECT TO anon USING ((status = 'published'::public.page_status));\r\n\r\nDROP POLICY IF EXISTS pages_delete_policy ON public.pages;\r\nCREATE POLICY pages_delete_policy ON public.pages FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS pages_insert_policy ON public.pages;\r\nCREATE POLICY pages_insert_policy ON public.pages FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS pages_read_policy ON public.pages;\r\nCREATE POLICY pages_read_policy ON public.pages FOR SELECT TO authenticated USING (((status = 'published'::public.page_status) OR ((author_id = ( SELECT auth.uid() AS uid)) AND (status <> 'published'::public.page_status)) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))));\r\n\r\nDROP POLICY IF EXISTS pages_update_policy ON public.pages;\r\nCREATE POLICY pages_update_policy ON public.pages FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.post_revisions ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS post_revisions_delete_policy ON public.post_revisions;\r\nCREATE POLICY post_revisions_delete_policy ON public.post_revisions FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS post_revisions_insert_policy ON public.post_revisions;\r\nCREATE POLICY post_revisions_insert_policy ON public.post_revisions FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS post_revisions_read_policy ON public.post_revisions;\r\nCREATE POLICY post_revisions_read_policy ON public.post_revisions FOR SELECT TO authenticated USING (true);\r\n\r\nDROP POLICY IF EXISTS post_revisions_update_policy ON public.post_revisions;\r\nCREATE POLICY post_revisions_update_policy ON public.post_revisions FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS posts_anon_read_policy ON public.posts;\r\nCREATE POLICY posts_anon_read_policy ON public.posts FOR SELECT TO anon USING (((status = 'published'::public.page_status) AND ((published_at IS NULL) OR (published_at <= now()))));\r\n\r\nDROP POLICY IF EXISTS posts_delete_policy ON public.posts;\r\nCREATE POLICY posts_delete_policy ON public.posts FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS posts_insert_policy ON public.posts;\r\nCREATE POLICY posts_insert_policy ON public.posts FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS posts_read_policy ON public.posts;\r\nCREATE POLICY posts_read_policy ON public.posts FOR SELECT TO authenticated USING ((((status = 'published'::public.page_status) AND ((published_at IS NULL) OR (published_at <= now()))) OR ((author_id = ( SELECT auth.uid() AS uid)) AND (status <> 'published'::public.page_status)) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))));\r\n\r\nDROP POLICY IF EXISTS posts_update_policy ON public.posts;\r\nCREATE POLICY posts_update_policy ON public.posts FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.privacy_consent_logs ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS privacy_consent_logs_admin_read_policy ON public.privacy_consent_logs;\r\nCREATE POLICY privacy_consent_logs_admin_read_policy ON public.privacy_consent_logs FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS privacy_consent_logs_service_role_policy ON public.privacy_consent_logs;\r\nCREATE POLICY privacy_consent_logs_service_role_policy ON public.privacy_consent_logs TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.product_attribute_terms ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS product_attribute_terms_delete_policy ON public.product_attribute_terms;\r\nCREATE POLICY product_attribute_terms_delete_policy ON public.product_attribute_terms FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_attribute_terms_insert_policy ON public.product_attribute_terms;\r\nCREATE POLICY product_attribute_terms_insert_policy ON public.product_attribute_terms FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_attribute_terms_update_policy ON public.product_attribute_terms;\r\nCREATE POLICY product_attribute_terms_update_policy ON public.product_attribute_terms FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.product_attributes ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS product_attributes_delete_policy ON public.product_attributes;\r\nCREATE POLICY product_attributes_delete_policy ON public.product_attributes FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_attributes_insert_policy ON public.product_attributes;\r\nCREATE POLICY product_attributes_insert_policy ON public.product_attributes FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_attributes_update_policy ON public.product_attributes;\r\nCREATE POLICY product_attributes_update_policy ON public.product_attributes FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.product_categories ENABLE ROW LEVEL SECURITY;\r\n\r\nALTER TABLE public.product_drafts ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS product_drafts_delete_policy ON public.product_drafts;\r\nCREATE POLICY product_drafts_delete_policy ON public.product_drafts FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS product_drafts_insert_policy ON public.product_drafts;\r\nCREATE POLICY product_drafts_insert_policy ON public.product_drafts FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS product_drafts_select_policy ON public.product_drafts;\r\nCREATE POLICY product_drafts_select_policy ON public.product_drafts FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS product_drafts_update_policy ON public.product_drafts;\r\nCREATE POLICY product_drafts_update_policy ON public.product_drafts FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.product_freemius_sale_coupons ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS product_freemius_sale_coupons_admin_policy ON public.product_freemius_sale_coupons;\r\nCREATE POLICY product_freemius_sale_coupons_admin_policy ON public.product_freemius_sale_coupons TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_freemius_sale_coupons_service_role_policy ON public.product_freemius_sale_coupons;\r\nCREATE POLICY product_freemius_sale_coupons_service_role_policy ON public.product_freemius_sale_coupons TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.product_media ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS product_media_delete_policy ON public.product_media;\r\nCREATE POLICY product_media_delete_policy ON public.product_media FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_media_insert_policy ON public.product_media;\r\nCREATE POLICY product_media_insert_policy ON public.product_media FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_media_update_policy ON public.product_media;\r\nCREATE POLICY product_media_update_policy ON public.product_media FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.product_variants ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS product_variants_delete_policy ON public.product_variants;\r\nCREATE POLICY product_variants_delete_policy ON public.product_variants FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_variants_insert_policy ON public.product_variants;\r\nCREATE POLICY product_variants_insert_policy ON public.product_variants FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS product_variants_update_policy ON public.product_variants;\r\nCREATE POLICY product_variants_update_policy ON public.product_variants FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.products ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS products_delete_policy ON public.products;\r\nCREATE POLICY products_delete_policy ON public.products FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS products_insert_policy ON public.products;\r\nCREATE POLICY products_insert_policy ON public.products FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS products_update_policy ON public.products;\r\nCREATE POLICY products_update_policy ON public.products FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS profiles_insert_policy ON public.profiles;\r\nCREATE POLICY profiles_insert_policy ON public.profiles FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS profiles_read_policy ON public.profiles;\r\nCREATE POLICY profiles_read_policy ON public.profiles FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS profiles_service_role_policy ON public.profiles;\r\nCREATE POLICY profiles_service_role_policy ON public.profiles TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS profiles_update_policy ON public.profiles;\r\nCREATE POLICY profiles_update_policy ON public.profiles FOR UPDATE TO authenticated USING (((id = ( SELECT auth.uid() AS uid)) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))) WITH CHECK (((id = ( SELECT auth.uid() AS uid)) OR (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)));\r\n\r\nALTER TABLE public.shipping_zone_locations ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS shipping_zone_locations_delete_policy ON public.shipping_zone_locations;\r\nCREATE POLICY shipping_zone_locations_delete_policy ON public.shipping_zone_locations FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS shipping_zone_locations_insert_policy ON public.shipping_zone_locations;\r\nCREATE POLICY shipping_zone_locations_insert_policy ON public.shipping_zone_locations FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS shipping_zone_locations_update_policy ON public.shipping_zone_locations;\r\nCREATE POLICY shipping_zone_locations_update_policy ON public.shipping_zone_locations FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.shipping_zone_methods ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS shipping_zone_methods_delete_policy ON public.shipping_zone_methods;\r\nCREATE POLICY shipping_zone_methods_delete_policy ON public.shipping_zone_methods FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS shipping_zone_methods_insert_policy ON public.shipping_zone_methods;\r\nCREATE POLICY shipping_zone_methods_insert_policy ON public.shipping_zone_methods FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS shipping_zone_methods_update_policy ON public.shipping_zone_methods;\r\nCREATE POLICY shipping_zone_methods_update_policy ON public.shipping_zone_methods FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.shipping_zones ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS shipping_zones_delete_policy ON public.shipping_zones;\r\nCREATE POLICY shipping_zones_delete_policy ON public.shipping_zones FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS shipping_zones_insert_policy ON public.shipping_zones;\r\nCREATE POLICY shipping_zones_insert_policy ON public.shipping_zones FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS shipping_zones_update_policy ON public.shipping_zones;\r\nCREATE POLICY shipping_zones_update_policy ON public.shipping_zones FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.site_settings ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;\r\nCREATE POLICY site_settings_delete_policy ON public.site_settings FOR DELETE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nDROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;\r\nCREATE POLICY site_settings_insert_policy ON public.site_settings FOR INSERT TO authenticated WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nDROP POLICY IF EXISTS site_settings_read_policy ON public.site_settings;\r\nCREATE POLICY site_settings_read_policy ON public.site_settings FOR SELECT USING (((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT auth.role() AS role) = 'authenticated'::text) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nDROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;\r\nCREATE POLICY site_settings_update_policy ON public.site_settings FOR UPDATE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)))) WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nALTER TABLE public.system_alerts ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS system_alerts_select_admin ON public.system_alerts;\r\nCREATE POLICY system_alerts_select_admin ON public.system_alerts FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS system_alerts_update_admin ON public.system_alerts;\r\nCREATE POLICY system_alerts_update_admin ON public.system_alerts FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nALTER TABLE public.system_configuration ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS system_configuration_admin_delete ON public.system_configuration;\r\nCREATE POLICY system_configuration_admin_delete ON public.system_configuration FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS system_configuration_admin_insert ON public.system_configuration;\r\nCREATE POLICY system_configuration_admin_insert ON public.system_configuration FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS system_configuration_admin_select ON public.system_configuration;\r\nCREATE POLICY system_configuration_admin_select ON public.system_configuration FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS system_configuration_admin_update ON public.system_configuration;\r\nCREATE POLICY system_configuration_admin_update ON public.system_configuration FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\r\n\r\nDROP POLICY IF EXISTS system_configuration_service_role_all ON public.system_configuration;\r\nCREATE POLICY system_configuration_service_role_all ON public.system_configuration TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.tax_rates ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS tax_rates_delete_policy ON public.tax_rates;\r\nCREATE POLICY tax_rates_delete_policy ON public.tax_rates FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS tax_rates_insert_policy ON public.tax_rates;\r\nCREATE POLICY tax_rates_insert_policy ON public.tax_rates FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS tax_rates_update_policy ON public.tax_rates;\r\nCREATE POLICY tax_rates_update_policy ON public.tax_rates FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nALTER TABLE public.translations ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS translations_delete_policy ON public.translations;\r\nCREATE POLICY translations_delete_policy ON public.translations FOR DELETE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS translations_insert_policy ON public.translations;\r\nCREATE POLICY translations_insert_policy ON public.translations FOR INSERT TO authenticated WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nDROP POLICY IF EXISTS translations_read_policy ON public.translations;\r\nCREATE POLICY translations_read_policy ON public.translations FOR SELECT USING (true);\r\n\r\nDROP POLICY IF EXISTS translations_update_policy ON public.translations;\r\nCREATE POLICY translations_update_policy ON public.translations FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\r\n\r\nALTER TABLE public.ucp_cart_sessions ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS ucp_cart_sessions_service_role_policy ON public.ucp_cart_sessions;\r\nCREATE POLICY ucp_cart_sessions_service_role_policy ON public.ucp_cart_sessions TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.user_addresses ENABLE ROW LEVEL SECURITY;\r\n\r\nALTER TABLE public.user_security_settings ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS user_security_settings_insert_own_policy ON public.user_security_settings;\r\nCREATE POLICY user_security_settings_insert_own_policy ON public.user_security_settings FOR INSERT TO authenticated WITH CHECK ((( SELECT auth.uid() AS uid) = user_id));\r\n\r\nDROP POLICY IF EXISTS user_security_settings_select_own_policy ON public.user_security_settings;\r\nCREATE POLICY user_security_settings_select_own_policy ON public.user_security_settings FOR SELECT TO authenticated USING ((( SELECT auth.uid() AS uid) = user_id));\r\n\r\nDROP POLICY IF EXISTS user_security_settings_service_role_policy ON public.user_security_settings;\r\nCREATE POLICY user_security_settings_service_role_policy ON public.user_security_settings TO service_role USING (true) WITH CHECK (true);\r\n\r\nDROP POLICY IF EXISTS user_security_settings_update_own_policy ON public.user_security_settings;\r\nCREATE POLICY user_security_settings_update_own_policy ON public.user_security_settings FOR UPDATE TO authenticated USING ((( SELECT auth.uid() AS uid) = user_id)) WITH CHECK ((( SELECT auth.uid() AS uid) = user_id));\r\n\r\nALTER TABLE public.user_trusted_devices ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS user_trusted_devices_delete_own_policy ON public.user_trusted_devices;\r\nCREATE POLICY user_trusted_devices_delete_own_policy ON public.user_trusted_devices FOR DELETE TO authenticated USING ((( SELECT auth.uid() AS uid) = user_id));\r\n\r\nDROP POLICY IF EXISTS user_trusted_devices_select_own_policy ON public.user_trusted_devices;\r\nCREATE POLICY user_trusted_devices_select_own_policy ON public.user_trusted_devices FOR SELECT TO authenticated USING ((( SELECT auth.uid() AS uid) = user_id));\r\n\r\nDROP POLICY IF EXISTS user_trusted_devices_service_role_policy ON public.user_trusted_devices;\r\nCREATE POLICY user_trusted_devices_service_role_policy ON public.user_trusted_devices TO service_role USING (true) WITH CHECK (true);\r\n\r\nALTER TABLE public.variant_attribute_mapping ENABLE ROW LEVEL SECURITY;\r\n\r\nDROP POLICY IF EXISTS variant_attribute_mapping_delete_policy ON public.variant_attribute_mapping;\r\nCREATE POLICY variant_attribute_mapping_delete_policy ON public.variant_attribute_mapping FOR DELETE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS variant_attribute_mapping_insert_policy ON public.variant_attribute_mapping;\r\nCREATE POLICY variant_attribute_mapping_insert_policy ON public.variant_attribute_mapping FOR INSERT TO authenticated WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nDROP POLICY IF EXISTS variant_attribute_mapping_update_policy ON public.variant_attribute_mapping;\r\nCREATE POLICY variant_attribute_mapping_update_policy ON public.variant_attribute_mapping FOR UPDATE TO authenticated USING ((( SELECT public.is_admin() AS is_admin) IS TRUE)) WITH CHECK ((( SELECT public.is_admin() AS is_admin) IS TRUE));\r\n\r\nGRANT USAGE ON SCHEMA public TO postgres;\r\n\r\nGRANT USAGE ON SCHEMA public TO anon;\r\n\r\nGRANT USAGE ON SCHEMA public TO authenticated;\r\n\r\nGRANT USAGE ON SCHEMA public TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.apply_order_inventory_deduction(p_order_id uuid) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.apply_order_inventory_deduction(p_order_id uuid) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.apply_order_inventory_deduction(p_order_id uuid) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.assign_order_invoice_metadata(p_order_id uuid, p_paid_at timestamp with time zone) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.assign_order_invoice_metadata(p_order_id uuid, p_paid_at timestamp with time zone) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.assign_order_invoice_metadata(p_order_id uuid, p_paid_at timestamp with time zone) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.clear_currency_price_overrides(target_currency text) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.clear_currency_price_overrides(target_currency text) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.clear_currency_price_overrides(target_currency text) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_custom_block_fields(candidate jsonb) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_custom_block_fields(candidate jsonb) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_custom_block_fields(candidate jsonb) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_custom_block_layout_schema(candidate jsonb) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_custom_block_layout_schema(candidate jsonb) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_custom_block_layout_schema(candidate jsonb) TO service_role;\r\n\r\nGRANT ALL ON TABLE public.custom_block_definitions TO anon;\r\n\r\nGRANT ALL ON TABLE public.custom_block_definitions TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.custom_block_definitions TO service_role;\r\n\r\nREVOKE ALL ON FUNCTION public.duplicate_block_definition(target_id uuid) FROM PUBLIC;\r\n\r\nGRANT ALL ON FUNCTION public.duplicate_block_definition(target_id uuid) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.duplicate_block_definition(target_id uuid) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.duplicate_block_definition(target_id uuid) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.format_order_invoice_number(p_value bigint) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.format_order_invoice_number(p_value bigint) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.format_order_invoice_number(p_value bigint) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.generate_order_invoice_number() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.generate_order_invoice_number() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.generate_order_invoice_number() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.get_current_user_role() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.get_current_user_role() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.get_current_user_role() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.get_default_currency_code() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.get_default_currency_code() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.get_default_currency_code() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.get_ecommerce_track_quantities() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.get_ecommerce_track_quantities() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.get_ecommerce_track_quantities() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.get_my_claim(claim text) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.get_my_claim(claim text) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.get_my_claim(claim text) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_blocks_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_blocks_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_blocks_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_coupon_freemius_mappings_write() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_coupon_freemius_mappings_write() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_coupon_freemius_mappings_write() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_coupons_write() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_coupons_write() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_coupons_write() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_default_currency_change() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_default_currency_change() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_default_currency_change() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_inventory_item_change() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_inventory_item_change() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_inventory_item_change() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_inventory_items_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_inventory_items_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_inventory_items_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_languages_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_languages_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_languages_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_media_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_media_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_media_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_navigation_items_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_navigation_items_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_navigation_items_update() TO service_role;\r\n\r\nREVOKE ALL ON FUNCTION public.handle_new_user() FROM PUBLIC;\r\n\r\nGRANT ALL ON FUNCTION public.handle_new_user() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_new_user() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_new_user() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_pages_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_pages_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_pages_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_posts_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_posts_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_posts_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_product_freemius_sale_coupons_write() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_product_freemius_sale_coupons_write() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_product_freemius_sale_coupons_write() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_shipping_zone_locations_write() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_shipping_zone_locations_write() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_shipping_zone_locations_write() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_tax_rates_write() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_tax_rates_write() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_tax_rates_write() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.handle_ucp_cart_sessions_update() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.handle_ucp_cart_sessions_update() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.handle_ucp_cart_sessions_update() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.is_admin() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.is_admin() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.is_admin() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_currency_amount_map(amounts jsonb) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_currency_amount_map(amounts jsonb) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_currency_amount_map(amounts jsonb) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_sale_price_map(prices jsonb, sale_prices jsonb) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_sale_price_map(prices jsonb, sale_prices jsonb) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.is_valid_sale_price_map(prices jsonb, sale_prices jsonb) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.normalize_currency_amount_map(amounts jsonb) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.normalize_currency_amount_map(amounts jsonb) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.normalize_currency_amount_map(amounts jsonb) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.set_currency_defaults() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.set_currency_defaults() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.set_currency_defaults() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.set_current_timestamp_updated_at() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.set_current_timestamp_updated_at() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.set_current_timestamp_updated_at() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.sync_currency_price_maps() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.sync_currency_price_maps() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.sync_currency_price_maps() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.sync_inventory_cache_for_sku(p_sku text) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.sync_inventory_cache_for_sku(p_sku text) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.sync_inventory_cache_for_sku(p_sku text) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.sync_legacy_price_columns_for_currency(target_currency text) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.sync_legacy_price_columns_for_currency(target_currency text) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.sync_legacy_price_columns_for_currency(target_currency text) TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.sync_shipping_method_currency_maps() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.sync_shipping_method_currency_maps() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.sync_shipping_method_currency_maps() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.update_product_ratings() TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.update_product_ratings() TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.update_product_ratings() TO service_role;\r\n\r\nGRANT ALL ON FUNCTION public.upsert_product_with_variants(product_payload jsonb) TO anon;\r\n\r\nGRANT ALL ON FUNCTION public.upsert_product_with_variants(product_payload jsonb) TO authenticated;\r\n\r\nGRANT ALL ON FUNCTION public.upsert_product_with_variants(product_payload jsonb) TO service_role;\r\n\r\nGRANT ALL ON TABLE public.blocks TO anon;\r\n\r\nGRANT ALL ON TABLE public.blocks TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.blocks TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.blocks_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.blocks_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.blocks_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.categories TO anon;\r\n\r\nGRANT ALL ON TABLE public.categories TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.categories TO service_role;\r\n\r\nGRANT ALL ON TABLE public.cms_interactions TO anon;\r\n\r\nGRANT ALL ON TABLE public.cms_interactions TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.cms_interactions TO service_role;\r\n\r\nGRANT ALL ON TABLE public.content_drafts TO anon;\r\n\r\nGRANT ALL ON TABLE public.content_drafts TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.content_drafts TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.content_drafts_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.content_drafts_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.content_drafts_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.cortex_ai_db_mutation_audit TO anon;\r\n\r\nGRANT ALL ON TABLE public.cortex_ai_db_mutation_audit TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.cortex_ai_db_mutation_audit TO service_role;\r\n\r\nGRANT ALL ON TABLE public.coupon_freemius_mappings TO anon;\r\n\r\nGRANT ALL ON TABLE public.coupon_freemius_mappings TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.coupon_freemius_mappings TO service_role;\r\n\r\nGRANT ALL ON TABLE public.coupon_products TO anon;\r\n\r\nGRANT ALL ON TABLE public.coupon_products TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.coupon_products TO service_role;\r\n\r\nGRANT ALL ON TABLE public.coupon_redemptions TO anon;\r\n\r\nGRANT ALL ON TABLE public.coupon_redemptions TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.coupon_redemptions TO service_role;\r\n\r\nGRANT ALL ON TABLE public.coupons TO anon;\r\n\r\nGRANT ALL ON TABLE public.coupons TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.coupons TO service_role;\r\n\r\nGRANT ALL ON TABLE public.currencies TO anon;\r\n\r\nGRANT ALL ON TABLE public.currencies TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.currencies TO service_role;\r\n\r\nGRANT ALL ON TABLE public.email_2fa_challenges TO anon;\r\n\r\nGRANT ALL ON TABLE public.email_2fa_challenges TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.email_2fa_challenges TO service_role;\r\n\r\nGRANT ALL ON TABLE public.freemius_plans TO anon;\r\n\r\nGRANT ALL ON TABLE public.freemius_plans TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.freemius_plans TO service_role;\r\n\r\nGRANT ALL ON TABLE public.freemius_pricing TO anon;\r\n\r\nGRANT ALL ON TABLE public.freemius_pricing TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.freemius_pricing TO service_role;\r\n\r\nGRANT ALL ON TABLE public.inventory_items TO anon;\r\n\r\nGRANT ALL ON TABLE public.inventory_items TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.inventory_items TO service_role;\r\n\r\nGRANT ALL ON TABLE public.languages TO anon;\r\n\r\nGRANT ALL ON TABLE public.languages TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.languages TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.languages_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.languages_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.languages_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.logos TO anon;\r\n\r\nGRANT ALL ON TABLE public.logos TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.logos TO service_role;\r\n\r\nGRANT ALL ON TABLE public.media TO anon;\r\n\r\nGRANT ALL ON TABLE public.media TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.media TO service_role;\r\n\r\nGRANT ALL ON TABLE public.navigation_items TO anon;\r\n\r\nGRANT ALL ON TABLE public.navigation_items TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.navigation_items TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.navigation_items_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.navigation_items_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.navigation_items_id_seq TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.order_invoice_number_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.order_invoice_number_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.order_invoice_number_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.order_items TO anon;\r\n\r\nGRANT ALL ON TABLE public.order_items TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.order_items TO service_role;\r\n\r\nGRANT ALL ON TABLE public.orders TO anon;\r\n\r\nGRANT ALL ON TABLE public.orders TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.orders TO service_role;\r\n\r\nGRANT ALL ON TABLE public.package_activations TO anon;\r\n\r\nGRANT ALL ON TABLE public.package_activations TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.package_activations TO service_role;\r\n\r\nGRANT ALL ON TABLE public.page_revisions TO anon;\r\n\r\nGRANT ALL ON TABLE public.page_revisions TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.page_revisions TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.page_revisions_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.page_revisions_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.page_revisions_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.pages TO anon;\r\n\r\nGRANT ALL ON TABLE public.pages TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.pages TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.pages_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.pages_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.pages_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.post_revisions TO anon;\r\n\r\nGRANT ALL ON TABLE public.post_revisions TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.post_revisions TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.post_revisions_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.post_revisions_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.post_revisions_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.posts TO anon;\r\n\r\nGRANT ALL ON TABLE public.posts TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.posts TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.posts_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.posts_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.posts_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.privacy_consent_logs TO anon;\r\n\r\nGRANT ALL ON TABLE public.privacy_consent_logs TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.privacy_consent_logs TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_attribute_terms TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_attribute_terms TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_attribute_terms TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_attributes TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_attributes TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_attributes TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_categories TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_categories TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_categories TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_drafts TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_drafts TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_drafts TO service_role;\r\n\r\nGRANT ALL ON SEQUENCE public.product_drafts_id_seq TO anon;\r\n\r\nGRANT ALL ON SEQUENCE public.product_drafts_id_seq TO authenticated;\r\n\r\nGRANT ALL ON SEQUENCE public.product_drafts_id_seq TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_freemius_sale_coupons TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_freemius_sale_coupons TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_freemius_sale_coupons TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_media TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_media TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_media TO service_role;\r\n\r\nGRANT ALL ON TABLE public.product_variants TO anon;\r\n\r\nGRANT ALL ON TABLE public.product_variants TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.product_variants TO service_role;\r\n\r\nGRANT ALL ON TABLE public.products TO anon;\r\n\r\nGRANT ALL ON TABLE public.products TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.products TO service_role;\r\n\r\nGRANT ALL ON TABLE public.profiles TO anon;\r\n\r\nGRANT ALL ON TABLE public.profiles TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.profiles TO service_role;\r\n\r\nGRANT ALL ON TABLE public.shipping_zone_locations TO anon;\r\n\r\nGRANT ALL ON TABLE public.shipping_zone_locations TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.shipping_zone_locations TO service_role;\r\n\r\nGRANT ALL ON TABLE public.shipping_zone_methods TO anon;\r\n\r\nGRANT ALL ON TABLE public.shipping_zone_methods TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.shipping_zone_methods TO service_role;\r\n\r\nGRANT ALL ON TABLE public.shipping_zones TO anon;\r\n\r\nGRANT ALL ON TABLE public.shipping_zones TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.shipping_zones TO service_role;\r\n\r\nGRANT ALL ON TABLE public.site_settings TO anon;\r\n\r\nGRANT ALL ON TABLE public.site_settings TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.site_settings TO service_role;\r\n\r\nGRANT ALL ON TABLE public.system_alerts TO anon;\r\n\r\nGRANT ALL ON TABLE public.system_alerts TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.system_alerts TO service_role;\r\n\r\nGRANT ALL ON TABLE public.system_configuration TO anon;\r\n\r\nGRANT ALL ON TABLE public.system_configuration TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.system_configuration TO service_role;\r\n\r\nGRANT ALL ON TABLE public.tax_rates TO anon;\r\n\r\nGRANT ALL ON TABLE public.tax_rates TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.tax_rates TO service_role;\r\n\r\nGRANT ALL ON TABLE public.translations TO anon;\r\n\r\nGRANT ALL ON TABLE public.translations TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.translations TO service_role;\r\n\r\nGRANT ALL ON TABLE public.ucp_cart_sessions TO anon;\r\n\r\nGRANT ALL ON TABLE public.ucp_cart_sessions TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.ucp_cart_sessions TO service_role;\r\n\r\nGRANT ALL ON TABLE public.user_addresses TO anon;\r\n\r\nGRANT ALL ON TABLE public.user_addresses TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.user_addresses TO service_role;\r\n\r\nGRANT ALL ON TABLE public.user_security_settings TO anon;\r\n\r\nGRANT ALL ON TABLE public.user_security_settings TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.user_security_settings TO service_role;\r\n\r\nGRANT ALL ON TABLE public.user_trusted_devices TO anon;\r\n\r\nGRANT ALL ON TABLE public.user_trusted_devices TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.user_trusted_devices TO service_role;\r\n\r\nGRANT ALL ON TABLE public.variant_attribute_mapping TO anon;\r\n\r\nGRANT ALL ON TABLE public.variant_attribute_mapping TO authenticated;\r\n\r\nGRANT ALL ON TABLE public.variant_attribute_mapping TO service_role;\r\n\r\n-- Re-attached: trigger lives on auth.users, which a public-only pg_dump omits (see migration 005).\r\nDROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;\r\nCREATE TRIGGER on_auth_user_created\r\n AFTER INSERT ON auth.users\r\n FOR EACH ROW\r\n EXECUTE FUNCTION public.handle_new_user();\r\nREVOKE EXECUTE ON FUNCTION public.handle_new_user() FROM PUBLIC, anon, authenticated;\r\n"
31
+ },
32
+ {
33
+ "version": "00000000000003",
34
+ "name": "00000000000003_baseline_seed.sql",
35
+ "sql": "-- AUTO-GENERATED baseline (re-baseline of migrations 000..044). Idempotent; safe to replay.\n-- 03 · seed: canonical NextBlock demo content (no users, no secrets)\n-- Regenerate via tools/scripts/rebaseline-transform.mjs. Do not hand-edit.\n\nINSERT INTO public.languages (id, code, name, is_default, is_active, created_at, updated_at) VALUES (1, 'en', 'English', true, true, '2026-07-03 17:52:15.444652+00', '2026-07-03 17:52:15.444652+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.languages (id, code, name, is_default, is_active, created_at, updated_at) VALUES (2, 'fr', 'Français', false, true, '2026-07-03 17:52:15.444652+00', '2026-07-03 17:52:15.444652+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.media (id, uploader_id, file_name, object_key, file_type, size_bytes, description, width, height, blur_data_url, variants, file_path, folder, created_at, updated_at) VALUES ('ea6fdaf5-f8de-416c-9f2b-b689b7a4ed38', NULL, 'nextblock-logo-small.webp', 'images/nextblock-logo-small.webp', 'image/webp', 10000, 'NextBlock™ Site Logo', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.media (id, uploader_id, file_name, object_key, file_type, size_bytes, description, width, height, blur_data_url, variants, file_path, folder, created_at, updated_at) VALUES ('5bb07d4c-ca14-49ab-96bb-febbed31aded', NULL, 'NBcover.webp', 'images/NBcover.webp', 'image/webp', 180000, 'NextBlock™ architecture overview cover image', 1024, 572, NULL, NULL, NULL, NULL, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.media (id, uploader_id, file_name, object_key, file_type, size_bytes, description, width, height, blur_data_url, variants, file_path, folder, created_at, updated_at) VALUES ('641ddf75-5c90-41df-8b83-e7c298f30a6a', NULL, 'extensibility.webp', 'images/extensibility.webp', 'image/webp', 246808, 'NextBlock™ extensibility editorial artwork', 1024, 559, NULL, NULL, NULL, NULL, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.media (id, uploader_id, file_name, object_key, file_type, size_bytes, description, width, height, blur_data_url, variants, file_path, folder, created_at, updated_at) VALUES ('e6ed9c34-6ef6-47a8-b836-e6dbf73511fa', NULL, 'included.webp', 'images/included.webp', 'image/webp', 237478, 'NextBlock™ getting-started platform artwork', 1024, 559, NULL, NULL, NULL, NULL, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.media (id, uploader_id, file_name, object_key, file_type, size_bytes, description, width, height, blur_data_url, variants, file_path, folder, created_at, updated_at) VALUES ('77b9da47-4182-470f-9070-fa6cd11c6aea', NULL, 'programmer-upscaled.webp', 'images/programmer-upscaled.webp', 'image/webp', 780000, 'NextBlock™ setup guide cover image', 8192, 2632, NULL, NULL, NULL, NULL, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.media (id, uploader_id, file_name, object_key, file_type, size_bytes, description, width, height, blur_data_url, variants, file_path, folder, created_at, updated_at) VALUES ('dfa0e881-70b6-4767-b9b0-7a6293db5009', NULL, 'commerce-plan.webp', 'images/commerce-plan.webp', 'image/webp', 269854, 'NextBlock™ commerce roadmap artwork', 1024, 559, NULL, NULL, NULL, NULL, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.media (id, uploader_id, file_name, object_key, file_type, size_bytes, description, width, height, blur_data_url, variants, file_path, folder, created_at, updated_at) VALUES ('5f5a0d6d-aed4-4b6f-b966-ea5d5b17b7a6', NULL, 'commerce-wide.webp', 'images/commerce-wide.webp', 'image/webp', 250584, 'NextBlock™ Commerce editorial feature image', 1024, 434, NULL, NULL, NULL, NULL, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.media (id, uploader_id, file_name, object_key, file_type, size_bytes, description, width, height, blur_data_url, variants, file_path, folder, created_at, updated_at) VALUES ('38ec38c2-899f-47b0-8364-56a574527078', NULL, 'cortex-ai.webp', 'images/cortex-ai.webp', 'image/webp', 298588, 'NextBlock Cortex AI editorial feature image', 1024, 571, NULL, NULL, 'images/cortex-ai.webp', 'images', '2026-07-03 17:52:15.643901+00', '2026-07-03 17:52:15.643901+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.pages (id, language_id, author_id, title, slug, status, meta_title, meta_description, version, translation_group_id, created_at, updated_at, feature_image_id) VALUES (1, 1, NULL, 'Home', 'home', 'published', NULL, NULL, 1, '0098e4f0-e5f3-4e28-acfc-bb9fdb3a4a6b', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.pages (id, language_id, author_id, title, slug, status, meta_title, meta_description, version, translation_group_id, created_at, updated_at, feature_image_id) VALUES (2, 2, NULL, 'Accueil', 'accueil', 'published', NULL, NULL, 1, '0098e4f0-e5f3-4e28-acfc-bb9fdb3a4a6b', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.pages (id, language_id, author_id, title, slug, status, meta_title, meta_description, version, translation_group_id, created_at, updated_at, feature_image_id) VALUES (3, 1, NULL, 'Articles', 'articles', 'published', NULL, NULL, 1, '50adeea5-5040-4ae7-bacd-992647920982', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.pages (id, language_id, author_id, title, slug, status, meta_title, meta_description, version, translation_group_id, created_at, updated_at, feature_image_id) VALUES (4, 2, NULL, 'Articles', 'articles', 'published', NULL, NULL, 1, '50adeea5-5040-4ae7-bacd-992647920982', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.pages (id, language_id, author_id, title, slug, status, meta_title, meta_description, version, translation_group_id, created_at, updated_at, feature_image_id) VALUES (5, 1, NULL, 'Contact Us', 'contact', 'published', NULL, NULL, 1, 'e3b28669-7e38-47bd-9189-7db2353b52dc', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.pages (id, language_id, author_id, title, slug, status, meta_title, meta_description, version, translation_group_id, created_at, updated_at, feature_image_id) VALUES (6, 2, NULL, 'Contactez-nous', 'contact', 'published', NULL, NULL, 1, 'e3b28669-7e38-47bd-9189-7db2353b52dc', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.pages (id, language_id, author_id, title, slug, status, meta_title, meta_description, version, translation_group_id, created_at, updated_at, feature_image_id) VALUES (7, 1, NULL, 'Privacy Policy', 'privacy-policy', 'published', 'Privacy Policy', 'How we collect, use, disclose, and protect your personal information under Quebec Law 25, PIPEDA, and CASL.', 1, '8cd91839-518d-4b57-87db-441e59a71a95', '2026-07-03 17:52:15.699946+00', '2026-07-03 17:52:15.699946+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.pages (id, language_id, author_id, title, slug, status, meta_title, meta_description, version, translation_group_id, created_at, updated_at, feature_image_id) VALUES (8, 2, NULL, 'Politique de confidentialité', 'politique-de-confidentialite', 'published', 'Politique de confidentialité', 'Comment nous recueillons, utilisons, communiquons et protégeons vos renseignements personnels en vertu de la Loi 25, de la LPRPDE et de la LCAP.', 1, '8cd91839-518d-4b57-87db-441e59a71a95', '2026-07-03 17:52:15.699946+00', '2026-07-03 17:52:15.699946+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.pages (id, language_id, author_id, title, slug, status, meta_title, meta_description, version, translation_group_id, created_at, updated_at, feature_image_id) VALUES (9, 1, NULL, 'Terms of Service', 'terms-of-service', 'published', 'Terms of Service', 'The terms governing your use of NextBlock™ CMS, free and open-source software licensed under the AGPL-3.0.', 1, 'b5eb82c7-95d6-4308-82d8-3ac2816f5d46', '2026-07-03 17:52:15.699946+00', '2026-07-03 17:52:15.699946+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.pages (id, language_id, author_id, title, slug, status, meta_title, meta_description, version, translation_group_id, created_at, updated_at, feature_image_id) VALUES (10, 2, NULL, 'Conditions d''utilisation', 'conditions-utilisation', 'published', 'Conditions d''utilisation', 'Les conditions régissant votre utilisation de NextBlock™ CMS, un logiciel libre sous licence AGPL-3.0.', 1, 'b5eb82c7-95d6-4308-82d8-3ac2816f5d46', '2026-07-03 17:52:15.699946+00', '2026-07-03 17:52:15.699946+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.posts (id, language_id, author_id, title, slug, label, excerpt, subtitle, status, published_at, meta_title, meta_description, feature_image_id, version, translation_group_id, created_at, updated_at) VALUES (1, 1, NULL, 'How NextBlock™ Works: A Look Under the Hood', 'how-nextblock-works', 'Architecture', 'Under the hood of the monorepo, block registry, and editor stack that power NextBlock.', 'A guided tour of the monorepo, block registry, editor stack, and open-core architecture behind NextBlock.', 'published', NULL, NULL, NULL, '5bb07d4c-ca14-49ab-96bb-febbed31aded', 1, 'de8b8593-1ef6-4e66-8d1f-8b3e7ffb811d', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.posts (id, language_id, author_id, title, slug, label, excerpt, subtitle, status, published_at, meta_title, meta_description, feature_image_id, version, translation_group_id, created_at, updated_at) VALUES (2, 2, NULL, 'Comment NextBlock™ Fonctionne : Regard Sous le Capot', 'comment-nextblock-fonctionne', 'Architecture', 'Sous le capot du monorepo, du registre de blocs et de l editeur qui propulsent NextBlock.', 'Une visite guidee du monorepo, du registre de blocs, de l editeur et de l architecture open-core de NextBlock.', 'published', NULL, NULL, NULL, '5bb07d4c-ca14-49ab-96bb-febbed31aded', 1, 'de8b8593-1ef6-4e66-8d1f-8b3e7ffb811d', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.posts (id, language_id, author_id, title, slug, label, excerpt, subtitle, status, published_at, meta_title, meta_description, feature_image_id, version, translation_group_id, created_at, updated_at) VALUES (5, 1, NULL, 'NextBlock™ Commerce: Multi-Currency, Tax Sync & Beyond', 'nextblock-commerce-guide', 'Commerce', 'Storefront architecture, checkout flows, and premium commerce capabilities inside NextBlock.', 'A closer look at the commerce module, from multi-currency and tax sync to shipping, inventory, and provider-aware checkout.', 'published', NULL, NULL, NULL, '5f5a0d6d-aed4-4b6f-b966-ea5d5b17b7a6', 1, '23773a34-e54b-40c6-89b5-b09ead17ae60', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.posts (id, language_id, author_id, title, slug, label, excerpt, subtitle, status, published_at, meta_title, meta_description, feature_image_id, version, translation_group_id, created_at, updated_at) VALUES (6, 2, NULL, 'NextBlock™ Commerce : Multi-Devises, Taxes Automatiques et Plus', 'guide-commerce-nextblock', 'Commerce', 'Architecture boutique, parcours de paiement et fonctions commerce premium au coeur de NextBlock.', 'Un apercu du module commerce : multi-devises, sync taxes, expedition, inventaire et paiements connectes.', 'published', NULL, NULL, NULL, '5f5a0d6d-aed4-4b6f-b966-ea5d5b17b7a6', 1, '23773a34-e54b-40c6-89b5-b09ead17ae60', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.posts (id, language_id, author_id, title, slug, label, excerpt, subtitle, status, published_at, meta_title, meta_description, feature_image_id, version, translation_group_id, created_at, updated_at) VALUES (7, 1, NULL, 'NextBlock Cortex AI Guide', 'nextblock-cortex-ai-guide', 'AI Copilot', 'A practical guide to Cortex AI, the block-aware assistant for model routing, BYOK controls, and faster editorial production inside NextBlock.', 'See how Cortex AI brings structured generation, provider choice, and safer content workflows directly into the NextBlock editor.', 'published', '2026-07-03 17:52:15.643901+00', 'NextBlock Cortex AI Guide', 'Learn how NextBlock Cortex AI helps teams generate, refine, and translate structured block content with model routing and BYOK controls.', '38ec38c2-899f-47b0-8364-56a574527078', 1, '669489e7-5900-496e-8b73-2aba7514a71b', '2026-07-03 17:52:15.643901+00', '2026-07-03 17:52:15.643901+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.posts (id, language_id, author_id, title, slug, label, excerpt, subtitle, status, published_at, meta_title, meta_description, feature_image_id, version, translation_group_id, created_at, updated_at) VALUES (3, 1, NULL, 'How to Install NextBlock: Every Setup Option Explained', 'how-to-setup-nextblock', 'Getting Started', 'Every way to install NextBlock — one-click cloud deploy, CLI scaffold, git clone, or self-hosted Docker — with copy-paste steps for each.', 'Four ways to launch NextBlock: a one-click Vercel deploy, npm create nextblock, git clone with the browser setup wizard, or a fully local Docker stack.', 'published', NULL, 'How to Install NextBlock CMS — Vercel, CLI, Git or Docker', 'Install NextBlock in minutes — one-click Vercel deploy, npm create nextblock, git clone, or self-hosted Docker. No config files, no manual SQL.', '77b9da47-4182-470f-9070-fa6cd11c6aea', 1, '461cde55-74cb-4112-8c3d-a25882eeedce', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.799297+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.posts (id, language_id, author_id, title, slug, label, excerpt, subtitle, status, published_at, meta_title, meta_description, feature_image_id, version, translation_group_id, created_at, updated_at) VALUES (4, 2, NULL, 'Installer NextBlock : toutes les options expliquées', 'comment-configurer-nextblock', 'Mise En Route', 'Toutes les façons d''installer NextBlock — cloud en un clic, CLI, git clone ou Docker auto-hébergé — avec les étapes à copier-coller.', 'Quatre façons de lancer NextBlock : déploiement Vercel en un clic, npm create nextblock, git clone avec l''assistant dans le navigateur, ou une pile Docker 100 % locale.', 'published', NULL, 'Installer NextBlock — Vercel, CLI, Git ou Docker', 'Installez NextBlock en quelques minutes : déploiement Vercel en un clic, npm create nextblock, git clone ou Docker auto-hébergé. Sans config ni SQL.', '77b9da47-4182-470f-9070-fa6cd11c6aea', 1, '461cde55-74cb-4112-8c3d-a25882eeedce', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.799297+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (2, 1, NULL, 1, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"none\"}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"level\": 2, \"textAlign\": \"center\", \"text_content\": \"Key Features: The Three Pillars of NextBlock™\"}, \"block_type\": \"heading\"}, {\"content\": {\"html_content\": \"<p class=''text-lg text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto''>NextBlock™ is a holistic platform that unites performance, editorial experience, and developer control so every stakeholder delivers their best work.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<div class=''grid gap-8 md:grid-cols-3 mt-12''><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 backdrop-blur-sm hover:bg-slate-100 dark:hover:bg-white/10 transition-colors duration-300''><div class=''w-12 h-12 rounded-xl flex items-center justify-center text-black dark:text-white mb-6''><svg class=''w-6 h-6'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M13 10V3L4 14h7v7l9-11h-7z''></path></svg></div><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Built for Speed.</h3><p class=''text-sm text-slate-600 dark:text-slate-400 leading-relaxed''>Architected for 100% Lighthouse scores with global delivery and near-instant FCP.</p><ul class=''mt-6 space-y-3 text-sm text-slate-600 dark:text-slate-400''><li><strong class=''text-slate-800 dark:text-slate-200''>Edge Caching &amp; ISR:</strong> Serve pages worldwide.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Critical CSS:</strong> Inline styles to eliminate blocking.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Image Opt:</strong> AVIF &amp; blurred placeholders.</li></ul></div><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 backdrop-blur-sm hover:bg-slate-100 dark:hover:bg-white/10 transition-colors duration-300''><div class=''w-12 h-12 rounded-xl flex items-center justify-center text-black dark:text-white mb-6''><svg class=''w-6 h-6'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z''></path></svg></div><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Editor-First Experience.</h3><p class=''text-sm text-slate-600 dark:text-slate-400 leading-relaxed''>A low-code, Notion-style block editor empowers teams to ship pages without engineering help.</p><ul class=''mt-6 space-y-3 text-sm text-slate-600 dark:text-slate-400''><li><strong class=''text-slate-800 dark:text-slate-200''>Notion-Style:</strong> Slash commands &amp; drag-and-drop.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Bilingual:</strong> Manage locales from one interface.</li><li><strong class=''text-slate-800 dark:text-slate-200''>History:</strong> Restore any version with a click.</li></ul></div><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 backdrop-blur-sm hover:bg-slate-100 dark:hover:bg-white/10 transition-colors duration-300''><div class=''w-12 h-12 bg-white/50 dark:bg-white/10 rounded-xl flex items-center justify-center mb-6''><svg class=''w-6 h-6 text-slate-900 dark:text-white'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10''></path></svg></div><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Infinitely Extensible.</h3><p class=''text-sm text-slate-700 dark:text-slate-200 leading-relaxed''>Open-source control with a clean Nx monorepo and a typed SDK for limitless customization.</p><ul class=''mt-6 space-y-3 text-sm text-slate-700 dark:text-slate-200''><li><strong class=''text-slate-900 dark:text-white''>Open Source:</strong> Own the code &amp; data forever.</li><li><strong class=''text-slate-900 dark:text-white''>Nx Monorepo:</strong> Scale confidently.</li><li><strong class=''text-slate-900 dark:text-white''>Developer SDK:</strong> Scaffold blocks in minutes.</li></ul></div></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 1, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (3, 1, NULL, 1, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#0f172a\", \"position\": 0}, {\"color\": \"#020817\", \"position\": 100}], \"direction\": \"180deg\"}}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<h2 class=''text-3xl md:text-4xl font-bold text-white text-center mb-6''>Built with the Best.</h2>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<p class=''text-slate-400 text-center max-w-2xl mx-auto''>Every layer of NextBlock™ leans on proven developer-first technology so the platform feels familiar, performant, and trustworthy from day one.</p><div class=''grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-8 gap-4 mt-10 text-sm font-semibold text-center text-white''><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Next.js</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>React</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Supabase</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Stripe</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Tailwind</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Tiptap</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Vercel</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Nx</div></div>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<h2 class=''text-3xl md:text-4xl font-bold text-white text-center mb-6 mt-16''>Powerful for Developers. Intuitive for Editors.</h2>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<div class=''grid md:grid-cols-2 gap-8 mt-10 text-white''><div class=''p-8 rounded-3xl border border-white/10 bg-white/5 backdrop-blur-sm''><h3 class=''text-xl font-bold mb-6 text-blue-400''>For Content Creators</h3><ul class=''space-y-4 text-sm text-slate-300''><li><strong class=''text-white block mb-1''>Intuitive Block Editor</strong>Drag-and-drop layouts with a Notion-like interface.</li><li><strong class=''text-white block mb-1''>Rich Content Blocks</strong>Deploy heroes, galleries, testimonials, and more in one click.</li><li><strong class=''text-white block mb-1''>Effortless Media Management</strong>Organize assets with folders, tags, and bulk actions.</li><li><strong class=''text-white block mb-1''>Worry-Free Revisions</strong>Automatic version history with instant restore.</li></ul></div><div class=''p-8 rounded-3xl border border-white/10 bg-gradient-to-br from-white/5 to-white/[0.02] backdrop-blur-sm''><h3 class=''text-xl font-bold mb-6 text-purple-400''>For Developers</h3><ul class=''space-y-4 text-sm text-slate-300''><li><strong class=''text-white block mb-1''>Next.js 16 Core</strong>Server Components, ISR, and Edge Functions ready out of the box.</li><li><strong class=''text-white block mb-1''>Supabase Integration</strong>Postgres, auth, storage, and real-time APIs without glue code.</li><li><strong class=''text-white block mb-1''>Monorepo Ready</strong>Nx-powered dev experience for scalable architectures.</li><li><strong class=''text-white block mb-1''>Extensible Block SDK</strong>Ship fully typed custom blocks and widgets.</li></ul></div></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 2, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (4, 1, NULL, 1, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#022c22\", \"position\": 0}, {\"color\": \"#0f172a\", \"position\": 50}, {\"color\": \"#020817\", \"position\": 100}], \"direction\": \"135deg\"}}, \"column_gap\": \"xl\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<p class=''text-xs uppercase tracking-[0.25em] text-emerald-400 font-bold mb-4''>Now Available — Premium Module</p><h2 class=''text-4xl md:text-5xl font-bold text-white mb-6 leading-tight''>Turn Your CMS Into<br/>a Full Storefront.</h2><p class=''text-lg text-slate-300 max-w-2xl leading-relaxed mb-8''>NextBlock™ Commerce transforms your content platform into a complete e-commerce engine. Products, checkout, multi-currency, taxes, shipping, invoices — all natively integrated into the block editor you already know.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"url\": \"/article/nextblock-commerce-guide\", \"size\": \"lg\", \"text\": \"Explore Commerce Features →\", \"variant\": \"default\"}, \"block_type\": \"button\"}, {\"content\": {\"url\": \"https://nextblock.dev/product/nextblock-commerce-pro-commerce-license\", \"size\": \"lg\", \"text\": \"Get a License\", \"variant\": \"outline\"}, \"block_type\": \"button\"}], [{\"content\": {\"html_content\": \"<div class=''rounded-3xl overflow-hidden border border-emerald-500/20 bg-gradient-to-br from-white/5 to-emerald-500/5 shadow-2xl p-6 backdrop-blur-sm''><img src=''/images/commerce-square.webp'' alt=''NextBlock™ Commerce dashboard showing product management'' class=''w-full h-auto rounded-2xl shadow-lg'' /><div class=''mt-4 grid grid-cols-3 gap-3 text-center''><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-emerald-400''>∞</p><p class=''text-xs text-slate-400''>Currencies</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-emerald-400''>2</p><p class=''text-xs text-slate-400''>Providers</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-emerald-400''>Auto</p><p class=''text-xs text-slate-400''>Tax Sync</p></div></div></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 2}, \"vertical_alignment\": \"center\"}', 3, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (5, 1, NULL, 1, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"none\"}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"level\": 2, \"textAlign\": \"center\", \"text_content\": \"Everything You Need to Sell Online\"}, \"block_type\": \"heading\"}, {\"content\": {\"html_content\": \"<p class=''text-lg text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto mb-12''>NextBlock™ Commerce ships a complete e-commerce toolkit so you can go from catalog to checkout without third-party plugins.</p><div class=''grid gap-6 md:grid-cols-2 lg:grid-cols-3''><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><div class=''w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-500/10 flex items-center justify-center mb-4''><svg class=''w-5 h-5 text-emerald-600 dark:text-emerald-400'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z''></path></svg></div><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Multi-Currency</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Real-time FX rates, rounding modes, charm pricing, and automatic product price sync across unlimited currencies.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><div class=''w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-500/10 flex items-center justify-center mb-4''><svg class=''w-5 h-5 text-emerald-600 dark:text-emerald-400'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M9 14l6-6m-5.5.5h.01m4.99 5h.01M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16l3.5-2 3.5 2 3.5-2 3.5 2z''></path></svg></div><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Tax Automation</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Manual stacked tax rates (GST + PST) or fully automatic calculation via Stripe Tax — you choose.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><div class=''w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-500/10 flex items-center justify-center mb-4''><svg class=''w-5 h-5 text-emerald-600 dark:text-emerald-400'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4''></path></svg></div><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Shipping Zones</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Zone-based rate resolution with country and state matching, per-currency pricing, and free-shipping thresholds.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><div class=''w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-500/10 flex items-center justify-center mb-4''><svg class=''w-5 h-5 text-emerald-600 dark:text-emerald-400'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z''></path></svg></div><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Stripe &amp; Freemius Checkout</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Stripe for physical products, Freemius for digital licensing — provider-aware checkout with inventory validation.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><div class=''w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-500/10 flex items-center justify-center mb-4''><svg class=''w-5 h-5 text-emerald-600 dark:text-emerald-400'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4''></path></svg></div><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Inventory Tracking</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Automatic quantity deduction on payment with resilient fallback paths and variant-level stock management.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><div class=''w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-500/10 flex items-center justify-center mb-4''><svg class=''w-5 h-5 text-emerald-600 dark:text-emerald-400'' fill=''none'' stroke=''currentColor'' viewBox=''0 0 24 24''><path stroke-linecap=''round'' stroke-linejoin=''round'' stroke-width=''2'' d=''M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z''></path></svg></div><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Orders &amp; Invoices</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Full order lifecycle management, stable invoice numbering, printable documents, and exportable order reports.</p></div></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 4, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (6, 1, NULL, 1, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#1e1b4b\", \"position\": 0}, {\"color\": \"#0f172a\", \"position\": 50}, {\"color\": \"#020817\", \"position\": 100}], \"direction\": \"135deg\"}}, \"column_gap\": \"xl\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<div class=''rounded-3xl overflow-hidden border border-violet-500/20 bg-gradient-to-br from-white/5 to-violet-500/5 shadow-2xl p-6 backdrop-blur-sm''><img src=''/images/cortex-ai-square.webp'' alt=''NextBlock™ Cortex AI dashboard showing block generator'' class=''w-full h-auto rounded-2xl shadow-lg'' /><div class=''mt-4 grid grid-cols-3 gap-3 text-center''><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-violet-400''>OpenRouter</p><p class=''text-xs text-slate-400''>AI Gateway</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-violet-400''>BYOK</p><p class=''text-xs text-slate-400''>Cost Control</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-violet-400''>Zod</p><p class=''text-xs text-slate-400''>Typed Blocks</p></div></div></div>\"}, \"block_type\": \"text\"}], [{\"content\": {\"html_content\": \"<p class=''text-xs uppercase tracking-[0.25em] text-violet-400 font-bold mb-4''>Now Available — AI Copilot</p><h2 class=''text-4xl md:text-5xl font-bold text-white mb-6 leading-tight''>Supercharge Your<br/>Content with AI.</h2><p class=''text-lg text-slate-300 max-w-2xl leading-relaxed mb-8''>NextBlock™ Cortex AI brings native block-level intelligence directly to your editor. Generate copy, refactor structures, and automate translations in one click, built directly on our high-performance architecture.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"url\": \"/article/nextblock-cortex-ai-guide\", \"size\": \"lg\", \"text\": \"Explore AI Capabilities →\", \"variant\": \"default\"}, \"block_type\": \"button\"}, {\"content\": {\"url\": \"https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license\", \"size\": \"lg\", \"text\": \"Get a License\", \"variant\": \"outline\"}, \"block_type\": \"button\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 2}, \"vertical_alignment\": \"center\"}', 5, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (7, 1, NULL, 1, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"none\"}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"level\": 2, \"textAlign\": \"center\", \"text_content\": \"More Than a CMS. An Ecosystem.\"}, \"block_type\": \"heading\"}, {\"content\": {\"html_content\": \"<p class=''text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto''>NextBlock™ is building a sustainable open-core roadmap so the platform grows with your business.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<div class=''grid gap-6 lg:grid-cols-[0.75fr_1.25fr] mt-10 items-stretch''><div class=''overflow-hidden rounded-[2rem] border border-slate-200 dark:border-white/10 bg-slate-950 shadow-2xl''><img src=''/images/goals.webp'' alt=''Roadmap board outlining the NextBlock™ ecosystem and premium module direction'' class=''h-full w-full object-cover'' /><div class=''border-t border-white/10 bg-slate-950/95 px-6 py-5''><p class=''text-xs uppercase tracking-[0.24em] text-emerald-300 mb-2 font-bold''>Roadmap in motion</p><p class=''text-sm text-slate-300 mb-0''>Commerce ships first, then the broader ecosystem grows around plugins, blocks, and partner-built modules.</p></div></div><div class=''grid gap-6''><div class=''p-10 rounded-3xl border border-emerald-500/20 bg-gradient-to-br from-emerald-50 to-white dark:from-emerald-500/5 dark:to-white/5 hover:border-emerald-500/40 transition-colors''><p class=''text-xs uppercase tracking-wide text-emerald-600 dark:text-emerald-400 mb-2 font-bold''>Available now</p><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>NextBlock™ Commerce</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Transform your site into a composable storefront with products, checkout, multi-currency pricing, tax automation, and commerce blocks that live beside your editorial content.</p></div><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-violet-500/30 transition-colors''><p class=''text-xs uppercase tracking-wide text-violet-700 dark:text-violet-300 mb-2 font-bold''>Build the future</p><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Plugin and block marketplace</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>A community marketplace gives developers room to publish, sell, and distribute custom blocks, themes, integrations, and partner modules.</p></div></div></div>\"}, \"block_type\": \"text\"}, {\"content\": {\"level\": 2, \"textAlign\": \"center\", \"text_content\": \"Join Our Community.\"}, \"block_type\": \"heading\"}, {\"content\": {\"html_content\": \"<p class=''text-slate-600 dark:text-slate-400 text-center mx-auto''>NextBlock™ is being built in the open. Star the repo, share feedback, and help define the future of performance-first content management.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<div class=''grid gap-4 md:grid-cols-3 mt-10 text-sm''><a class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 transition-all hover:scale-[1.02]'' href=''https://github.com/nextblock-cms'' target=''_blank'' rel=''noopener noreferrer''><strong class=''block text-base text-slate-900 dark:text-white mb-1''>GitHub</strong><span class=''text-slate-600 dark:text-slate-400''>Star the repo &amp; contribute</span></a><a class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 transition-all hover:scale-[1.02]'' href=''https://x.com/NextBlockCMS'' target=''_blank'' rel=''noopener noreferrer''><strong class=''block text-base text-slate-900 dark:text-white mb-1''>X (Twitter)</strong><span class=''text-slate-600 dark:text-slate-400''>Follow updates &amp; announcements</span></a><a class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 transition-all hover:scale-[1.02]'' href=''https://dev.to/nextblockcms'' target=''_blank'' rel=''noopener noreferrer''><strong class=''block text-base text-slate-900 dark:text-white mb-1''>Dev.to</strong><span class=''text-slate-600 dark:text-slate-400''>Read technical deep dives</span></a></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 6, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (1, 1, NULL, 1, 'section', '{\"is_hero\": true, \"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#020817\", \"position\": 0}, {\"color\": \"#0f172a\", \"position\": 50}, {\"color\": \"#1e293b\", \"position\": 100}], \"direction\": \"135deg\"}}, \"column_gap\": \"xl\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<h1 class=''text-5xl md:text-6xl font-extrabold tracking-tight text-white text-center leading-tight''>Build <span class=''relative inline-block mx-1 group''><span class=''absolute inset-0 bg-gradient-to-r from-blue-600 to-cyan-400 translate-y-1 md:translate-y-2 transform -skew-x-12 rounded-sm shadow-lg group-hover:skew-x-0 transition-transform duration-300 ease-out''></span><span class=''relative text-white italic px-1''>Blazing-Fast</span></span><br class=''md:hidden'' /> Websites.</h1>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<p class=''text-xl text-slate-300 text-center max-w-3xl mx-auto mt-4 leading-relaxed''>NextBlock™ is the open-source, developer-first Next.js CMS that merges 100% Lighthouse scores with a powerful visual block editor.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"url\": \"/article/how-to-setup-nextblock\", \"size\": \"lg\", \"text\": \"Get Started\", \"variant\": \"default\", \"position\": \"center\"}, \"block_type\": \"button\"}, {\"content\": {\"url\": \"https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&project-name=nextblock&repository-name=nextblock&stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D\", \"size\": \"lg\", \"text\": \"Deploy on Vercel\", \"variant\": \"outline\", \"position\": \"center\"}, \"block_type\": \"button\"}, {\"content\": {\"html_content\": \"<div class=''flex flex-wrap justify-center gap-6 text-sm uppercase tracking-wide text-slate-400 mt-8''><a href=''https://github.com/nextblock-cms'' target=''_blank'' rel=''noopener noreferrer'' class=''hover:text-white transition-colors''>GitHub</a><a href=''https://x.com/NextBlockCMS'' target=''_blank'' rel=''noopener noreferrer'' class=''hover:text-white transition-colors''>X</a><a href=''https://www.linkedin.com/in/nextblock/'' target=''_blank'' rel=''noopener noreferrer'' class=''hover:text-white transition-colors''>LinkedIn</a><a href=''https://dev.to/nextblockcms'' target=''_blank'' rel=''noopener noreferrer'' class=''hover:text-white transition-colors''>Dev.to</a><a href=''https://www.npmjs.com/~nextblockcms'' target=''_blank'' rel=''noopener noreferrer'' class=''hover:text-white transition-colors''>npm</a></div>\"}, \"block_type\": \"text\"}], [{\"content\": {\"html_content\": \"<div class=''p-10 border border-white/10 rounded-3xl bg-white/5 backdrop-blur-xl shadow-2xl relative overflow-hidden group''><div class=''absolute inset-0 bg-gradient-to-br from-blue-500/10 to-purple-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-500''></div><div class=''relative z-10''><p class=''text-xs text-white uppercase tracking-widest font-semibold mb-2''>Why teams switch</p><p class=''text-3xl font-bold text-white mb-2''>100% Lighthouse</p><p class=''text-base text-slate-300 mb-6''>Edge-rendered marketing sites, launches, and docs with uncompromising performance.</p><ul class=''space-y-3 text-sm text-slate-200''><li><span class=''text-blue-400 mr-2''>&#10003;</span> Next.js 16 with ISR and edge caching</li><li><span class=''text-blue-400 mr-2''>&#10003;</span> Supabase auth, data, and storage</li><li><span class=''text-blue-400 mr-2''>&#10003;</span> Notion-style block editor powered by Tiptap</li></ul><div class=''mt-6 rounded-2xl overflow-hidden border border-white/10 shadow-lg''><div class=''relative w-full aspect-video''><iframe class=''absolute inset-0 h-full w-full border-0'' src=''https://www.youtube.com/embed/71MyfoL4YVM?si=jtlDXV6cSC8rDgz0'' title=''NextBlock demo video'' allow=''accelerated-motion; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'' referrerpolicy=''strict-origin-when-cross-origin'' loading=''lazy'' allowfullscreen></iframe></div></div></div></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 2}, \"vertical_alignment\": \"center\"}', 0, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.851553+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (10, 3, NULL, 1, 'posts_grid', '{\"title\": \"Latest Deep Dives\", \"columns\": 3, \"postsPerPage\": 6, \"showPagination\": true}', 1, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (12, 5, NULL, 1, 'section', '{\"padding\": {\"top\": \"lg\", \"bottom\": \"lg\"}, \"background\": {\"type\": \"none\"}, \"column_blocks\": [[{\"content\": {\"html_content\": \"<div class=''max-w-2xl mx-auto text-center''><h2 class=''text-2xl font-bold mb-4''>Open Source & Community Driven</h2><p class=''text-slate-600 dark:text-slate-400 mb-6''>NextBlock™ is built in the open. We rely on developers and editors like you to help us define the roadmap. Whether it''s a bug report, a feature request, or just a shoutout, every message helps us move faster.</p></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 1, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (15, 6, NULL, 2, 'section', '{\"padding\": {\"top\": \"lg\", \"bottom\": \"lg\"}, \"background\": {\"type\": \"none\"}, \"column_blocks\": [[{\"content\": {\"html_content\": \"<div class=''max-w-2xl mx-auto text-center''><h2 class=''text-2xl font-bold mb-4''>Open Source & Communautaire</h2><p class=''text-slate-600 dark:text-slate-400 mb-6''>NextBlock™ est construit en public. Nous comptons sur les développeurs et éditeurs comme vous pour définir notre roadmap. Qu''il s''agisse d''un bug, d''une suggestion ou d''un simple salut, chaque message compte.</p></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 1, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (13, 5, NULL, 1, 'form', '{\"fields\": [{\"label\": \"Name\", \"temp_id\": \"name\", \"field_type\": \"text\", \"is_required\": true, \"placeholder\": \"Your name\"}, {\"label\": \"Email\", \"temp_id\": \"email\", \"field_type\": \"email\", \"is_required\": true, \"placeholder\": \"your@email.com\"}, {\"label\": \"Message\", \"temp_id\": \"message\", \"field_type\": \"textarea\", \"is_required\": true, \"placeholder\": \"How can we help?\"}], \"recipient_email\": \"contact@example.com\", \"success_message\": \"Thank you for your feedback! We''ll get back to you soon.\", \"submit_button_text\": \"Send Message\"}', 2, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.760236+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (16, 6, NULL, 2, 'form', '{\"fields\": [{\"label\": \"Nom\", \"temp_id\": \"nom\", \"field_type\": \"text\", \"is_required\": true, \"placeholder\": \"Votre nom\"}, {\"label\": \"Email\", \"temp_id\": \"email\", \"field_type\": \"email\", \"is_required\": true, \"placeholder\": \"votre@email.com\"}, {\"label\": \"Message\", \"temp_id\": \"message\", \"field_type\": \"textarea\", \"is_required\": true, \"placeholder\": \"Comment pouvons-nous vous aider ?\"}], \"recipient_email\": \"contact@example.com\", \"success_message\": \"Merci pour vos retours ! Nous vous répondrons bientôt.\", \"submit_button_text\": \"Envoyer le message\"}', 2, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.760236+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (17, 2, NULL, 2, 'section', '{\"is_hero\": true, \"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#020817\", \"position\": 0}, {\"color\": \"#0f172a\", \"position\": 50}, {\"color\": \"#1e293b\", \"position\": 100}], \"direction\": \"135deg\"}}, \"column_gap\": \"xl\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<h1 class=''text-5xl md:text-6xl font-bold tracking-tight text-white text-center drop-shadow-lg''>Créez des sites <span class=''relative inline-block mx-1 group''><span class=''absolute inset-0 bg-gradient-to-r from-blue-600 to-cyan-400 translate-y-1 md:translate-y-2 transform -skew-x-12 rounded-sm shadow-lg group-hover:skew-x-0 transition-transform duration-300 ease-out''></span><span class=''relative text-white italic px-1''>Ultra-Rapides</span></span><br class=''md:hidden'' />.</h1>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<p class=''text-xl text-slate-300 text-center max-w-3xl mx-auto mt-4 leading-relaxed''>NextBlock™ est le CMS Next.js open-source alliant scores Lighthouse parfaits et éditeur visuel puissant.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"url\": \"/article/comment-configurer-nextblock\", \"size\": \"lg\", \"text\": \"Commencer\", \"variant\": \"default\", \"position\": \"center\"}, \"block_type\": \"button\"}, {\"content\": {\"url\": \"https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&project-name=nextblock&repository-name=nextblock&stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D\", \"size\": \"lg\", \"text\": \"Déployer sur Vercel\", \"variant\": \"outline\", \"position\": \"center\"}, \"block_type\": \"button\"}, {\"content\": {\"html_content\": \"<div class=''flex flex-wrap justify-center gap-6 text-sm uppercase tracking-wide text-slate-400 mt-8''><a href=''https://github.com/nextblock-cms'' target=''_blank'' rel=''noopener noreferrer'' class=''hover:text-white transition-colors''>GitHub</a><a href=''https://x.com/NextBlockCMS'' target=''_blank'' rel=''noopener noreferrer'' class=''hover:text-white transition-colors''>X</a><a href=''https://www.linkedin.com/in/nextblock/'' target=''_blank'' rel=''noopener noreferrer'' class=''hover:text-white transition-colors''>LinkedIn</a><a href=''https://dev.to/nextblockcms'' target=''_blank'' rel=''noopener noreferrer'' class=''hover:text-white transition-colors''>Dev.to</a><a href=''https://www.npmjs.com/~nextblockcms'' target=''_blank'' rel=''noopener noreferrer'' class=''hover:text-white transition-colors''>npm</a></div>\"}, \"block_type\": \"text\"}], [{\"content\": {\"html_content\": \"<div class=''p-10 border border-white/10 rounded-3xl bg-white/5 backdrop-blur-xl shadow-2xl relative overflow-hidden group''><div class=''absolute inset-0 bg-gradient-to-br from-blue-500/10 to-purple-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-500''></div><div class=''relative z-10''><p class=''text-xs text-white uppercase tracking-widest font-semibold mb-2''>Pourquoi migrer</p><p class=''text-3xl font-bold text-white mb-2''>100% Lighthouse</p><p class=''text-base text-slate-300 mb-6''>Sites marketing et docs rendus à l''edge avec des performances irréprochables.</p><ul class=''space-y-3 text-sm text-slate-200''><li><span class=''text-blue-400 mr-2''>&#10003;</span> Next.js 16 avec ISR et cache edge</li><li><span class=''text-blue-400 mr-2''>&#10003;</span> Supabase pour l''auth, les données et le stockage</li><li><span class=''text-blue-400 mr-2''>&#10003;</span> Éditeur de blocs type Notion sur Tiptap</li></ul><div class=''mt-6 rounded-2xl overflow-hidden border border-white/10 shadow-lg''><div class=''relative w-full aspect-video''><iframe class=''absolute inset-0 h-full w-full border-0'' src=''https://www.youtube.com/embed/71MyfoL4YVM?si=jtlDXV6cSC8rDgz0'' title=''Vidéo de démonstration NextBlock'' allow=''accelerated-motion; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'' referrerpolicy=''strict-origin-when-cross-origin'' loading=''lazy'' allowfullscreen></iframe></div></div></div></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 2}, \"vertical_alignment\": \"center\"}', 0, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.851553+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (18, 2, NULL, 2, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"none\"}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"level\": 2, \"textAlign\": \"center\", \"text_content\": \"Fonctionnalités clés : les trois piliers de NextBlock™\"}, \"block_type\": \"heading\"}, {\"content\": {\"html_content\": \"<p class=''text-lg text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto''>NextBlock™ unifie performances, expérience éditoriale et contrôle développeur pour que chaque équipe livre son meilleur travail.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<div class=''grid gap-8 md:grid-cols-3 mt-12''><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 backdrop-blur-sm hover:bg-slate-100 dark:hover:bg-white/10 transition-colors duration-300''><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Vitesse Extrême.</h3><p class=''text-sm text-slate-600 dark:text-slate-400 leading-relaxed''>Pensé pour des scores Lighthouse parfaits avec une diffusion mondiale.</p><ul class=''mt-6 space-y-3 text-sm text-slate-600 dark:text-slate-400''><li><strong class=''text-slate-800 dark:text-slate-200''>Edge Caching:</strong> Servez vos pages partout.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Critical CSS:</strong> Styles en ligne pour éviter les blocages.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Images Opt:</strong> AVIF et placeholders floutés.</li></ul></div><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 backdrop-blur-sm hover:bg-slate-100 dark:hover:bg-white/10 transition-colors duration-300''><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Expérience Éditeur.</h3><p class=''text-sm text-slate-600 dark:text-slate-400 leading-relaxed''>Un éditeur façon Notion pour publier sans dépendre des développeurs.</p><ul class=''mt-6 space-y-3 text-sm text-slate-600 dark:text-slate-400''><li><strong class=''text-slate-800 dark:text-slate-200''>Visuel:</strong> Héros, galeries, témoignages.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Média:</strong> Dossiers, tags et actions groupées.</li><li><strong class=''text-slate-800 dark:text-slate-200''>Historique:</strong> Restauration complète.</li></ul></div><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 backdrop-blur-sm hover:bg-slate-100 dark:hover:bg-white/10 transition-colors duration-300''><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Extensible à l''Infini.</h3><p class=''text-sm text-slate-700 dark:text-slate-200 leading-relaxed''>Un socle Next.js + Supabase modulaire, extensible et auto-hébergeable.</p><ul class=''mt-6 space-y-3 text-sm text-slate-700 dark:text-slate-200''><li><strong class=''text-slate-900 dark:text-white''>SDK de blocs:</strong> Composants typés.</li><li><strong class=''text-slate-900 dark:text-white''>CLI:</strong> Générez modules en minutes.</li><li><strong class=''text-slate-900 dark:text-white''>Monorepo Nx:</strong> Dépendances maintenables.</li></ul></div></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 1, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (19, 2, NULL, 2, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#0f172a\", \"position\": 0}, {\"color\": \"#020817\", \"position\": 100}], \"direction\": \"180deg\"}}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<h2 class=''text-3xl md:text-4xl font-bold text-white text-center mb-6''>Conçu avec les meilleurs outils.</h2>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<p class=''text-slate-400 text-center max-w-2xl mx-auto''>Chaque couche de NextBlock™ repose sur des technologies éprouvées pour une expérience familière et performante.</p><div class=''grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-8 gap-4 mt-10 text-sm font-semibold text-center text-white''><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Next.js</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>React</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Supabase</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Stripe</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Tailwind</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Tiptap</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Vercel</div><div class=''p-4 rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition-colors''>Nx</div></div>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<h2 class=''text-3xl md:text-4xl font-bold text-white text-center mb-6 mt-16''>Puissant pour les développeurs. Intuitif pour les éditeurs.</h2>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<div class=''grid md:grid-cols-2 gap-8 mt-10 text-white''><div class=''p-8 rounded-3xl border border-white/10 bg-white/5 backdrop-blur-sm''><h3 class=''text-xl font-bold mb-6 text-blue-400''>Pour les créateurs</h3><ul class=''space-y-4 text-sm text-slate-300''><li><strong class=''text-white block mb-1''>Éditeur de blocs</strong>Glisser-déposer façon Notion.</li><li><strong class=''text-white block mb-1''>Blocs riches</strong>Héros, galeries, témoignages.</li><li><strong class=''text-white block mb-1''>Médiathèque</strong>Dossiers, tags et actions groupées.</li><li><strong class=''text-white block mb-1''>Versions sécurisées</strong>Historique et restauration instantanée.</li></ul></div><div class=''p-8 rounded-3xl border border-white/10 bg-gradient-to-br from-white/5 to-white/[0.02] backdrop-blur-sm''><h3 class=''text-xl font-bold mb-6 text-purple-400''>Pour les développeurs</h3><ul class=''space-y-4 text-sm text-slate-300''><li><strong class=''text-white block mb-1''>Next.js 16</strong>Server Components, ISR et Edge prêts à l''emploi.</li><li><strong class=''text-white block mb-1''>Supabase</strong>Postgres, auth, stockage, temps réel.</li><li><strong class=''text-white block mb-1''>Monorepo Nx</strong>Dépendances lisibles et centrales.</li><li><strong class=''text-white block mb-1''>SDK de blocs</strong>Widgets typés et extensibles.</li></ul></div></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 2, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (20, 2, NULL, 2, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#022c22\", \"position\": 0}, {\"color\": \"#0f172a\", \"position\": 50}, {\"color\": \"#020817\", \"position\": 100}], \"direction\": \"135deg\"}}, \"column_gap\": \"xl\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<p class=''text-xs uppercase tracking-[0.25em] text-emerald-400 font-bold mb-4''>Disponible — Module Premium</p><h2 class=''text-4xl md:text-5xl font-bold text-white mb-6 leading-tight''>Transformez votre CMS<br/>en vitrine complète.</h2><p class=''text-lg text-slate-300 max-w-2xl leading-relaxed mb-8''>NextBlock™ Commerce transforme votre plateforme de contenu en moteur e-commerce complet. Produits, checkout, multi-devises, taxes, expédition, factures — le tout intégré nativement dans l''éditeur de blocs que vous connaissez déjà.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"url\": \"/article/guide-commerce-nextblock\", \"size\": \"lg\", \"text\": \"Découvrir Commerce →\", \"variant\": \"default\"}, \"block_type\": \"button\"}, {\"content\": {\"url\": \"https://nextblock.dev/product/nextblock-commerce-pro-commerce-license\", \"size\": \"lg\", \"text\": \"Obtenir une licence\", \"variant\": \"outline\"}, \"block_type\": \"button\"}], [{\"content\": {\"html_content\": \"<div class=''rounded-3xl overflow-hidden border border-emerald-500/20 bg-gradient-to-br from-white/5 to-emerald-500/5 shadow-2xl p-6 backdrop-blur-sm''><img src=''/images/commerce-square.webp'' alt=''Tableau de bord NextBlock™ Commerce'' class=''w-full h-auto rounded-2xl shadow-lg'' /><div class=''mt-4 grid grid-cols-3 gap-3 text-center''><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-emerald-400''>∞</p><p class=''text-xs text-slate-400''>Devises</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-emerald-400''>2</p><p class=''text-xs text-slate-400''>Fournisseurs</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-emerald-400''>Auto</p><p class=''text-xs text-slate-400''>Taxes</p></div></div></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 2}, \"vertical_alignment\": \"center\"}', 3, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (21, 2, NULL, 2, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"none\"}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"level\": 2, \"textAlign\": \"center\", \"text_content\": \"Tout pour vendre en ligne\"}, \"block_type\": \"heading\"}, {\"content\": {\"html_content\": \"<p class=''text-lg text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto mb-12''>NextBlock™ Commerce livre une boîte à outils e-commerce complète pour aller du catalogue au paiement sans plugins tiers.</p><div class=''grid gap-6 md:grid-cols-2 lg:grid-cols-3''><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Multi-Devises</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Taux de change en temps réel, modes d''arrondi, prix charme et synchronisation automatique sur toutes les devises.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Taxes Automatiques</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Taux manuels empilés (TPS + TVQ) ou calcul automatique via Stripe Tax — à vous de choisir.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Zones d''Expédition</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Résolution par pays et état, tarification par devise et seuils de livraison gratuite.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Stripe &amp; Freemius</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Stripe pour les produits physiques, Freemius pour les licences numériques — checkout intelligent avec validation d''inventaire.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Suivi d''Inventaire</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Déduction automatique des quantités au paiement avec gestion des stocks par variante.</p></div><div class=''p-8 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-emerald-500/30 transition-colors duration-300''><h3 class=''text-lg font-bold text-slate-900 dark:text-white mb-2''>Commandes &amp; Factures</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Gestion du cycle de vie des commandes, numérotation stable des factures et rapports de commandes exportables.</p></div></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 4, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (22, 2, NULL, 2, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#1e1b4b\", \"position\": 0}, {\"color\": \"#0f172a\", \"position\": 50}, {\"color\": \"#020817\", \"position\": 100}], \"direction\": \"135deg\"}}, \"column_gap\": \"xl\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<div class=''rounded-3xl overflow-hidden border border-violet-500/20 bg-gradient-to-br from-white/5 to-violet-500/5 shadow-2xl p-6 backdrop-blur-sm''><img src=''/images/cortex-ai-square.webp'' alt=''Tableau de bord NextBlock™ Cortex AI montrant le générateur de blocs'' class=''w-full h-auto rounded-2xl shadow-lg'' /><div class=''mt-4 grid grid-cols-3 gap-3 text-center''><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-violet-400''>OpenRouter</p><p class=''text-xs text-slate-400''>Passerelle IA</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-violet-400''>BYOK</p><p class=''text-xs text-slate-400''>Contrôle des coûts</p></div><div class=''p-3 rounded-xl bg-white/5 border border-white/10''><p class=''text-lg font-bold text-violet-400''>Zod</p><p class=''text-xs text-slate-400''>Blocs typés</p></div></div></div>\"}, \"block_type\": \"text\"}], [{\"content\": {\"html_content\": \"<p class=''text-xs uppercase tracking-[0.25em] text-violet-400 font-bold mb-4''>Disponible — Copilote IA</p><h2 class=''text-4xl md:text-5xl font-bold text-white mb-6 leading-tight''>Boostez votre<br/>contenu avec l''IA.</h2><p class=''text-lg text-slate-300 max-w-2xl leading-relaxed mb-8''>NextBlock™ Cortex AI apporte une intelligence native au niveau des blocs directement dans votre éditeur. Générez du texte, restructurez vos contenus et automatisez les traductions en un clic, le tout propulsé par notre architecture haute performance.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"url\": \"/article/nextblock-cortex-ai-guide\", \"size\": \"lg\", \"text\": \"Découvrir l''IA →\", \"variant\": \"default\"}, \"block_type\": \"button\"}, {\"content\": {\"url\": \"https://nextblock.dev/product/nextblock-cortex-ai-cortex-ai-license\", \"size\": \"lg\", \"text\": \"Obtenir une licence\", \"variant\": \"outline\"}, \"block_type\": \"button\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 2}, \"vertical_alignment\": \"center\"}', 5, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (23, 2, NULL, 2, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"none\"}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"level\": 2, \"textAlign\": \"center\", \"text_content\": \"Plus qu''un CMS. Un écosystème.\"}, \"block_type\": \"heading\"}, {\"content\": {\"html_content\": \"<p class=''text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto''>NextBlock™ construit une feuille de route open-core durable qui évolue avec votre activité.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<div class=''grid gap-6 lg:grid-cols-[0.75fr_1.25fr] mt-10 items-stretch''><div class=''overflow-hidden rounded-[2rem] border border-slate-200 dark:border-white/10 bg-slate-950 shadow-2xl''><img src=''/images/goals.webp'' alt=''Tableau de roadmap montrant la direction de l''ecosysteme NextBlock™ et des modules premium'' class=''h-full w-full object-cover'' /><div class=''border-t border-white/10 bg-slate-950/95 px-6 py-5''><p class=''text-xs uppercase tracking-[0.24em] text-emerald-300 mb-2 font-bold''>Roadmap en mouvement</p><p class=''text-sm text-slate-300 mb-0''>Le commerce arrive en premier, puis l''ecosysteme s''etend avec des plugins, des blocs et des modules construits par les partenaires.</p></div></div><div class=''grid gap-6''><div class=''p-10 rounded-3xl border border-emerald-500/20 bg-gradient-to-br from-emerald-50 to-white dark:from-emerald-500/5 dark:to-white/5 hover:border-emerald-500/40 transition-colors''><p class=''text-xs uppercase tracking-wide text-emerald-600 dark:text-emerald-400 mb-2 font-bold''>Disponible maintenant</p><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>NextBlock™ Commerce</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Transformez votre site en vitrine composable avec produits, checkout, tarification multi-devise, taxes automatiques et blocs commerce relies a votre contenu editorial.</p></div><div class=''p-10 rounded-3xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:border-violet-500/30 transition-colors''><p class=''text-xs uppercase tracking-wide text-violet-700 dark:text-violet-300 mb-2 font-bold''>Construire la suite</p><h3 class=''text-xl font-bold text-slate-900 dark:text-white mb-3''>Marketplace de plugins et blocs</h3><p class=''text-sm text-slate-600 dark:text-slate-400''>Une marketplace communautaire ouvrira la voie a la publication, la vente et la distribution de blocs, themes, integrations et modules partenaires.</p></div></div></div>\"}, \"block_type\": \"text\"}, {\"content\": {\"level\": 2, \"textAlign\": \"center\", \"text_content\": \"Rejoignez la communauté.\"}, \"block_type\": \"heading\"}, {\"content\": {\"html_content\": \"<p class=''text-slate-600 dark:text-slate-400 text-center max-w-3xl mx-auto''>NextBlock™ se construit en public. Ajoutez une étoile, partagez vos retours et façonnez l''avenir du CMS orienté performance.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<div class=''grid gap-4 md:grid-cols-3 mt-10 text-sm''><a class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 transition-all hover:scale-[1.02]'' href=''https://github.com/nextblock-cms'' target=''_blank'' rel=''noopener noreferrer''><strong class=''block text-base text-slate-900 dark:text-white mb-1''>GitHub</strong><span class=''text-slate-600 dark:text-slate-400''>Ajoutez une étoile &amp; contribuez</span></a><a class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 transition-all hover:scale-[1.02]'' href=''https://x.com/NextBlockCMS'' target=''_blank'' rel=''noopener noreferrer''><strong class=''block text-base text-slate-900 dark:text-white mb-1''>X (Twitter)</strong><span class=''text-slate-600 dark:text-slate-400''>Suivez les annonces</span></a><a class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 transition-all hover:scale-[1.02]'' href=''https://dev.to/nextblockcms'' target=''_blank'' rel=''noopener noreferrer''><strong class=''block text-base text-slate-900 dark:text-white mb-1''>Dev.to</strong><span class=''text-slate-600 dark:text-slate-400''>Lisez nos articles techniques</span></a></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 6, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (26, 4, NULL, 2, 'posts_grid', '{\"title\": \"Derniers articles\", \"columns\": 3, \"postsPerPage\": 6, \"showPagination\": true}', 1, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (9, 3, NULL, 1, 'section', '{\"is_hero\": true, \"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#020817\", \"position\": 0}, {\"color\": \"#1e293b\", \"position\": 100}], \"direction\": \"135deg\"}}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<p class=''text-sm uppercase tracking-[0.3em] text-blue-400 font-bold text-center md:text-left mb-4''>The Nextblock Journal</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<h2 class=''text-4xl md:text-5xl font-bold text-white text-center md:text-left mb-6''>Deep dives into performance, DX, and visual editing.</h2>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<p class=''text-slate-300 text-lg max-w-xl mx-auto md:mx-0 text-center md:text-left leading-relaxed''>Explore architectural walkthroughs, Supabase recipes, and block editor experiments written by the Nextblock core team.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"url\": \"/articles#latest\", \"size\": \"lg\", \"text\": \"Explore Articles\", \"variant\": \"default\"}, \"block_type\": \"button\"}, {\"content\": {\"url\": \"https://github.com/nextblock-cms/nextblock/discussions\", \"size\": \"lg\", \"text\": \"Subscribe for Updates\", \"variant\": \"outline\"}, \"block_type\": \"button\"}], [{\"content\": {\"html_content\": \"<div class=''h-full flex items-center justify-center rounded-3xl overflow-hidden border border-white/10 bg-white/5 shadow-2xl p-4 backdrop-blur-sm''><img src=''/images/developer.webp'' alt=''Developer working with the Nextblock stack'' class=''w-full object-cover rounded-2xl shadow-lg'' style=''max-width: 400px;'' /></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 2}}', 0, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (11, 5, NULL, 1, 'section', '{\"is_hero\": true, \"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#020817\", \"position\": 0}, {\"color\": \"#0f172a\", \"position\": 100}], \"direction\": \"135deg\"}}, \"column_blocks\": [[{\"content\": {\"level\": 1, \"textAlign\": \"center\", \"textColor\": \"white\", \"text_content\": \"Let''s Build the Future Together\"}, \"block_type\": \"heading\"}, {\"content\": {\"html_content\": \"<p class=''text-xl text-slate-300 text-center max-w-3xl mx-auto mt-4''>NextBlock™ is an open-source project driven by community feedback. We''d love to hear your thoughts, ideas, or questions.</p>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 0, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (14, 6, NULL, 2, 'section', '{\"is_hero\": true, \"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#020817\", \"position\": 0}, {\"color\": \"#0f172a\", \"position\": 100}], \"direction\": \"135deg\"}}, \"column_blocks\": [[{\"content\": {\"level\": 1, \"textAlign\": \"center\", \"textColor\": \"white\", \"text_content\": \"Bâtissons le futur ensemble\"}, \"block_type\": \"heading\"}, {\"content\": {\"html_content\": \"<p class=''text-xl text-slate-300 text-center max-w-3xl mx-auto mt-4''>NextBlock™ est un projet open-source propulsé par vos retours. Nous serions ravis d''entendre vos idées ou vos questions.</p>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 0, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (24, 2, NULL, 2, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#020817\", \"position\": 0}, {\"color\": \"#0f172a\", \"position\": 100}], \"direction\": \"180deg\"}}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<h2 class=''text-3xl md:text-4xl font-bold text-center text-white mb-4''>Des questions ?</h2>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<p class=''text-center text-base text-slate-300 max-w-2xl mx-auto''>NextBlock™ co-construit avec des partenaires : fonctionnalités, modules sponsorisés et direction produit.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"url\": \"/contact\", \"size\": \"lg\", \"text\": \"Nous contacter\", \"variant\": \"default\", \"position\": \"center\"}, \"block_type\": \"button\"}, {\"content\": {\"url\": \"https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&project-name=nextblock&repository-name=nextblock&stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D\", \"size\": \"lg\", \"text\": \"Déployer sur Vercel\", \"variant\": \"outline\", \"position\": \"center\"}, \"block_type\": \"button\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 7, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.840619+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (25, 4, NULL, 2, 'section', '{\"is_hero\": true, \"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#020817\", \"position\": 0}, {\"color\": \"#1e293b\", \"position\": 100}], \"direction\": \"135deg\"}}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<p class=''text-sm uppercase tracking-[0.3em] text-blue-400 font-bold text-center md:text-left mb-4''>Le journal Nextblock</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<h2 class=''text-4xl md:text-5xl font-bold text-white text-center md:text-left mb-6''>Plongées dans la performance, l''expérience dev et l''édition visuelle.</h2>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<p class=''text-lg max-w-xl mx-auto md:mx-0 text-center md:text-left text-slate-300 leading-relaxed''>Walkthroughs d''architecture, recettes Supabase et expérimentations éditeur écrits par l''équipe Nextblock.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"url\": \"/articles#latest\", \"size\": \"lg\", \"text\": \"Explorer les articles\", \"variant\": \"default\"}, \"block_type\": \"button\"}, {\"content\": {\"url\": \"https://github.com/nextblock-cms/nextblock/discussions\", \"size\": \"lg\", \"text\": \"S''abonner aux mises à jour\", \"variant\": \"outline\"}, \"block_type\": \"button\"}], [{\"content\": {\"html_content\": \"<div class=''rounded-3xl overflow-hidden border border-white/10 bg-white/5 shadow-2xl p-4 backdrop-blur-sm''><img src=''/images/developer.webp'' alt=''Développeur travaillant avec la stack Nextblock'' class=''w-full object-cover rounded-2xl shadow-lg'' style=''max-width: 400px;'' /></div>\"}, \"block_type\": \"text\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 2}}', 0, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (31, NULL, 5, 1, 'text', '{\"html_content\": \"<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock™ Commerce is the first premium module in the ecosystem: a source-available storefront layer that plugs directly into the same editorial system as the CMS. It is built for teams that want content, catalog, and checkout to live inside one product surface instead of three disconnected tools.</p>\\n\\n<div class=''grid gap-4 md:grid-cols-3 my-10''>\\n <div class=''rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Commerce core</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Catalog + checkout</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Products, variants, orders, shipping, and invoices all plug into the existing CMS shell.</p>\\n </div>\\n <div class=''rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200''>Global selling</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Multi-currency ready</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Automatic FX sync, rounding strategies, and per-product overrides keep international pricing practical.</p>\\n </div>\\n <div class=''rounded-3xl border border-indigo-200/70 bg-indigo-50/70 p-6 dark:border-indigo-500/20 dark:bg-indigo-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:text-indigo-200''>Operator workflow</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Provider-aware flow</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Stripe and Freemius are handled differently so the storefront can stay clean without hiding complexity.</p>\\n </div>\\n</div>\\n\\n<h2>Product Catalog</h2>\\n<p>Commerce supports physical and digital products with variants, attributes, localized product media, independent pricing, SKUs, and stock levels. Product assets stay in the same media library editors already use for marketing pages, so content and commerce teams are not working in separate silos.</p>\\n\\n<h2>Multi-Currency Engine</h2>\\n<p>The pricing engine is built for real-world stores, not just a demo checkout:</p>\\n<ul>\\n <li><strong>Unlimited currencies</strong> with ISO codes, symbols, and stored exchange rates</li>\\n <li><strong>Automatic FX sync</strong> from Frankfurter or a custom provider via <code>FX_API_BASE_URL</code></li>\\n <li><strong>Rounding modes</strong> including nearest, up, down, and charm pricing like <code>9.99</code></li>\\n <li><strong>Store-managed auto-sync</strong> so product prices convert when rates refresh</li>\\n <li><strong>Rebasing</strong> when the default currency changes</li>\\n <li><strong>Per-product overrides</strong> when a catalog item needs explicit pricing in specific markets</li>\\n</ul>\\n\\n<h2>Tax Automation</h2>\\n<p>Teams can stay manual when they need control, or delegate tax math to Stripe Tax when they want automation:</p>\\n<div class=''grid md:grid-cols-2 gap-6 my-6''>\\n <div class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5''>\\n <h4 class=''font-bold text-slate-900 dark:text-white mb-2''>Manual mode</h4>\\n <p class=''text-sm text-slate-600 dark:text-slate-400''>Define rates by country and optional state or province. Stacked taxes such as GST + PST are supported, and tax lines are stored in <code>orders.tax_details</code>.</p>\\n </div>\\n <div class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5''>\\n <h4 class=''font-bold text-slate-900 dark:text-white mb-2''>Automatic mode</h4>\\n <p class=''text-sm text-slate-600 dark:text-slate-400''>Stripe Tax calculates the final amounts. Product and shipping tax codes travel with the line items, and the webhook resync replaces provisional values with final totals.</p>\\n </div>\\n</div>\\n\\n<h2>Shipping and Checkout</h2>\\n<p>Shipping zones match by country and state or province, support localized method names, per-currency pricing, free-shipping thresholds, and priority-based fallbacks when an exact match is not found.</p>\\n<p>The checkout layer is provider-aware:</p>\\n<ul>\\n <li><strong>Stripe</strong> handles physical goods, inventory checks, shipping calculation, tax, customer upserts, and Checkout Sessions</li>\\n <li><strong>Freemius</strong> handles digital licensing, plan resolution, and checkout URLs with sandbox support</li>\\n <li>Mixed-provider carts are rejected so the buyer journey stays understandable</li>\\n</ul>\\n\\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\\n <img src=''/images/commerce-plan.webp'' alt=''Commerce roadmap board outlining premium module goals and future storefront capabilities for NextBlock™'' class=''w-full h-auto object-cover'' />\\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>Commerce is positioned as the first premium module in a larger roadmap, which makes it feel like part of a growing platform instead of a bolt-on add-on.</figcaption>\\n</figure>\\n\\n<h2>Inventory, Orders, and Invoices</h2>\\n<p>When quantity tracking is enabled, checkout validates requested quantities against <code>inventory_items</code>. On payment confirmation, <code>apply_order_inventory_deduction()</code> reduces stock with a resilient fallback path that can use direct SQL if the RPC layer fails.</p>\\n<ul>\\n <li>Order statuses move from <code>pending</code> to <code>paid</code> to <code>shipped</code>, with cancellation and refund states available too</li>\\n <li>Invoice numbering is generated through database functions for consistency</li>\\n <li>Printable invoice documents pull from <code>invoice_settings</code></li>\\n <li>Customers can review order history and invoice access from the storefront side</li>\\n <li><strong>Coming soon:</strong> exportable order reporting and analytics dashboards</li>\\n</ul>\\n\\n<h2>Commerce Surfaces Inside the CMS</h2>\\n<p>When the ecommerce package is active, the CMS exposes product list, create, and edit views with media and variants, inventory management, order detail screens, shipping configuration, payment provider settings, tax setup, and currency management. The important part is not only that those screens exist, but that they feel native inside the same shell your content team is already using.</p>\"}', 0, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (32, NULL, 6, 2, 'text', '{\"html_content\": \"<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock™ Commerce est le premier module premium de l''ecosysteme : une couche storefront source-available qui se branche directement sur le meme systeme editorial que le CMS. L''objectif est de rapprocher contenu, catalogue et checkout dans une seule surface produit.</p>\\n\\n<div class=''grid gap-4 md:grid-cols-3 my-10''>\\n <div class=''rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Base commerce</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Catalogue + checkout</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Produits, variantes, commandes, livraison et factures vivent dans le meme shell CMS.</p>\\n </div>\\n <div class=''rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200''>Vente globale</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Multi-devise</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Sync FX automatique, strategies d''arrondi et overrides par produit rendent les prix internationaux realistes.</p>\\n </div>\\n <div class=''rounded-3xl border border-indigo-200/70 bg-indigo-50/70 p-6 dark:border-indigo-500/20 dark:bg-indigo-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:text-indigo-200''>Workflow operateur</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Par fournisseur</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Stripe et Freemius sont traites differemment pour garder un parcours d''achat propre sans cacher la complexite.</p>\\n </div>\\n</div>\\n\\n<h2>Catalogue produits</h2>\\n<p>Le module gere produits physiques et numeriques avec variantes, attributs, medias localises, prix independants, SKU et niveaux de stock. Les assets produits restent dans la meme bibliotheque media que les pages marketing, ce qui evite de separer equipes contenu et equipes commerce.</p>\\n\\n<h2>Moteur multi-devise</h2>\\n<p>Le moteur tarifaire vise un vrai usage boutique, pas seulement une demo :</p>\\n<ul>\\n <li><strong>Devises illimitees</strong> avec codes ISO, symboles et taux stockes</li>\\n <li><strong>Synchronisation FX automatique</strong> depuis Frankfurter ou un provider custom via <code>FX_API_BASE_URL</code></li>\\n <li><strong>Modes d''arrondi</strong> dont nearest, up, down et prix charme comme <code>9.99</code></li>\\n <li><strong>Auto-sync magasin</strong> pour convertir les prix quand les taux changent</li>\\n <li><strong>Rebasement</strong> lorsqu''on change la devise par defaut</li>\\n <li><strong>Overrides par produit</strong> quand un article demande un prix fixe sur certains marches</li>\\n</ul>\\n\\n<h2>Taxes automatiques</h2>\\n<p>Les equipes peuvent rester en mode manuel ou confier le calcul a Stripe Tax :</p>\\n<div class=''grid md:grid-cols-2 gap-6 my-6''>\\n <div class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5''>\\n <h4 class=''font-bold text-slate-900 dark:text-white mb-2''>Mode manuel</h4>\\n <p class=''text-sm text-slate-600 dark:text-slate-400''>Definition des taux par pays et eventuellement par province. Les taxes empilees comme TPS + TVQ sont supportees, avec stockage dans <code>orders.tax_details</code>.</p>\\n </div>\\n <div class=''p-6 rounded-2xl border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5''>\\n <h4 class=''font-bold text-slate-900 dark:text-white mb-2''>Mode automatique</h4>\\n <p class=''text-sm text-slate-600 dark:text-slate-400''>Stripe Tax calcule les montants finaux. Les codes fiscaux voyagent avec les line items et le webhook remplace les valeurs provisoires par les montants definitifs.</p>\\n </div>\\n</div>\\n\\n<h2>Livraison et checkout</h2>\\n<p>Les zones de livraison correspondent par pays et etat ou province, gerent des noms localises, des prix par devise, des seuils de livraison gratuite, et des fallbacks par priorite quand aucune correspondance exacte n''est trouvee.</p>\\n<p>Le checkout est conscient du fournisseur :</p>\\n<ul>\\n <li><strong>Stripe</strong> gere les biens physiques, les verifications d''inventaire, la livraison, les taxes, les clients et les Checkout Sessions</li>\\n <li><strong>Freemius</strong> gere les licences numeriques, la resolution des plans et les URLs de checkout avec support sandbox</li>\\n <li>Les paniers melangeant plusieurs fournisseurs sont refuses pour garder un parcours plus clair</li>\\n</ul>\\n\\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\\n <img src=''/images/commerce-plan.webp'' alt=''Tableau de roadmap commerce montrant les objectifs premium et les futures capacites storefront de NextBlock™'' class=''w-full h-auto object-cover'' />\\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>Le commerce est presente comme le premier module premium d''une feuille de route plus large, ce qui renforce l''idee d''une vraie plateforme en croissance.</figcaption>\\n</figure>\\n\\n<h2>Inventaire, commandes et factures</h2>\\n<p>Quand le suivi des quantites est actif, le checkout valide les demandes contre <code>inventory_items</code>. A la confirmation du paiement, <code>apply_order_inventory_deduction()</code> retire le stock avec un chemin de repli resilient si la couche RPC echoue.</p>\\n<ul>\\n <li>Les statuts de commande passent de <code>pending</code> a <code>paid</code> puis <code>shipped</code>, avec annulation et remboursement si besoin</li>\\n <li>La numerotation des factures est geree par des fonctions SQL pour rester coherente</li>\\n <li>Les documents facture tirent leurs informations de <code>invoice_settings</code></li>\\n <li>Les clients peuvent consulter leur historique et leurs factures</li>\\n <li><strong>Bientot :</strong> exports de commandes et tableaux de bord analytiques</li>\\n</ul>\\n\\n<h2>Surfaces commerce dans le CMS</h2>\\n<p>Quand le package ecommerce est actif, le CMS expose les vues produit, edition avec medias et variantes, gestion d''inventaire, detail des commandes, configuration livraison, parametres de paiement, taxes et devises. L''enjeu principal est que tout cela paraisse natif dans le meme shell que l''equipe contenu utilise deja.</p>\"}', 0, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (33, NULL, 7, 1, 'text', '{\"html_content\": \"\\n<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock Cortex AI is the AI layer built for the way NextBlock pages are actually composed. Instead of treating your site like a blank document, it understands blocks, sections, editor constraints, and the model choices your team wants to use.</p>\\n\\n<div class=''grid gap-4 md:grid-cols-3 my-10''>\\n <div class=''rounded-3xl border border-violet-200/70 bg-violet-50/80 p-6 dark:border-violet-500/20 dark:bg-violet-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Model routing</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Pick the right model</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Route generation through OpenRouter or your configured provider strategy so each task can balance speed, quality, and cost.</p>\\n </div>\\n <div class=''rounded-3xl border border-sky-200/70 bg-sky-50/80 p-6 dark:border-sky-500/20 dark:bg-sky-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200''>BYOK control</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Use your own keys</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Keep provider credentials under your control while still giving editors a clean, guided AI workflow in the CMS.</p>\\n </div>\\n <div class=''rounded-3xl border border-emerald-200/70 bg-emerald-50/80 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Typed output</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Generate valid blocks</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Schema-aware generation helps AI output land as usable page structure instead of loose copy that needs a rebuild.</p>\\n </div>\\n</div>\\n\\n<h2>Why Cortex AI Belongs Inside the Editor</h2>\\n<p>Generic chat tools can draft copy, but they do not know the difference between a hero, a card grid, a product description, and a localized article. Cortex AI lives closer to the editing surface, so generation can respect the same block contracts that render on the public site.</p>\\n<p>That makes AI useful for practical production work: drafting a landing section, tightening an excerpt, expanding a product story, translating a post, or reshaping a rough idea into a block layout that already fits the system.</p>\\n\\n<div class=''rounded-[2rem] border border-slate-200/80 bg-slate-50/90 p-6 my-10 dark:border-white/10 dark:bg-slate-900/70''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Editorial workflow</p>\\n <div class=''grid gap-5 md:grid-cols-2 mt-5''>\\n <div class=''rounded-2xl border border-slate-200 bg-white p-5 dark:border-white/10 dark:bg-slate-950/50''>\\n <h3 class=''mt-0 text-xl text-slate-900 dark:text-white''>Faster first drafts</h3>\\n <p class=''text-sm text-slate-600 dark:text-slate-300''>Start with a structured prompt and get a section, article outline, or product narrative that already matches the site voice.</p>\\n </div>\\n <div class=''rounded-2xl border border-slate-200 bg-white p-5 dark:border-white/10 dark:bg-slate-950/50''>\\n <h3 class=''mt-0 text-xl text-slate-900 dark:text-white''>Cleaner revisions</h3>\\n <p class=''text-sm text-slate-600 dark:text-slate-300''>Ask for shorter, clearer, more technical, more polished, or locale-ready output without leaving the content screen.</p>\\n </div>\\n </div>\\n</div>\\n\\n<h2>Model Routing and Cost Control</h2>\\n<p>Cortex AI is designed around provider choice. Teams can route requests through the configured AI gateway, choose task-appropriate models, and keep bring-your-own-key setups separate from the editor experience. Editors get a simple control surface while developers keep the operational knobs.</p>\\n<ul>\\n <li>Use fast, inexpensive models for drafts, variations, and rewrites</li>\\n <li>Reserve stronger models for long-form strategy, technical copy, or difficult transformations</li>\\n <li>Keep provider keys and routing defaults in the server-side configuration layer</li>\\n <li>Make cost and quality decisions without changing the public rendering system</li>\\n</ul>\\n\\n<h2>Block-Aware Generation</h2>\\n<p>The important shift is that Cortex AI is not just writing text. It is meant to produce content that can map back to NextBlock surfaces: section copy, article bodies, product descriptions, headings, calls to action, and localized variants. That reduces the cleanup step that usually happens after copying AI text from a separate tool.</p>\\n<p>Because the output is shaped around existing block contracts, the generated content feels native in the editor and predictable on the frontend.</p>\\n\\n<h2>Safer Team Workflows</h2>\\n<p>AI works best when it is helpful without becoming invisible infrastructure. Cortex AI keeps humans in the loop: editors review the output, developers control the available providers, and the CMS keeps the generated content inside the same revision and publishing flow as everything else.</p>\\n\\n<h2>A Practical Launch Flow</h2>\\n<ol>\\n <li>Draft the article, landing section, or product story from a focused prompt.</li>\\n <li>Refine the result against the brand voice and target audience.</li>\\n <li>Generate a localized version or shorter excerpt for cards and metadata.</li>\\n <li>Review the content in the NextBlock editor, publish, and keep iterating through normal revisions.</li>\\n</ol>\\n\\n<p>Cortex AI turns the CMS into a more capable production surface: not a replacement for editorial judgment, but a faster way to shape good ideas into structured pages that are ready to ship.</p>\\n\"}', 0, '2026-07-03 17:52:15.643901+00', '2026-07-03 17:52:15.643901+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (34, 7, NULL, 1, 'text', '{\"html_content\": \"\\n<h1>Privacy Policy</h1>\\n<p><em>Last updated: June 4, 2026</em></p>\\n<p>NextBlock™ CMS (\\\"we\\\", \\\"us\\\", or \\\"our\\\") respects your privacy and is committed to protecting your personal information in accordance with Quebec''s <em>Act respecting the protection of personal information in the private sector</em> (Law 25), the federal <em>Personal Information Protection and Electronic Documents Act</em> (PIPEDA), and Canada''s Anti-Spam Legislation (CASL).</p>\\n\\n<h2>1. Person responsible for personal information</h2>\\n<p>Our Privacy Officer is responsible for our compliance with applicable privacy laws. You may reach them at <a href=\\\"mailto:{{privacy_email}}\\\">{{privacy_email}}</a>.</p>\\n\\n<h2>2. What we collect</h2>\\n<ul>\\n <li><strong>Account information</strong> &mdash; name, email address, and credentials when you register.</li>\\n <li><strong>Usage and device data</strong> &mdash; collected only with your consent through analytics technologies.</li>\\n <li><strong>Communications</strong> &mdash; messages you send us and your marketing preferences.</li>\\n</ul>\\n\\n<h2>3. Why we collect it and your consent</h2>\\n<p>We collect personal information for clearly identified purposes: to provide and secure our services, to communicate with you, and &mdash; only with your express, opt-in consent &mdash; for analytics and marketing. Consistent with Law 25, non-essential cookies and trackers remain disabled until you actively accept them, and you may withdraw your consent at any time.</p>\\n\\n<h2>4. Cookies and tracking technologies</h2>\\n<p>Strictly necessary cookies keep the site working and require no consent. Analytics and marketing technologies are loaded <strong>only after</strong> you opt in through our consent banner. Your choice is recorded so we can honour it and demonstrate accountability.</p>\\n\\n<h2>5. Disclosure and sharing</h2>\\n<p>We do not sell your personal information. We share it only with service providers who help us operate the platform under contractual confidentiality obligations, or where required by law.</p>\\n\\n<h2>6. Retention</h2>\\n<p>We keep personal information only for as long as necessary to fulfil the purposes described above or as required by law, after which it is securely destroyed or anonymized.</p>\\n\\n<h2>7. Your rights</h2>\\n<p>Subject to applicable law, you have the right to access, rectify, and delete your personal information, to withdraw consent, to data portability, and to be informed about automated processing. To exercise these rights, contact our Privacy Officer at <a href=\\\"mailto:{{privacy_email}}\\\">{{privacy_email}}</a>.</p>\\n\\n<h2>8. Commercial electronic messages (CASL)</h2>\\n<p>We send commercial electronic messages only with your consent. Every message identifies us and includes a working unsubscribe mechanism that we honour promptly.</p>\\n\\n<h2>9. Safeguards</h2>\\n<p>We use appropriate physical, organizational, and technological measures &mdash; including encryption in transit and access controls &mdash; to protect personal information against loss, theft, and unauthorized access.</p>\\n\\n<h2>10. Open-source software</h2>\\n<p>NextBlock™ CMS is free, open-source software distributed under the GNU Affero General Public License v3. When you self-host NextBlock, you are the operator responsible for the personal information processed by your own deployment, and this policy serves as a starting point you may adapt to your organization.</p>\\n\\n<h2>11. Changes to this policy</h2>\\n<p>We may update this policy from time to time. Material changes will be communicated through the site, and the \\\"last updated\\\" date will be revised.</p>\\n\\n<h2>12. Contact us</h2>\\n<p>Questions or complaints? Contact NextBlock™ CMS at <a href=\\\"mailto:{{privacy_email}}\\\">{{privacy_email}}</a>. You may also contact the Commission d''accès à l''information du Québec or the Office of the Privacy Commissioner of Canada.</p>\\n\"}', 0, '2026-07-03 17:52:15.699946+00', '2026-07-03 17:52:15.760236+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (35, 8, NULL, 2, 'text', '{\"html_content\": \"\\n<h1>Politique de confidentialité</h1>\\n<p><em>Dernière mise à jour : 4 juin 2026</em></p>\\n<p>NextBlock™ CMS (« nous ») respecte votre vie privée et s''engage à protéger vos renseignements personnels conformément à la <em>Loi sur la protection des renseignements personnels dans le secteur privé</em> du Québec (Loi 25), à la <em>Loi sur la protection des renseignements personnels et les documents électroniques</em> (LPRPDE) et à la Loi canadienne anti-pourriel (LCAP).</p>\\n\\n<h2>1. Responsable de la protection des renseignements personnels</h2>\\n<p>Notre responsable de la protection des renseignements personnels veille au respect des lois applicables. Vous pouvez le joindre à <a href=\\\"mailto:{{privacy_email}}\\\">{{privacy_email}}</a>.</p>\\n\\n<h2>2. Renseignements que nous recueillons</h2>\\n<ul>\\n <li><strong>Renseignements de compte</strong> &mdash; nom, adresse courriel et identifiants lors de l''inscription.</li>\\n <li><strong>Données d''utilisation et d''appareil</strong> &mdash; recueillies uniquement avec votre consentement au moyen de technologies d''analyse.</li>\\n <li><strong>Communications</strong> &mdash; les messages que vous nous envoyez et vos préférences marketing.</li>\\n</ul>\\n\\n<h2>3. Finalités et consentement</h2>\\n<p>Nous recueillons des renseignements personnels à des fins clairement déterminées : fournir et sécuriser nos services, communiquer avec vous et &mdash; uniquement avec votre consentement exprès &mdash; à des fins d''analyse et de marketing. Conformément à la Loi 25, les témoins et traceurs non essentiels demeurent désactivés tant que vous ne les avez pas acceptés, et vous pouvez retirer votre consentement en tout temps.</p>\\n\\n<h2>4. Témoins et technologies de suivi</h2>\\n<p>Les témoins strictement nécessaires assurent le fonctionnement du site et ne requièrent aucun consentement. Les technologies d''analyse et de marketing ne sont chargées qu''<strong>après</strong> votre consentement explicite. Votre choix est enregistré afin de le respecter.</p>\\n\\n<h2>5. Communication à des tiers</h2>\\n<p>Nous ne vendons pas vos renseignements personnels. Nous ne les communiquons qu''à des fournisseurs qui nous aident à exploiter la plateforme, sous obligation de confidentialité, ou lorsque la loi l''exige.</p>\\n\\n<h2>6. Conservation</h2>\\n<p>Nous ne conservons les renseignements personnels que le temps nécessaire aux fins décrites ou exigé par la loi, après quoi ils sont détruits ou anonymisés de façon sécuritaire.</p>\\n\\n<h2>7. Vos droits</h2>\\n<p>Sous réserve de la loi applicable, vous avez le droit d''accéder à vos renseignements, de les rectifier et de les supprimer, de retirer votre consentement, à la portabilité de vos données et d''être informé du traitement automatisé. Pour exercer ces droits, écrivez à <a href=\\\"mailto:{{privacy_email}}\\\">{{privacy_email}}</a>.</p>\\n\\n<h2>8. Messages électroniques commerciaux (LCAP)</h2>\\n<p>Nous n''envoyons des messages électroniques commerciaux qu''avec votre consentement. Chaque message nous identifie et comporte un mécanisme de désabonnement fonctionnel que nous respectons rapidement.</p>\\n\\n<h2>9. Mesures de sécurité</h2>\\n<p>Nous employons des mesures physiques, organisationnelles et technologiques appropriées &mdash; dont le chiffrement en transit et le contrôle des accès &mdash; pour protéger vos renseignements.</p>\\n\\n<h2>10. Logiciel libre</h2>\\n<p>NextBlock™ CMS est un logiciel libre et à code source ouvert distribué sous la licence publique générale GNU Affero v3. Lorsque vous hébergez NextBlock vous-même, vous êtes l''exploitant responsable des renseignements personnels traités par votre propre instance, et la présente politique vous sert de point de départ adaptable à votre organisation.</p>\\n\\n<h2>11. Modifications</h2>\\n<p>Nous pouvons mettre à jour cette politique. Les changements importants seront communiqués sur le site et la date de mise à jour sera révisée.</p>\\n\\n<h2>12. Nous joindre</h2>\\n<p>Des questions ou des plaintes ? Contactez NextBlock™ CMS à <a href=\\\"mailto:{{privacy_email}}\\\">{{privacy_email}}</a>. Vous pouvez aussi vous adresser à la Commission d''accès à l''information du Québec.</p>\\n\"}', 0, '2026-07-03 17:52:15.699946+00', '2026-07-03 17:52:15.760236+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (36, 9, NULL, 1, 'text', '{\"html_content\": \"\\n<h1>Terms of Service</h1>\\n<p><em>Last updated: June 4, 2026</em></p>\\n\\n<h2>1. Acceptance of terms</h2>\\n<p>By accessing or using NextBlock™ CMS and the services we provide (the \\\"Services\\\"), you agree to be bound by these Terms of Service. If you do not agree, do not use the Services.</p>\\n\\n<h2>2. Free and open-source software</h2>\\n<p>NextBlock™ CMS is free, open-source software licensed under the <strong>GNU Affero General Public License, version 3 (AGPL-3.0)</strong> or, at your option, any later version. You are free to run, study, share, and modify the software under the terms of that license. A copy of the license is distributed with the software and is also available at <a href=\\\"https://www.gnu.org/licenses/agpl-3.0.html\\\">gnu.org/licenses/agpl-3.0.html</a>.</p>\\n<p>Copyright © 2025 NextBlock™ CMS.</p>\\n\\n<h2>3. Source code availability</h2>\\n<p>In accordance with section 13 of the AGPL-3.0, if you run a modified version of NextBlock™ CMS and make it available to users over a network, you must prominently offer those users access to the Corresponding Source of your modified version, free of charge, through a standard or customary means of facilitating copying of software.</p>\\n\\n<h2>4. Trademarks</h2>\\n<p>The AGPL-3.0 grants broad rights to the software''s source code but does <strong>not</strong> grant any rights to our trade names, trademarks, or service marks. \\\"NextBlock™\\\", the NextBlock™ CMS name, and associated logos remain our property and may not be used in a way that suggests endorsement or affiliation without our prior written permission.</p>\\n\\n<h2>5. Accounts and acceptable use</h2>\\n<p>If you create an account, you are responsible for safeguarding your credentials and for all activity under your account, and you agree to notify us promptly of any unauthorized use. You agree not to misuse the Services, including by attempting to disrupt them, access them without authorization, or use them for unlawful purposes.</p>\\n\\n<h2>6. No warranty</h2>\\n<p>As stated in section 15 of the AGPL-3.0, the software is provided \\\"as is\\\", without warranty of any kind, either expressed or implied, including, without limitation, the implied warranties of merchantability and fitness for a particular purpose. The entire risk as to the quality and performance of the software is with you.</p>\\n\\n<h2>7. Limitation of liability</h2>\\n<p>As stated in section 16 of the AGPL-3.0, and to the fullest extent permitted by applicable law, in no event will any copyright holder, or any other party who modifies or conveys the software, be liable to you for damages, including any general, special, incidental, or consequential damages arising out of the use or inability to use the software.</p>\\n\\n<h2>8. Governing law</h2>\\n<p>These Terms are governed by the laws of the Province of Quebec and the federal laws of Canada applicable therein, without regard to conflict-of-law principles. Nothing in these Terms limits any mandatory consumer-protection rights you may have under those laws.</p>\\n\\n<h2>9. Changes</h2>\\n<p>We may revise these Terms from time to time. Material changes will be communicated through the Services, and continued use of the Services after changes take effect constitutes acceptance of the revised Terms.</p>\\n\\n<h2>10. Contact</h2>\\n<p>Questions about these Terms? Contact NextBlock™ CMS at <a href=\\\"mailto:{{privacy_email}}\\\">{{privacy_email}}</a>.</p>\\n\"}', 0, '2026-07-03 17:52:15.699946+00', '2026-07-03 17:52:15.760236+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (37, 10, NULL, 2, 'text', '{\"html_content\": \"\\n<h1>Conditions d''utilisation</h1>\\n<p><em>Dernière mise à jour : 4 juin 2026</em></p>\\n\\n<h2>1. Acceptation des conditions</h2>\\n<p>En accédant à NextBlock™ CMS et aux services que nous fournissons (les « Services ») ou en les utilisant, vous acceptez d''être lié par les présentes conditions d''utilisation. Si vous n''êtes pas d''accord, n''utilisez pas les Services.</p>\\n\\n<h2>2. Logiciel libre et à code source ouvert</h2>\\n<p>NextBlock™ CMS est un logiciel libre et à code source ouvert distribué sous la <strong>licence publique générale GNU Affero, version 3 (AGPL-3.0)</strong> ou, à votre choix, toute version ultérieure. Vous êtes libre d''exécuter, d''étudier, de partager et de modifier le logiciel selon les termes de cette licence. Une copie de la licence est fournie avec le logiciel et est aussi disponible à <a href=\\\"https://www.gnu.org/licenses/agpl-3.0.html\\\">gnu.org/licenses/agpl-3.0.html</a>.</p>\\n<p>Droit d''auteur © 2025 NextBlock™ CMS.</p>\\n\\n<h2>3. Disponibilité du code source</h2>\\n<p>Conformément à l''article 13 de l''AGPL-3.0, si vous exploitez une version modifiée de NextBlock™ CMS et la rendez accessible à des utilisateurs sur un réseau, vous devez offrir clairement à ces utilisateurs l''accès au code source correspondant de votre version modifiée, gratuitement, par un moyen usuel de copie de logiciels.</p>\\n\\n<h2>4. Marques de commerce</h2>\\n<p>L''AGPL-3.0 accorde de larges droits sur le code source du logiciel, mais <strong>n''accorde aucun droit</strong> sur nos noms commerciaux, marques de commerce ou marques de service. « NextBlock™ », le nom NextBlock™ CMS et les logos associés demeurent notre propriété et ne peuvent être utilisés d''une manière laissant entendre une approbation ou une affiliation sans notre autorisation écrite préalable.</p>\\n\\n<h2>5. Comptes et utilisation acceptable</h2>\\n<p>Si vous créez un compte, vous êtes responsable de la protection de vos identifiants et de toute activité effectuée à partir de votre compte, et vous vous engagez à nous aviser rapidement de toute utilisation non autorisée. Vous vous engagez à ne pas détourner les Services, notamment en tentant de les perturber, d''y accéder sans autorisation ou de les utiliser à des fins illégales.</p>\\n\\n<h2>6. Absence de garantie</h2>\\n<p>Comme l''énonce l''article 15 de l''AGPL-3.0, le logiciel est fourni « tel quel », sans garantie d''aucune sorte, expresse ou implicite, y compris, sans s''y limiter, les garanties implicites de qualité marchande et d''adéquation à un usage particulier. Vous assumez l''entièreté du risque quant à la qualité et au rendement du logiciel.</p>\\n\\n<h2>7. Limitation de responsabilité</h2>\\n<p>Comme l''énonce l''article 16 de l''AGPL-3.0, et dans toute la mesure permise par la loi applicable, en aucun cas un titulaire de droits d''auteur ou toute autre partie qui modifie ou transmet le logiciel ne saurait être tenu responsable envers vous de dommages, y compris tout dommage général, spécial, accessoire ou consécutif découlant de l''utilisation ou de l''impossibilité d''utiliser le logiciel.</p>\\n\\n<h2>8. Droit applicable</h2>\\n<p>Les présentes conditions sont régies par les lois de la province de Québec et les lois fédérales du Canada qui y sont applicables, sans égard aux règles de conflit de lois. Rien dans les présentes conditions ne limite les droits impératifs de protection du consommateur dont vous pourriez bénéficier en vertu de ces lois.</p>\\n\\n<h2>9. Modifications</h2>\\n<p>Nous pouvons réviser ces conditions de temps à autre. Les changements importants seront communiqués au moyen des Services, et l''utilisation continue des Services après leur entrée en vigueur vaut acceptation.</p>\\n\\n<h2>10. Nous joindre</h2>\\n<p>Des questions sur ces conditions ? Contactez NextBlock™ CMS à <a href=\\\"mailto:{{privacy_email}}\\\">{{privacy_email}}</a>.</p>\\n\"}', 0, '2026-07-03 17:52:15.699946+00', '2026-07-03 17:52:15.760236+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (40, NULL, 3, 1, 'text', '{\"html_content\": \"<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock is an open-source, AI-native Next.js CMS built on Supabase — and installing it no longer involves config files, terminal wizards, or manual SQL. There are four ways to get running, and they all end in the same place: a browser <strong>setup wizard</strong> that connects your database, configures media storage, and creates your admin account for you. Pick the path that fits, follow the steps, and you will be publishing in minutes.</p>\\n\\n<div class=''grid gap-5 md:grid-cols-2 my-10''>\\n <a href=''#one-click-vercel'' class=''block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10''>\\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200''>Fastest &middot; about 3 minutes</p>\\n <h3 class=''mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>One-click deploy on Vercel</h3>\\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>A live production site with a managed database. No terminal, no environment variables, nothing to copy.</p>\\n </a>\\n <a href=''#npm-create'' class=''block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10''>\\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Recommended for new projects</p>\\n <h3 class=''mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock</h3>\\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Scaffold a standalone Next.js app with NextBlock built in, then finish setup in your browser.</p>\\n </a>\\n <a href=''#git-clone'' class=''block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10''>\\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Full source code</p>\\n <h3 class=''mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Clone the repository</h3>\\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Run the complete monorepo — the CMS, every package, and the docs. Made for contributors and platform teams.</p>\\n </a>\\n <a href=''#docker'' class=''block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10''>\\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200''>100% local &middot; no accounts</p>\\n <h3 class=''mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Self-hosted with Docker</h3>\\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>One command boots the entire stack on your machine — database, auth, storage, and the CMS. No cloud services at all.</p>\\n </a>\\n</div>\\n\\n<p class=''text-sm text-slate-500 dark:text-slate-400''>Not sure which to pick? If you want a live website with the least effort, choose <a href=''#one-click-vercel''>Vercel</a>. If you want a local project to build on, choose <a href=''#npm-create''>npm create nextblock</a>.</p>\\n\\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\\n <img src=''/images/included.webp'' alt=''NextBlock CMS platform overview showing the block editor, CMS dashboard, and integrations included with every installation'' class=''w-full h-auto object-cover'' />\\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>Whichever path you choose, you get the same block editor, the same CMS, and the same database schema.</figcaption>\\n</figure>\\n\\n<h2 id=''one-click-vercel''>Option 1: One-Click Deploy on Vercel</h2>\\n<p>The fastest way to get a production NextBlock site. One button creates your own copy of NextBlock on GitHub, provisions a managed Supabase database, and deploys the site — you never open a terminal or copy a single key.</p>\\n<ol class=''space-y-2''>\\n <li><strong>Click Deploy to Vercel</strong> and sign in — Vercel clones NextBlock into a new repository you own.</li>\\n <li><strong>Create the Supabase database</strong> when prompted: pick a name and a region. Vercel connects it to the project and injects the keys before the first build.</li>\\n <li><strong>Open your new site</strong> once the build finishes. Every fresh instance takes you straight to the setup wizard.</li>\\n <li><strong>Create your administrator account.</strong> It is confirmed instantly — no verification email — and you land in the CMS dashboard.</li>\\n</ol>\\n<div class=''my-8''>\\n <a href=''https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200''>Deploy to Vercel &rarr;</a>\\n</div>\\n<div class=''rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200''>Zero configuration</p>\\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>There are no environment variables to fill in. Media storage automatically uses your connected Supabase project, security secrets are derived for you, and database migrations run automatically on every production build. Adding a custom domain later? Set <code>NEXT_PUBLIC_URL</code> in your Vercel project and redeploy.</p>\\n</div>\\n<p>Your site also keeps itself current: the dashboard checklist includes a one-click <strong>Connect GitHub</strong> step that installs a daily workflow syncing your copy with the latest NextBlock release.</p>\\n\\n<h2 id=''npm-create''>Option 2: Scaffold a Project with npm create nextblock</h2>\\n<p>The best starting point for building your own site. The CLI scaffolds a standalone Next.js application with NextBlock already wired in — no monorepo, no workspace tooling — and hands everything else to the browser wizard.</p>\\n<p>Before you start, install <a href=''https://nodejs.org'' target=''_blank'' rel=''noopener''>Node.js 20 or newer</a> (it includes npm), then create a free project at <a href=''https://supabase.com'' target=''_blank'' rel=''noopener''>supabase.com</a> (or pick the CLI''s Docker mode during creation and skip cloud accounts entirely).</p>\\n<pre><code>npm create nextblock@latest my-site\\ncd my-site\\nnpm run dev</code></pre>\\n<p>Open <code>http://localhost:3000/setup</code> and let the wizard take over:</p>\\n<ol class=''space-y-2''>\\n <li><strong>Connect Supabase</strong> — paste your project URL, publishable (anon) key, secret (service role) key, and a personal access token so the wizard can apply the database schema for you.</li>\\n <li><strong>Choose media storage</strong> — plug in a Cloudflare R2 bucket for images and files, or leave it blank to use your connected Supabase project''s storage.</li>\\n <li><strong>Create your administrator</strong> — the wizard applies every migration, generates the app secrets, writes <code>.env.local</code>, creates your confirmed admin account, and signs you in. Restart <code>npm run dev</code> once afterwards so the fresh environment is baked into the app.</li>\\n</ol>\\n<div class=''rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Premium modules</p>\\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Need a store? One command adds products, checkout, orders, and coupons — license-gated and ready when you are: <code>npx create-nextblock activate ecommerce</code></p>\\n</div>\\n\\n<h2 id=''git-clone''>Option 3: Clone the Repository</h2>\\n<p>Run the full Nx monorepo: the CMS application, every shared package, the CLI source, and the documentation. This is the path for contributors, plugin authors, and teams that customize the platform itself.</p>\\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\\ncd nextblock\\nnpm install\\nnpx nx serve nextblock</code></pre>\\n<p>Open <code>http://localhost:4200</code> — a fresh install redirects every page to <code>/setup</code>, where the same three-step wizard connects Supabase, configures storage, and creates your admin. It validates your keys, writes <code>.env.local</code> with generated secrets, and applies all migrations over the Supabase Management API — no Supabase CLI required.</p>\\n<div class=''rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>No terminal setup</p>\\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Configuration moved entirely to the browser — there is no interactive terminal step anymore. When the wizard finishes you are signed in as the administrator; restart the dev server once afterwards so the fresh environment is baked into the app bundle.</p>\\n</div>\\n\\n<h2 id=''docker''>Option 4: Self-Hosted with Docker</h2>\\n<p>Everything runs on your machine: Supabase''s Postgres and auth engines, a PostgREST API behind a Kong gateway, S3-compatible MinIO storage, and the CMS itself. No Supabase account, no Vercel, no email service — ideal for evaluations, air-gapped environments, and anyone who wants a fully self-hosted CMS with complete data ownership.</p>\\n<p>With <a href=''https://www.docker.com/products/docker-desktop/'' target=''_blank'' rel=''noopener''>Docker Desktop</a> installed and running:</p>\\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\\ncd nextblock\\nnpm install\\nnpm run docker:setup</code></pre>\\n<p>The command asks nothing: it generates secure keys, builds the stack, applies every migration, and starts the services. When it finishes, open <code>http://localhost:3000</code> — the setup wizard already has the database and MinIO storage wired up, so the only step left is creating your administrator (confirmed instantly, no email required).</p>\\n<div class=''rounded-3xl border border-amber-200 bg-amber-50/80 p-6 my-8 dark:border-amber-500/20 dark:bg-amber-500/10''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200''>Day-2 commands</p>\\n <pre class=''mt-4 mb-0''><code># rebuild and restart the stack\\nnpm run docker:up\\n\\n# stop the stack (your data persists in Docker volumes)\\nnpm run docker:down\\n\\n# follow the application logs\\nnpm run docker:logs</code></pre>\\n</div>\\n\\n<h2 id=''after-install''>After You Install: Your First 10 Minutes</h2>\\n<p>Every path drops you at <code>/cms/dashboard</code>, signed in as the first administrator. A built-in onboarding checklist walks you through the rest:</p>\\n<ul class=''space-y-2''>\\n <li><strong>Add your branding</strong> — upload your logo and set the site title.</li>\\n <li><strong>Set your footer</strong> — copyright line and footer navigation.</li>\\n <li><strong>Configure email (SMTP)</strong> — under Settings, so password resets and invitations can send.</li>\\n <li><strong>Optional extras</strong> — connect analytics, enable bot protection, and (on Vercel) turn on automatic updates.</li>\\n</ul>\\n<p>From there, see how the platform fits together in <a href=''/article/how-nextblock-works''>How NextBlock Works</a>, add a storefront with the <a href=''/article/nextblock-commerce-guide''>Commerce guide</a>, or meet your AI copilot in the <a href=''/article/nextblock-cortex-ai-guide''>Cortex AI guide</a>.</p>\\n\\n<h2 id=''faq''>Installation FAQ</h2>\\n<h3>What do I need installed?</h3>\\n<p>Nothing for the Vercel path — it runs entirely in the browser. For <code>npm create nextblock</code>: <a href=''https://nodejs.org'' target=''_blank'' rel=''noopener''>Node.js 20+</a> (which includes npm). For the cloned repository: Node.js 20+ and git. For Docker: those plus Docker Desktop.</p>\\n<h3>Is NextBlock free?</h3>\\n<p>Yes — the core of NextBlock is a 100% free, open-source CMS (AGPL). Premium packages such as e-commerce and Cortex AI are optional and activate with a license key. Both Vercel and Supabase offer free tiers, so a starter site can run at no cost.</p>\\n<h3>Do I need a Supabase account?</h3>\\n<p>On Vercel, the database is created for you during the deploy. For <code>npm create nextblock</code> and the cloned repository you bring a free Supabase project. With Docker you need no cloud accounts at all.</p>\\n<h3>Do I have to run migrations or SQL by hand?</h3>\\n<p>No. The setup wizard, the Vercel build, and the Docker stack all apply the database schema automatically — and re-running is always safe.</p>\\n<h3>Can I switch paths later?</h3>\\n<p>Yes. Every path runs the same application and the same database schema, so you can prototype locally with Docker today and deploy to Vercel tomorrow. NextBlock deploys like any standard Next.js app.</p>\\n<h3>How do I update NextBlock?</h3>\\n<p>On Vercel, the Connect GitHub onboarding step enables a daily automatic sync with upstream. On a cloned repository, <code>git pull</code>, run <code>npm run db:migrate</code>, then restart (on production builds, pending migrations apply automatically). With Docker, pull the latest code and run <code>npm run docker:up</code>.</p>\\n\\n<div class=''rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5''>\\n <p class=''mt-0 text-2xl font-semibold text-slate-900 dark:text-white''>Ready to launch?</p>\\n <p class=''text-sm text-slate-600 dark:text-slate-300''>Pick your path above, or jump straight to the fastest one.</p>\\n <div class=''mt-5 flex flex-wrap justify-center gap-3''>\\n <a href=''https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200''>Deploy on Vercel</a>\\n <a href=''https://github.com/nextblock-cms/nextblock'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50''>View on GitHub</a>\\n </div>\\n</div>\"}', 0, '2026-07-03 17:52:15.799297+00', '2026-07-03 17:52:15.799297+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (41, NULL, 4, 2, 'text', '{\"html_content\": \"<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock est un CMS open source et natif IA, construit sur Next.js et Supabase — et son installation ne passe plus par des fichiers de configuration, des assistants en ligne de commande ou du SQL manuel. Il existe quatre façons de démarrer, et elles aboutissent toutes au même endroit : un <strong>assistant de configuration</strong> dans le navigateur qui connecte votre base de données, configure le stockage des médias et crée votre compte administrateur. Choisissez le chemin qui vous convient, suivez les étapes, et vous publierez en quelques minutes.</p>\\n\\n<div class=''grid gap-5 md:grid-cols-2 my-10''>\\n <a href=''#one-click-vercel'' class=''block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10''>\\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200''>Le plus rapide &middot; environ 3 minutes</p>\\n <h3 class=''mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Déploiement Vercel en un clic</h3>\\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Un site de production en ligne avec une base de données gérée. Pas de terminal, pas de variables d''environnement, rien à copier.</p>\\n </a>\\n <a href=''#npm-create'' class=''block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10''>\\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Recommandé pour les nouveaux projets</p>\\n <h3 class=''mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>npm create nextblock</h3>\\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Générez une app Next.js autonome avec NextBlock intégré, puis terminez la configuration dans votre navigateur.</p>\\n </a>\\n <a href=''#git-clone'' class=''block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10''>\\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Code source complet</p>\\n <h3 class=''mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Cloner le dépôt</h3>\\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Faites tourner le monorepo complet — le CMS, tous les packages et la documentation. Conçu pour les contributeurs et les équipes plateforme.</p>\\n </a>\\n <a href=''#docker'' class=''block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10''>\\n <p class=''mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200''>100 % local &middot; aucun compte</p>\\n <h3 class=''mt-3 mb-2 text-xl font-semibold text-slate-900 dark:text-white''>Auto-hébergé avec Docker</h3>\\n <p class=''mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300''>Une seule commande démarre toute la pile sur votre machine — base de données, auth, stockage et le CMS. Aucun service cloud.</p>\\n </a>\\n</div>\\n\\n<p class=''text-sm text-slate-500 dark:text-slate-400''>Vous hésitez ? Pour un site en ligne avec le minimum d''effort, choisissez <a href=''#one-click-vercel''>Vercel</a>. Pour un projet local à personnaliser, choisissez <a href=''#npm-create''>npm create nextblock</a>.</p>\\n\\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\\n <img src=''/images/included.webp'' alt=''Aperçu de la plateforme NextBlock : éditeur de blocs, tableau de bord CMS et intégrations incluses dans chaque installation'' class=''w-full h-auto object-cover'' />\\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>Quel que soit le chemin choisi, vous obtenez le même éditeur de blocs, le même CMS et le même schéma de base de données.</figcaption>\\n</figure>\\n\\n<h2 id=''one-click-vercel''>Option 1 : Déploiement Vercel en un clic</h2>\\n<p>Le moyen le plus rapide d''obtenir un site NextBlock en production. Un seul bouton crée votre propre copie de NextBlock sur GitHub, provisionne une base de données Supabase gérée et déploie le site — sans jamais ouvrir un terminal ni copier la moindre clé.</p>\\n<ol class=''space-y-2''>\\n <li><strong>Cliquez sur Deploy to Vercel</strong> et connectez-vous — Vercel clone NextBlock dans un nouveau dépôt qui vous appartient.</li>\\n <li><strong>Créez la base de données Supabase</strong> quand on vous le demande : choisissez un nom et une région. Vercel la connecte au projet et injecte les clés avant le premier build.</li>\\n <li><strong>Ouvrez votre nouveau site</strong> une fois le build terminé. Toute nouvelle instance vous amène directement à l''assistant de configuration.</li>\\n <li><strong>Créez votre compte administrateur.</strong> Il est confirmé instantanément — aucun email de vérification — et vous arrivez dans le tableau de bord du CMS.</li>\\n</ol>\\n<div class=''my-8''>\\n <a href=''https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200''>Déployer sur Vercel &rarr;</a>\\n</div>\\n<div class=''rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200''>Zéro configuration</p>\\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Aucune variable d''environnement à remplir. Le stockage des médias utilise automatiquement votre projet Supabase connecté, les secrets de sécurité sont dérivés pour vous, et les migrations de base de données s''exécutent automatiquement à chaque build de production. Un domaine personnalisé plus tard ? Définissez <code>NEXT_PUBLIC_URL</code> dans votre projet Vercel et redéployez.</p>\\n</div>\\n<p>Par ailleurs, votre site reste à jour tout seul : la checklist du tableau de bord inclut une étape <strong>Connect GitHub</strong> en un clic qui installe un workflow quotidien synchronisant votre copie avec la dernière version de NextBlock.</p>\\n\\n<h2 id=''npm-create''>Option 2 : Créer un projet avec npm create nextblock</h2>\\n<p>Le meilleur point de départ pour construire votre propre site. Le CLI génère une application Next.js autonome avec NextBlock déjà intégré — sans monorepo ni outillage de workspace — et confie tout le reste à l''assistant dans le navigateur.</p>\\n<p>Avant de commencer, installez <a href=''https://nodejs.org'' target=''_blank'' rel=''noopener''>Node.js 20 ou plus récent</a> (npm inclus), puis créez un projet gratuit sur <a href=''https://supabase.com'' target=''_blank'' rel=''noopener''>supabase.com</a> (ou choisissez le mode Docker du CLI pendant la création et passez-vous entièrement de comptes cloud).</p>\\n<pre><code>npm create nextblock@latest mon-site\\ncd mon-site\\nnpm run dev</code></pre>\\n<p>Ouvrez <code>http://localhost:3000/setup</code> et laissez l''assistant faire le travail :</p>\\n<ol class=''space-y-2''>\\n <li><strong>Connectez Supabase</strong> — collez l''URL du projet, la clé publiable (anon), la clé secrète (service role) et un jeton d''accès personnel pour que l''assistant applique le schéma de base de données à votre place.</li>\\n <li><strong>Choisissez le stockage des médias</strong> — branchez un bucket Cloudflare R2 pour les images et les fichiers, ou laissez les champs vides pour utiliser le stockage de votre projet Supabase.</li>\\n <li><strong>Créez votre administrateur</strong> — l''assistant applique toutes les migrations, génère les secrets de l''application, écrit <code>.env.local</code>, crée votre compte admin confirmé et vous connecte. Redémarrez ensuite <code>npm run dev</code> une fois pour que le nouvel environnement soit intégré à l''application.</li>\\n</ol>\\n<div class=''rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200''>Modules premium</p>\\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Besoin d''une boutique ? Une seule commande ajoute produits, paiement, commandes et coupons — activés par clé de licence, prêts quand vous l''êtes : <code>npx create-nextblock activate ecommerce</code></p>\\n</div>\\n\\n<h2 id=''git-clone''>Option 3 : Cloner le dépôt</h2>\\n<p>Faites tourner le monorepo Nx complet : l''application CMS, tous les packages partagés, le code du CLI et la documentation. C''est le chemin des contributeurs, des auteurs de plugins et des équipes qui personnalisent la plateforme elle-même.</p>\\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\\ncd nextblock\\nnpm install\\nnpx nx serve nextblock</code></pre>\\n<p>Ouvrez <code>http://localhost:4200</code> — une nouvelle installation redirige chaque page vers <code>/setup</code>, où le même assistant en trois étapes connecte Supabase, configure le stockage et crée votre admin. Il valide vos clés, écrit <code>.env.local</code> avec des secrets générés, et applique toutes les migrations via l''API de management Supabase — sans CLI Supabase.</p>\\n<div class=''rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Zéro configuration dans le terminal</p>\\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>La configuration se fait entièrement dans le navigateur — il n''y a plus d''étape interactive en ligne de commande. Quand l''assistant termine, vous êtes connecté en administrateur ; redémarrez ensuite le serveur de développement une fois pour que le nouvel environnement soit intégré au bundle de l''application.</p>\\n</div>\\n\\n<h2 id=''docker''>Option 4 : Auto-hébergé avec Docker</h2>\\n<p>Tout tourne sur votre machine : les moteurs Postgres et auth de Supabase, une API PostgREST derrière une passerelle Kong, un stockage MinIO compatible S3 et le CMS lui-même. Pas de compte Supabase, pas de Vercel, pas de service d''email — idéal pour les évaluations, les environnements isolés et la pleine propriété de vos données.</p>\\n<p>Avec <a href=''https://www.docker.com/products/docker-desktop/'' target=''_blank'' rel=''noopener''>Docker Desktop</a> installé et démarré :</p>\\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\\ncd nextblock\\nnpm install\\nnpm run docker:setup</code></pre>\\n<p>La commande ne pose aucune question : elle génère des clés sécurisées, construit la pile, applique toutes les migrations et démarre les services. Quand elle se termine, ouvrez <code>http://localhost:3000</code> — la base de données et le stockage MinIO sont déjà connectés dans l''assistant de configuration, il ne reste qu''à créer votre administrateur (confirmé instantanément, sans email).</p>\\n<div class=''rounded-3xl border border-amber-200 bg-amber-50/80 p-6 my-8 dark:border-amber-500/20 dark:bg-amber-500/10''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200''>Commandes du quotidien</p>\\n <pre class=''mt-4 mb-0''><code># reconstruire et redémarrer la pile\\nnpm run docker:up\\n\\n# arrêter la pile (vos données persistent dans les volumes Docker)\\nnpm run docker:down\\n\\n# suivre les logs de l''application\\nnpm run docker:logs</code></pre>\\n</div>\\n\\n<h2 id=''after-install''>Après l''installation : vos 10 premières minutes</h2>\\n<p>Chaque chemin vous dépose sur <code>/cms/dashboard</code>, connecté en tant que premier administrateur. Une checklist de démarrage intégrée vous guide pour la suite :</p>\\n<ul class=''space-y-2''>\\n <li><strong>Ajoutez votre identité visuelle</strong> — téléversez votre logo et définissez le titre du site.</li>\\n <li><strong>Réglez votre pied de page</strong> — mention de copyright et navigation du pied de page.</li>\\n <li><strong>Configurez l''email (SMTP)</strong> — dans les réglages, pour que les réinitialisations de mot de passe et les invitations partent bien.</li>\\n <li><strong>Extras optionnels</strong> — connectez vos outils d''analytics, activez la protection anti-bots et (sur Vercel) les mises à jour automatiques.</li>\\n</ul>\\n<p>Ensuite, découvrez comment la plateforme s''articule dans <a href=''/article/comment-nextblock-fonctionne''>Comment NextBlock fonctionne</a>, ou ajoutez une boutique avec le <a href=''/article/guide-commerce-nextblock''>guide Commerce</a>.</p>\\n\\n<h2 id=''faq''>FAQ d''installation</h2>\\n<h3>Que dois-je installer ?</h3>\\n<p>Rien pour le chemin Vercel — tout se passe dans le navigateur. Pour <code>npm create nextblock</code> : <a href=''https://nodejs.org'' target=''_blank'' rel=''noopener''>Node.js 20+</a> (npm inclus). Pour le dépôt cloné : Node.js 20+ et git. Pour Docker : ajoutez Docker Desktop.</p>\\n<h3>NextBlock est-il gratuit ?</h3>\\n<p>Oui — le cœur du CMS est 100 % gratuit et open source (AGPL). Les packages premium comme l''e-commerce et Cortex AI sont optionnels et s''activent avec une clé de licence. Vercel et Supabase proposent chacun une offre gratuite : un site de départ peut donc tourner sans frais.</p>\\n<h3>Ai-je besoin d''un compte Supabase ?</h3>\\n<p>Sur Vercel, la base de données est créée pour vous pendant le déploiement. Pour <code>npm create nextblock</code> et le dépôt cloné, il vous faut un projet Supabase gratuit. Avec Docker, aucun compte cloud n''est nécessaire.</p>\\n<h3>Dois-je exécuter des migrations ou du SQL à la main ?</h3>\\n<p>Non. L''assistant de configuration, le build Vercel et la pile Docker appliquent tous le schéma de base de données automatiquement — et relancer l''opération est toujours sans risque.</p>\\n<h3>Puis-je changer de chemin plus tard ?</h3>\\n<p>Oui. Chaque chemin exécute la même application et le même schéma de base de données : vous pouvez prototyper en local avec Docker aujourd''hui et déployer sur Vercel demain. NextBlock se déploie comme n''importe quelle app Next.js.</p>\\n<h3>Comment mettre à jour NextBlock ?</h3>\\n<p>Sur Vercel, l''étape Connect GitHub de la checklist active une synchronisation quotidienne automatique. Sur un dépôt cloné, <code>git pull</code>, lancez <code>npm run db:migrate</code>, puis redémarrez (lors des builds de production, les migrations en attente s''appliquent automatiquement). Avec Docker, récupérez le dernier code et lancez <code>npm run docker:up</code>.</p>\\n\\n<div class=''rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5''>\\n <p class=''mt-0 text-2xl font-semibold text-slate-900 dark:text-white''>Prêt à vous lancer ?</p>\\n <p class=''text-sm text-slate-600 dark:text-slate-300''>Choisissez votre chemin ci-dessus, ou passez directement au plus rapide.</p>\\n <div class=''mt-5 flex flex-wrap justify-center gap-3''>\\n <a href=''https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200''>Déployer sur Vercel</a>\\n <a href=''https://github.com/nextblock-cms/nextblock'' target=''_blank'' rel=''noopener'' class=''inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50''>Voir sur GitHub</a>\\n </div>\\n</div>\"}', 0, '2026-07-03 17:52:15.799297+00', '2026-07-03 17:52:15.799297+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (8, 1, NULL, 1, 'section', '{\"padding\": {\"top\": \"xl\", \"bottom\": \"xl\"}, \"background\": {\"type\": \"gradient\", \"gradient\": {\"type\": \"linear\", \"stops\": [{\"color\": \"#020817\", \"position\": 0}, {\"color\": \"#0f172a\", \"position\": 100}], \"direction\": \"180deg\"}}, \"column_gap\": \"lg\", \"column_blocks\": [[{\"content\": {\"html_content\": \"<h2 class=''text-3xl md:text-4xl font-bold text-center text-white mb-4''>Have Questions?</h2>\"}, \"block_type\": \"text\"}, {\"content\": {\"html_content\": \"<p class=''text-center text-lg text-slate-300 mx-auto mb-8''>NextBlock™ partners with early adopters to co-build features, sponsor modules, and shape the product direction.</p>\"}, \"block_type\": \"text\"}, {\"content\": {\"url\": \"/contact\", \"size\": \"lg\", \"text\": \"Get in Touch\", \"variant\": \"default\", \"position\": \"center\"}, \"block_type\": \"button\"}, {\"content\": {\"url\": \"https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&project-name=nextblock&repository-name=nextblock&stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D\", \"size\": \"lg\", \"text\": \"Deploy on Vercel\", \"variant\": \"outline\", \"position\": \"center\"}, \"block_type\": \"button\"}]], \"container_type\": \"container\", \"responsive_columns\": {\"mobile\": 1, \"tablet\": 1, \"desktop\": 1}}', 6, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.840619+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (27, NULL, 1, 1, 'text', '{\"html_content\": \"<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock™ is designed so the hosted CMS, the open-source starter, and the developer tooling all feel like the same product. The shared Nx workspace, typed block contracts, and reusable editor package keep product polish and developer velocity moving together.</p>\\n\\n<div class=''grid gap-4 md:grid-cols-3 my-10''>\\n <div class=''rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200''>One codebase</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Shared foundation</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Marketing pages, CMS screens, and the starter template evolve together instead of drifting apart.</p>\\n </div>\\n <div class=''rounded-3xl border border-indigo-200/70 bg-indigo-50/70 p-6 dark:border-indigo-500/20 dark:bg-indigo-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:text-indigo-200''>Typed content</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Blocks with guardrails</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Zod schemas, defaults, and renderer contracts make every custom block safer to ship.</p>\\n </div>\\n <div class=''rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Editorial UX</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Product-grade editing</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>The Tiptap layer gives editors a richer surface without hiding the underlying HTML power.</p>\\n </div>\\n</div>\\n\\n<div class=''flex flex-col md:flex-row gap-8 items-center my-12''>\\n <div class=''w-full md:w-1/2 space-y-4''>\\n <h2>Monorepo Layout and Dependency Flow</h2>\\n <p>The <code>apps/nextblock</code> directory contains the production Next.js experience, including the public site and authenticated CMS shell. The <code>apps/create-nextblock</code> CLI mirrors that foundation so teams can start from the same product decisions instead of rebuilding them from scratch.</p>\\n <ul class=''list-disc pl-6 space-y-2 text-sm''>\\n <li><strong>@nextblock-cms/ui</strong> - UI components, tokens, and shared design primitives</li>\\n <li><strong>@nextblock-cms/utils</strong> - translations, environment guards, and storage helpers</li>\\n <li><strong>@nextblock-cms/db</strong> - migrations, typed database access, and generated types</li>\\n <li><strong>@nextblock-cms/editor</strong> - the reusable Tiptap v3 editing surface</li>\\n <li><strong>@nextblock-cms/sdk</strong> - typed contracts for block authorship and validation</li>\\n <li><strong>@nextblock-cms/ecommerce</strong> - the premium commerce module when activated</li>\\n </ul>\\n <p>Run <code>nx graph</code> and you can see exactly how changes ripple through the workspace. Path aliases from <code>tsconfig.base.json</code> and the shared Tailwind setup help keep design parity between marketing pages, admin screens, and generated projects.</p>\\n </div>\\n <aside class=''w-full md:w-1/2 rounded-[2rem] border border-slate-200/80 bg-white p-4 shadow-xl dark:border-white/10 dark:bg-white/5''>\\n <div class=''relative aspect-video overflow-hidden rounded-2xl''><iframe class=''absolute inset-0 h-full w-full border-0'' src=''https://www.youtube.com/embed/DNqU8ez9qjs?si=p2oIy0f-n7wiaBmO'' title=''How NextBlock™ Works'' allow=''accelerated-motion; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'' referrerpolicy=''strict-origin-when-cross-origin'' loading=''lazy'' allowfullscreen></iframe></div>\\n <p class=''mt-3 text-sm text-slate-500 dark:text-slate-400''>Nx makes every workspace relationship visible, which is exactly why the starter, CMS, and packages stay aligned.</p>\\n </aside>\\n</div>\\n\\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\\n <img src=''/images/extensibility.webp'' alt=''NextBlock™ extensibility artwork showing the CMS connected to reusable modules and integrations'' class=''w-full h-auto object-cover'' />\\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>A single visual system spans content modeling, editing, and future premium modules like commerce.</figcaption>\\n</figure>\\n\\n<h2>Block Registry as Product Surface</h2>\\n<p>The block registry in <code>apps/nextblock/lib/blocks/blockRegistry.ts</code> is the source of truth for available block types, Zod schemas, starter content, and editor or renderer components. Today that includes everything from <code>text</code> and <code>heading</code> to <code>section</code>, <code>posts_grid</code>, <code>checkout</code>, and <code>product_details</code>.</p>\\n<p>Sections support nested column arrays, so layouts can be composed like real pages instead of flat content lists. Helpers such as <code>getBlockDefinition()</code>, <code>getInitialContent()</code>, and <code>validateBlockContent()</code> keep that flexibility strongly typed.</p>\\n\\n<h2>The Editing Layer</h2>\\n<p>The <code>@nextblock-cms/editor</code> package wraps Tiptap v3 into a reusable editorial surface with slash commands, floating and bubble menus, drag handles, tables, task lists, character counts, and syntax-highlighted code blocks. It deliberately preserves richer HTML so advanced teams are not boxed into a simplified subset.</p>\\n\\n<h2>Inside the CMS Shell</h2>\\n<p>Within <code>apps/nextblock/app/cms</code>, each feature area follows a repeatable pattern: list pages, create and edit routes, scoped client components, and server actions that wrap Supabase mutations. The result feels consistent for editors while keeping credentials and permissions on the server side.</p>\\n\\n<h2>Open Core Without Product Drift</h2>\\n<p>The core CMS is open source under AGPL. Premium modules like <code>@nextblock-cms/ecommerce</code> remain source-available but are activated through <code>package_activations</code> and <code>verifyPackageOnline()</code>. That means the same shell can stay clean for open-source users while revealing commerce surfaces only when the license is active.</p>\\n\\n<h2>Why It Holds Together</h2>\\n<p>The Nx workspace keeps libraries honest, the Next.js app enforces UI consistency, Supabase migrations codify access rules, and the Tiptap editor gives collaborators the same authoring experience regardless of deployment. When a team runs <code>npm create nextblock</code>, they inherit the full operating model, not just a pile of files.</p>\"}', 0, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.859357+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.blocks (id, page_id, post_id, language_id, block_type, content, \"order\", created_at, updated_at, product_id) VALUES (28, NULL, 2, 2, 'text', '{\"html_content\": \"<p class=''text-lg leading-8 text-slate-700 dark:text-slate-300''>NextBlock™ relie le CMS h&eacute;berg&eacute;, le starter open source et les outils dev dans un m&ecirc;me socle produit. Le workspace Nx, les contrats de blocs typ&eacute;s et l''&eacute;diteur partag&eacute; permettent d''avancer vite sans sacrifier la coh&eacute;rence.</p>\\n\\n<div class=''grid gap-4 md:grid-cols-3 my-10''>\\n <div class=''rounded-3xl border border-sky-200/70 bg-sky-50/70 p-6 dark:border-sky-500/20 dark:bg-sky-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-200''>Socle unique</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Une m&ecirc;me base</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Le site public, le shell CMS et le starter gardent les m&ecirc;mes choix produit et la m&ecirc;me direction visuelle.</p>\\n </div>\\n <div class=''rounded-3xl border border-indigo-200/70 bg-indigo-50/70 p-6 dark:border-indigo-500/20 dark:bg-indigo-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:text-indigo-200''>Contenu typ&eacute;</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Blocs avec garde-fous</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>Sch&eacute;mas Zod, contenus par d&eacute;faut et contrats de rendu rendent les extensions plus s&ucirc;res &agrave; maintenir.</p>\\n </div>\\n <div class=''rounded-3xl border border-emerald-200/70 bg-emerald-50/70 p-6 dark:border-emerald-500/20 dark:bg-emerald-500/10''>\\n <p class=''text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200''>Exp&eacute;rience &eacute;ditoriale</p>\\n <h3 class=''mt-3 text-xl font-semibold text-slate-900 dark:text-white''>Edition premium</h3>\\n <p class=''mt-3 text-sm text-slate-600 dark:text-slate-300''>La couche Tiptap donne aux &eacute;diteurs une interface riche sans masquer la puissance HTML pour les cas avanc&eacute;s.</p>\\n </div>\\n</div>\\n\\n<div class=''flex flex-col md:flex-row gap-8 items-center my-12''>\\n <div class=''w-full md:w-1/2 space-y-4''>\\n <h2>Architecture monorepo et flux de d&eacute;pendances</h2>\\n <p>Le dossier <code>apps/nextblock</code> contient l''exp&eacute;rience Next.js en production, incluant le site public et le shell CMS authentifi&eacute;. Le CLI <code>apps/create-nextblock</code> reprend cette base pour que les nouveaux projets partent des m&ecirc;mes d&eacute;cisions produit.</p>\\n <ul class=''list-disc pl-6 space-y-2 text-sm''>\\n <li><strong>@nextblock-cms/ui</strong> - composants UI, tokens et primitives visuelles partag&eacute;es</li>\\n <li><strong>@nextblock-cms/utils</strong> - traductions, gardes d''environnement et helpers de stockage</li>\\n <li><strong>@nextblock-cms/db</strong> - migrations, acc&egrave;s base typ&eacute; et types g&eacute;n&eacute;r&eacute;s</li>\\n <li><strong>@nextblock-cms/editor</strong> - la surface d''&eacute;dition Tiptap v3 r&eacute;utilisable</li>\\n <li><strong>@nextblock-cms/sdk</strong> - contrats typ&eacute;s pour l''auteuring et la validation des blocs</li>\\n <li><strong>@nextblock-cms/ecommerce</strong> - le module commerce premium lorsqu''il est activ&eacute;</li>\\n </ul>\\n <p>Lancez <code>nx graph</code> et vous voyez imm&eacute;diatement comment un changement se propage. Les alias de <code>tsconfig.base.json</code> et la configuration Tailwind partag&eacute;e aident &agrave; garder une vraie parit&eacute; entre marketing, back-office et projets g&eacute;n&eacute;r&eacute;s.</p>\\n </div>\\n <aside class=''w-full md:w-1/2 rounded-[2rem] border border-slate-200/80 bg-white p-4 shadow-xl dark:border-white/10 dark:bg-white/5''>\\n <div class=''relative aspect-video overflow-hidden rounded-2xl''><iframe class=''absolute inset-0 h-full w-full border-0'' src=''https://www.youtube.com/embed/DNqU8ez9qjs?si=p2oIy0f-n7wiaBmO'' title=''Comment NextBlock™ fonctionne'' allow=''accelerated-motion; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'' referrerpolicy=''strict-origin-when-cross-origin'' loading=''lazy'' allowfullscreen></iframe></div>\\n <p class=''mt-3 text-sm text-slate-500 dark:text-slate-400''>Nx rend visibles les relations du workspace, ce qui aide le starter, le CMS et les packages a rester alignes.</p>\\n </aside>\\n</div>\\n\\n<figure class=''my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10''>\\n <img src=''/images/extensibility.webp'' alt=''Visuel NextBlock™ montrant le CMS relie a des modules reutilisables et des integrations'' class=''w-full h-auto object-cover'' />\\n <figcaption class=''border-t border-white/10 px-6 py-4 text-sm text-slate-300''>Un seul langage visuel relie la modelisation de contenu, l''edition et les futurs modules premium comme le commerce.</figcaption>\\n</figure>\\n\\n<h2>Le registre de blocs comme surface produit</h2>\\n<p>Le registre dans <code>apps/nextblock/lib/blocks/blockRegistry.ts</code> d&eacute;finit les types disponibles, les sch&eacute;mas Zod, les contenus de d&eacute;part et les composants d''&eacute;dition ou de rendu. On y trouve aujourd''hui des blocs comme <code>text</code>, <code>heading</code>, <code>section</code>, <code>posts_grid</code>, <code>checkout</code> et <code>product_details</code>.</p>\\n<p>Les sections supportent des colonnes imbriqu&eacute;es, ce qui permet de composer de vraies pages plut&ocirc;t qu''une simple liste de contenu. Des helpers comme <code>getBlockDefinition()</code>, <code>getInitialContent()</code> and <code>validateBlockContent()</code> gardent cette flexibilit&eacute; bien typ&eacute;e.</p>\\n\\n<h2>La couche d''edition</h2>\\n<p>Le package <code>@nextblock-cms/editor</code> enveloppe Tiptap v3 dans une surface &eacute;ditoriale r&eacute;utilisable avec slash commands, menus contextuels, drag handles, tableaux, listes de taches, compteurs et blocs de code. Le but est de conserver un HTML riche quand une equipe en a besoin.</p>\\n\\n<h2>A l''interieur du shell CMS</h2>\\n<p>Dans <code>apps/nextblock/app/cms</code>, chaque zone suit un motif lisible : pages de liste, routes de creation et d''edition, composants clients cibles et server actions qui encapsulent les mutations Supabase. Les editeurs y gagnent une interface coherente et les identifiants restent cote serveur.</p>\\n\\n<h2>Open core sans derive produit</h2>\\n<p>Le coeur du CMS est open source sous AGPL. Les modules premium comme <code>@nextblock-cms/ecommerce</code> restent disponibles en source mais sont actives via <code>package_activations</code> et <code>verifyPackageOnline()</code>. Le meme shell peut donc rester simple pour l''open source tout en deverrouillant les surfaces commerce au bon moment.</p>\\n\\n<h2>Pourquoi l''ensemble tient</h2>\\n<p>Le workspace Nx garde les librairies honnetes, l''app Next.js maintient la coherence UI, les migrations Supabase codifient les regles d''acces, et l''editeur Tiptap donne la meme experience de contribution quel que soit le deploiement. Quand une equipe lance <code>npm create nextblock</code>, elle recupere une facon de travailler complete, pas juste des fichiers.</p>\"}', 0, '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.859357+00', NULL) ON CONFLICT DO NOTHING;\nINSERT INTO public.currencies (id, code, symbol, exchange_rate, is_default, is_active, rounding_mode, rounding_increment, rounding_charm_amount, auto_update_exchange_rate, exchange_rate_updated_at, exchange_rate_source, auto_sync_product_prices, created_at, updated_at) VALUES ('f26e8339-d3cd-46f2-b11e-12569c30d4a8', 'USD', '$', 1.0000000000, true, true, 'none', 1, NULL, false, '2026-07-03 17:52:15.444652+00', 'store-default', false, '2026-07-03 17:52:15.444652+00', '2026-07-03 17:52:15.444652+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.logos (id, name, media_id, created_at) VALUES ('d45ef03d-27fc-4099-8b46-1cdf82d658d2', 'NextBlock™ Logo', 'ea6fdaf5-f8de-416c-9f2b-b689b7a4ed38', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.navigation_items (id, language_id, menu_key, label, url, parent_id, \"order\", page_id, translation_group_id, created_at, updated_at) VALUES (1, 1, 'HEADER', 'Home', '/', NULL, 0, 1, 'b6bd9818-a8d1-4a17-ac6b-d5b1f178a297', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.navigation_items (id, language_id, menu_key, label, url, parent_id, \"order\", page_id, translation_group_id, created_at, updated_at) VALUES (2, 1, 'HEADER', 'Articles', '/articles', NULL, 1, 3, '8c269d8b-db64-47ab-8ce0-29f65dbbe867', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.navigation_items (id, language_id, menu_key, label, url, parent_id, \"order\", page_id, translation_group_id, created_at, updated_at) VALUES (3, 1, 'HEADER', 'Contact', '/contact', NULL, 3, 5, 'cd630aed-aeb8-4acf-934b-9ccbbe8ee9bd', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.navigation_items (id, language_id, menu_key, label, url, parent_id, \"order\", page_id, translation_group_id, created_at, updated_at) VALUES (4, 2, 'HEADER', 'Accueil', '/accueil', NULL, 0, 2, 'aaa0c842-c996-4a07-bb3a-7562f865b6a4', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.navigation_items (id, language_id, menu_key, label, url, parent_id, \"order\", page_id, translation_group_id, created_at, updated_at) VALUES (5, 2, 'HEADER', 'Articles', '/articles', NULL, 1, 4, 'af69b017-3762-4c11-b709-37fb56081b99', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.navigation_items (id, language_id, menu_key, label, url, parent_id, \"order\", page_id, translation_group_id, created_at, updated_at) VALUES (6, 2, 'HEADER', 'Contact', '/contact', NULL, 3, 6, '21a91d4b-a494-4b3a-8d28-c21c414042cf', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.navigation_items (id, language_id, menu_key, label, url, parent_id, \"order\", page_id, translation_group_id, created_at, updated_at) VALUES (7, 1, 'FOOTER', 'Privacy Policy', '/privacy-policy', NULL, 0, 7, 'c8f2e6a3-7d28-4174-a8f7-0179226ae79a', '2026-07-03 17:52:15.753698+00', '2026-07-03 17:52:15.753698+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.navigation_items (id, language_id, menu_key, label, url, parent_id, \"order\", page_id, translation_group_id, created_at, updated_at) VALUES (8, 1, 'FOOTER', 'Terms of Service', '/terms-of-service', NULL, 1, 9, '4838ca4b-eaad-495c-ad7c-1750984bc8d9', '2026-07-03 17:52:15.753698+00', '2026-07-03 17:52:15.753698+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.navigation_items (id, language_id, menu_key, label, url, parent_id, \"order\", page_id, translation_group_id, created_at, updated_at) VALUES (9, 2, 'FOOTER', 'Politique de confidentialité', '/politique-de-confidentialite', NULL, 0, 8, 'c8f2e6a3-7d28-4174-a8f7-0179226ae79a', '2026-07-03 17:52:15.753698+00', '2026-07-03 17:52:15.753698+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.navigation_items (id, language_id, menu_key, label, url, parent_id, \"order\", page_id, translation_group_id, created_at, updated_at) VALUES (10, 2, 'FOOTER', 'Conditions d''utilisation', '/conditions-utilisation', NULL, 1, 10, '4838ca4b-eaad-495c-ad7c-1750984bc8d9', '2026-07-03 17:52:15.753698+00', '2026-07-03 17:52:15.753698+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.shipping_zones (id, name, priority_order, created_at, updated_at) VALUES ('4026af0e-5752-44fb-b7f7-ba5ac722cadc', 'North America', 10, '2026-07-03 17:52:15.25122+00', '2026-07-03 17:52:15.25122+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.shipping_zone_locations (id, zone_id, country_code, state_code, postal_code, created_at) VALUES ('c25d8fee-f0b6-43d5-8af5-5af4a47cb123', '4026af0e-5752-44fb-b7f7-ba5ac722cadc', 'US', NULL, NULL, '2026-07-03 17:52:15.25122+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.shipping_zone_locations (id, zone_id, country_code, state_code, postal_code, created_at) VALUES ('3d3d4bf6-7cb1-41b9-a5e1-1eb9098e640f', '4026af0e-5752-44fb-b7f7-ba5ac722cadc', 'CA', NULL, NULL, '2026-07-03 17:52:15.25122+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.shipping_zone_locations (id, zone_id, country_code, state_code, postal_code, created_at) VALUES ('63d47fff-7148-48ff-8cc1-5d3ba99fa022', '4026af0e-5752-44fb-b7f7-ba5ac722cadc', 'MX', NULL, NULL, '2026-07-03 17:52:15.25122+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.shipping_zone_methods (id, zone_id, method_type, cost_amount, cost_currency, min_order_amount, name, name_translations, currency_pricing_mode, cost_amounts, min_order_amounts, created_at, updated_at) VALUES ('ea112a82-e05c-46bc-a5e7-5bb62528aa5d', '4026af0e-5752-44fb-b7f7-ba5ac722cadc', 'flat_rate', 1500, 'USD', 0, 'Standard Shipping', '{\"fr\": \"Livraison standard\"}', 'auto', '{\"USD\": 1500}', '{\"USD\": 0}', '2026-07-03 17:52:15.25122+00', '2026-07-03 17:52:15.25122+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.shipping_zone_methods (id, zone_id, method_type, cost_amount, cost_currency, min_order_amount, name, name_translations, currency_pricing_mode, cost_amounts, min_order_amounts, created_at, updated_at) VALUES ('32d473d1-e3d4-4741-a7f5-63d87a387e08', '4026af0e-5752-44fb-b7f7-ba5ac722cadc', 'free_shipping', 0, 'USD', 10000, 'Free Shipping (Orders over $100)', '{\"fr\": \"Livraison gratuite (commandes de plus de 100 $)\"}', 'auto', '{\"USD\": 0}', '{\"USD\": 10000}', '2026-07-03 17:52:15.25122+00', '2026-07-03 17:52:15.25122+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('footer_copyright', '{\"en\": \"© {year} Nextblock CMS. All rights reserved.\", \"fr\": \"© {year} Nextblock CMS. Tous droits réservés.\"}') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('enabled_payment_providers', '{\"stripe\": false, \"freemius\": false}') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('site_title', '\"NextBlock™ CMS\"') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('site_description', '\"NextBlock™ is an open-source CMS on Next.js + Supabase — a visual block editor, blazing-fast multilingual pages, and built-in e-commerce.\"') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('site_keywords', '\"NextBlock, CMS, Next.js, Supabase, headless CMS, block editor, visual page builder, multilingual, e-commerce, open source\"') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('ecommerce_inventory_settings', '{\"enable_taxes\": false, \"track_quantities\": true}') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('invoice_settings', '{\"email\": \"\", \"phone\": \"\", \"address\": {\"city\": \"\", \"line1\": \"\", \"line2\": \"\", \"state\": \"\", \"postal_code\": \"\", \"country_code\": \"CA\"}, \"business_name\": \"\", \"tax_registrations\": []}') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('privacy_settings', '{\"gtm_id\": \"\", \"corporate\": {\"address\": \"\", \"legal_name\": \"\", \"support_email\": \"\"}, \"banner_enabled\": true, \"custom_scripts\": \"\", \"ga_measurement_id\": \"\"}') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('email_public', '{\"host\": \"\", \"port\": \"\", \"secure\": true, \"fromName\": \"\", \"fromEmail\": \"\"}') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('payment_public', '{\"stripe\": {\"publishableKey\": \"\"}, \"freemius\": {\"productId\": \"\", \"publicKey\": \"\", \"developerId\": \"\", \"sandboxEnabled\": false}}') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('onboarding_state', '{\"skipped\": [], \"dismissed\": false}') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('security_settings', '{\"enforce_staff_2fa\": true, \"trusted_device_days\": 30}') ON CONFLICT DO NOTHING;\nINSERT INTO public.site_settings (key, value) VALUES ('is_admin_created', 'false') ON CONFLICT DO NOTHING;\nINSERT INTO public.system_configuration (id, auto_accept_signups, settings) VALUES (1, false, '{}'::jsonb) ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('continue_with_github', '{\"en\": \"Continue with GitHub\", \"es\": \"Continuar con GitHub\", \"fr\": \"Continuer avec GitHub\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('or_continue_with', '{\"en\": \"Or continue with\", \"es\": \"O continuar con\", \"fr\": \"Ou continuer avec\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('customer_profile', '{\"en\": \"Customer Profile\", \"es\": \"Perfil de Cliente\", \"fr\": \"Profil Client\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('personal_information', '{\"en\": \"Personal Information\", \"es\": \"Información Personal\", \"fr\": \"Informations Personnelles\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('full_name', '{\"en\": \"Full Name\", \"es\": \"Nombre Completo\", \"fr\": \"Nom Complet\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('github_username', '{\"en\": \"GitHub Username\", \"es\": \"Nombre de usuario de GitHub\", \"fr\": \"Nom d''utilisateur GitHub\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('github_username_help', '{\"en\": \"Required only for purchasing developer licenses.\", \"es\": \"Requerido solo para comprar licencias de desarrollador.\", \"fr\": \"Requis uniquement pour l''achat de licences développeur.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('phone_number', '{\"en\": \"Phone Number\", \"es\": \"Número de Teléfono\", \"fr\": \"Numéro de Téléphone\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('billing_address', '{\"en\": \"Billing Address\", \"es\": \"Dirección de Facturación\", \"fr\": \"Adresse de Facturation\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('address_line_1', '{\"en\": \"Address Line 1\", \"es\": \"Dirección Línea 1\", \"fr\": \"Adresse Ligne 1\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('address_line_2', '{\"en\": \"Address Line 2 (Optional)\", \"es\": \"Dirección Línea 2 (Opcional)\", \"fr\": \"Adresse Ligne 2 (Optionnel)\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('city', '{\"en\": \"City\", \"es\": \"Ciudad\", \"fr\": \"Ville\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('state_province', '{\"en\": \"State / Province\", \"es\": \"Estado / Provincia\", \"fr\": \"État / Province\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('postal_zip_code', '{\"en\": \"Postal / Zip Code\", \"es\": \"Código Postal\", \"fr\": \"Code Postal\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('country', '{\"en\": \"Country\", \"es\": \"País\", \"fr\": \"Pays\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('save_profile', '{\"en\": \"Save Profile\", \"es\": \"Guardar Perfil\", \"fr\": \"Enregistrer le Profil\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('saving', '{\"en\": \"Saving...\", \"es\": \"Guardando...\", \"fr\": \"Enregistrement...\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_updated_success', '{\"en\": \"Profile updated successfully\", \"es\": \"Perfil actualizado con éxito\", \"fr\": \"Profil mis à jour avec succès\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_update_failed', '{\"en\": \"Failed to update profile\", \"es\": \"Error al actualizar el perfil\", \"fr\": \"Échec de la mise à jour du profil\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('address_required', '{\"en\": \"Address is required\", \"es\": \"La dirección es obligatoria\", \"fr\": \"L''adresse est requise\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('city_required', '{\"en\": \"City is required\", \"es\": \"La ciudad es obligatoria\", \"fr\": \"La ville est requise\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('zip_code_required', '{\"en\": \"Zip Code is required\", \"es\": \"El código postal es obligatorio\", \"fr\": \"Le code postal est requis\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('country_required', '{\"en\": \"Country is required\", \"es\": \"El país es obligatorio\", \"fr\": \"Le pays est requis\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('enter_valid_json', '{\"en\": \"Enter valid JSON for billing address.\", \"es\": \"Ingrese JSON válido para la dirección de facturación.\", \"fr\": \"Entrez un JSON valide pour l''adresse de facturation.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('public_profile', '{\"en\": \"Public Profile\", \"es\": \"Perfil Público\", \"fr\": \"Profil Public\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('details', '{\"en\": \"Account Details\", \"es\": \"Detalles de la Cuenta\", \"fr\": \"Détails du Compte\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('identity', '{\"en\": \"Identity\", \"es\": \"Identidad\", \"fr\": \"Identité\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('website', '{\"en\": \"Website\", \"es\": \"Sitio Web\", \"fr\": \"Site Web\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('avatar_url', '{\"en\": \"Avatar URL\", \"es\": \"URL del Avatar\", \"fr\": \"URL de l''Avatar\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('connect_github', '{\"en\": \"Connect GitHub\", \"es\": \"Conectar GitHub\", \"fr\": \"Connecter GitHub\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('github_link_failed', '{\"en\": \"Failed to link GitHub account\", \"es\": \"Error al vincular cuenta de GitHub\", \"fr\": \"Échec de la liaison du compte GitHub\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('save_changes', '{\"en\": \"Save Changes\", \"es\": \"Guardar Cambios\", \"fr\": \"Enregistrer les Modifications\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('github_connected', '{\"en\": \"GitHub Connected\", \"es\": \"GitHub Conectado\", \"fr\": \"GitHub Connecté\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('linked_to', '{\"en\": \"Linked to\", \"es\": \"Vinculado a\", \"fr\": \"Lié à\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('optional', '{\"en\": \"Optional\", \"fr\": \"Optionnel\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('shipping_address', '{\"en\": \"Shipping Address\", \"fr\": \"Adresse de livraison\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_settings_title', '{\"en\": \"Profile Settings\", \"fr\": \"Paramètres du profil\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_settings_description', '{\"en\": \"Keep your contact details and default addresses up to date for faster checkout.\", \"fr\": \"Gardez vos coordonnées et adresses par défaut à jour pour un paiement plus rapide.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_not_found', '{\"en\": \"Profile not found.\", \"fr\": \"Profil introuvable.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_basic_info_help', '{\"en\": \"This information appears on your account and helps us prepare your orders.\", \"fr\": \"Ces informations apparaissent sur votre compte et nous aident à préparer vos commandes.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_address_defaults_help', '{\"en\": \"These default addresses are prefilled during checkout and can still be edited for each order.\", \"fr\": \"Ces adresses par défaut sont préremplies au paiement et restent modifiables pour chaque commande.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('use_billing_for_shipping', '{\"en\": \"Use billing address for shipping\", \"fr\": \"Utiliser l''adresse de facturation pour la livraison\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_use_billing_for_shipping_help', '{\"en\": \"Keep one default address unless you regularly ship somewhere else.\", \"fr\": \"Gardez une seule adresse par défaut sauf si vous faites souvent livrer ailleurs.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('checkout_complete_billing_address', '{\"en\": \"Please complete your billing address before continuing.\", \"fr\": \"Veuillez compléter votre adresse de facturation avant de continuer.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('checkout_complete_shipping_address', '{\"en\": \"Please complete your shipping address before continuing.\", \"fr\": \"Veuillez compléter votre adresse de livraison avant de continuer.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('checkout_prefill_notice', '{\"en\": \"Using your saved account details for {email}. You can still adjust them for this order.\", \"fr\": \"Nous utilisons les renseignements enregistrés pour {email}. Vous pouvez toujours les ajuster pour cette commande.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('checkout_billing_address_help', '{\"en\": \"We use this address for payment verification and invoicing.\", \"fr\": \"Nous utilisons cette adresse pour la vérification du paiement et la facturation.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('checkout_use_billing_for_shipping_help', '{\"en\": \"Uncheck this if you want your order delivered to a different address.\", \"fr\": \"Décochez ceci si vous souhaitez faire livrer votre commande à une autre adresse.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('checkout_shipping_address_help', '{\"en\": \"Choose where physical items should be delivered.\", \"fr\": \"Choisissez où les articles physiques doivent être livrés.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('checkout_payment_only_notice', '{\"en\": \"Stripe checkout is kept focused on payment because your address details are already collected here.\", \"fr\": \"Le paiement Stripe reste centré sur le paiement puisque vos coordonnées sont déjà recueillies ici.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.signup_existing_account_hint', '{\"en\": \"That email may already be registered. Try signing in or resetting your password.\", \"fr\": \"Cette adresse e-mail est peut-être déjà utilisée. Essayez de vous connecter ou de réinitialiser votre mot de passe.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.signup_check_email_profile', '{\"en\": \"Check your email to confirm your account. We''ll bring you to your profile next so you can finish setting up your details.\", \"fr\": \"Vérifiez votre e-mail pour confirmer votre compte. Nous vous amènerons ensuite à votre profil pour terminer la configuration de vos renseignements.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.add_to_cart', '{\"en\": \"Add to Cart\", \"fr\": \"Ajouter au panier\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.added_to_cart', '{\"en\": \"Added to cart\", \"fr\": \"Ajouté au panier\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.no_image', '{\"en\": \"No Image\", \"fr\": \"Pas d''image\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.view_details', '{\"en\": \"View Details\", \"fr\": \"Voir les détails\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.item_added_desc', '{\"en\": \"The item has been added to your cart.\", \"fr\": \"L''article a été ajouté à votre panier.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout', '{\"en\": \"Checkout\", \"fr\": \"Paiement\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.cart', '{\"en\": \"Cart\", \"fr\": \"Panier\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.subtotal', '{\"en\": \"Subtotal\", \"fr\": \"Sous-total\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shipping', '{\"en\": \"Shipping\", \"fr\": \"Livraison\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.tax', '{\"en\": \"Tax\", \"fr\": \"Taxes\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.total', '{\"en\": \"Total\", \"fr\": \"Total\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.order_summary', '{\"en\": \"Order Summary\", \"fr\": \"Résumé de la commande\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.order_summary_desc', '{\"en\": \"Review your items before proceeding to payment.\", \"fr\": \"Vérifiez vos articles avant de procéder au paiement.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.payment_details', '{\"en\": \"Payment Details\", \"fr\": \"Détails du paiement\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shipping_info', '{\"en\": \"Shipping Information\", \"fr\": \"Informations de livraison\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.delivery_notice', '{\"en\": \"Digital product - No shipping required\", \"fr\": \"Produit numérique - Aucune livraison requise\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.back_to_shop', '{\"en\": \"Back to Shop\", \"fr\": \"Retour à la boutique\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.empty_cart', '{\"en\": \"Your cart is empty\", \"fr\": \"Votre panier est vide\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.no_products_found', '{\"en\": \"No products found\", \"fr\": \"Aucun produit trouvé\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.featured_products', '{\"en\": \"Featured Products\", \"fr\": \"Produits vedettes\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.latest_products', '{\"en\": \"Latest Products\", \"fr\": \"Derniers produits\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.search_products', '{\"en\": \"Search products...\", \"fr\": \"Rechercher des produits...\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.price_low_to_high', '{\"en\": \"Price: Low to High\", \"fr\": \"Prix : Croissant\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.price_high_to_low', '{\"en\": \"Price: High to Low\", \"fr\": \"Prix : Décroissant\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.newest', '{\"en\": \"Newest\", \"fr\": \"Nouveautés\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.filters', '{\"en\": \"Filters\", \"fr\": \"Filtres\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.apply_filters', '{\"en\": \"Apply Filters\", \"fr\": \"Appliquer les filtres\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.clear_all', '{\"en\": \"Clear All\", \"fr\": \"Tout effacer\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.digital_notice', '{\"en\": \"This is a digital product. You will receive access instructions via email after purchase.\", \"fr\": \"Ceci est un produit numérique. Vous recevrez les instructions d''accès par e-mail après l''achat.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shopping_cart', '{\"en\": \"Shopping Cart\", \"fr\": \"Panier d''achat\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.cart_empty', '{\"en\": \"Your cart is empty\", \"fr\": \"Votre panier est vide\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.cart_empty_description', '{\"en\": \"Looks like you haven''t added anything to your cart yet.\", \"fr\": \"On dirait que vous n''avez encore rien ajouté à votre panier.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.continue_shopping', '{\"en\": \"Continue Shopping\", \"fr\": \"Continuer vos achats\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.go_to_shop', '{\"en\": \"Go to Shop\", \"fr\": \"Aller à la boutique\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_successful', '{\"en\": \"Checkout Successful\", \"fr\": \"Paiement Réussi\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.sandbox_notice', '{\"en\": \"This is a Sandbox environment. The Freemius checkout is skipped here for demo purposes.\", \"fr\": \"Ceci est un environnement de bac à sable. Le paiement Freemius est sauté ici à des fins de démonstration.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.license_notice', '{\"en\": \"To purchase a real license for your self-hosted NextBlock™ instance, visit:\", \"fr\": \"Pour acheter une vraie licence pour votre instance NextBlock™ auto-hébergée, visitez :\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.purchase_at', '{\"en\": \"Purchase at nextblock.dev\", \"fr\": \"Acheter sur nextblock.dev\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.qty', '{\"en\": \"Qty\", \"fr\": \"Qté\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.quantity', '{\"en\": \"Quantity\", \"fr\": \"Quantité\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.product', '{\"en\": \"Product\", \"fr\": \"Produit\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.price', '{\"en\": \"Price\", \"fr\": \"Prix\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.secure_payment', '{\"en\": \"Secure payment processing\", \"fr\": \"Traitement sécurisé du paiement\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shipping_taxes_notice', '{\"en\": \"* Taxes and shipping will be calculated on the next step.\", \"fr\": \"* Les taxes et les frais de livraison seront calculés à l''étape suivante.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shipping_taxes_calculated', '{\"en\": \"Shipping & taxes calculated at checkout.\", \"fr\": \"Livraison et taxes calculées lors du paiement.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.email_address', '{\"en\": \"Email Address\", \"fr\": \"Adresse e-mail\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.pay_now', '{\"en\": \"Pay Now\", \"fr\": \"Payer maintenant\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.proceed_to_checkout', '{\"en\": \"Proceed to Checkout\", \"fr\": \"Passer à la caisse\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.processing', '{\"en\": \"Processing...\", \"fr\": \"Traitement...\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.invalid_email', '{\"en\": \"Please enter a valid email address.\", \"fr\": \"Veuillez entrer une adresse e-mail valide.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_failed', '{\"en\": \"Checkout failed: \", \"fr\": \"Le paiement a échoué : \"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.generic_error', '{\"en\": \"An error occurred. Please try again.\", \"fr\": \"Une erreur est survenue. Veuillez réessayer.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_popup_blocked', '{\"en\": \"Checkout popup blocked or failed to load. Falling back to direct link.\", \"fr\": \"Le popup de paiement a été bloqué ou n''a pas pu être chargé. Retour au lien direct.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.view_full_cart', '{\"en\": \"View Full Cart\", \"fr\": \"Voir le panier complet\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.ready_to_checkout', '{\"en\": \"Ready to Checkout?\", \"fr\": \"Prêt à passer au paiement ?\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.sale_badge', '{\"en\": \"Sale {percent}% Off\", \"fr\": \"Solde {percent}% de rabais\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.low_stock', '{\"en\": \"Only {count} left\", \"fr\": \"Plus que {count} en stock\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.instant_digital_delivery', '{\"en\": \"Instant Digital Delivery\", \"fr\": \"Livraison numérique instantanée\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.free_shipping', '{\"en\": \"Free Shipping\", \"fr\": \"Livraison gratuite\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.secure_checkout', '{\"en\": \"Secure Checkout\", \"fr\": \"Paiement sécurisé\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.no_description', '{\"en\": \"No description available.\", \"fr\": \"Aucune description disponible.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_overlay_title', '{\"en\": \"Order Checkout\", \"fr\": \"Paiement de la commande\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.email_placeholder', '{\"en\": \"you@example.com\", \"fr\": \"vous@exemple.com\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.contact_information', '{\"en\": \"Contact Information\", \"fr\": \"Informations de contact\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shipping_address', '{\"en\": \"Shipping Address\", \"fr\": \"Adresse de livraison\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shipping_method', '{\"en\": \"Shipping Method\", \"fr\": \"Mode de livraison\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.available_rates', '{\"en\": \"Available Rates\", \"fr\": \"Tarifs disponibles\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.calculating', '{\"en\": \"Calculating...\", \"fr\": \"Calcul en cours...\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.select_rate', '{\"en\": \"Select a shipping rate\", \"fr\": \"Sélectionnez un tarif de livraison\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.enter_postal_code', '{\"en\": \"Enter postal code\", \"fr\": \"Entrez le code postal\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.free', '{\"en\": \"Free\", \"fr\": \"Gratuit\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.first_last_name', '{\"en\": \"First & Last Name\", \"fr\": \"Nom et prénom\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.address', '{\"en\": \"Address\", \"fr\": \"Adresse\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.city', '{\"en\": \"City\", \"fr\": \"Ville\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.state_province', '{\"en\": \"State / Province\", \"fr\": \"État / Province\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.zip_postal', '{\"en\": \"ZIP / Postal Code\", \"fr\": \"Code postal\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.postal_code', '{\"en\": \"Postal Code\", \"fr\": \"Code postal\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.zip_postal_code', '{\"en\": \"ZIP / Postal Code\", \"fr\": \"Code postal\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.estimate_shipping', '{\"en\": \"Estimate Shipping\", \"fr\": \"Estimer la livraison\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.calculate', '{\"en\": \"Calculate\", \"fr\": \"Calculer\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.no_rates_found', '{\"en\": \"No shipping rates found for this region.\", \"fr\": \"Aucun tarif de livraison trouvé pour cette région.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.no_rates_for_region', '{\"en\": \"No shipping rates found for this region.\", \"fr\": \"Aucun tarif de livraison trouvé pour cette région.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.enter_address_for_rates', '{\"en\": \"Enter your address to see shipping rates.\", \"fr\": \"Entrez votre adresse pour voir les tarifs de livraison.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.secure_checkout_guarantee', '{\"en\": \"Secure checkout guaranteed\", \"fr\": \"Paiement sécurisé garanti\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.full_name', '{\"en\": \"Full Name\", \"fr\": \"Nom complet\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.country', '{\"en\": \"Country\", \"fr\": \"Pays\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.freemius_trial_preference_title', '{\"en\": \"How would you like to start your trial?\", \"es\": \"¿Cómo te gustaría comenzar tu prueba?\", \"fr\": \"Comment souhaitez-vous commencer votre essai ?\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.pricing_unavailable', '{\"en\": \"Pricing Unavailable\", \"es\": \"Precios no disponibles\", \"fr\": \"Tarification indisponible\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.monthly', '{\"en\": \"Monthly\", \"es\": \"Mensual\", \"fr\": \"Mensuel\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.annual', '{\"en\": \"Annual\", \"es\": \"Anual\", \"fr\": \"Annuel\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.lifetime', '{\"en\": \"Lifetime\", \"es\": \"De por vida\", \"fr\": \"À vie\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.year', '{\"en\": \"year\", \"es\": \"año\", \"fr\": \"an\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.month', '{\"en\": \"month\", \"es\": \"mes\", \"fr\": \"mois\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.freemius_trial_no_card', '{\"en\": \"Start Free Trial (No card required)\", \"es\": \"Comenzar prueba gratuita (Sin tarjeta)\", \"fr\": \"Commencer l''essai gratuit (Sans carte requise)\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.freemius_trial_with_card', '{\"en\": \"Enter Payment Details Now (Still get full trial length free)\", \"es\": \"Ingresar detalles de pago ahora (Aún obtienes toda la duración de la prueba gratis)\", \"fr\": \"Entrer les détails de paiement maintenant (Vous bénéficiez toujours de toute la durée de l''essai gratuitement)\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.freemius_trial_with_card_help', '{\"en\": \"You will not be billed until the trial ends. Cancel anytime.\", \"es\": \"No se te cobrará hasta que termine la prueba. Cancela en cualquier momento.\", \"fr\": \"Vous ne serez pas facturé avant la fin de l''essai. Annulez à tout moment.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.choose_your_options', '{\"en\": \"Choose Your Options\", \"fr\": \"Choisissez vos options\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.variant_availability_help', '{\"en\": \"Select a combination to resolve the exact variant price and availability.\", \"fr\": \"Selectionnez une combinaison pour afficher le prix exact et la disponibilite de la variante.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.in_stock', '{\"en\": \"{count} in stock\", \"fr\": \"{count} en stock\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.out_of_stock', '{\"en\": \"Out of stock\", \"fr\": \"Rupture de stock\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.select_options', '{\"en\": \"Select Options\", \"fr\": \"Choisir des options\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.variant_selection_required', '{\"en\": \"Select one term from every dropdown to resolve a variation.\", \"fr\": \"Selectionnez une valeur dans chaque liste pour afficher la variante correspondante.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.tax_calculated_on_stripe', '{\"en\": \"Calculated on Stripe\", \"es\": \"Calculado en Stripe\", \"fr\": \"Calculé sur Stripe\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('checkout_stripe_tax_finalized_notice', '{\"en\": \"Tax will be finalized by Stripe Tax on the payment step.\", \"es\": \"El impuesto se finalizará con Stripe Tax en el paso de pago.\", \"fr\": \"La taxe sera finalisée par Stripe Tax à l''étape du paiement.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('branding', '{\"en\": \"Branding\", \"fr\": \"Image de marque\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('company_name', '{\"en\": \"Company name\", \"fr\": \"Nom de l''entreprise\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('invoice', '{\"en\": \"Invoice\", \"fr\": \"Facture\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('invoice_number', '{\"en\": \"Invoice #\", \"fr\": \"Facture no\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('paid_on', '{\"en\": \"Paid on\", \"fr\": \"Paye le\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('bill_to', '{\"en\": \"Bill to\", \"fr\": \"Facturer a\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ship_to', '{\"en\": \"Ship to\", \"fr\": \"Livrer a\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('print_invoice', '{\"en\": \"Print / Save as PDF\", \"fr\": \"Imprimer / Enregistrer en PDF\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('tax_registrations', '{\"en\": \"Tax registrations\", \"fr\": \"Inscriptions fiscales\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('invoice_settings', '{\"en\": \"Invoice settings\", \"fr\": \"Parametres de facture\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('business_name', '{\"en\": \"Business name\", \"fr\": \"Nom de l''entreprise\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('order_number', '{\"en\": \"Order #\", \"fr\": \"Commande no\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('print_invoice_help', '{\"en\": \"Use your browser print dialog to save this invoice as a PDF.\", \"fr\": \"Utilisez la boite de dialogue d''impression de votre navigateur pour enregistrer cette facture en PDF.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('return_home', '{\"en\": \"Return to Home\", \"fr\": \"Retour a l''accueil\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('receipt_finalizing', '{\"en\": \"Finalizing your invoice and payment details...\", \"fr\": \"Finalisation de votre facture et des details du paiement...\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('receipt_not_ready', '{\"en\": \"Your invoice will appear here once the payment sync is complete.\", \"fr\": \"Votre facture apparaitra ici une fois la synchronisation du paiement terminee.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('tax_breakdown', '{\"en\": \"Tax breakdown\", \"fr\": \"Detail des taxes\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('amount', '{\"en\": \"Amount\", \"fr\": \"Montant\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('price', '{\"en\": \"Price\", \"fr\": \"Prix\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('from', '{\"en\": \"From\", \"fr\": \"De\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('account_navigation', '{\"en\": \"Account\", \"fr\": \"Compte\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('account_orders', '{\"en\": \"Orders\", \"fr\": \"Commandes\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('change_my_password', '{\"en\": \"Change my password\", \"fr\": \"Changer mon mot de passe\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_orders_title', '{\"en\": \"My orders\", \"fr\": \"Mes commandes\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_orders_description', '{\"en\": \"Review your recent purchases and open printable invoices.\", \"fr\": \"Consultez vos achats récents et ouvrez vos factures imprimables.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_orders_empty', '{\"en\": \"You do not have any orders yet.\", \"fr\": \"Vous n''avez pas encore de commandes.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_order_detail_title', '{\"en\": \"Order invoice\", \"fr\": \"Facture de commande\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_order_detail_description', '{\"en\": \"Review and print your finalized invoice.\", \"fr\": \"Consultez et imprimez votre facture finalisée.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_order_invoice_pending', '{\"en\": \"The printable invoice will appear here once this order has been finalized.\", \"fr\": \"La facture imprimable apparaîtra ici une fois que cette commande aura été finalisée.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_password_title', '{\"en\": \"Change your password\", \"fr\": \"Changer votre mot de passe\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('profile_password_description', '{\"en\": \"Update your account password without leaving your profile.\", \"fr\": \"Mettez à jour le mot de passe de votre compte sans quitter votre profil.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('new_password', '{\"en\": \"New password\", \"fr\": \"Nouveau mot de passe\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('confirm_new_password', '{\"en\": \"Confirm new password\", \"fr\": \"Confirmer le nouveau mot de passe\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('password_updated_success', '{\"en\": \"Password updated successfully.\", \"fr\": \"Mot de passe mis à jour avec succès.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('password_update_failed', '{\"en\": \"Password update failed.\", \"fr\": \"La mise à jour du mot de passe a échoué.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('passwords_do_not_match', '{\"en\": \"Passwords do not match.\", \"fr\": \"Les mots de passe ne correspondent pas.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('order_date', '{\"en\": \"Date\", \"fr\": \"Date\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('order_status_paid', '{\"en\": \"Paid\", \"fr\": \"Payée\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('order_status_pending', '{\"en\": \"Pending\", \"fr\": \"En attente\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('order_status_trial', '{\"en\": \"Trial\", \"fr\": \"Essai\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('order_status_shipped', '{\"en\": \"Shipped\", \"fr\": \"Expédiée\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('order_status_cancelled', '{\"en\": \"Cancelled\", \"fr\": \"Annulée\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('order_status_refunded', '{\"en\": \"Refunded\", \"fr\": \"Remboursée\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('back_to_orders', '{\"en\": \"Back to orders\", \"fr\": \"Retour aux commandes\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_trial_started', '{\"en\": \"Trial started\", \"fr\": \"Essai demarre\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_order_pending', '{\"en\": \"Order pending\", \"fr\": \"Commande en attente\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('select_an_option', '{\"en\": \"Select an option\", \"es\": \"Selecciona una opcion\", \"fr\": \"Selectionnez une option\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shipping_method_required', '{\"en\": \"Please select a shipping method before continuing.\", \"es\": \"Selecciona un metodo de envio antes de continuar.\", \"fr\": \"Veuillez selectionner un mode de livraison avant de continuer.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.waiting_on_address_info', '{\"en\": \"Complete your shipping address to view available shipping options.\", \"es\": \"Completa tu direccion de envio para ver las opciones de envio disponibles.\", \"fr\": \"Completez votre adresse de livraison pour voir les options de livraison disponibles.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.calculating_shipping', '{\"en\": \"Calculating shipping...\", \"es\": \"Calculando el envio...\", \"fr\": \"Calcul de la livraison...\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.sandbox_checkout_stripe_description', '{\"en\": \"This simulated step represents the Stripe checkout for physical products.\", \"es\": \"Este paso simulado representa el pago de Stripe para productos fisicos.\", \"fr\": \"Cette etape simulee represente le paiement Stripe pour les produits physiques.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.sandbox_checkout_freemius_description', '{\"en\": \"This simulated step represents the Freemius checkout for digital products.\", \"es\": \"Este paso simulado representa el pago de Freemius para productos digitales.\", \"fr\": \"Cette etape simulee represente le paiement Freemius pour les produits numeriques.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.digital_label', '{\"en\": \"Digital\", \"es\": \"Digital\", \"fr\": \"Numerique\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.physical_label', '{\"en\": \"Physical\", \"es\": \"Fisico\", \"fr\": \"Physique\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.physical_products', '{\"en\": \"Physical products\", \"es\": \"Productos fisicos\", \"fr\": \"Produits physiques\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.digital_products', '{\"en\": \"Digital products\", \"es\": \"Productos digitales\", \"fr\": \"Produits numeriques\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.estimated_total', '{\"en\": \"Estimated total\", \"es\": \"Total estimado\", \"fr\": \"Total estime\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.stripe_checkout_title', '{\"en\": \"Stripe Checkout\", \"es\": \"Pago con Stripe\", \"fr\": \"Paiement Stripe\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.stripe_checkout_description', '{\"en\": \"Pay for physical products in one Stripe checkout session.\", \"es\": \"Paga los productos fisicos en una sola sesion de Stripe.\", \"fr\": \"Payez les produits physiques dans une seule session Stripe.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.item_count_one', '{\"en\": \"{count} item\", \"es\": \"{count} articulo\", \"fr\": \"{count} article\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.item_count_other', '{\"en\": \"{count} items\", \"es\": \"{count} articulos\", \"fr\": \"{count} articles\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.physical_subtotal', '{\"en\": \"Physical subtotal\", \"es\": \"Subtotal fisico\", \"fr\": \"Sous-total physique\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.total_on_stripe', '{\"en\": \"Total on Stripe\", \"es\": \"Total en Stripe\", \"fr\": \"Total sur Stripe\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_physical_products', '{\"en\": \"Checkout Physical Products\", \"es\": \"Pagar productos fisicos\", \"fr\": \"Payer les produits physiques\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shipping_taxes_collected_on_stripe', '{\"en\": \"Shipping and taxes are only collected during the Stripe step for physical products.\", \"es\": \"El envio y los impuestos solo se cobran durante el paso de Stripe para productos fisicos.\", \"fr\": \"La livraison et les taxes sont percues uniquement a l''etape Stripe pour les produits physiques.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.freemius_checkout_title', '{\"en\": \"Freemius Checkout\", \"es\": \"Pago con Freemius\", \"fr\": \"Paiement Freemius\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.freemius_checkout_description', '{\"en\": \"Digital products use the Freemius checkout flow.\", \"es\": \"Los productos digitales usan el flujo de pago de Freemius.\", \"fr\": \"Les produits numeriques utilisent le flux de paiement Freemius.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.license_count_one', '{\"en\": \"{count} license\", \"es\": \"{count} licencia\", \"fr\": \"{count} licence\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.license_count_other', '{\"en\": \"{count} licenses\", \"es\": \"{count} licencias\", \"fr\": \"{count} licences\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_billing_cycle_monthly', '{\"en\": \"Monthly subscription\", \"es\": \"Suscripcion mensual\", \"fr\": \"Abonnement mensuel\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_billing_cycle_annual', '{\"en\": \"Annual subscription\", \"es\": \"Suscripcion anual\", \"fr\": \"Abonnement annuel\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_billing_cycle_lifetime', '{\"en\": \"Lifetime subscription\", \"es\": \"Suscripcion de por vida\", \"fr\": \"Abonnement a vie\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_product', '{\"en\": \"Checkout {title}\", \"es\": \"Pagar {title}\", \"fr\": \"Paiement de {title}\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_digital_product', '{\"en\": \"Checkout Digital Product\", \"es\": \"Pagar producto digital\", \"fr\": \"Payer le produit numerique\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.digital_subtotal', '{\"en\": \"Digital subtotal\", \"es\": \"Subtotal digital\", \"fr\": \"Sous-total numerique\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.freemius_multi_checkout_notice', '{\"en\": \"Freemius licenses are completed one at a time, so each digital product gets its own checkout action.\", \"es\": \"Las licencias de Freemius se completan una por una, por lo que cada producto digital tiene su propia accion de pago.\", \"fr\": \"Les licences Freemius se finalisent une a la fois, donc chaque produit numerique a sa propre action de paiement.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.freemius_tax_notice', '{\"en\": \"Taxes and compliance for digital products are handled inside the Freemius checkout.\", \"es\": \"Los impuestos y la conformidad para los productos digitales se gestionan dentro del pago de Freemius.\", \"fr\": \"Les taxes et la conformite pour les produits numeriques sont gerees dans le paiement Freemius.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('continue_checkout', '{\"en\": \"Continue Checkout\", \"fr\": \"Continuer le paiement\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('checkout_success_sync_failed', '{\"en\": \"We could not finalize your invoice yet. Please refresh shortly.\", \"fr\": \"Nous n''avons pas encore pu finaliser votre facture. Veuillez rafraichir la page sous peu.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shipping_country_required', '{\"en\": \"Country is required to calculate shipping.\", \"fr\": \"Le pays est requis pour calculer la livraison.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.signup_success_title', '{\"en\": \"Check your inbox\", \"fr\": \"Vérifiez votre boîte de réception\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.shipping_calculation_failed', '{\"en\": \"We couldn''t calculate shipping right now. Please try again.\", \"fr\": \"Nous n''avons pas pu calculer la livraison pour le moment. Veuillez reessayer.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_license_inactive', '{\"en\": \"The ecommerce module license is inactive.\", \"fr\": \"La licence du module ecommerce est inactive.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_invalid_items', '{\"en\": \"Your checkout items could not be processed.\", \"fr\": \"Les articles de votre commande n''ont pas pu etre traites.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_provider_items_required', '{\"en\": \"Each checkout step must include items assigned to a payment provider.\", \"fr\": \"Chaque etape de paiement doit inclure des articles associes a un fournisseur de paiement.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_mixed_provider_steps', '{\"en\": \"Products with different payment providers must be purchased in separate checkout steps.\", \"fr\": \"Les produits utilisant differents fournisseurs de paiement doivent etre achetes en etapes separees.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_freemius_single_item', '{\"en\": \"Freemius products must be purchased one at a time.\", \"fr\": \"Les produits Freemius doivent etre achetes un a la fois.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_billing_address_required', '{\"en\": \"A billing address is required to continue checkout.\", \"fr\": \"Une adresse de facturation est requise pour continuer le paiement.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_internal_server_error', '{\"en\": \"Something went wrong while preparing your checkout. Please try again.\", \"fr\": \"Une erreur s''est produite lors de la preparation de votre paiement. Veuillez reessayer.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_missing_session_id', '{\"en\": \"We couldn''t find a checkout session to finalize.\", \"fr\": \"Nous n''avons pas trouve de session de paiement a finaliser.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_payment_pending', '{\"en\": \"Your payment is still pending.\", \"fr\": \"Votre paiement est toujours en attente.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_success_order_not_found', '{\"en\": \"We couldn''t find the order linked to this checkout.\", \"fr\": \"Nous n''avons pas trouve la commande liee a ce paiement.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_success_invalid_reference', '{\"en\": \"This checkout reference can''t be finalized from this page.\", \"fr\": \"Cette reference de paiement ne peut pas etre finalisee depuis cette page.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_success_inventory_update_failed', '{\"en\": \"We couldn''t update inventory for this order.\", \"fr\": \"Nous n''avons pas pu mettre a jour l''inventaire pour cette commande.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.checkout_success_status_update_failed', '{\"en\": \"We couldn''t update the order status.\", \"fr\": \"Nous n''avons pas pu mettre a jour le statut de la commande.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.unknown_error', '{\"en\": \"Unknown error\", \"es\": \"Error desconocido\", \"fr\": \"Erreur inconnue\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.get_license', '{\"en\": \"Get License\", \"es\": \"Obtener Licencia\", \"fr\": \"Obtenir la licence\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.added_to_cart_success', '{\"en\": \"{item} added to your cart.\", \"es\": \"{item} añadido al carrito.\", \"fr\": \"{item} ajouté à votre panier.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.added_to_cart_error', '{\"en\": \"Could not add item to cart.\", \"es\": \"No se pudo añadir el artículo al carrito.\", \"fr\": \"Impossible d''ajouter l''article au panier.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('edit_product', '{\"en\": \"Edit Product\", \"fr\": \"Modifier le produit\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.trigger', '{\"en\": \"Search\", \"fr\": \"Rechercher\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.title', '{\"en\": \"Search\", \"fr\": \"Rechercher\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.description', '{\"en\": \"Search published pages, posts, and products.\", \"fr\": \"Rechercher dans les pages, articles et produits publies.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.placeholder', '{\"en\": \"Search...\", \"fr\": \"Rechercher...\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.filter_all', '{\"en\": \"All\", \"fr\": \"Tout\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.filter_pages', '{\"en\": \"Pages\", \"fr\": \"Pages\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.filter_posts', '{\"en\": \"Posts\", \"fr\": \"Articles\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.filter_products', '{\"en\": \"Products\", \"fr\": \"Produits\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.result_page', '{\"en\": \"Page\", \"fr\": \"Page\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.result_post', '{\"en\": \"Post\", \"fr\": \"Article\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.result_product', '{\"en\": \"Product\", \"fr\": \"Produit\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.recent', '{\"en\": \"Recent\", \"fr\": \"Recents\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.error_title', '{\"en\": \"Search is unavailable.\", \"fr\": \"La recherche est indisponible.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.error_description', '{\"en\": \"Please try again in a moment.\", \"fr\": \"Veuillez reessayer dans un instant.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.empty_title', '{\"en\": \"No results found.\", \"fr\": \"Aucun resultat trouve.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('global_search.empty_description', '{\"en\": \"Try another search term.\", \"fr\": \"Essayez un autre terme de recherche.\"}', '2026-07-03 17:52:15.459858+00', '2026-07-03 17:52:15.459858+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('sign_in', '{\"en\": \"Sign in\", \"fr\": \"Connexion\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('sign_up', '{\"en\": \"Sign up\", \"fr\": \"Inscription\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('sign_out', '{\"en\": \"Sign out\", \"fr\": \"Déconnexion\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('dont_have_account', '{\"en\": \"Don''t have an account?\", \"fr\": \"Pas encore de compte ?\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('email', '{\"en\": \"Email\", \"fr\": \"Email\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('you_at_example_com', '{\"en\": \"you@example.com\", \"fr\": \"vous@example.com\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('password', '{\"en\": \"Password\", \"fr\": \"Mot de passe\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('forgot_password', '{\"en\": \"Forgot Password?\", \"fr\": \"Mot de passe oublié ?\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('your_password', '{\"en\": \"Your password\", \"fr\": \"Votre mot de passe\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('signing_in_pending', '{\"en\": \"Signing In...\", \"fr\": \"Connexion en cours...\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('already_have_account', '{\"en\": \"Already have an account?\", \"fr\": \"Déjà un compte ?\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('signing_up_pending', '{\"en\": \"Signing up...\", \"fr\": \"Inscription en cours...\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reset_password', '{\"en\": \"Reset Password\", \"fr\": \"Réinitialiser le mot de passe\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.signup_form_description', '{\"en\": \"Create your account in one quick step. We''ll email you a confirmation link before you finish your profile.\", \"fr\": \"Créez votre compte en une étape rapide. Nous vous enverrons un lien de confirmation avant de terminer votre profil.\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.signup_success_badge', '{\"en\": \"Signup received\", \"fr\": \"Inscription reçue\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.signup_success_step_confirm', '{\"en\": \"Open the email we sent and confirm your address.\", \"fr\": \"Ouvrez l''e-mail envoyé et confirmez votre adresse.\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.signup_success_step_profile', '{\"en\": \"After confirmation, we''ll bring you to your profile to finish setup.\", \"fr\": \"Après confirmation, nous vous amènerons à votre profil pour terminer la configuration.\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.signup_success_step_spam', '{\"en\": \"If it doesn''t arrive soon, check spam, junk, or promotions.\", \"fr\": \"S''il n''arrive pas bientôt, vérifiez les dossiers spam, indésirables ou promotions.\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.signup_use_different_email', '{\"en\": \"Use a different email\", \"fr\": \"Utiliser une autre adresse e-mail\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.back_to_sign_in', '{\"en\": \"Back to sign in\", \"fr\": \"Retour à la connexion\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('auth.signup_rate_limit', '{\"en\": \"You''re trying too quickly. Please wait a moment before requesting another confirmation email.\", \"fr\": \"Vous allez trop vite. Veuillez attendre un instant avant de demander un nouvel e-mail de confirmation.\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('blog_prefix', '{\"en\": \"article\", \"fr\": \"article\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('edit_page', '{\"en\": \"Edit Page\", \"fr\": \"Éditer la page\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('edit_post', '{\"en\": \"Edit Post\", \"fr\": \"Éditer l''article\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('open_main_menu', '{\"en\": \"Open main menu\", \"fr\": \"Ouvrir le menu principal\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('mobile_navigation_menu', '{\"en\": \"Mobile navigation menu\", \"fr\": \"Menu de navigation mobile\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('cms_dashboard', '{\"en\": \"CMS Dashboard\", \"fr\": \"Tableau de bord CMS\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('update_env_file_warning', '{\"en\": \"Please update .env.local file with anon key and url\", \"fr\": \"Veuillez mettre à jour .env.local avec l''anon key et l''URL\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('greeting', '{\"en\": \"Hey, {username}!\", \"fr\": \"Salut, {username} !\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('theme_switcher', '{\"en\": \"Theme Switcher\", \"fr\": \"Sélecteur de thème\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('theme_light', '{\"en\": \"Light\", \"fr\": \"Clair\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('theme_dark', '{\"en\": \"Dark\", \"fr\": \"Sombre\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('theme_system', '{\"en\": \"System\", \"fr\": \"Système\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('theme_vibrant', '{\"en\": \"Vibrant\", \"fr\": \"Vibrant\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('sandbox_mode_banner', '{\"en\": \"Sandbox Mode: Data is public and resets every 15 minutes.\", \"fr\": \"Mode Sandbox : Les données sont publiques et réinitialisées toutes les 15 minutes.\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('demo_access_title', '{\"en\": \"Demo Access\", \"fr\": \"Accès Démo\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('demo_access_desc', '{\"en\": \"This is a demo site. You may use the following credentials to access the admin section:\", \"fr\": \"Ceci est un site de démonstration. Vous pouvez utiliser les identifiants suivants pour accéder à l''administration :\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('demo_user_label', '{\"en\": \"User:\", \"fr\": \"Utilisateur :\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('demo_password_label', '{\"en\": \"Password:\", \"fr\": \"Mot de passe :\"}', '2026-07-03 17:52:15.479414+00', '2026-07-03 17:52:15.479414+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('ecommerce.on_sale', '{\"en\": \"On Sale\", \"es\": \"En oferta\", \"fr\": \"En solde\"}', '2026-07-03 17:52:15.69428+00', '2026-07-03 17:52:15.69428+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('remember_this_device', '{\"en\": \"Remember this device\", \"fr\": \"Se souvenir de cet appareil\"}', '2026-07-03 17:52:15.699946+00', '2026-07-03 17:52:15.699946+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.aria_label', '{\"en\": \"Privacy consent\", \"fr\": \"Consentement à la confidentialité\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.title', '{\"en\": \"We value your privacy\", \"fr\": \"Nous respectons votre vie privée\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.description_before_policy_link', '{\"en\": \"We use only essential cookies by default. With your consent we also use analytics to improve the site. See our\", \"fr\": \"Nous utilisons seulement les témoins essentiels par défaut. Avec votre consentement, nous utilisons aussi l''analytique pour améliorer le site. Consultez notre\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.privacy_policy_link', '{\"en\": \"Privacy Policy\", \"fr\": \"politique de confidentialité\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.description_after_policy_link', '{\"en\": \".\", \"fr\": \".\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.privacy_policy_href', '{\"en\": \"/privacy-policy\", \"fr\": \"/politique-de-confidentialite\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.necessary_label', '{\"en\": \"Necessary\", \"fr\": \"Nécessaires\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.necessary_help', '{\"en\": \"Always on\", \"fr\": \"Toujours actifs\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.analytics_label', '{\"en\": \"Analytics\", \"fr\": \"Analytique\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.analytics_help', '{\"en\": \"Usage insights\", \"fr\": \"Statistiques d''utilisation\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.marketing_label', '{\"en\": \"Marketing\", \"fr\": \"Marketing\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.marketing_help', '{\"en\": \"Personalized content\", \"fr\": \"Contenu personnalisé\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.save_choices', '{\"en\": \"Save choices\", \"fr\": \"Enregistrer mes choix\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.accept_all', '{\"en\": \"Accept all\", \"fr\": \"Tout accepter\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.reject_all', '{\"en\": \"Reject all\", \"fr\": \"Tout refuser\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.hide_options', '{\"en\": \"Hide options\", \"fr\": \"Masquer les options\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('privacy.consent.manage_options', '{\"en\": \"Manage options\", \"fr\": \"Gérer les options\"}', '2026-07-03 17:52:15.805991+00', '2026-07-03 17:52:15.805991+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.customer_reviews', '{\"en\": \"Customer Reviews\", \"fr\": \"Avis clients\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.share_thoughts', '{\"en\": \"Share your thoughts and experience with this product.\", \"fr\": \"Partagez vos impressions et votre expérience avec ce produit.\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.write_review', '{\"en\": \"Write a Review\", \"fr\": \"Rédiger un avis\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.cancel_review', '{\"en\": \"Cancel Review\", \"fr\": \"Annuler l''avis\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.login_to_write', '{\"en\": \"Please log in to write a review.\", \"fr\": \"Veuillez vous connecter pour rédiger un avis.\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.write_your_review', '{\"en\": \"Write your review\", \"fr\": \"Rédigez votre avis\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.rating', '{\"en\": \"Rating\", \"fr\": \"Note\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.description', '{\"en\": \"Review Description\", \"fr\": \"Description de l''avis\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.description_placeholder', '{\"en\": \"What did you like or dislike? How does it perform?\", \"fr\": \"Qu''avez-vous aimé ou déploré ? Comment se comporte-t-il ?\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.submit_review', '{\"en\": \"Submit Review\", \"fr\": \"Soumettre l''avis\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.submitting', '{\"en\": \"Submitting...\", \"fr\": \"Envoi en cours...\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.success_pending', '{\"en\": \"Your review has been submitted successfully and is pending moderation.\", \"fr\": \"Votre avis a été soumis avec succès et est en attente de modération.\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.cancel', '{\"en\": \"Cancel\", \"fr\": \"Annuler\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.helpful', '{\"en\": \"Helpful\", \"fr\": \"Utile\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.no_reviews', '{\"en\": \"No reviews yet\", \"fr\": \"Aucun avis pour le moment\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.be_the_first', '{\"en\": \"There are no reviews for this product yet. Be the first to write one!\", \"fr\": \"Il n''y a pas encore d''avis sur ce produit. Soyez le premier à en rédiger un !\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.load_more', '{\"en\": \"Load More Reviews\", \"fr\": \"Charger plus d''avis\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.review_count_one', '{\"en\": \"{count} review\", \"fr\": \"{count} avis\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('reviews.review_count_other', '{\"en\": \"{count} reviews\", \"fr\": \"{count} avis\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.discussion', '{\"en\": \"Discussion & Comments\", \"fr\": \"Discussion & Commentaires\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.join_conversation', '{\"en\": \"Join the conversation and express your thoughts.\", \"fr\": \"Rejoignez la conversation et exprimez vos pensées.\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.write_comment', '{\"en\": \"Write a Comment\", \"fr\": \"Écrire un commentaire\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.cancel_comment', '{\"en\": \"Cancel Comment\", \"fr\": \"Annuler le commentaire\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.login_to_write', '{\"en\": \"Please log in to write a comment.\", \"fr\": \"Veuillez vous connecter pour écrire un commentaire.\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.join_discussion', '{\"en\": \"Join the Discussion\", \"fr\": \"Rejoindre la discussion\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.your_message', '{\"en\": \"Your Message\", \"fr\": \"Votre message\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.message_placeholder', '{\"en\": \"What are your thoughts on this article?\", \"fr\": \"Quelles sont vos pensées sur cet article ?\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.post_comment', '{\"en\": \"Post Comment\", \"fr\": \"Publier le commentaire\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.success_pending', '{\"en\": \"Your comment has been submitted successfully and is pending moderation.\", \"fr\": \"Votre commentaire a été soumis avec succès et est en attente de modération.\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.like', '{\"en\": \"Like\", \"fr\": \"J''aime\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.no_comments', '{\"en\": \"No comments yet\", \"fr\": \"Aucun commentaire pour le moment\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.be_the_first', '{\"en\": \"Be the first to share your thoughts!\", \"fr\": \"Soyez le premier à partager vos pensées !\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES ('comments.load_more', '{\"en\": \"Load More Comments\", \"fr\": \"Charger plus de commentaires\"}', '2026-07-03 17:52:15.834477+00', '2026-07-03 17:52:15.834477+00') ON CONFLICT DO NOTHING;\nSELECT pg_catalog.setval('public.blocks_id_seq', COALESCE((SELECT MAX(id) FROM public.blocks), 1), true);\nSELECT pg_catalog.setval('public.languages_id_seq', COALESCE((SELECT MAX(id) FROM public.languages), 1), true);\nSELECT pg_catalog.setval('public.navigation_items_id_seq', COALESCE((SELECT MAX(id) FROM public.navigation_items), 1), true);\nSELECT pg_catalog.setval('public.pages_id_seq', COALESCE((SELECT MAX(id) FROM public.pages), 1), true);\nSELECT pg_catalog.setval('public.posts_id_seq', COALESCE((SELECT MAX(id) FROM public.posts), 1), true);\n"
36
+ },
37
+ {
38
+ "version": "00000000000004",
39
+ "name": "00000000000004_default_logo_email_safe.sql",
40
+ "sql": "-- Swap the seeded DEFAULT site logo from the WebP asset to a bundled PNG.\r\n--\r\n-- WebP (and AVIF) don't render in Outlook and several other email clients, so the default\r\n-- logo shown in transactional emails was a broken image out of the box. The PNG is a\r\n-- /public bundled asset (registered in resolveMediaUrl's BUNDLED_PUBLIC_MEDIA_KEYS), so it\r\n-- resolves to `<site>/images/nextblock-logo-button-tiny.png` and renders everywhere.\r\n--\r\n-- Forward-only, data-only (no schema change), idempotent, and GUARDED so it never\r\n-- overrides a logo an operator has since chosen: it only repoints the seeded default logo\r\n-- row while that row still points at the seeded WebP media.\r\n\r\nINSERT INTO public.media (\r\n id, uploader_id, file_name, object_key, file_type, size_bytes, description,\r\n width, height, blur_data_url, variants, file_path, folder, created_at, updated_at\r\n) VALUES (\r\n 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d', NULL, 'nextblock-logo-button-tiny.png',\r\n 'images/nextblock-logo-button-tiny.png', 'image/png', 10000, 'Default site logo',\r\n NULL, NULL, NULL, NULL, NULL, NULL, now(), now()\r\n) ON CONFLICT DO NOTHING;\r\n\r\nUPDATE public.logos\r\nSET media_id = 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d'\r\nWHERE id = 'd45ef03d-27fc-4099-8b46-1cdf82d658d2'\r\n AND media_id = 'ea6fdaf5-f8de-416c-9f2b-b689b7a4ed38';\r\n"
41
+ },
42
+ {
43
+ "version": "00000000000005",
44
+ "name": "00000000000005_add_custom_canonical.sql",
45
+ "sql": "-- Add a nullable `custom_canonical` column to pages, posts and products.\r\n--\r\n-- This is the per-content manual canonical override used by each route's\r\n-- generateMetadata(): when set, it wins over the default self-referencing\r\n-- `<site_url>/<slug>` canonical (see app/lib/seo.ts buildCanonicalUrl). NULL/empty\r\n-- keeps the self-referencing default, so existing rows are unaffected.\r\n--\r\n-- Forward-only and idempotent (ADD COLUMN IF NOT EXISTS). No backfill: a NULL value\r\n-- is the \"use the default canonical\" sentinel.\r\n\r\nALTER TABLE public.pages ADD COLUMN IF NOT EXISTS custom_canonical text;\r\nALTER TABLE public.posts ADD COLUMN IF NOT EXISTS custom_canonical text;\r\nALTER TABLE public.products ADD COLUMN IF NOT EXISTS custom_canonical text;\r\n"
46
+ },
47
+ {
48
+ "version": "00000000000006",
49
+ "name": "00000000000006_home_live_demo_promo.sql",
50
+ "sql": "-- 00000000000006_home_sandbox_promo.sql\n-- Adds a \"Live Demo\" promo section to the English (slug 'home') and French\n-- (slug 'accueil') home pages that drives visitors to the public sandbox at\n-- https://cms.nextblock.dev -- production (nextblock.dev) never mentioned it before.\n--\n-- The section is inserted right after the hero (order 1); existing home-page blocks\n-- shift down by one so relative order is preserved. Each block carries a sentinel\n-- comment (nb-sandbox-promo) inside its HTML so the sandbox reset route can strip it\n-- there: a CTA pointing at the sandbox is pointless *inside* the sandbox itself.\n--\n-- Forward-only and idempotent: the NOT EXISTS sentinel guard makes a re-run a no-op,\n-- and page lookups are by (language code, slug) so explicit IDs never collide with\n-- user-created blocks on a live database.\n\nDO $body$\nDECLARE\n v_en_lang integer;\n v_fr_lang integer;\n v_en_page integer;\n v_fr_page integer;\nBEGIN\n SELECT id INTO v_en_lang FROM public.languages WHERE code = 'en' LIMIT 1;\n SELECT id INTO v_fr_lang FROM public.languages WHERE code = 'fr' LIMIT 1;\n\n -- English home page\n SELECT id INTO v_en_page\n FROM public.pages\n WHERE slug = 'home' AND language_id = v_en_lang\n ORDER BY id\n LIMIT 1;\n\n IF v_en_page IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM public.blocks\n WHERE page_id = v_en_page AND content::text LIKE '%nb-sandbox-promo%'\n ) THEN\n UPDATE public.blocks\n SET \"order\" = \"order\" + 1\n WHERE page_id = v_en_page AND \"order\" >= 1;\n\n INSERT INTO public.blocks (page_id, language_id, block_type, content, \"order\")\n VALUES (v_en_page, v_en_lang, 'section', $en$\n{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#0b1e3d\",\"position\":50},{\"color\":\"#0e2a4d\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"column_gap\":\"xl\",\"vertical_alignment\":\"center\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<!--nb-sandbox-promo--><p class='text-xs uppercase tracking-[0.25em] text-cyan-400 font-bold mb-4'>Live Demo &middot; No Signup</p><h2 class='text-4xl md:text-5xl font-bold text-white mb-6 leading-tight'>Try the full CMS,<br/>live right now.</h2><p class='text-lg text-slate-300 max-w-xl leading-relaxed mb-8'>Don't just read about NextBlock&trade; &mdash; take the wheel. Our public sandbox is a complete, pre-loaded install where you can build pages with the block editor, manage bilingual content, and browse a working demo store. It resets every hour, so explore freely and break whatever you like.</p>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='flex flex-col sm:flex-row gap-4'><a href='https://cms.nextblock.dev' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white bg-gradient-to-r from-blue-600 to-cyan-500 hover:from-blue-500 hover:to-cyan-400 shadow-lg shadow-blue-500/25 transition-all no-underline'>Explore the Live Demo &rarr;</a><a href='https://cms.nextblock.dev/sign-in' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white border border-white/20 hover:bg-white/10 backdrop-blur-sm transition-all no-underline'>Open the CMS Dashboard &rarr;</a></div><p class='text-xs text-slate-400 mt-4'>Demo login <span class='font-mono text-slate-300'>demo@nextblock.dev</span> / <span class='font-mono text-slate-300'>password</span> &middot; Resets hourly</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='rounded-3xl overflow-hidden border border-white/10 bg-white/5 backdrop-blur-xl shadow-2xl'><div class='flex items-center gap-2 px-4 py-3 border-b border-white/10 bg-white/5'><span class='w-3 h-3 rounded-full bg-red-400/70'></span><span class='w-3 h-3 rounded-full bg-yellow-400/70'></span><span class='w-3 h-3 rounded-full bg-green-400/70'></span><span class='ml-3 flex-1 truncate rounded-md bg-black/30 px-3 py-1 text-xs font-mono text-slate-300'>cms.nextblock.dev</span></div><div class='p-6 sm:p-8'><p class='text-sm text-slate-300 mb-5'>A living NextBlock&trade; install &mdash; everything here is real and clickable:</p><ul class='space-y-4 text-sm text-slate-200'><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Full block editor</strong> with slash commands &amp; drag-and-drop.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Bilingual</strong> English / French content management.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>A complete demo storefront</strong> with live checkout.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Wipes clean every hour</strong> &mdash; always a fresh start.</span></li></ul></div></div>\"}}]]}\n$en$::jsonb, 1);\n END IF;\n\n -- French home page\n SELECT id INTO v_fr_page\n FROM public.pages\n WHERE slug = 'accueil' AND language_id = v_fr_lang\n ORDER BY id\n LIMIT 1;\n\n IF v_fr_page IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM public.blocks\n WHERE page_id = v_fr_page AND content::text LIKE '%nb-sandbox-promo%'\n ) THEN\n UPDATE public.blocks\n SET \"order\" = \"order\" + 1\n WHERE page_id = v_fr_page AND \"order\" >= 1;\n\n INSERT INTO public.blocks (page_id, language_id, block_type, content, \"order\")\n VALUES (v_fr_page, v_fr_lang, 'section', $fr$\n{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#0b1e3d\",\"position\":50},{\"color\":\"#0e2a4d\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"column_gap\":\"xl\",\"vertical_alignment\":\"center\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<!--nb-sandbox-promo--><p class='text-xs uppercase tracking-[0.25em] text-cyan-400 font-bold mb-4'>D&eacute;mo en direct &middot; Sans inscription</p><h2 class='text-4xl md:text-5xl font-bold text-white mb-6 leading-tight'>Essayez le CMS complet,<br/>en direct d&egrave;s maintenant.</h2><p class='text-lg text-slate-300 max-w-xl leading-relaxed mb-8'>Ne vous contentez pas de lire &agrave; propos de NextBlock&trade; &mdash; prenez les commandes. Notre bac &agrave; sable public est une installation compl&egrave;te et pr&eacute;charg&eacute;e o&ugrave; vous pouvez cr&eacute;er des pages avec l'&eacute;diteur de blocs, g&eacute;rer du contenu bilingue et parcourir une vraie boutique de d&eacute;monstration. Il se r&eacute;initialise chaque heure : explorez librement et cassez tout ce que vous voulez.</p>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='flex flex-col sm:flex-row gap-4'><a href='https://cms.nextblock.dev' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white bg-gradient-to-r from-blue-600 to-cyan-500 hover:from-blue-500 hover:to-cyan-400 shadow-lg shadow-blue-500/25 transition-all no-underline'>D&eacute;couvrir la d&eacute;mo &rarr;</a><a href='https://cms.nextblock.dev/sign-in' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white border border-white/20 hover:bg-white/10 backdrop-blur-sm transition-all no-underline'>Ouvrir le tableau de bord &rarr;</a></div><p class='text-xs text-slate-400 mt-4'>Identifiants d&eacute;mo <span class='font-mono text-slate-300'>demo@nextblock.dev</span> / <span class='font-mono text-slate-300'>password</span> &middot; R&eacute;initialis&eacute; chaque heure</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='rounded-3xl overflow-hidden border border-white/10 bg-white/5 backdrop-blur-xl shadow-2xl'><div class='flex items-center gap-2 px-4 py-3 border-b border-white/10 bg-white/5'><span class='w-3 h-3 rounded-full bg-red-400/70'></span><span class='w-3 h-3 rounded-full bg-yellow-400/70'></span><span class='w-3 h-3 rounded-full bg-green-400/70'></span><span class='ml-3 flex-1 truncate rounded-md bg-black/30 px-3 py-1 text-xs font-mono text-slate-300'>cms.nextblock.dev</span></div><div class='p-6 sm:p-8'><p class='text-sm text-slate-300 mb-5'>Une installation NextBlock&trade; bien vivante &mdash; tout ici est r&eacute;el et cliquable :</p><ul class='space-y-4 text-sm text-slate-200'><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>&Eacute;diteur de blocs complet</strong> : commandes slash et glisser-d&eacute;poser.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Gestion bilingue</strong> du contenu fran&ccedil;ais / anglais.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Une boutique de d&eacute;monstration compl&egrave;te</strong> avec paiement r&eacute;el.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Remis &agrave; z&eacute;ro chaque heure</strong> &mdash; toujours un nouveau d&eacute;part.</span></li></ul></div></div>\"}}]]}\n$fr$::jsonb, 1);\n END IF;\nEND\n$body$;\n"
51
+ },
52
+ {
53
+ "version": "00000000000007",
54
+ "name": "00000000000007_home_live_demo_promo_copy_fix.sql",
55
+ "sql": "-- 00000000000007_home_live_demo_promo_copy_fix.sql\n-- Corrects the \"Live Demo\" promo that migration 006 seeded on the EN (slug 'home')\n-- and FR (slug 'accueil') home pages. 006 was already applied to production and the\n-- sandbox, so per the append-only rule this ships the corrections as a forward-only\n-- UPDATE instead of editing 006 (which would never re-run and would desync the file\n-- from what was actually applied).\n--\n-- Fixes: reset cadence \"hourly\" -> \"daily\" (the reset cron in vercel.json is daily,\n-- 0 3 * * *, not hourly); FR \"avec paiement reel\" -> \"un parcours de paiement complet\"\n-- (sandbox checkout is simulated); FR anglicism \"lire a propos de\" -> \"lire des\n-- articles sur\"; and an accessible WCAG-AA primary CTA (solid blue-600, was white on\n-- a low-contrast cyan gradient).\n--\n-- Idempotent: re-running sets identical content. Targets exactly the promo blocks via\n-- the nb-sandbox-promo sentinel, scoped by language. On the sandbox the promo is still\n-- stripped afterward by removeSandboxPromoSections in the reset-sandbox route.\n--\n-- NOTE: keep the word \"sandbox\" OUT of this filename -- generate-sandbox-reset.ts\n-- excludes any migration whose filename contains \"sandbox\" from the reset bundle.\n\nDO $body$\nDECLARE\n v_en_lang integer;\n v_fr_lang integer;\nBEGIN\n SELECT id INTO v_en_lang FROM public.languages WHERE code = 'en' LIMIT 1;\n SELECT id INTO v_fr_lang FROM public.languages WHERE code = 'fr' LIMIT 1;\n\n UPDATE public.blocks\n SET content = $en$\n{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#0b1e3d\",\"position\":50},{\"color\":\"#0e2a4d\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"column_gap\":\"xl\",\"vertical_alignment\":\"center\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<!--nb-sandbox-promo--><p class='text-xs uppercase tracking-[0.25em] text-cyan-400 font-bold mb-4'>Live Demo &middot; No Signup</p><h2 class='text-4xl md:text-5xl font-bold text-white mb-6 leading-tight'>Try the full CMS,<br/>live right now.</h2><p class='text-lg text-slate-300 max-w-xl leading-relaxed mb-8'>Don't just read about NextBlock&trade; &mdash; take the wheel. Our public sandbox is a complete, pre-loaded install where you can build pages with the block editor, manage bilingual content, and browse a working demo store. It resets daily, so explore freely and break whatever you like.</p>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='flex flex-col sm:flex-row gap-4'><a href='https://cms.nextblock.dev' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 shadow-lg shadow-cyan-500/20 transition-all no-underline'>Explore the Live Demo &rarr;</a><a href='https://cms.nextblock.dev/sign-in' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white border border-white/20 hover:bg-white/10 backdrop-blur-sm transition-all no-underline'>Open the CMS Dashboard &rarr;</a></div><p class='text-xs text-slate-400 mt-4'>Demo login <span class='font-mono text-slate-300'>demo@nextblock.dev</span> / <span class='font-mono text-slate-300'>password</span> &middot; Resets daily</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='rounded-3xl overflow-hidden border border-white/10 bg-white/5 backdrop-blur-xl shadow-2xl'><div class='flex items-center gap-2 px-4 py-3 border-b border-white/10 bg-white/5'><span class='w-3 h-3 rounded-full bg-red-400/70'></span><span class='w-3 h-3 rounded-full bg-yellow-400/70'></span><span class='w-3 h-3 rounded-full bg-green-400/70'></span><span class='ml-3 flex-1 truncate rounded-md bg-black/30 px-3 py-1 text-xs font-mono text-slate-300'>cms.nextblock.dev</span></div><div class='p-6 sm:p-8'><p class='text-sm text-slate-300 mb-5'>A living NextBlock&trade; install &mdash; everything here is real and clickable:</p><ul class='space-y-4 text-sm text-slate-200'><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Full block editor</strong> with slash commands &amp; drag-and-drop.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Bilingual</strong> English / French content management.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>A complete demo storefront</strong> with a full checkout flow.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Wipes clean daily</strong> &mdash; always a fresh start.</span></li></ul></div></div>\"}}]]}\n$en$::jsonb,\n updated_at = now()\n WHERE block_type = 'section'\n AND page_id IS NOT NULL\n AND language_id = v_en_lang\n AND content::text LIKE '%nb-sandbox-promo%';\n\n UPDATE public.blocks\n SET content = $fr$\n{\"container_type\":\"container\",\"background\":{\"type\":\"gradient\",\"gradient\":{\"type\":\"linear\",\"direction\":\"135deg\",\"stops\":[{\"color\":\"#020817\",\"position\":0},{\"color\":\"#0b1e3d\",\"position\":50},{\"color\":\"#0e2a4d\",\"position\":100}]}},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":2},\"column_gap\":\"xl\",\"vertical_alignment\":\"center\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<!--nb-sandbox-promo--><p class='text-xs uppercase tracking-[0.25em] text-cyan-400 font-bold mb-4'>D&eacute;mo en direct &middot; Sans inscription</p><h2 class='text-4xl md:text-5xl font-bold text-white mb-6 leading-tight'>Essayez le CMS complet,<br/>en direct d&egrave;s maintenant.</h2><p class='text-lg text-slate-300 max-w-xl leading-relaxed mb-8'>Ne vous contentez pas de lire des articles sur NextBlock&trade; &mdash; prenez les commandes. Notre bac &agrave; sable public est une installation compl&egrave;te et pr&eacute;charg&eacute;e o&ugrave; vous pouvez cr&eacute;er des pages avec l'&eacute;diteur de blocs, g&eacute;rer du contenu bilingue et parcourir une vraie boutique de d&eacute;monstration. Il se r&eacute;initialise chaque jour : explorez librement et cassez tout ce que vous voulez.</p>\"}},{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='flex flex-col sm:flex-row gap-4'><a href='https://cms.nextblock.dev' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 shadow-lg shadow-cyan-500/20 transition-all no-underline'>D&eacute;couvrir la d&eacute;mo &rarr;</a><a href='https://cms.nextblock.dev/sign-in' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white border border-white/20 hover:bg-white/10 backdrop-blur-sm transition-all no-underline'>Ouvrir le tableau de bord &rarr;</a></div><p class='text-xs text-slate-400 mt-4'>Identifiants d&eacute;mo <span class='font-mono text-slate-300'>demo@nextblock.dev</span> / <span class='font-mono text-slate-300'>password</span> &middot; R&eacute;initialis&eacute; chaque jour</p>\"}}],[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<div class='rounded-3xl overflow-hidden border border-white/10 bg-white/5 backdrop-blur-xl shadow-2xl'><div class='flex items-center gap-2 px-4 py-3 border-b border-white/10 bg-white/5'><span class='w-3 h-3 rounded-full bg-red-400/70'></span><span class='w-3 h-3 rounded-full bg-yellow-400/70'></span><span class='w-3 h-3 rounded-full bg-green-400/70'></span><span class='ml-3 flex-1 truncate rounded-md bg-black/30 px-3 py-1 text-xs font-mono text-slate-300'>cms.nextblock.dev</span></div><div class='p-6 sm:p-8'><p class='text-sm text-slate-300 mb-5'>Une installation NextBlock&trade; bien vivante &mdash; tout ici est r&eacute;el et cliquable :</p><ul class='space-y-4 text-sm text-slate-200'><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>&Eacute;diteur de blocs complet</strong> : commandes slash et glisser-d&eacute;poser.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Gestion bilingue</strong> du contenu fran&ccedil;ais / anglais.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Une boutique de d&eacute;monstration compl&egrave;te</strong> avec un parcours de paiement complet.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Remis &agrave; z&eacute;ro chaque jour</strong> &mdash; toujours un nouveau d&eacute;part.</span></li></ul></div></div>\"}}]]}\n$fr$::jsonb,\n updated_at = now()\n WHERE block_type = 'section'\n AND page_id IS NOT NULL\n AND language_id = v_fr_lang\n AND content::text LIKE '%nb-sandbox-promo%';\nEND\n$body$;\n"
56
+ },
57
+ {
58
+ "version": "00000000000008",
59
+ "name": "00000000000008_setup_article_reorder.sql",
60
+ "sql": "-- 00000000000008_setup_article_reorder.sql\r\n-- Reorders and rewrites the \"How to Install NextBlock\" setup guide on BOTH language\r\n-- twins: EN (post slug 'how-to-setup-nextblock', post_id=3 / block id=40) and FR\r\n-- (post slug 'comment-configurer-nextblock', post_id=4 / block id=41), both seeded in\r\n-- the baseline seed 00000000000003. New path order, presented as a complexity ladder\r\n-- (simplest -> most hands-on): (1) One-click Vercel, (2) npm create nextblock -> Docker\r\n-- (100% local), (3) npm create nextblock -> your own Supabase + Cloudflare R2, (4) git\r\n-- clone the monorepo (with a note that Docker works there too). The Vercel section now\r\n-- tells users to make the new GitHub repo Public so automatic updates work.\r\n--\r\n-- Forward-only and append-only: the baseline 003 is already applied everywhere and\r\n-- never replays, so this ships the new copy as an idempotent UPDATE (re-running sets\r\n-- identical content). Data-only -- no schema change. blocks.content is JSONB; written\r\n-- with PostgreSQL dollar-quoting to avoid single-quote doubling (same style as 006/007).\r\n\r\nUPDATE public.blocks\r\n SET content = $nben$\r\n{\"html_content\":\"<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock is an open-source, AI-native Next.js CMS built on Supabase — and installing it no longer involves config files, terminal wizards, or manual SQL. Below are four ways to get running, <strong>ordered from the simplest to the most hands-on</strong>: the first is a single click, the last is the full source for people who want to help build NextBlock. They all end in the same place — a browser <strong>setup wizard</strong> that connects your database, configures media storage, and creates your admin account for you.</p>\\n\\n<div class='mt-8 mb-6 flex items-center gap-4 text-[0.7rem] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>\\n <span class='flex-shrink-0'>Simplest</span>\\n <span class='h-1.5 flex-1 rounded-full bg-gradient-to-r from-blue-400 via-violet-400 to-emerald-400'></span>\\n <span class='flex-shrink-0'>Most control</span>\\n</div>\\n\\n<div class='grid gap-5 md:grid-cols-2 my-6'>\\n <a href='#one-click-vercel' class='block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 text-sm font-bold text-white'>1</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-blue-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Easiest &middot; one click</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Deploy on Vercel</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>A live production site with a managed database. No terminal, no accounts to wire up, nothing to copy.</p>\\n </a>\\n <a href='#npm-docker' class='block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-amber-500 text-sm font-bold text-white'>2</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-amber-500'></span><span class='h-1.5 w-5 rounded-full bg-amber-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Easy &middot; 100% local</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Docker</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>One prompt scaffolds a project and boots the whole stack — database, auth, storage, CMS — on your machine. No cloud accounts.</p>\\n </a>\\n <a href='#npm-cloud' class='block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-violet-600 text-sm font-bold text-white'>3</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Intermediate &middot; your own cloud</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Supabase + R2</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Scaffold a standalone app on your own managed Supabase project and Cloudflare R2, ready to deploy anywhere.</p>\\n </a>\\n <a href='#git-clone' class='block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-emerald-600 text-sm font-bold text-white'>4</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Advanced &middot; for contributors</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Clone the repository</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Run the complete monorepo — the CMS, every package, and the docs. For people who want to help build NextBlock (Docker works here too).</p>\\n </a>\\n</div>\\n\\n<p class='text-sm text-slate-500 dark:text-slate-400'>Not sure where to start? Work down the list — pick the first option whose requirements you already have. Most people should start with <a href='#one-click-vercel'>Vercel</a>.</p>\\n\\n<figure class='my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10'>\\n <img src='/images/included.webp' alt='NextBlock CMS platform overview showing the block editor, CMS dashboard, and integrations included with every installation' class='w-full h-auto object-cover' />\\n <figcaption class='border-t border-white/10 px-6 py-4 text-sm text-slate-300'>Whichever path you choose, you get the same block editor, the same CMS, and the same database schema.</figcaption>\\n</figure>\\n\\n<h2 id='one-click-vercel'>Option 1: One-Click Deploy on Vercel</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-blue-700 dark:border-blue-500/20 dark:bg-blue-500/10 dark:text-blue-200'>Step 1 &middot; Easiest</span><span class='text-slate-500 dark:text-slate-400'>Best for a live site with the least possible effort — no terminal, no accounts to wire up.</span></p>\\n<p>The fastest way to get a production NextBlock site. One button creates your own copy of NextBlock on GitHub, provisions a managed Supabase database, and deploys the site — you never open a terminal or copy a single key.</p>\\n<ol class='space-y-2'>\\n <li><strong>Click Deploy to Vercel</strong> and sign in — Vercel clones NextBlock into a new repository you own.</li>\\n <li><strong>Name the repository — and make it Public.</strong> On Vercel&rsquo;s first step you choose the repository name; set its visibility to <strong>Public</strong> rather than Private. A public repo is what unlocks one-click automatic updates later, so it&rsquo;s the recommended choice.</li>\\n <li><strong>Create the Supabase database</strong> when prompted: pick a name and a region. Vercel connects it to the project and injects the keys before the first build.</li>\\n <li><strong>Open your new site</strong> once the build finishes. Every fresh instance takes you straight to the setup wizard.</li>\\n <li><strong>Create your administrator account.</strong> It is confirmed instantly — no verification email — and you land in the CMS dashboard.</li>\\n</ol>\\n<div class='my-8'>\\n <a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' target='_blank' rel='noopener' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Deploy to Vercel &rarr;</a>\\n</div>\\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Recommended &middot; make the repo public</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>When Vercel asks you to name the new repository, choose <strong>Public</strong>. It costs nothing and it&rsquo;s what lets the dashboard&rsquo;s one-click <strong>Connect GitHub</strong> step install a daily workflow that keeps your site synced with the latest NextBlock release. You can still switch a private repo to public later in GitHub — but starting public is the smoothest path to automatic updates.</p>\\n</div>\\n<div class='rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Zero configuration</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>There are no environment variables to fill in. Media storage automatically uses your connected Supabase project, security secrets are derived for you, and database migrations run automatically on every production build. Adding a custom domain later? Set <code>NEXT_PUBLIC_URL</code> in your Vercel project and redeploy.</p>\\n</div>\\n\\n<h2 id='npm-docker'>Option 2: npm create nextblock &rarr; Docker (100% Local)</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-amber-700 dark:border-amber-500/20 dark:bg-amber-500/10 dark:text-amber-200'>Step 2 &middot; Easy</span><span class='text-slate-500 dark:text-slate-400'>Best for trying everything on your own machine with zero cloud accounts. Needs Docker Desktop.</span></p>\\n<p>Run NextBlock entirely on your own machine with no cloud accounts at all. The CLI scaffolds a standalone Next.js app and then boots the full stack in Docker for you: Supabase&rsquo;s Postgres and auth engines, a PostgREST API behind a Kong gateway, S3-compatible MinIO storage, and the CMS itself — ideal for evaluations, air-gapped environments, and anyone who wants complete data ownership.</p>\\n<p>Before you start, install <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20 or newer</a> (it includes npm) and <a href='https://www.docker.com/products/docker-desktop/' target='_blank' rel='noopener'>Docker Desktop</a>, and make sure Docker Desktop is running.</p>\\n<pre><code>npm create nextblock@latest my-site</code></pre>\\n<ol class='space-y-2'>\\n <li><strong>Choose the hosting profile.</strong> At the first prompt, pick <em>Local Self-Hosted Docker Mode (One-Click Local Sandbox)</em>, then confirm that Docker Desktop is installed and running.</li>\\n <li><strong>Let it run.</strong> The CLI copies the template, installs dependencies, generates secure keys, and boots the whole stack with a single Docker command — no questions asked. The first run pulls images and builds the app, so give it a few minutes; every database migration is applied automatically as the stack comes up.</li>\\n <li><strong>Open <code>http://localhost:3000</code></strong> — it redirects to the setup wizard. Because the database and MinIO storage are already wired up, the connection and media-storage steps are skipped: the only thing left is creating your administrator (confirmed instantly, no email required). You land in the CMS dashboard.</li>\\n</ol>\\n<div class='rounded-3xl border border-amber-200 bg-amber-50/80 p-6 my-8 dark:border-amber-500/20 dark:bg-amber-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Day-2 commands</p>\\n <pre class='mt-4 mb-0'><code># rebuild and restart the stack\\nnpm run docker:up\\n\\n# stop the stack (your data persists in Docker volumes)\\nnpm run docker:down\\n\\n# follow the application logs\\nnpm run docker:logs</code></pre>\\n <p class='mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200'>Docker mode is offered only in the interactive prompt — don&rsquo;t pass <code>--yes</code>, which forces the managed-cloud path below.</p>\\n</div>\\n\\n<h2 id='npm-cloud'>Option 3: npm create nextblock &rarr; Your Own Supabase + Cloudflare</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-violet-200 bg-violet-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-violet-700 dark:border-violet-500/20 dark:bg-violet-500/10 dark:text-violet-200'>Step 3 &middot; Intermediate</span><span class='text-slate-500 dark:text-slate-400'>Best for building your own site on managed cloud. Needs a Supabase project and an R2 bucket.</span></p>\\n<p>The best starting point for building your own site on managed cloud. The CLI scaffolds a standalone Next.js application with NextBlock already wired in — no monorepo, no workspace tooling — pointed at a Supabase project and Cloudflare R2 bucket you control, and it deploys anywhere Next.js runs.</p>\\n<p>Before you start, install <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20 or newer</a>, create a free project at <a href='https://supabase.com' target='_blank' rel='noopener'>supabase.com</a>, and set up a <a href='https://developers.cloudflare.com/r2/' target='_blank' rel='noopener'>Cloudflare R2</a> bucket for your images and files.</p>\\n<pre><code>npm create nextblock@latest my-site\\ncd my-site\\nnpm run dev</code></pre>\\n<p>At the first prompt, choose <em>Managed Cloud Mode (Vercel + Supabase Cloud)</em> and name your project. Then open <code>http://localhost:3000/setup</code> and let the wizard take over:</p>\\n<ol class='space-y-2'>\\n <li><strong>Connect Supabase</strong> — paste your project URL, publishable (anon) key, secret (service role) key, and a personal access token so the wizard can apply the database schema for you.</li>\\n <li><strong>Add Cloudflare R2</strong> — enter your R2 account ID, bucket name, access key ID, secret access key, and the bucket&rsquo;s public URL for serving images and files.</li>\\n <li><strong>Create your administrator</strong> — the wizard applies every migration, generates the app secrets, writes <code>.env.local</code>, creates your confirmed admin account, and signs you in. Restart <code>npm run dev</code> once afterwards so the fresh environment is baked into the app.</li>\\n</ol>\\n<div class='rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Premium modules</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Need a store? One command adds products, checkout, orders, and coupons — license-gated and ready when you are: <code>npx create-nextblock activate ecommerce</code></p>\\n</div>\\n\\n<h2 id='git-clone'>Option 4: Clone the Repository</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-200'>Step 4 &middot; Advanced</span><span class='text-slate-500 dark:text-slate-400'>Best for contributors and teams who want to customize the platform itself. Needs Node.js and git.</span></p>\\n<p>Run the full Nx monorepo: the CMS application, every shared package, the CLI source, and the documentation. This is the path for people who want to take part in the project — contributors, plugin authors, and teams that customize the platform itself.</p>\\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\\ncd nextblock\\nnpm install\\nnpx nx serve nextblock</code></pre>\\n<p>Open <code>http://localhost:4200</code> — a fresh install redirects every page to <code>/setup</code>, where the same three-step wizard connects Supabase, configures storage, and creates your admin. It validates your keys, writes <code>.env.local</code> with generated secrets, and applies all migrations over the Supabase Management API — no Supabase CLI required.</p>\\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Prefer to stay local? Use Docker here too</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>You don&rsquo;t need cloud accounts to develop against the monorepo. With Docker Desktop running, one command boots the same self-hosted stack as Option 2 — Postgres, auth, storage, and the CMS — straight from your clone:</p>\\n <pre class='mt-4 mb-0'><code>npm run docker:setup</code></pre>\\n <p class='mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200'>When it finishes, open <code>http://localhost:3000</code> and create your administrator. Note the monorepo has no <code>npm run dev</code> — use <code>npx nx serve nextblock</code> (port 4200) for the cloud path.</p>\\n</div>\\n\\n<h2 id='after-install'>After You Install: Your First 10 Minutes</h2>\\n<p>Every path drops you at <code>/cms/dashboard</code>, signed in as the first administrator. A built-in onboarding checklist walks you through the rest:</p>\\n<ul class='space-y-2'>\\n <li><strong>Add your branding</strong> — upload your logo and set the site title.</li>\\n <li><strong>Set your footer</strong> — copyright line and footer navigation.</li>\\n <li><strong>Configure email (SMTP)</strong> — under Settings, so password resets and invitations can send.</li>\\n <li><strong>Optional extras</strong> — connect analytics, enable bot protection, and (on Vercel) turn on automatic updates.</li>\\n</ul>\\n<p>From there, see how the platform fits together in <a href='/article/how-nextblock-works'>How NextBlock Works</a>, add a storefront with the <a href='/article/nextblock-commerce-guide'>Commerce guide</a>, or meet your AI copilot in the <a href='/article/nextblock-cortex-ai-guide'>Cortex AI guide</a>.</p>\\n\\n<h2 id='faq'>Installation FAQ</h2>\\n<h3>What do I need installed?</h3>\\n<p>Nothing for the Vercel path — it runs entirely in the browser. For <code>npm create nextblock</code> in Docker mode: <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20+</a> and Docker Desktop. For the managed-cloud path: Node.js 20+ plus a Supabase project and a Cloudflare R2 bucket. For the cloned repository: Node.js 20+ and git (add Docker Desktop if you want to run the local stack).</p>\\n<h3>Is NextBlock free?</h3>\\n<p>Yes — the core of NextBlock is a 100% free, open-source CMS (AGPL). Premium packages such as e-commerce and Cortex AI are optional and activate with a license key. Both Vercel and Supabase offer free tiers, so a starter site can run at no cost.</p>\\n<h3>Do I need a Supabase account?</h3>\\n<p>On Vercel, the database is created for you during the deploy. For the managed-cloud <code>npm create nextblock</code> path and the cloned repository you bring a free Supabase project. With Docker — whether through the CLI&rsquo;s Docker mode or <code>npm run docker:setup</code> in the clone — you need no cloud accounts at all.</p>\\n<h3>Do I have to run migrations or SQL by hand?</h3>\\n<p>No. The setup wizard, the Vercel build, and the Docker stack all apply the database schema automatically — and re-running is always safe.</p>\\n<h3>Can I switch paths later?</h3>\\n<p>Yes. Every path runs the same application and the same database schema, so you can prototype locally with Docker today and deploy to Vercel tomorrow. NextBlock deploys like any standard Next.js app.</p>\\n<h3>How do I update NextBlock?</h3>\\n<p>On Vercel, the Connect GitHub onboarding step enables a daily automatic sync with upstream (this needs your repository to be public). On a cloned repository, <code>git pull</code>, run <code>npm run db:migrate</code>, then restart (on production builds, pending migrations apply automatically). With Docker, pull the latest code and run <code>npm run docker:up</code>.</p>\\n\\n<div class='rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5'>\\n <p class='mt-0 text-2xl font-semibold text-slate-900 dark:text-white'>Ready to launch?</p>\\n <p class='text-sm text-slate-600 dark:text-slate-300'>Pick your path above, or jump straight to the fastest one.</p>\\n <div class='mt-5 flex flex-wrap justify-center gap-3'>\\n <a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' target='_blank' rel='noopener' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Deploy on Vercel</a>\\n <a href='https://github.com/nextblock-cms/nextblock' target='_blank' rel='noopener' class='inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50'>View on GitHub</a>\\n </div>\\n</div>\"}\r\n$nben$::jsonb,\r\n updated_at = now()\r\n WHERE post_id = 3\r\n AND block_type = 'text';\r\n\r\nUPDATE public.blocks\r\n SET content = $nbfr$\r\n{\"html_content\":\"<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock est un CMS open source et natif IA, construit sur Next.js et Supabase — et son installation ne passe plus par des fichiers de configuration, des assistants en ligne de commande ou du SQL manuel. Voici quatre façons de démarrer, <strong>classées de la plus simple à la plus technique</strong> : la première tient en un clic, la dernière donne le code source complet pour celles et ceux qui veulent participer à NextBlock. Elles aboutissent toutes au même endroit — un <strong>assistant de configuration</strong> dans le navigateur qui connecte votre base de données, configure le stockage des médias et crée votre compte administrateur.</p>\\n\\n<div class='mt-8 mb-6 flex items-center gap-4 text-[0.7rem] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>\\n <span class='flex-shrink-0'>Le plus simple</span>\\n <span class='h-1.5 flex-1 rounded-full bg-gradient-to-r from-blue-400 via-violet-400 to-emerald-400'></span>\\n <span class='flex-shrink-0'>Le plus de contrôle</span>\\n</div>\\n\\n<div class='grid gap-5 md:grid-cols-2 my-6'>\\n <a href='#one-click-vercel' class='block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 text-sm font-bold text-white'>1</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-blue-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Le plus simple &middot; un clic</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Déployer sur Vercel</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Un site de production en ligne avec une base de données gérée. Pas de terminal, aucun compte à configurer, rien à copier.</p>\\n </a>\\n <a href='#npm-docker' class='block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-amber-500 text-sm font-bold text-white'>2</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-amber-500'></span><span class='h-1.5 w-5 rounded-full bg-amber-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Facile &middot; 100 % local</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Docker</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Un seul prompt génère un projet et démarre toute la pile — base de données, auth, stockage, CMS — sur votre machine. Aucun compte cloud.</p>\\n </a>\\n <a href='#npm-cloud' class='block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-violet-600 text-sm font-bold text-white'>3</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-violet-500'></span><span class='h-1.5 w-5 rounded-full bg-slate-200 dark:bg-white/10'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Intermédiaire &middot; votre propre cloud</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Supabase + R2</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Générez une app autonome sur votre propre projet Supabase géré et Cloudflare R2, prête à déployer partout.</p>\\n </a>\\n <a href='#git-clone' class='block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <div class='flex items-center justify-between mb-4'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-emerald-600 text-sm font-bold text-white'>4</span>\\n <span class='inline-flex items-center gap-1'><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span><span class='h-1.5 w-5 rounded-full bg-emerald-500'></span></span>\\n </div>\\n <p class='mt-0 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Avancé &middot; pour les contributeurs</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Cloner le dépôt</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Faites tourner le monorepo complet — le CMS, tous les packages et la documentation. Pour celles et ceux qui veulent aider à construire NextBlock (Docker fonctionne ici aussi).</p>\\n </a>\\n</div>\\n\\n<p class='text-sm text-slate-500 dark:text-slate-400'>Vous ne savez pas par où commencer ? Descendez la liste — choisissez la première option dont vous avez déjà les prérequis. La plupart des gens devraient commencer par <a href='#one-click-vercel'>Vercel</a>.</p>\\n\\n<figure class='my-12 overflow-hidden rounded-[2rem] border border-slate-200/80 bg-slate-950 shadow-2xl dark:border-white/10'>\\n <img src='/images/included.webp' alt='Aperçu de la plateforme NextBlock : éditeur de blocs, tableau de bord CMS et intégrations incluses dans chaque installation' class='w-full h-auto object-cover' />\\n <figcaption class='border-t border-white/10 px-6 py-4 text-sm text-slate-300'>Quel que soit le chemin choisi, vous obtenez le même éditeur de blocs, le même CMS et le même schéma de base de données.</figcaption>\\n</figure>\\n\\n<h2 id='one-click-vercel'>Option 1 : Déploiement Vercel en un clic</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-blue-700 dark:border-blue-500/20 dark:bg-blue-500/10 dark:text-blue-200'>Étape 1 &middot; Le plus simple</span><span class='text-slate-500 dark:text-slate-400'>Idéal pour un site en ligne avec le minimum d'effort — pas de terminal, aucun compte à configurer.</span></p>\\n<p>Le moyen le plus rapide d'obtenir un site NextBlock en production. Un seul bouton crée votre propre copie de NextBlock sur GitHub, provisionne une base de données Supabase gérée et déploie le site — sans jamais ouvrir un terminal ni copier la moindre clé.</p>\\n<ol class='space-y-2'>\\n <li><strong>Cliquez sur Deploy to Vercel</strong> et connectez-vous — Vercel clone NextBlock dans un nouveau dépôt qui vous appartient.</li>\\n <li><strong>Nommez le dépôt — et rendez-le Public.</strong> À la première étape sur Vercel, vous choisissez le nom du dépôt ; réglez sa visibilité sur <strong>Public</strong> plutôt que Privé. Un dépôt public est ce qui débloque les mises à jour automatiques en un clic plus tard — c'est donc le choix recommandé.</li>\\n <li><strong>Créez la base de données Supabase</strong> quand on vous le demande : choisissez un nom et une région. Vercel la connecte au projet et injecte les clés avant le premier build.</li>\\n <li><strong>Ouvrez votre nouveau site</strong> une fois le build terminé. Toute nouvelle instance vous amène directement à l'assistant de configuration.</li>\\n <li><strong>Créez votre compte administrateur.</strong> Il est confirmé instantanément — aucun email de vérification — et vous arrivez dans le tableau de bord du CMS.</li>\\n</ol>\\n<div class='my-8'>\\n <a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' target='_blank' rel='noopener' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Déployer sur Vercel &rarr;</a>\\n</div>\\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Recommandé &middot; rendez le dépôt public</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Quand Vercel vous demande de nommer le nouveau dépôt, choisissez <strong>Public</strong>. C'est gratuit, et c'est ce qui permet à l'étape <strong>Connect GitHub</strong> du tableau de bord d'installer un workflow quotidien qui garde votre site synchronisé avec la dernière version de NextBlock. Vous pourrez toujours passer un dépôt privé en public plus tard dans GitHub — mais démarrer en public est le chemin le plus simple vers les mises à jour automatiques.</p>\\n</div>\\n<div class='rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Zéro configuration</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Aucune variable d'environnement à remplir. Le stockage des médias utilise automatiquement votre projet Supabase connecté, les secrets de sécurité sont dérivés pour vous, et les migrations de base de données s'exécutent automatiquement à chaque build de production. Un domaine personnalisé plus tard ? Définissez <code>NEXT_PUBLIC_URL</code> dans votre projet Vercel et redéployez.</p>\\n</div>\\n\\n<h2 id='npm-docker'>Option 2 : npm create nextblock &rarr; Docker (100 % local)</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-amber-700 dark:border-amber-500/20 dark:bg-amber-500/10 dark:text-amber-200'>Étape 2 &middot; Facile</span><span class='text-slate-500 dark:text-slate-400'>Idéal pour tout essayer sur votre machine sans aucun compte cloud. Nécessite Docker Desktop.</span></p>\\n<p>Faites tourner NextBlock entièrement sur votre machine, sans aucun compte cloud. Le CLI génère une application Next.js autonome puis démarre toute la pile dans Docker pour vous : les moteurs Postgres et auth de Supabase, une API PostgREST derrière une passerelle Kong, un stockage MinIO compatible S3 et le CMS lui-même — idéal pour les évaluations, les environnements isolés et la pleine propriété de vos données.</p>\\n<p>Avant de commencer, installez <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20 ou plus récent</a> (npm inclus) et <a href='https://www.docker.com/products/docker-desktop/' target='_blank' rel='noopener'>Docker Desktop</a>, et assurez-vous que Docker Desktop est démarré.</p>\\n<pre><code>npm create nextblock@latest mon-site</code></pre>\\n<ol class='space-y-2'>\\n <li><strong>Choisissez le profil d'hébergement.</strong> Au premier prompt, choisissez <em>Local Self-Hosted Docker Mode (One-Click Local Sandbox)</em>, puis confirmez que Docker Desktop est installé et démarré.</li>\\n <li><strong>Laissez faire.</strong> Le CLI copie le template, installe les dépendances, génère des clés sécurisées et démarre toute la pile avec une seule commande Docker — sans aucune question. Le premier lancement télécharge les images et construit l'app, comptez donc quelques minutes ; chaque migration de base de données est appliquée automatiquement au démarrage de la pile.</li>\\n <li><strong>Ouvrez <code>http://localhost:3000</code></strong> — vous êtes redirigé vers l'assistant de configuration. Comme la base de données et le stockage MinIO sont déjà connectés, les étapes de connexion et de stockage sont ignorées : il ne reste qu'à créer votre administrateur (confirmé instantanément, sans email). Vous arrivez dans le tableau de bord du CMS.</li>\\n</ol>\\n<div class='rounded-3xl border border-amber-200 bg-amber-50/80 p-6 my-8 dark:border-amber-500/20 dark:bg-amber-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Commandes du quotidien</p>\\n <pre class='mt-4 mb-0'><code># reconstruire et redémarrer la pile\\nnpm run docker:up\\n\\n# arrêter la pile (vos données persistent dans les volumes Docker)\\nnpm run docker:down\\n\\n# suivre les logs de l'application\\nnpm run docker:logs</code></pre>\\n <p class='mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200'>Le mode Docker n'est proposé que dans le prompt interactif — ne passez pas <code>--yes</code>, qui force le mode cloud géré ci-dessous.</p>\\n</div>\\n\\n<h2 id='npm-cloud'>Option 3 : npm create nextblock &rarr; votre propre Supabase + Cloudflare</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-violet-200 bg-violet-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-violet-700 dark:border-violet-500/20 dark:bg-violet-500/10 dark:text-violet-200'>Étape 3 &middot; Intermédiaire</span><span class='text-slate-500 dark:text-slate-400'>Idéal pour construire votre propre site sur du cloud géré. Nécessite un projet Supabase et un bucket R2.</span></p>\\n<p>Le meilleur point de départ pour construire votre propre site sur du cloud géré. Le CLI génère une application Next.js autonome avec NextBlock déjà intégré — sans monorepo ni outillage de workspace — reliée à un projet Supabase et un bucket Cloudflare R2 que vous contrôlez, et elle se déploie partout où Next.js tourne.</p>\\n<p>Avant de commencer, installez <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20 ou plus récent</a>, créez un projet gratuit sur <a href='https://supabase.com' target='_blank' rel='noopener'>supabase.com</a>, et configurez un bucket <a href='https://developers.cloudflare.com/r2/' target='_blank' rel='noopener'>Cloudflare R2</a> pour vos images et fichiers.</p>\\n<pre><code>npm create nextblock@latest mon-site\\ncd mon-site\\nnpm run dev</code></pre>\\n<p>Au premier prompt, choisissez <em>Managed Cloud Mode (Vercel + Supabase Cloud)</em> et nommez votre projet. Ouvrez ensuite <code>http://localhost:3000/setup</code> et laissez l'assistant faire le travail :</p>\\n<ol class='space-y-2'>\\n <li><strong>Connectez Supabase</strong> — collez l'URL du projet, la clé publiable (anon), la clé secrète (service role) et un jeton d'accès personnel pour que l'assistant applique le schéma de base de données à votre place.</li>\\n <li><strong>Ajoutez Cloudflare R2</strong> — saisissez votre identifiant de compte R2, le nom du bucket, la clé d'accès (access key ID), la clé secrète et l'URL publique du bucket pour servir vos images et fichiers.</li>\\n <li><strong>Créez votre administrateur</strong> — l'assistant applique toutes les migrations, génère les secrets de l'application, écrit <code>.env.local</code>, crée votre compte admin confirmé et vous connecte. Redémarrez ensuite <code>npm run dev</code> une fois pour que le nouvel environnement soit intégré à l'application.</li>\\n</ol>\\n<div class='rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Modules premium</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Besoin d'une boutique ? Une seule commande ajoute produits, paiement, commandes et coupons — activés par clé de licence, prêts quand vous l'êtes : <code>npx create-nextblock activate ecommerce</code></p>\\n</div>\\n\\n<h2 id='git-clone'>Option 4 : Cloner le dépôt</h2>\\n<p class='mt-2 mb-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm'><span class='inline-flex items-center gap-2 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-200'>Étape 4 &middot; Avancé</span><span class='text-slate-500 dark:text-slate-400'>Idéal pour les contributeurs et les équipes qui veulent personnaliser la plateforme. Nécessite Node.js et git.</span></p>\\n<p>Faites tourner le monorepo Nx complet : l'application CMS, tous les packages partagés, le code du CLI et la documentation. C'est le chemin de celles et ceux qui veulent participer au projet — contributeurs, auteurs de plugins et équipes qui personnalisent la plateforme elle-même.</p>\\n<pre><code>git clone https://github.com/nextblock-cms/nextblock.git\\ncd nextblock\\nnpm install\\nnpx nx serve nextblock</code></pre>\\n<p>Ouvrez <code>http://localhost:4200</code> — une nouvelle installation redirige chaque page vers <code>/setup</code>, où le même assistant en trois étapes connecte Supabase, configure le stockage et crée votre admin. Il valide vos clés, écrit <code>.env.local</code> avec des secrets générés, et applique toutes les migrations via l'API de management Supabase — sans CLI Supabase.</p>\\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Vous préférez rester local ? Docker fonctionne ici aussi</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Pas besoin de comptes cloud pour développer sur le monorepo. Avec Docker Desktop démarré, une seule commande lance la même pile auto-hébergée que l'option 2 — Postgres, auth, stockage et le CMS — directement depuis votre clone :</p>\\n <pre class='mt-4 mb-0'><code>npm run docker:setup</code></pre>\\n <p class='mt-4 mb-0 text-sm text-slate-700 dark:text-slate-200'>Quand c'est terminé, ouvrez <code>http://localhost:3000</code> et créez votre administrateur. Notez que le monorepo n'a pas de <code>npm run dev</code> — utilisez <code>npx nx serve nextblock</code> (port 4200) pour le chemin cloud.</p>\\n</div>\\n\\n<h2 id='after-install'>Après l'installation : vos 10 premières minutes</h2>\\n<p>Chaque chemin vous dépose sur <code>/cms/dashboard</code>, connecté en tant que premier administrateur. Une checklist de démarrage intégrée vous guide pour la suite :</p>\\n<ul class='space-y-2'>\\n <li><strong>Ajoutez votre identité visuelle</strong> — téléversez votre logo et définissez le titre du site.</li>\\n <li><strong>Réglez votre pied de page</strong> — mention de copyright et navigation du pied de page.</li>\\n <li><strong>Configurez l'email (SMTP)</strong> — dans les réglages, pour que les réinitialisations de mot de passe et les invitations partent bien.</li>\\n <li><strong>Extras optionnels</strong> — connectez vos outils d'analytics, activez la protection anti-bots et (sur Vercel) les mises à jour automatiques.</li>\\n</ul>\\n<p>Ensuite, découvrez comment la plateforme s'articule dans <a href='/article/comment-nextblock-fonctionne'>Comment NextBlock fonctionne</a>, ou ajoutez une boutique avec le <a href='/article/guide-commerce-nextblock'>guide Commerce</a>.</p>\\n\\n<h2 id='faq'>FAQ d'installation</h2>\\n<h3>Que dois-je installer ?</h3>\\n<p>Rien pour le chemin Vercel — tout se passe dans le navigateur. Pour <code>npm create nextblock</code> en mode Docker : <a href='https://nodejs.org' target='_blank' rel='noopener'>Node.js 20+</a> et Docker Desktop. Pour le mode cloud géré : Node.js 20+ ainsi qu'un projet Supabase et un bucket Cloudflare R2. Pour le dépôt cloné : Node.js 20+ et git (ajoutez Docker Desktop si vous voulez faire tourner la pile locale).</p>\\n<h3>NextBlock est-il gratuit ?</h3>\\n<p>Oui — le cœur du CMS est 100 % gratuit et open source (AGPL). Les packages premium comme l'e-commerce et Cortex AI sont optionnels et s'activent avec une clé de licence. Vercel et Supabase proposent chacun une offre gratuite : un site de départ peut donc tourner sans frais.</p>\\n<h3>Ai-je besoin d'un compte Supabase ?</h3>\\n<p>Sur Vercel, la base de données est créée pour vous pendant le déploiement. Pour le chemin cloud géré <code>npm create nextblock</code> et le dépôt cloné, il vous faut un projet Supabase gratuit. Avec Docker — via le mode Docker du CLI ou <code>npm run docker:setup</code> dans le clone — aucun compte cloud n'est nécessaire.</p>\\n<h3>Dois-je exécuter des migrations ou du SQL à la main ?</h3>\\n<p>Non. L'assistant de configuration, le build Vercel et la pile Docker appliquent tous le schéma de base de données automatiquement — et relancer l'opération est toujours sans risque.</p>\\n<h3>Puis-je changer de chemin plus tard ?</h3>\\n<p>Oui. Chaque chemin exécute la même application et le même schéma de base de données : vous pouvez prototyper en local avec Docker aujourd'hui et déployer sur Vercel demain. NextBlock se déploie comme n'importe quelle app Next.js.</p>\\n<h3>Comment mettre à jour NextBlock ?</h3>\\n<p>Sur Vercel, l'étape Connect GitHub de la checklist active une synchronisation quotidienne automatique (cela nécessite un dépôt public). Sur un dépôt cloné, <code>git pull</code>, lancez <code>npm run db:migrate</code>, puis redémarrez (lors des builds de production, les migrations en attente s'appliquent automatiquement). Avec Docker, récupérez le dernier code et lancez <code>npm run docker:up</code>.</p>\\n\\n<div class='rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5'>\\n <p class='mt-0 text-2xl font-semibold text-slate-900 dark:text-white'>Prêt à vous lancer ?</p>\\n <p class='text-sm text-slate-600 dark:text-slate-300'>Choisissez votre chemin ci-dessus, ou passez directement au plus rapide.</p>\\n <div class='mt-5 flex flex-wrap justify-center gap-3'>\\n <a href='https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnextblock-cms%2Fnextblock&amp;project-name=nextblock&amp;repository-name=nextblock&amp;stores=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D' target='_blank' rel='noopener' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Déployer sur Vercel</a>\\n <a href='https://github.com/nextblock-cms/nextblock' target='_blank' rel='noopener' class='inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50'>Voir sur GitHub</a>\\n </div>\\n</div>\"}\r\n$nbfr$::jsonb,\r\n updated_at = now()\r\n WHERE post_id = 4\r\n AND block_type = 'text';\r\n"
61
+ },
62
+ {
63
+ "version": "00000000000009",
64
+ "name": "00000000000009_home_live_demo_promo_contrast.sql",
65
+ "sql": "-- 00000000000009_home_live_demo_promo_contrast.sql\n-- Restyles the \"Live Demo\" promo that migrations 006/007 seeded on the EN (slug 'home')\n-- and FR (slug 'accueil') home pages. 006/007 gave it a fixed dark-navy GRADIENT section\n-- background -- and a gradient background is an inline style, so it renders identically\n-- in light and dark theme. Sitting directly under the (also dark) hero, that made the\n-- first two sections both dark in light mode and killed the old dark-hero -> white-second\n-- contrast rhythm.\n--\n-- Fix (append-only, do NOT edit 006/007 -- both already applied to prod + sandbox):\n-- switch the section background to \"none\" (transparent -> inherits the theme-aware page\n-- background: light in light theme, dark in dark theme) and render the promo as a single\n-- self-contained ELEVATED PANEL. The panel is a soft light card in light theme and keeps\n-- the rich navy look in dark theme (dark: gradient), with the browser mockup deliberately\n-- kept dark in both themes as a \"product screenshot\" focal point. The <!--nb-sandbox-promo-->\n-- sentinel is preserved so the reset-sandbox route still strips it on cms.nextblock.dev.\n--\n-- Idempotent: re-running sets identical content. Targets exactly the promo blocks via the\n-- nb-sandbox-promo sentinel, scoped by language (mirrors 00000000000007). blocks.content is\n-- JSONB, written with dollar-quoting.\n--\n-- NOTE: keep the word \"sandbox\" OUT of this filename -- generate-sandbox-reset.ts excludes\n-- any migration whose filename contains \"sandbox\" from the sandbox reset bundle.\n\nDO $body$\nDECLARE\n v_en_lang integer;\n v_fr_lang integer;\nBEGIN\n SELECT id INTO v_en_lang FROM public.languages WHERE code = 'en' LIMIT 1;\n SELECT id INTO v_fr_lang FROM public.languages WHERE code = 'fr' LIMIT 1;\n\n UPDATE public.blocks\n SET content = $en$\n{\"container_type\":\"container\",\"background\":{\"type\":\"none\"},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"column_gap\":\"xl\",\"vertical_alignment\":\"center\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<!--nb-sandbox-promo--><div class='relative overflow-hidden rounded-[2.5rem] border border-slate-200 bg-gradient-to-br from-white via-slate-50 to-blue-50 shadow-2xl shadow-slate-900/10 dark:border-white/10 dark:from-slate-900 dark:via-slate-900 dark:to-blue-950 dark:shadow-black/40'><div class='grid gap-10 p-8 sm:p-12 lg:grid-cols-2 lg:items-center'><div><p class='text-xs uppercase tracking-[0.25em] text-cyan-600 dark:text-cyan-400 font-bold mb-4'>Live Demo &middot; No Signup</p><h2 class='text-4xl md:text-5xl font-bold text-slate-900 dark:text-white mb-6 leading-tight'>Try the full CMS,<br/>live right now.</h2><p class='text-lg text-slate-600 dark:text-slate-300 max-w-xl leading-relaxed mb-8'>Don't just read about NextBlock&trade; &mdash; take the wheel. Our public sandbox is a complete, pre-loaded install where you can build pages with the block editor, manage bilingual content, and browse a working demo store. It resets daily, so explore freely and break whatever you like.</p><div class='flex flex-col sm:flex-row gap-4'><a href='https://cms.nextblock.dev' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 shadow-lg shadow-blue-500/25 transition-all no-underline'>Explore the Live Demo &rarr;</a><a href='https://cms.nextblock.dev/sign-in' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-slate-700 dark:text-white border border-slate-300 dark:border-white/20 hover:bg-slate-100 dark:hover:bg-white/10 transition-all no-underline'>Open the CMS Dashboard &rarr;</a></div><p class='text-xs text-slate-500 dark:text-slate-400 mt-4'>Demo login <span class='font-mono text-slate-700 dark:text-slate-300'>demo@nextblock.dev</span> / <span class='font-mono text-slate-700 dark:text-slate-300'>password</span> &middot; Resets daily</p></div><div class='rounded-3xl overflow-hidden border border-slate-800 bg-slate-900 shadow-xl shadow-slate-900/20 dark:border-white/10 dark:bg-white/5 dark:backdrop-blur-xl'><div class='flex items-center gap-2 px-4 py-3 border-b border-white/10 bg-white/5'><span class='w-3 h-3 rounded-full bg-red-400/70'></span><span class='w-3 h-3 rounded-full bg-yellow-400/70'></span><span class='w-3 h-3 rounded-full bg-green-400/70'></span><span class='ml-3 flex-1 truncate rounded-md bg-black/30 px-3 py-1 text-xs font-mono text-slate-300'>cms.nextblock.dev</span></div><div class='p-6 sm:p-8'><p class='text-sm text-slate-300 mb-5'>A living NextBlock&trade; install &mdash; everything here is real and clickable:</p><ul class='space-y-4 text-sm text-slate-200'><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Full block editor</strong> with slash commands &amp; drag-and-drop.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Bilingual</strong> English / French content management.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>A complete demo storefront</strong> with a full checkout flow.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Wipes clean daily</strong> &mdash; always a fresh start.</span></li></ul></div></div></div></div>\"}}]]}\n$en$::jsonb,\n updated_at = now()\n WHERE block_type = 'section'\n AND page_id IS NOT NULL\n AND language_id = v_en_lang\n AND content::text LIKE '%nb-sandbox-promo%';\n\n UPDATE public.blocks\n SET content = $fr$\n{\"container_type\":\"container\",\"background\":{\"type\":\"none\"},\"responsive_columns\":{\"mobile\":1,\"tablet\":1,\"desktop\":1},\"column_gap\":\"xl\",\"vertical_alignment\":\"center\",\"padding\":{\"top\":\"xl\",\"bottom\":\"xl\"},\"column_blocks\":[[{\"block_type\":\"text\",\"content\":{\"html_content\":\"<!--nb-sandbox-promo--><div class='relative overflow-hidden rounded-[2.5rem] border border-slate-200 bg-gradient-to-br from-white via-slate-50 to-blue-50 shadow-2xl shadow-slate-900/10 dark:border-white/10 dark:from-slate-900 dark:via-slate-900 dark:to-blue-950 dark:shadow-black/40'><div class='grid gap-10 p-8 sm:p-12 lg:grid-cols-2 lg:items-center'><div><p class='text-xs uppercase tracking-[0.25em] text-cyan-600 dark:text-cyan-400 font-bold mb-4'>D&eacute;mo en direct &middot; Sans inscription</p><h2 class='text-4xl md:text-5xl font-bold text-slate-900 dark:text-white mb-6 leading-tight'>Essayez le CMS complet,<br/>en direct d&egrave;s maintenant.</h2><p class='text-lg text-slate-600 dark:text-slate-300 max-w-xl leading-relaxed mb-8'>Ne vous contentez pas de lire des articles sur NextBlock&trade; &mdash; prenez les commandes. Notre bac &agrave; sable public est une installation compl&egrave;te et pr&eacute;charg&eacute;e o&ugrave; vous pouvez cr&eacute;er des pages avec l'&eacute;diteur de blocs, g&eacute;rer du contenu bilingue et parcourir une vraie boutique de d&eacute;monstration. Il se r&eacute;initialise chaque jour : explorez librement et cassez tout ce que vous voulez.</p><div class='flex flex-col sm:flex-row gap-4'><a href='https://cms.nextblock.dev' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 shadow-lg shadow-blue-500/25 transition-all no-underline'>D&eacute;couvrir la d&eacute;mo &rarr;</a><a href='https://cms.nextblock.dev/sign-in' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-slate-700 dark:text-white border border-slate-300 dark:border-white/20 hover:bg-slate-100 dark:hover:bg-white/10 transition-all no-underline'>Ouvrir le tableau de bord &rarr;</a></div><p class='text-xs text-slate-500 dark:text-slate-400 mt-4'>Identifiants d&eacute;mo <span class='font-mono text-slate-700 dark:text-slate-300'>demo@nextblock.dev</span> / <span class='font-mono text-slate-700 dark:text-slate-300'>password</span> &middot; R&eacute;initialis&eacute; chaque jour</p></div><div class='rounded-3xl overflow-hidden border border-slate-800 bg-slate-900 shadow-xl shadow-slate-900/20 dark:border-white/10 dark:bg-white/5 dark:backdrop-blur-xl'><div class='flex items-center gap-2 px-4 py-3 border-b border-white/10 bg-white/5'><span class='w-3 h-3 rounded-full bg-red-400/70'></span><span class='w-3 h-3 rounded-full bg-yellow-400/70'></span><span class='w-3 h-3 rounded-full bg-green-400/70'></span><span class='ml-3 flex-1 truncate rounded-md bg-black/30 px-3 py-1 text-xs font-mono text-slate-300'>cms.nextblock.dev</span></div><div class='p-6 sm:p-8'><p class='text-sm text-slate-300 mb-5'>Une installation NextBlock&trade; bien vivante &mdash; tout ici est r&eacute;el et cliquable :</p><ul class='space-y-4 text-sm text-slate-200'><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>&Eacute;diteur de blocs complet</strong> : commandes slash et glisser-d&eacute;poser.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Gestion bilingue</strong> du contenu fran&ccedil;ais / anglais.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Une boutique de d&eacute;monstration compl&egrave;te</strong> avec un parcours de paiement complet.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Remis &agrave; z&eacute;ro chaque jour</strong> &mdash; toujours un nouveau d&eacute;part.</span></li></ul></div></div></div></div>\"}}]]}\n$fr$::jsonb,\n updated_at = now()\n WHERE block_type = 'section'\n AND page_id IS NOT NULL\n AND language_id = v_fr_lang\n AND content::text LIKE '%nb-sandbox-promo%';\nEND\n$body$;\n"
66
+ },
67
+ {
68
+ "version": "00000000000010",
69
+ "name": "00000000000010_drop_github_username.sql",
70
+ "sql": "-- Drop the github_username profile column and remove the GitHub-auth artifacts.\r\n--\r\n-- GitHub OAuth sign-in / account-linking was removed from the app: it cannot be\r\n-- provisioned as part of a one-click deployment (each install needs its own GitHub\r\n-- OAuth app + client secret + Supabase's manual-linking toggle, none of which can\r\n-- ship in a migration). github_username was only ever populated from that OAuth\r\n-- flow, so the column and its seed strings are now dead.\r\n--\r\n-- NOTE: the self-update \"Connect GitHub\" flow (lib/updates/*, ConnectGitHubButton)\r\n-- is a separate feature and is intentionally left untouched — the connect_github\r\n-- translation is kept for it.\r\n\r\n-- 1. Rewrite handle_new_user() so new signups no longer read/populate github_username.\r\nCREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS trigger\r\n LANGUAGE plpgsql SECURITY DEFINER\r\n SET search_path TO 'public'\r\n AS $$\r\nDECLARE\r\n admin_flag_set boolean := false;\r\n user_role public.user_role;\r\n v_full_name text;\r\n v_avatar_url text;\r\nBEGIN\r\n INSERT INTO public.site_settings (key, value)\r\n VALUES ('is_admin_created', 'false'::jsonb)\r\n ON CONFLICT (key) DO NOTHING;\r\n\r\n SELECT COALESCE(value::jsonb::boolean, false)\r\n INTO admin_flag_set\r\n FROM public.site_settings\r\n WHERE key = 'is_admin_created'\r\n FOR UPDATE;\r\n\r\n IF admin_flag_set = false THEN\r\n user_role := 'ADMIN'::public.user_role;\r\n\r\n UPDATE public.site_settings\r\n SET value = 'true'::jsonb\r\n WHERE key = 'is_admin_created';\r\n ELSE\r\n user_role := 'USER'::public.user_role;\r\n END IF;\r\n\r\n v_full_name := NEW.raw_user_meta_data->>'full_name';\r\n v_avatar_url := NEW.raw_user_meta_data->>'avatar_url';\r\n\r\n INSERT INTO public.profiles (\r\n id,\r\n role,\r\n full_name,\r\n avatar_url\r\n )\r\n VALUES (\r\n NEW.id,\r\n user_role,\r\n v_full_name,\r\n v_avatar_url\r\n )\r\n ON CONFLICT (id) DO UPDATE SET\r\n full_name = EXCLUDED.full_name,\r\n avatar_url = EXCLUDED.avatar_url;\r\n\r\n RETURN NEW;\r\nEND;\r\n$$;\r\n\r\n-- 2. Drop the column now that nothing writes it.\r\nALTER TABLE public.profiles DROP COLUMN IF EXISTS github_username;\r\n\r\n-- 3. Remove the GitHub-auth-only translation strings. connect_github is intentionally\r\n-- kept — it is still used by the self-update \"Connect GitHub\" button.\r\nDELETE FROM public.translations\r\nWHERE key IN (\r\n 'continue_with_github',\r\n 'github_username',\r\n 'github_username_help',\r\n 'github_link_failed',\r\n 'github_connected'\r\n);\r\n"
71
+ },
72
+ {
73
+ "version": "00000000000011",
74
+ "name": "00000000000011_language_detection_admin_only.sql",
75
+ "sql": "-- Restrict writes to the language-detection settings row to ADMIN only.\r\n--\r\n-- `site_settings.language_detection_settings` is a non-sensitive, anon-READABLE\r\n-- key (the request proxy reads it with the anon client to pick a visitor's first\r\n-- language). The CMS surfaces it under /cms/settings (ADMIN-only) and the server\r\n-- action guards with an ADMIN check, but the baseline write policies let ADMIN\r\n-- *or* WRITER write any non-sensitive key — so a WRITER could change site-wide\r\n-- detection directly via PostgREST. This migration adds the key to the ADMIN-only\r\n-- write group (INSERT/UPDATE/DELETE) to match the UI boundary, while leaving the\r\n-- SELECT policy untouched so the proxy's anon read keeps working.\r\n--\r\n-- Forward-only; recreates the three write policies idempotently.\r\n\r\nDROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;\r\nCREATE POLICY site_settings_insert_policy ON public.site_settings FOR INSERT TO authenticated WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nDROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;\r\nCREATE POLICY site_settings_update_policy ON public.site_settings FOR UPDATE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)))) WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nDROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;\r\nCREATE POLICY site_settings_delete_policy ON public.site_settings FOR DELETE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n"
76
+ },
77
+ {
78
+ "version": "00000000000012",
79
+ "name": "00000000000012_cortex_ai_stock_photo_settings.sql",
80
+ "sql": "-- Protect the Cortex AI stock-photo provider API keys stored in site_settings.\r\n--\r\n-- The stock-photo search tool (search_stock_photos) can read a Pexels or Unsplash\r\n-- API key from site_settings so the key lives in the database instead of an env var.\r\n-- Those two rows are SECRETS and must never be publicly readable: like the OpenRouter\r\n-- BYOK key, they belong in the sensitive-keys group that only authenticated ADMINs can\r\n-- read or write. The baseline site_settings SELECT policy makes any key NOT in that\r\n-- group anon-readable, so we add the two stock-photo keys to every policy's sensitive\r\n-- array. The value stored is an encrypted envelope, but we protect the row regardless.\r\n--\r\n-- Forward-only; recreates all four site_settings policies idempotently, preserving the\r\n-- existing sensitive keys (including language_detection_settings on the write policies,\r\n-- which stays anon-READABLE and is therefore not added to the SELECT policy).\r\n\r\nDROP POLICY IF EXISTS site_settings_read_policy ON public.site_settings;\r\nCREATE POLICY site_settings_read_policy ON public.site_settings FOR SELECT USING (((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) AND (( SELECT auth.role() AS role) = 'authenticated'::text) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nDROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;\r\nCREATE POLICY site_settings_insert_policy ON public.site_settings FOR INSERT TO authenticated WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nDROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;\r\nCREATE POLICY site_settings_update_policy ON public.site_settings FOR UPDATE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)))) WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n\r\nDROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;\r\nCREATE POLICY site_settings_delete_policy ON public.site_settings FOR DELETE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\r\n"
81
+ },
82
+ {
83
+ "version": "00000000000013",
84
+ "name": "00000000000013_youtube_nocookie_embeds.sql",
85
+ "sql": "-- 00000000000013_youtube_nocookie_embeds.sql\r\n--\r\n-- Lighthouse Best Practices scored 96 instead of 100: the `inspector-issues` audit\r\n-- (binary, weight 1 of 27 -> 26/27 = 96) failed with a DevTools \"Cookie\" issue\r\n-- attributed to www.youtube.com/embed/71MyfoL4YVM. The seeded home-page hero and the\r\n-- seeded articles embed raw <iframe> markup pointing at www.youtube.com, whose player\r\n-- writes cookies on load.\r\n--\r\n-- The real fix is the render-time click-to-play facade added in\r\n-- apps/nextblock/components/media/YouTubeFacade.tsx (the nocookie host does NOT stop\r\n-- the player's document.cookie writes -- only not loading the player does). This\r\n-- migration is the data-hygiene half: it rewrites every stored embed to the\r\n-- privacy-enhanced host so any surface that still renders a raw iframe (published-lib\r\n-- renderers, CMS previews, exports, downstream installs on an older app build) is at\r\n-- least cookie-reduced, and so the stored content matches what we ship.\r\n--\r\n-- It also fixes the bogus Permissions Policy feature in the seeded allow attribute:\r\n-- 'accelerated-motion' is not a real feature (Chrome logs \"Unrecognized feature\");\r\n-- the canonical YouTube embed uses 'accelerometer'.\r\n--\r\n-- Append-only: 00000000000003_baseline_seed.sql is applied everywhere and is NOT edited.\r\n-- Scoped by CONTENT MATCH, never by block id -- ids drift on installs where the demo\r\n-- content was re-created (see the convention documented in 00000000000006:11-13 and\r\n-- used by 007/009). This is a pure host-substring swap, so it never clobbers a user's\r\n-- edited copy, it also repairs author-pasted embeds, and it is naturally idempotent\r\n-- (a second run matches zero rows) which matters because the nightly sandbox reset\r\n-- replays the whole migration chain from a truncated history.\r\n--\r\n-- No dollar-quoting is needed here: neither search nor replacement literal contains a\r\n-- single quote, and neither contains a JSON metacharacter, so the ::text round-trip\r\n-- always re-parses as valid JSON. Query strings (?si=...) are preserved.\r\n--\r\n-- NOTE: keep the word \"sandbox\" OUT of this filename -- generate-sandbox-reset.ts\r\n-- excludes any migration whose filename contains \"sandbox\".\r\n\r\n-- 1. Live page/post block content (the 4 seeded rows + any author-added embed).\r\nUPDATE public.blocks\r\n SET content = replace(\r\n replace(content::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE content IS NOT NULL\r\n AND (content::text LIKE '%www.youtube.com/embed/%'\r\n OR content::text LIKE '%accelerated-motion%');\r\n\r\n-- 2. Live Draft Mode snapshots. Publishing a pre-existing draft would otherwise write\r\n-- the cookie host straight back into public.blocks.\r\nUPDATE public.content_drafts\r\n SET blocks = replace(\r\n replace(blocks::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE blocks::text LIKE '%www.youtube.com/embed/%'\r\n OR blocks::text LIKE '%accelerated-motion%';\r\n\r\nUPDATE public.content_drafts\r\n SET meta = replace(\r\n replace(meta::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE meta::text LIKE '%www.youtube.com/embed/%'\r\n OR meta::text LIKE '%accelerated-motion%';\r\n\r\nUPDATE public.product_drafts\r\n SET blocks = replace(\r\n replace(blocks::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE blocks::text LIKE '%www.youtube.com/embed/%'\r\n OR blocks::text LIKE '%accelerated-motion%';\r\n\r\n-- product_drafts.meta carries short_description / description_json.\r\nUPDATE public.product_drafts\r\n SET meta = replace(\r\n replace(meta::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE meta::text LIKE '%www.youtube.com/embed/%'\r\n OR meta::text LIKE '%accelerated-motion%';\r\n\r\n-- 3. Revision history. apps/nextblock/app/cms/revisions/service.ts replays a snapshot\r\n-- verbatim, so one \"Restore version\" click would otherwise reintroduce the issue.\r\n-- URL-only rewrite; nothing else about the historical snapshot is touched.\r\nUPDATE public.page_revisions\r\n SET content = replace(\r\n replace(content::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE content::text LIKE '%www.youtube.com/embed/%'\r\n OR content::text LIKE '%accelerated-motion%';\r\n\r\nUPDATE public.post_revisions\r\n SET content = replace(\r\n replace(content::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE content::text LIKE '%www.youtube.com/embed/%'\r\n OR content::text LIKE '%accelerated-motion%';\r\n\r\n-- 4. Product rich text rendered on public /product/* routes.\r\nUPDATE public.products\r\n SET short_description = replace(\r\n replace(short_description, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )\r\n WHERE short_description IS NOT NULL\r\n AND (short_description LIKE '%www.youtube.com/embed/%'\r\n OR short_description LIKE '%accelerated-motion%');\r\n\r\nUPDATE public.products\r\n SET description_json = replace(\r\n replace(description_json::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE description_json IS NOT NULL\r\n AND (description_json::text LIKE '%www.youtube.com/embed/%'\r\n OR description_json::text LIKE '%accelerated-motion%');\r\n\r\n-- 5. Custom block definitions (rich-text defaults / emptyFallback markup rendered by\r\n-- DynamicLayoutEngine). Host-only swap, so the is_valid_custom_block_* CHECK\r\n-- constraints still hold. No-op on a fresh install.\r\nUPDATE public.custom_block_definitions\r\n SET fields = replace(\r\n replace(fields::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE fields::text LIKE '%www.youtube.com/embed/%'\r\n OR fields::text LIKE '%accelerated-motion%';\r\n\r\nUPDATE public.custom_block_definitions\r\n SET layout_schema = replace(\r\n replace(layout_schema::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),\r\n 'accelerated-motion', 'accelerometer'\r\n )::jsonb\r\n WHERE layout_schema::text LIKE '%www.youtube.com/embed/%'\r\n OR layout_schema::text LIKE '%accelerated-motion%';\r\n"
86
+ },
87
+ {
88
+ "version": "00000000000014",
89
+ "name": "00000000000014_site_themes.sql",
90
+ "sql": "-- Editable site themes.\n--\n-- Themes used to be hardcoded CSS classes in libs/ui/src/styles/theme.css\n-- (:root / .dark / .vibrant) with the switcher list duplicated in\n-- apps/nextblock/app/providers.tsx and components/theme-switcher.tsx. This moves\n-- the palette into the database so an ADMIN can retint the site, add themes and\n-- remove them from /cms/settings/global-css without a redeploy.\n--\n-- Rendering: apps/nextblock/lib/themes/buildThemeCss.ts turns each row into a\n-- `:root.<slug> { --token: value; ... }` rule injected into <head> by\n-- app/layout.tsx. The `:root.x` form is two-class specificity, so generated\n-- themes always beat the (0,1,0) fallback rules still shipped in theme.css for\n-- consumers of the published @nextblock-cms/ui package.\n--\n-- Forward-only and idempotent.\n\nCREATE TABLE IF NOT EXISTS public.site_themes (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n slug text NOT NULL,\n name text NOT NULL,\n description text,\n -- lucide-react icon name rendered by the theme switcher.\n icon text NOT NULL DEFAULT 'Palette',\n -- Drives the CSS `color-scheme` property and decides whether the theme also\n -- carries Tailwind's `.dark` class so `dark:` variants resolve correctly.\n color_scheme text NOT NULL DEFAULT 'light',\n -- Flat map of design token -> raw CSS value, keys WITHOUT the leading `--`,\n -- e.g. {\"background\": \"0 0% 100%\", \"radius\": \"0.75rem\"}.\n tokens jsonb NOT NULL DEFAULT '{}'::jsonb,\n -- Optional per-theme CSS, emitted nested inside the theme rule so it is\n -- automatically scoped. Authors use the `&` nesting selector,\n -- e.g. `& h1 { text-shadow: 0 0 5px hsl(var(--primary)); }`.\n extra_css text,\n -- System themes cannot be deleted: next-themes' `enableSystem` resolves to\n -- 'light' or 'dark', so those two slugs must always exist.\n is_system boolean DEFAULT false NOT NULL,\n is_default boolean DEFAULT false NOT NULL,\n is_active boolean DEFAULT true NOT NULL,\n sort_order integer DEFAULT 0 NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT site_themes_pkey PRIMARY KEY (id),\n CONSTRAINT site_themes_slug_key UNIQUE (slug),\n CONSTRAINT site_themes_color_scheme_check CHECK ((color_scheme = ANY (ARRAY['light'::text, 'dark'::text]))),\n -- The slug becomes a CSS class and a next-themes value; keep it safe for both.\n CONSTRAINT site_themes_slug_format_check CHECK ((slug ~ '^[a-z][a-z0-9-]{0,38}[a-z0-9]$')),\n CONSTRAINT site_themes_tokens_is_object_check CHECK ((jsonb_typeof(tokens) = 'object'))\n);\n\nCOMMENT ON TABLE public.site_themes IS 'Editable colour themes. Each row renders to a `:root.<slug>` CSS rule injected by the root layout. Publicly readable (anonymous visitors need the palette); only ADMIN may write.';\n\nCREATE INDEX IF NOT EXISTS site_themes_active_sort_idx ON public.site_themes USING btree (is_active, sort_order);\n\n-- Exactly one default theme.\nCREATE UNIQUE INDEX IF NOT EXISTS site_themes_single_default_idx ON public.site_themes USING btree (is_default) WHERE (is_default = true);\n\nDROP TRIGGER IF EXISTS set_site_themes_updated_at ON public.site_themes;\nCREATE TRIGGER set_site_themes_updated_at\n BEFORE UPDATE ON public.site_themes\n FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\n\n-- A system theme must never be deleted, whoever asks.\nCREATE OR REPLACE FUNCTION public.prevent_system_theme_delete() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path = ''\n AS $$\nBEGIN\n IF OLD.is_system THEN\n RAISE EXCEPTION 'Theme \"%\" is a system theme and cannot be deleted', OLD.slug\n USING ERRCODE = 'restrict_violation';\n END IF;\n RETURN OLD;\nEND;\n$$;\n\nDROP TRIGGER IF EXISTS trg_prevent_system_theme_delete ON public.site_themes;\nCREATE TRIGGER trg_prevent_system_theme_delete\n BEFORE DELETE ON public.site_themes\n FOR EACH ROW EXECUTE FUNCTION public.prevent_system_theme_delete();\n\n-- Promoting a theme to default demotes the previous one, so the unique partial\n-- index above can never trip on a normal \"make this the default\" write.\nCREATE OR REPLACE FUNCTION public.handle_default_theme_change() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path = ''\n AS $$\nBEGIN\n IF NEW.is_default THEN\n UPDATE public.site_themes\n SET is_default = false\n WHERE id <> NEW.id AND is_default;\n END IF;\n RETURN NEW;\nEND;\n$$;\n\nDROP TRIGGER IF EXISTS trg_handle_default_theme_change ON public.site_themes;\nCREATE TRIGGER trg_handle_default_theme_change\n AFTER INSERT OR UPDATE OF is_default ON public.site_themes\n FOR EACH ROW WHEN (NEW.is_default) EXECUTE FUNCTION public.handle_default_theme_change();\n\nALTER TABLE public.site_themes ENABLE ROW LEVEL SECURITY;\n\nGRANT ALL ON TABLE public.site_themes TO anon;\nGRANT ALL ON TABLE public.site_themes TO authenticated;\nGRANT ALL ON TABLE public.site_themes TO service_role;\n\nDROP POLICY IF EXISTS \"Public read active themes\" ON public.site_themes;\nCREATE POLICY \"Public read active themes\" ON public.site_themes\n FOR SELECT TO authenticated, anon USING (true);\n\nDROP POLICY IF EXISTS \"Admins insert themes\" ON public.site_themes;\nCREATE POLICY \"Admins insert themes\" ON public.site_themes\n FOR INSERT TO authenticated\n WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins update themes\" ON public.site_themes;\nCREATE POLICY \"Admins update themes\" ON public.site_themes\n FOR UPDATE TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role))\n WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins delete themes\" ON public.site_themes;\nCREATE POLICY \"Admins delete themes\" ON public.site_themes\n FOR DELETE TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Service role manages themes\" ON public.site_themes;\nCREATE POLICY \"Service role manages themes\" ON public.site_themes\n TO service_role USING (true) WITH CHECK (true);\n\n-- Seed the three shipped themes from libs/ui/src/styles/theme.css.\n-- `--warning` / `--warning-foreground` are declared in the Tailwind theme\n-- (libs/ui/tailwind.config.js) but were never defined in CSS, so bg-warning and\n-- text-warning resolved to an invalid colour. They are given real values here.\nINSERT INTO public.site_themes (slug, name, description, icon, color_scheme, is_system, is_default, sort_order, tokens, extra_css)\nVALUES\n (\n 'light', 'Light', 'Clean, technical, stark.', 'Sun', 'light', true, true, 10,\n '{\n \"background\": \"0 0% 100%\",\n \"foreground\": \"222 47% 11%\",\n \"card\": \"0 0% 100%\",\n \"card-foreground\": \"222 47% 11%\",\n \"popover\": \"0 0% 100%\",\n \"popover-foreground\": \"222 47% 11%\",\n \"primary\": \"211.55 50.26% 37.84%\",\n \"primary-foreground\": \"210 40% 98%\",\n \"secondary\": \"210 40% 96.1%\",\n \"secondary-foreground\": \"222 47% 11%\",\n \"muted\": \"210 40% 96.1%\",\n \"muted-foreground\": \"215 16% 47%\",\n \"accent\": \"210 40% 96.1%\",\n \"accent-foreground\": \"222 47% 11%\",\n \"destructive\": \"0 84.2% 60.2%\",\n \"destructive-foreground\": \"210 40% 98%\",\n \"warning\": \"38 92% 50%\",\n \"warning-foreground\": \"222 47% 11%\",\n \"border\": \"214.3 31.8% 91.4%\",\n \"input\": \"214.3 31.8% 91.4%\",\n \"ring\": \"211.55 50.26% 37.84%\",\n \"radius\": \"0.75rem\",\n \"chart-1\": \"211.55 50.26% 37.84%\",\n \"chart-2\": \"215 16% 47%\",\n \"chart-3\": \"215 25% 27%\",\n \"chart-4\": \"210 40% 96%\",\n \"chart-5\": \"214 32% 91%\"\n }'::jsonb,\n NULL\n ),\n (\n 'dark', 'Dark', 'Midnight / neon tech.', 'Moon', 'dark', true, false, 20,\n '{\n \"background\": \"222 47% 2%\",\n \"foreground\": \"210 40% 98%\",\n \"card\": \"222 47% 11%\",\n \"card-foreground\": \"210 40% 98%\",\n \"popover\": \"222 47% 11%\",\n \"popover-foreground\": \"210 40% 98%\",\n \"primary\": \"217 91% 60%\",\n \"primary-foreground\": \"222 47% 11%\",\n \"secondary\": \"217.2 32.6% 17.5%\",\n \"secondary-foreground\": \"210 40% 98%\",\n \"muted\": \"217.2 32.6% 17.5%\",\n \"muted-foreground\": \"215 20.2% 65.1%\",\n \"accent\": \"217.2 32.6% 17.5%\",\n \"accent-foreground\": \"210 40% 98%\",\n \"destructive\": \"0 62.8% 30.6%\",\n \"destructive-foreground\": \"210 40% 98%\",\n \"warning\": \"38 92% 50%\",\n \"warning-foreground\": \"222 47% 11%\",\n \"border\": \"217.2 32.6% 17.5%\",\n \"input\": \"217.2 32.6% 17.5%\",\n \"ring\": \"224 76% 48%\",\n \"radius\": \"0.75rem\",\n \"chart-1\": \"220 70% 50%\",\n \"chart-2\": \"160 60% 45%\",\n \"chart-3\": \"30 80% 55%\",\n \"chart-4\": \"280 65% 60%\",\n \"chart-5\": \"340 75% 55%\"\n }'::jsonb,\n NULL\n ),\n (\n 'vibrant', 'Vibrant', 'Cyberpunk neon.', 'Zap', 'dark', false, false, 30,\n '{\n \"background\": \"260 50% 5%\",\n \"foreground\": \"180 100% 90%\",\n \"card\": \"260 50% 8%\",\n \"card-foreground\": \"180 100% 90%\",\n \"popover\": \"260 50% 8%\",\n \"popover-foreground\": \"180 100% 90%\",\n \"primary\": \"320 100% 55%\",\n \"primary-foreground\": \"0 0% 100%\",\n \"secondary\": \"180 100% 50%\",\n \"secondary-foreground\": \"260 50% 5%\",\n \"muted\": \"260 30% 15%\",\n \"muted-foreground\": \"260 20% 65%\",\n \"accent\": \"280 100% 50%\",\n \"accent-foreground\": \"0 0% 100%\",\n \"destructive\": \"0 100% 50%\",\n \"destructive-foreground\": \"0 0% 100%\",\n \"warning\": \"60 100% 50%\",\n \"warning-foreground\": \"260 50% 5%\",\n \"border\": \"320 100% 55%\",\n \"input\": \"260 30% 15%\",\n \"ring\": \"320 100% 55%\",\n \"radius\": \"0px\",\n \"chart-1\": \"320 100% 55%\",\n \"chart-2\": \"180 100% 50%\",\n \"chart-3\": \"280 100% 50%\",\n \"chart-4\": \"60 100% 50%\",\n \"chart-5\": \"120 100% 50%\"\n }'::jsonb,\n '& h1, & h2, & h3, & h4, & h5, & h6 {\n text-shadow: 0 0 5px hsl(var(--primary)), 0 0 10px hsl(var(--secondary));\n}\n& button, & [role=\"button\"] {\n box-shadow: 0 0 5px hsl(var(--primary) / 0.5);\n transition: box-shadow 0.3s ease;\n}\n& button:hover, & [role=\"button\"]:hover {\n box-shadow: 0 0 15px hsl(var(--primary));\n}\n& .card, & [class*=\"card\"] {\n border: 1px solid hsl(var(--primary));\n box-shadow: 0 0 10px hsl(var(--primary) / 0.2);\n}\n& .border {\n border-color: hsl(var(--border));\n box-shadow: 0 0 5px hsl(var(--border) / 0.3);\n}'\n )\nON CONFLICT (slug) DO NOTHING;\n"
91
+ },
92
+ {
93
+ "version": "00000000000015",
94
+ "name": "00000000000015_scheduled_publishing.sql",
95
+ "sql": "-- Scheduled publishing for pages and products.\n--\n-- Posts already support scheduling: `posts.published_at` exists and every public\n-- read gates on `published_at IS NULL OR published_at <= now()`, so a row with\n-- status='published' and a future date is withheld until the date passes. Pages\n-- and products had no equivalent column, so \"go live on Tuesday\" was impossible\n-- for them. This adds the same column with the same semantics.\n--\n-- Visibility is derived from the (status, published_at) PAIR — no new enum value:\n--\n-- status = draft/archived -> not public, whatever the date\n-- status = published|active, published_at NULL -> public now\n-- status = published|active, date <= now() -> public now\n-- status = published|active, date > now() -> SCHEDULED (withheld)\n--\n-- Deriving \"scheduled\" instead of storing it keeps `page_status` unchanged (adding\n-- an enum value can't be done inside a transaction with other DDL in Postgres) and\n-- matches what posts have always done, so one code path covers all three types.\n--\n-- NULL is the safe default: every existing published row keeps rendering exactly as\n-- before, so this migration needs no backfill and changes no current behavior.\n--\n-- Forward-only and idempotent.\n\nALTER TABLE public.pages\n ADD COLUMN IF NOT EXISTS published_at timestamp with time zone;\n\nCOMMENT ON COLUMN public.pages.published_at IS\n 'Optional go-live moment. NULL = live as soon as status is published. A future value withholds the page from public reads until it passes (status stays \"published\"; the CMS renders that pair as \"Scheduled\").';\n\nALTER TABLE public.products\n ADD COLUMN IF NOT EXISTS published_at timestamp with time zone;\n\nCOMMENT ON COLUMN public.products.published_at IS\n 'Optional go-live moment. NULL = live as soon as status is active. A future value withholds the product from public reads until it passes (status stays \"active\"; the CMS renders that pair as \"Scheduled\").';\n\n-- Public listing/index queries filter on the (status, published_at) pair together\n-- (catalog, sitemap, page lookups), so a composite index serves them in one pass.\nCREATE INDEX IF NOT EXISTS pages_status_published_at_idx\n ON public.pages (status, published_at);\n\nCREATE INDEX IF NOT EXISTS products_status_published_at_idx\n ON public.products (status, published_at);\n"
96
+ },
97
+ {
98
+ "version": "00000000000016",
99
+ "name": "00000000000016_product_revisions_and_revision_baseline.sql",
100
+ "sql": "-- 00000000000016_product_revisions_and_revision_baseline.sql\n--\n-- Revision History, part 1 of 2 (schema). The application-side rewrite lives in\n-- apps/nextblock/app/cms/revisions/**.\n--\n-- Three things happen here:\n--\n-- 1. products.version — the monotonic counter the hybrid revision engine drives,\n-- mirroring pages.version / posts.version.\n--\n-- 2. product_revisions — a structural mirror of page_revisions / post_revisions.\n-- product_id is uuid (products.id is uuid, not bigint), and\n-- writes are gated on is_admin() to match products_*_policy\n-- rather than the ADMIN|WRITER pattern the page/post revision\n-- tables use. A WRITER who could insert a revision but not\n-- apply a restore would get a silent no-op restore, because\n-- PostgREST returns no error for an UPDATE matching zero rows.\n--\n-- 3. Revision baseline — every page, post and product gets a real `snapshot` row to\n-- restore to. Until now the CMS synthesised a fake \"Initial\n-- Version\" entry in the UI whose Restore button resolved to\n-- \"current metadata + zero blocks\" and wiped the content.\n-- There is now an actual stored baseline instead.\n--\n-- Case A (version = 1, no revisions at all): the live row IS\n-- version 1. This covers seeded content — 00000000000003\n-- inserts every page and post at version 1 and writes no\n-- revision rows — and everything authored since the CMS save\n-- path stopped recording revisions. Snapshotting it at\n-- version 1 is what makes \"restore the original seeded page\"\n-- real for the first time.\n--\n-- Case B (version > 1 but no snapshot at or below it): the\n-- true v1 is unrecoverable and is NOT fabricated. A snapshot\n-- of the current state is stored at the current version so the\n-- diff chain has a valid base and future restores resolve.\n--\n-- Forward-only, idempotent, and it modifies no existing row: every backfill is an\n-- INSERT ... WHERE NOT EXISTS ... ON CONFLICT DO NOTHING.\n\n-- ---------------------------------------------------------------------------\n-- 1. products.version\n-- ---------------------------------------------------------------------------\n\nALTER TABLE public.products\n ADD COLUMN IF NOT EXISTS version integer DEFAULT 1 NOT NULL;\n\nCOMMENT ON COLUMN public.products.version IS 'Monotonic version number for hybrid revisions.';\n\n-- ---------------------------------------------------------------------------\n-- 2. product_revisions\n-- ---------------------------------------------------------------------------\n\nCREATE TABLE IF NOT EXISTS public.product_revisions (\n id bigint NOT NULL,\n product_id uuid NOT NULL,\n author_id uuid,\n version integer NOT NULL,\n revision_type public.revision_type NOT NULL,\n content jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.product_revisions IS 'Hybrid (snapshot/diff) revisions for products.';\nCOMMENT ON COLUMN public.product_revisions.content IS 'If snapshot: full content; if diff: JSON Patch array.';\n\nDO $rb$\nBEGIN\n IF NOT EXISTS (\n SELECT 1 FROM pg_attribute\n WHERE attrelid = 'public.product_revisions'::regclass\n AND attname = 'id'\n AND attidentity <> ''\n ) THEN\n ALTER TABLE public.product_revisions\n ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (\n SEQUENCE NAME public.product_revisions_id_seq\n START WITH 1\n INCREMENT BY 1\n NO MINVALUE\n NO MAXVALUE\n CACHE 1\n );\n END IF;\nEND $rb$;\n\nDO $rb$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint\n WHERE conname = 'product_revisions_pkey'\n AND conrelid = 'public.product_revisions'::regclass) THEN\n ALTER TABLE ONLY public.product_revisions\n ADD CONSTRAINT product_revisions_pkey PRIMARY KEY (id);\n END IF;\nEND $rb$;\n\nDO $rb$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint\n WHERE conname = 'product_revisions_product_version_key'\n AND conrelid = 'public.product_revisions'::regclass) THEN\n ALTER TABLE ONLY public.product_revisions\n ADD CONSTRAINT product_revisions_product_version_key UNIQUE (product_id, version);\n END IF;\nEND $rb$;\n\nDO $rb$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint\n WHERE conname = 'product_revisions_author_id_fkey'\n AND conrelid = 'public.product_revisions'::regclass) THEN\n ALTER TABLE ONLY public.product_revisions\n ADD CONSTRAINT product_revisions_author_id_fkey\n FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;\n END IF;\nEND $rb$;\n\nDO $rb$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint\n WHERE conname = 'product_revisions_product_id_fkey'\n AND conrelid = 'public.product_revisions'::regclass) THEN\n ALTER TABLE ONLY public.product_revisions\n ADD CONSTRAINT product_revisions_product_id_fkey\n FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nCREATE INDEX IF NOT EXISTS idx_product_revisions_author_id\n ON public.product_revisions USING btree (author_id);\n\nCREATE INDEX IF NOT EXISTS idx_product_revisions_product_id_version\n ON public.product_revisions USING btree (product_id, version);\n\nALTER TABLE public.product_revisions ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS product_revisions_read_policy ON public.product_revisions;\nCREATE POLICY product_revisions_read_policy ON public.product_revisions\n FOR SELECT TO authenticated USING (true);\n\nDROP POLICY IF EXISTS product_revisions_insert_policy ON public.product_revisions;\nCREATE POLICY product_revisions_insert_policy ON public.product_revisions\n FOR INSERT TO authenticated\n WITH CHECK (((SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS product_revisions_update_policy ON public.product_revisions;\nCREATE POLICY product_revisions_update_policy ON public.product_revisions\n FOR UPDATE TO authenticated\n USING (((SELECT public.is_admin() AS is_admin) IS TRUE))\n WITH CHECK (((SELECT public.is_admin() AS is_admin) IS TRUE));\n\nDROP POLICY IF EXISTS product_revisions_delete_policy ON public.product_revisions;\nCREATE POLICY product_revisions_delete_policy ON public.product_revisions\n FOR DELETE TO authenticated\n USING (((SELECT public.is_admin() AS is_admin) IS TRUE));\n\nGRANT ALL ON TABLE public.product_revisions TO anon;\nGRANT ALL ON TABLE public.product_revisions TO authenticated;\nGRANT ALL ON TABLE public.product_revisions TO service_role;\nGRANT ALL ON SEQUENCE public.product_revisions_id_seq TO anon;\nGRANT ALL ON SEQUENCE public.product_revisions_id_seq TO authenticated;\nGRANT ALL ON SEQUENCE public.product_revisions_id_seq TO service_role;\n\n-- ---------------------------------------------------------------------------\n-- 3. Revision baseline backfill\n--\n-- The JSON shape must match FullPageContent / FullPostContent / FullProductContent\n-- in apps/nextblock/app/cms/revisions/utils.ts exactly, or the first diff taken\n-- against a baseline row will be full of phantom operations.\n--\n-- Timestamps are rendered with an explicit millisecond-precision UTC format so they\n-- match JavaScript's Date#toISOString() (\"2026-07-03T17:52:15.643Z\"). Postgres'\n-- default jsonb rendering of timestamptz (\"2026-07-03T17:52:15.643901+00:00\") would\n-- differ from the value the application writes and produce a spurious diff on the\n-- very next save.\n-- ---------------------------------------------------------------------------\n\n-- 3a. Pages\nINSERT INTO public.page_revisions (page_id, author_id, version, revision_type, content)\nSELECT\n p.id,\n NULL::uuid,\n p.version,\n 'snapshot'::public.revision_type,\n jsonb_build_object(\n 'meta', jsonb_build_object(\n 'title', p.title,\n 'slug', p.slug,\n 'language_id', p.language_id,\n 'status', p.status,\n 'meta_title', p.meta_title,\n 'meta_description', p.meta_description,\n 'custom_canonical', p.custom_canonical,\n 'published_at', to_char(p.published_at AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'),\n 'feature_image_id', p.feature_image_id\n ),\n 'blocks', COALESCE((\n SELECT jsonb_agg(\n jsonb_build_object(\n 'language_id', b.language_id,\n 'block_type', b.block_type,\n 'content', b.content,\n 'order', b.\"order\"\n ) ORDER BY b.\"order\" ASC, b.id ASC\n )\n FROM public.blocks b\n WHERE b.page_id = p.id\n ), '[]'::jsonb)\n )\n FROM public.pages p\n WHERE NOT EXISTS (\n SELECT 1 FROM public.page_revisions r\n WHERE r.page_id = p.id\n AND r.revision_type = 'snapshot'\n AND r.version <= p.version\n )\nON CONFLICT (page_id, version) DO NOTHING;\n\n-- 3b. Posts\nINSERT INTO public.post_revisions (post_id, author_id, version, revision_type, content)\nSELECT\n po.id,\n NULL::uuid,\n po.version,\n 'snapshot'::public.revision_type,\n jsonb_build_object(\n 'meta', jsonb_build_object(\n 'title', po.title,\n 'slug', po.slug,\n 'language_id', po.language_id,\n 'status', po.status,\n 'meta_title', po.meta_title,\n 'meta_description', po.meta_description,\n 'custom_canonical', po.custom_canonical,\n 'published_at', to_char(po.published_at AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'),\n 'feature_image_id', po.feature_image_id,\n 'label', po.label,\n 'excerpt', po.excerpt,\n 'subtitle', po.subtitle\n ),\n 'blocks', COALESCE((\n SELECT jsonb_agg(\n jsonb_build_object(\n 'language_id', b.language_id,\n 'block_type', b.block_type,\n 'content', b.content,\n 'order', b.\"order\"\n ) ORDER BY b.\"order\" ASC, b.id ASC\n )\n FROM public.blocks b\n WHERE b.post_id = po.id\n ), '[]'::jsonb)\n )\n FROM public.posts po\n WHERE NOT EXISTS (\n SELECT 1 FROM public.post_revisions r\n WHERE r.post_id = po.id\n AND r.revision_type = 'snapshot'\n AND r.version <= po.version\n )\nON CONFLICT (post_id, version) DO NOTHING;\n\n-- 3c. Products.\n--\n-- Content only. price/prices/sale_*/scheduled_*/stock/sku/average_rating/total_reviews\n-- are deliberately excluded from the snapshot: pricing and inventory are mutated from\n-- outside the editor (promotions, Freemius sync, order fulfilment), ratings are derived\n-- aggregates, and inventory_items is keyed by bare SKU text with no FK to products — so\n-- replaying commerce state on restore would reach rows the editor never touched.\n-- Restoring a product restores its content, not its commerce state.\nINSERT INTO public.product_revisions (product_id, author_id, version, revision_type, content)\nSELECT\n pr.id,\n NULL::uuid,\n pr.version,\n 'snapshot'::public.revision_type,\n jsonb_build_object(\n 'meta', jsonb_build_object(\n 'title', pr.title,\n 'slug', pr.slug,\n 'language_id', pr.language_id,\n 'status', pr.status,\n 'meta_title', pr.meta_title,\n 'meta_description', pr.meta_description,\n 'custom_canonical', pr.custom_canonical,\n 'published_at', to_char(pr.published_at AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'),\n 'short_description', pr.short_description,\n 'description_json', pr.description_json\n ),\n 'blocks', COALESCE((\n SELECT jsonb_agg(\n jsonb_build_object(\n 'language_id', b.language_id,\n 'block_type', b.block_type,\n 'content', b.content,\n 'order', b.\"order\"\n ) ORDER BY b.\"order\" ASC, b.id ASC\n )\n FROM public.blocks b\n WHERE b.product_id = pr.id\n ), '[]'::jsonb)\n )\n FROM public.products pr\n WHERE NOT EXISTS (\n SELECT 1 FROM public.product_revisions r\n WHERE r.product_id = pr.id\n AND r.revision_type = 'snapshot'\n AND r.version <= pr.version\n )\nON CONFLICT (product_id, version) DO NOTHING;\n"
101
+ },
102
+ {
103
+ "version": "00000000000017",
104
+ "name": "00000000000017_cortex_ai_mcp_server.sql",
105
+ "sql": "-- Cortex AI MCP (Model Context Protocol) server access.\n--\n-- Adds the bearer-token store that gates /api/mcp, the endpoint that exposes the\n-- Cortex AI tool registry to external MCP clients (Claude Code, Claude Desktop,\n-- Cursor, VS Code). Two pieces:\n--\n-- 1. public.mcp_access_tokens — one row per issued token. We store ONLY the\n-- SHA-256 hash of the token, never the token itself: the plaintext is shown\n-- to the admin exactly once at mint time and is unrecoverable afterwards, so\n-- a database leak cannot be replayed against the MCP endpoint. `token_prefix`\n-- is the non-secret leading fragment kept purely so the UI can tell two tokens\n-- apart in a list.\n--\n-- 2. cortex_ai_mcp_settings — a non-secret JSON site_settings row holding the\n-- server on/off switch and the localhost-trust flag. It is added to all four\n-- site_settings policies so only authenticated ADMINs can read or write it;\n-- the MCP route itself reads it through the service-role client, which\n-- bypasses RLS.\n--\n-- Forward-only. Recreates the four site_settings policies idempotently, preserving\n-- every key already in each policy's sensitive array (note that\n-- language_detection_settings stays anon-READABLE and so is absent from the SELECT\n-- policy, exactly as migration 00000000000012 left it).\n\nCREATE TABLE IF NOT EXISTS public.mcp_access_tokens (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n name text NOT NULL,\n -- Lowercase hex SHA-256 of the plaintext token. Unique so a lookup is a single\n -- indexed equality probe and duplicate mints are impossible.\n token_hash text NOT NULL UNIQUE,\n -- Non-secret display fragment, e.g. \"nbmcp_a1b2c3d4\". Never enough to authenticate.\n token_prefix text NOT NULL,\n -- 'read' grants the read-only tools; 'write' additionally grants the mutating ones.\n scopes text[] NOT NULL DEFAULT ARRAY['read', 'write']::text[],\n created_by uuid REFERENCES auth.users (id) ON DELETE SET NULL,\n created_at timestamptz NOT NULL DEFAULT now(),\n last_used_at timestamptz,\n expires_at timestamptz,\n revoked_at timestamptz\n);\n\nCOMMENT ON TABLE public.mcp_access_tokens IS\n 'Bearer tokens for the Cortex AI MCP server at /api/mcp. Stores SHA-256 hashes only; plaintext is displayed once at mint time.';\n\nCREATE INDEX IF NOT EXISTS mcp_access_tokens_token_hash_idx\n ON public.mcp_access_tokens (token_hash);\n\n-- Orders the admin token list newest-first without a sort.\nCREATE INDEX IF NOT EXISTS mcp_access_tokens_created_at_idx\n ON public.mcp_access_tokens (created_at DESC);\n\nALTER TABLE public.mcp_access_tokens ENABLE ROW LEVEL SECURITY;\n\n-- Tokens are credentials: admin-only, with no anon or WRITER access at all. The\n-- MCP route verifies them with the service-role client, which bypasses RLS.\nDROP POLICY IF EXISTS mcp_access_tokens_admin_select ON public.mcp_access_tokens;\nCREATE POLICY mcp_access_tokens_admin_select ON public.mcp_access_tokens\n FOR SELECT TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);\n\nDROP POLICY IF EXISTS mcp_access_tokens_admin_insert ON public.mcp_access_tokens;\nCREATE POLICY mcp_access_tokens_admin_insert ON public.mcp_access_tokens\n FOR INSERT TO authenticated\n WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);\n\nDROP POLICY IF EXISTS mcp_access_tokens_admin_update ON public.mcp_access_tokens;\nCREATE POLICY mcp_access_tokens_admin_update ON public.mcp_access_tokens\n FOR UPDATE TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role)\n WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);\n\nDROP POLICY IF EXISTS mcp_access_tokens_admin_delete ON public.mcp_access_tokens;\nCREATE POLICY mcp_access_tokens_admin_delete ON public.mcp_access_tokens\n FOR DELETE TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);\n\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.mcp_access_tokens TO authenticated;\nGRANT ALL ON public.mcp_access_tokens TO service_role;\n\n-- Add cortex_ai_mcp_settings to the admin-only site_settings group (all four policies).\nDROP POLICY IF EXISTS site_settings_read_policy ON public.site_settings;\nCREATE POLICY site_settings_read_policy ON public.site_settings FOR SELECT USING (((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT auth.role() AS role) = 'authenticated'::text) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n\nDROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;\nCREATE POLICY site_settings_insert_policy ON public.site_settings FOR INSERT TO authenticated WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n\nDROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;\nCREATE POLICY site_settings_update_policy ON public.site_settings FOR UPDATE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)))) WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n\nDROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;\nCREATE POLICY site_settings_delete_policy ON public.site_settings FOR DELETE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n"
106
+ },
107
+ {
108
+ "version": "00000000000018",
109
+ "name": "00000000000018_site_scripts.sql",
110
+ "sql": "-- Site scripts: admin-authored JavaScript injected into every page of the public site.\n--\n-- Rich-text blocks can already carry an inline <script>, but that script belongs to\n-- one block on one page. This table is for behaviour that spans the site: chat\n-- widgets, third-party embeds, and the scroll/animation helpers that page classes\n-- rely on. Each row gets a name, an on/off switch, and a defined injection point.\n--\n-- NOT the same thing as `site_settings.privacy_settings -> custom_scripts`, which is\n-- a single consent-gated blob for marketing tags and only fires once a visitor\n-- accepts cookies. Rows here are functional site code and run unconditionally, so\n-- anything requiring consent belongs in that setting instead, not this table.\n--\n-- Scripts are emitted with the request's CSP nonce by the root layout, so they run\n-- under the site's existing Content-Security-Policy rather than forcing it open.\n--\n-- Security posture: this is arbitrary JavaScript on every page, so writes are\n-- ADMIN-only (WRITER is deliberately excluded, unlike most content tables) and the\n-- public may read only rows that are switched on, so a half-written draft is never\n-- served to a visitor.\n\nCREATE TABLE IF NOT EXISTS public.site_scripts (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n name text NOT NULL,\n description text,\n -- Raw JavaScript, stored WITHOUT the surrounding <script> tag. The layout adds\n -- the tag so the nonce and attributes are always applied by us, never by the\n -- author. Ignored when `src` is set.\n code text DEFAULT ''::text NOT NULL,\n -- When set, an external script is loaded from this URL and `code` is ignored.\n src text,\n -- Where the tag is emitted. 'head' runs before first paint (blocking, use\n -- sparingly); 'body_end' runs once the markup exists and is the right default\n -- for anything that queries the DOM.\n placement text DEFAULT 'body_end'::text NOT NULL,\n -- Applies to external `src` scripts; inline code ignores it.\n load_strategy text DEFAULT 'default'::text NOT NULL,\n is_active boolean DEFAULT false NOT NULL,\n sort_order integer DEFAULT 0 NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT site_scripts_pkey PRIMARY KEY (id),\n CONSTRAINT site_scripts_placement_check\n CHECK ((placement = ANY (ARRAY['head'::text, 'body_start'::text, 'body_end'::text]))),\n CONSTRAINT site_scripts_load_strategy_check\n CHECK ((load_strategy = ANY (ARRAY['default'::text, 'defer'::text, 'async'::text]))),\n -- An external script must be https so it cannot be downgraded in transit.\n CONSTRAINT site_scripts_src_scheme_check\n CHECK ((src IS NULL OR src ~ '^https://')),\n -- A row has to actually do something: inline code or an external src.\n CONSTRAINT site_scripts_has_payload_check\n CHECK ((src IS NOT NULL OR length(btrim(code)) > 0))\n);\n\nCOMMENT ON TABLE public.site_scripts IS\n 'Admin-authored JavaScript injected into the public site by the root layout, with the request CSP nonce applied. Only is_active rows are publicly readable; only ADMIN may write. Distinct from privacy_settings.custom_scripts, which is consent-gated marketing tags.';\n\nCREATE INDEX IF NOT EXISTS site_scripts_active_placement_sort_idx\n ON public.site_scripts USING btree (is_active, placement, sort_order);\n\nDROP TRIGGER IF EXISTS set_site_scripts_updated_at ON public.site_scripts;\nCREATE TRIGGER set_site_scripts_updated_at\n BEFORE UPDATE ON public.site_scripts\n FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\n\nALTER TABLE public.site_scripts ENABLE ROW LEVEL SECURITY;\n\nGRANT ALL ON TABLE public.site_scripts TO anon;\nGRANT ALL ON TABLE public.site_scripts TO authenticated;\nGRANT ALL ON TABLE public.site_scripts TO service_role;\n\n-- Anonymous visitors need the active scripts to render the page. Inactive rows stay\n-- private so a half-written script is never exposed before it is switched on.\nDROP POLICY IF EXISTS \"Public read active site scripts\" ON public.site_scripts;\nCREATE POLICY \"Public read active site scripts\" ON public.site_scripts\n FOR SELECT TO authenticated, anon USING (is_active);\n\nDROP POLICY IF EXISTS \"Admins read all site scripts\" ON public.site_scripts;\nCREATE POLICY \"Admins read all site scripts\" ON public.site_scripts\n FOR SELECT TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins insert site scripts\" ON public.site_scripts;\nCREATE POLICY \"Admins insert site scripts\" ON public.site_scripts\n FOR INSERT TO authenticated\n WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins update site scripts\" ON public.site_scripts;\nCREATE POLICY \"Admins update site scripts\" ON public.site_scripts\n FOR UPDATE TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role))\n WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins delete site scripts\" ON public.site_scripts;\nCREATE POLICY \"Admins delete site scripts\" ON public.site_scripts\n FOR DELETE TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n"
111
+ },
112
+ {
113
+ "version": "00000000000019",
114
+ "name": "00000000000019_site_script_revisions.sql",
115
+ "sql": "-- Audit trail and undo for site scripts.\n--\n-- `site_scripts` ships arbitrary JavaScript to every visitor, which makes it the\n-- highest-privilege write in the CMS: a bad or malicious snippet can read cookies,\n-- watch checkout forms, or phone home. Content has Revision History for exactly this\n-- reason; code needs it more, not less. Every create/update/delete writes one row\n-- here, and every row is a complete, restorable snapshot — so this table is both the\n-- log (\"who shipped what, when, from where\") and the undo.\n--\n-- APPEND-ONLY BY CONSTRUCTION. There are no UPDATE or DELETE policies, and the\n-- trigger below rejects both even for the service role, which otherwise bypasses\n-- RLS. An audit trail that the compromised credential can rewrite is not an audit\n-- trail. Reverting therefore writes a NEW 'revert' row rather than removing history.\n--\n-- `script_id` and `actor_user_id` are deliberately PLAIN uuids with no foreign keys:\n-- an FK with ON DELETE SET NULL would have to UPDATE this table when a script or a\n-- profile is deleted, which the append-only trigger forbids. `script_name` is\n-- denormalised so a deleted script is still identifiable in the log.\n\nCREATE TABLE IF NOT EXISTS public.site_script_revisions (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n script_id uuid,\n script_name text NOT NULL,\n revision_type text NOT NULL,\n -- Null when the actor could not be resolved (e.g. a localhost dev connection).\n actor_user_id uuid,\n -- Which surface made the change, so an unexpected edit can be traced back to\n -- the dashboard or to an MCP token.\n source text DEFAULT 'cms'::text NOT NULL,\n summary text,\n -- Full restorable state of the script at this revision. For 'delete' it is the\n -- state immediately BEFORE removal, so restoring it brings the script back.\n snapshot jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT site_script_revisions_pkey PRIMARY KEY (id),\n CONSTRAINT site_script_revisions_type_check\n CHECK ((revision_type = ANY (ARRAY['create'::text, 'update'::text, 'delete'::text, 'revert'::text]))),\n CONSTRAINT site_script_revisions_source_check\n CHECK ((source = ANY (ARRAY['cms'::text, 'mcp'::text]))),\n CONSTRAINT site_script_revisions_snapshot_is_object_check\n CHECK ((jsonb_typeof(snapshot) = 'object'))\n);\n\nCOMMENT ON TABLE public.site_script_revisions IS\n 'Append-only audit log and undo history for site_scripts. Each row is a restorable snapshot. UPDATE and DELETE are blocked by trigger, including for the service role.';\n\nCREATE INDEX IF NOT EXISTS site_script_revisions_script_created_idx\n ON public.site_script_revisions USING btree (script_id, created_at DESC);\n\nCREATE INDEX IF NOT EXISTS site_script_revisions_created_idx\n ON public.site_script_revisions USING btree (created_at DESC);\n\n-- Enforced in the database rather than the application so it holds for every\n-- caller, including the service-role client the MCP server uses.\nCREATE OR REPLACE FUNCTION public.prevent_site_script_revision_rewrite() RETURNS trigger\n LANGUAGE plpgsql\n SET search_path = ''\n AS $$\nBEGIN\n RAISE EXCEPTION 'site_script_revisions is append-only; % is not permitted', TG_OP\n USING ERRCODE = 'restrict_violation';\nEND;\n$$;\n\nDROP TRIGGER IF EXISTS trg_site_script_revisions_append_only ON public.site_script_revisions;\nCREATE TRIGGER trg_site_script_revisions_append_only\n BEFORE UPDATE OR DELETE ON public.site_script_revisions\n FOR EACH ROW EXECUTE FUNCTION public.prevent_site_script_revision_rewrite();\n\nALTER TABLE public.site_script_revisions ENABLE ROW LEVEL SECURITY;\n\nGRANT SELECT, INSERT ON TABLE public.site_script_revisions TO authenticated;\nGRANT ALL ON TABLE public.site_script_revisions TO service_role;\n\n-- Read is ADMIN-only: snapshots contain the full source of scripts that may not be\n-- active yet, and the log itself reveals operational history.\nDROP POLICY IF EXISTS \"Admins read site script revisions\" ON public.site_script_revisions;\nCREATE POLICY \"Admins read site script revisions\" ON public.site_script_revisions\n FOR SELECT TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins insert site script revisions\" ON public.site_script_revisions;\nCREATE POLICY \"Admins insert site script revisions\" ON public.site_script_revisions\n FOR INSERT TO authenticated\n WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n"
116
+ },
117
+ {
118
+ "version": "00000000000020",
119
+ "name": "00000000000020_updating_article.sql",
120
+ "sql": "-- 00000000000020_updating_article.sql\n-- Seeds the \"How Updating NextBlock Works\" guide as an EN/FR twin pair, the companion\n-- to the install guide seeded in the baseline (00000000000003, slugs 'how-to-setup-nextblock'\n-- / 'comment-configurer-nextblock'). It documents the single `npm run update` command\n-- across all four install paths — one-click Vercel, npm create → Docker, npm create →\n-- managed cloud, and the cloned monorepo.\n--\n-- Forward-only and idempotent by construction:\n-- * posts carries UNIQUE (language_id, slug), so the inserts use ON CONFLICT DO NOTHING;\n-- * blocks has no natural unique key, so the body insert is guarded on NOT EXISTS;\n-- * ids are never hardcoded — posts.id and blocks.id are identity columns and a live\n-- database has real editor-created rows occupying the low ids the baseline used.\n-- * the feature image is looked up rather than asserted, so a site that deleted the\n-- seeded media row still gets the article (with no cover) instead of a failed migration.\n--\n-- Because the sandbox reset payload and the /setup wizard's embedded bundle are both\n-- generated FROM this directory, the article reaches a fresh install and every hourly\n-- sandbox reset with no extra wiring — regenerate them with `npm run generate:sandbox`\n-- and `npm run generate:migrations-bundle`.\n--\n-- blocks.content is JSONB written with PostgreSQL dollar-quoting (same style as 006/008)\n-- so the HTML can use ordinary single-quoted class attributes without quote doubling.\n\nDO $body$\nDECLARE\n v_group uuid := 'c0d3f1a2-8b47-4e19-9a52-7f6b1d4e8c30';\n v_image uuid;\n v_en_post integer;\n v_fr_post integer;\nBEGIN\n SELECT id INTO v_image\n FROM public.media\n WHERE id = '641ddf75-5c90-41df-8b83-e7c298f30a6a'::uuid;\n\n ---------------------------------------------------------------------------\n -- English\n ---------------------------------------------------------------------------\n INSERT INTO public.posts (\n language_id, author_id, title, slug, label, excerpt, subtitle, status,\n published_at, meta_title, meta_description, feature_image_id, version,\n translation_group_id\n )\n SELECT\n 1, NULL,\n 'How Updating NextBlock Works: One Command for Every Install',\n 'how-updating-works',\n 'Maintenance',\n 'However you installed NextBlock — one-click Vercel, the CLI, Docker or a git clone — a single npm run update brings the code, the dependencies and the database schema forward together.',\n 'Automatic upstream syncing on Vercel, and one command everywhere else: what npm run update does, what it never touches, and how to roll it back.',\n 'published', now(),\n 'How to Update NextBlock — One Command for Every Install',\n 'Update NextBlock in one step. npm run update pulls new code, installs dependencies and applies pending database migrations on Vercel, Docker, CLI and git-clone installs alike.',\n v_image, 1, v_group\n WHERE NOT EXISTS (\n SELECT 1 FROM public.posts WHERE language_id = 1 AND slug = 'how-updating-works'\n );\n\n SELECT id INTO v_en_post\n FROM public.posts\n WHERE language_id = 1 AND slug = 'how-updating-works'\n ORDER BY id\n LIMIT 1;\n\n IF v_en_post IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM public.blocks WHERE post_id = v_en_post\n ) THEN\n INSERT INTO public.blocks (post_id, language_id, block_type, content, \"order\")\n VALUES (v_en_post, 1, 'text', $nben$\n{\"html_content\":\"<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock ships improvements continuously — new blocks, editor fixes, security patches, and occasionally a database change that the new code depends on. Keeping up with all of that used to mean knowing which of the four install paths you were on. It no longer does. Every NextBlock project, however it was created, understands one command:</p>\\n\\n<div class='my-10 overflow-hidden rounded-[2rem] border border-slate-800 bg-slate-950 shadow-2xl'>\\n <div class='flex items-center gap-2 border-b border-white/10 px-5 py-3'>\\n <span class='h-3 w-3 rounded-full bg-red-400/70'></span>\\n <span class='h-3 w-3 rounded-full bg-yellow-400/70'></span>\\n <span class='h-3 w-3 rounded-full bg-green-400/70'></span>\\n <span class='ml-3 text-xs font-mono text-slate-400'>your project</span>\\n </div>\\n <div class='px-6 py-8 text-center'>\\n <p class='mt-0 mb-2 font-mono text-2xl sm:text-3xl font-semibold text-emerald-300'>npm run update</p>\\n <p class='mb-0 text-sm text-slate-400'>Code &middot; dependencies &middot; database schema &mdash; in that order, in one step.</p>\\n </div>\\n</div>\\n\\n<p>It figures out which kind of install it is running inside, picks the right source for new code, installs the matching dependencies, and then applies any database migrations the new version needs. If you would rather look before you leap, <code>npm run update -- --check</code> reports exactly what would change and touches nothing.</p>\\n\\n<h2 id='the-four-paths'>The four install paths, and how each one gets updates</h2>\\n<p class='text-slate-600 dark:text-slate-400'>These map one-to-one onto the four options in <a href='/article/how-to-setup-nextblock'>the install guide</a>. The command is the same everywhere; what differs is where the new code comes from.</p>\\n\\n<div class='grid gap-5 md:grid-cols-2 my-8'>\\n <a href='#vercel' class='block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 text-sm font-bold text-white'>1</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Fully automatic</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>One-click Vercel &amp; GitHub forks</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>A daily workflow merges upstream into your repository and Vercel redeploys. You do nothing.</p>\\n </a>\\n <a href='#docker' class='block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-amber-500 text-sm font-bold text-white'>2</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>One command</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Docker</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Update, then rebuild the local stack. Your Postgres and media volumes survive untouched.</p>\\n </a>\\n <a href='#cloud' class='block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-violet-600 text-sm font-bold text-white'>3</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>One command</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Supabase</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>New framework files come from npm; your own pages, routes and content are left alone.</p>\\n </a>\\n <a href='#clone' class='block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-emerald-600 text-sm font-bold text-white'>4</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>One command</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>git clone the monorepo</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>A guarded git pull or upstream merge, then dependencies and migrations. No manual steps.</p>\\n </a>\\n</div>\\n\\n<h2 id='vercel'>1. One-click Vercel and GitHub forks &mdash; hands-off</h2>\\n<p>This path updates itself. When you deployed, NextBlock created a repository you own; the dashboard&rsquo;s <strong>Connect GitHub</strong> onboarding step installs a workflow into it that runs <strong>every day at midnight UTC</strong> and can also be triggered by hand from your repository&rsquo;s <strong>Actions</strong> tab.</p>\\n<ol class='space-y-2'>\\n <li>The workflow merges the latest upstream NextBlock into your deploy branch.</li>\\n <li>A clean merge is pushed to your branch, which triggers an ordinary Vercel deployment.</li>\\n <li>During that production build, NextBlock applies any pending database migrations <em>before</em> the app is built &mdash; so new code never runs against an old schema.</li>\\n <li>If the merge conflicts, nothing is pushed. The workflow opens a GitHub issue instead, and your CMS dashboard shows an amber banner linking straight to it. Resolve it, close the issue, and the banner clears itself.</li>\\n</ol>\\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Make the repository public</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>A public repository is completely zero-config. On a private one, add a <code>NEXTBLOCK_GITHUB_TOKEN</code> environment variable with read access to issues so the conflict banner still works &mdash; and note that Vercel&rsquo;s free Hobby plan refuses to auto-deploy automated commits on private repositories, so the merge would land without deploying.</p>\\n</div>\\n<p>Working on a local clone of that fork? <code>npm run update</code> does the same merge on your machine, adding an <code>upstream</code> remote if it is missing, then installs dependencies and applies migrations.</p>\\n\\n<h2 id='docker'>2. npm create nextblock &rarr; Docker &mdash; update, then rebuild</h2>\\n<p>From your project directory:</p>\\n<pre><code>npm run update\\nnpm run docker:up</code></pre>\\n<p>The first command refreshes the application, its dependencies and the schema; the second rebuilds and restarts the containers. Your database and media live in Docker volumes and are never touched by either step &mdash; <code>docker:up</code> rebuilds images, not data.</p>\\n\\n<h2 id='cloud'>3. npm create nextblock &rarr; managed Supabase &mdash; one command</h2>\\n<pre><code>npm run update\\nnpm run build\\nnpm start</code></pre>\\n<p>Your project is a standalone Next.js app, so new framework code is fetched from the published <code>create-nextblock</code> package on npm &mdash; the exact artifact your project was scaffolded from, versioned in lockstep with the release. NextBlock refreshes the files it owns, merges the new dependency versions into your <code>package.json</code>, runs <code>npm install</code>, and then applies migrations.</p>\\n<div class='rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Deploying to Vercel from this project</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Run <code>npm run update</code> locally, commit the result, and push. Your production build applies any pending migrations on the way up, exactly as it does for one-click installs.</p>\\n</div>\\n\\n<h2 id='clone'>4. The cloned monorepo &mdash; one command</h2>\\n<pre><code>npm run update</code></pre>\\n<p>In a clone of the NextBlock repository this fast-forwards your checkout, reinstalls workspace dependencies and applies pending migrations. It refuses to run over uncommitted changes and tells you how to stash them first, so an update can never silently eat work in progress. If you have local commits, it stops and points you at <code>git pull --rebase</code> rather than guessing.</p>\\n\\n<h2 id='what-it-does'>What <code>npm run update</code> actually does</h2>\\n<ol class='space-y-2'>\\n <li><strong>Identifies the install.</strong> Monorepo or standalone app; git-backed or npm-backed; Docker or not.</li>\\n <li><strong>Updates the code</strong> from the right source &mdash; an upstream git merge, a fast-forward pull, or the published <code>create-nextblock</code> package.</li>\\n <li><strong>Installs dependencies</strong> with <code>npm install</code>, so the code and the packages it imports move together.</li>\\n <li><strong>Refreshes the migration files</strong> shipped inside <code>@nextblock-cms/db</code>, so the newest schema changes are on disk before anything is applied.</li>\\n <li><strong>Applies pending migrations</strong>, listing them first and asking before it writes.</li>\\n <li><strong>Clears the dashboard&rsquo;s update banner</strong> once the new version is really in place.</li>\\n</ol>\\n\\n<h3>Options</h3>\\n<div class='overflow-x-auto my-6'>\\n<table class='w-full text-left text-sm'>\\n <thead><tr class='border-b border-slate-200 dark:border-white/10'><th class='py-3 pr-4 font-semibold'>Command</th><th class='py-3 font-semibold'>What it does</th></tr></thead>\\n <tbody class='align-top'>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update</code></td><td class='py-3'>Code, dependencies and schema.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --check</code></td><td class='py-3'>Report what would change. Writes nothing.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --yes</code></td><td class='py-3'>Skip the confirmation prompts. Useful in CI.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --db-only</code></td><td class='py-3'>Apply pending migrations and nothing else.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --skip-db</code></td><td class='py-3'>Update code and dependencies, leave the database alone.</td></tr>\\n <tr><td class='py-3 pr-4'><code>npm run update -- --force</code></td><td class='py-3'>Run even when you are already on the latest version.</td></tr>\\n </tbody>\\n</table>\\n</div>\\n\\n<h2 id='database'>What happens to your database</h2>\\n<p>Schema changes are <strong>forward-only</strong>. NextBlock never rewrites or replays a migration that has already run: each one is applied and recorded in the same transaction, so a failure rolls back cleanly and leaves the database exactly as it was. Already-applied migrations are skipped by version, which makes re-running an update completely safe.</p>\\n<p>Migrations change <em>structure</em> &mdash; tables, columns, indexes, permissions. Your pages, posts, products, media and users are yours; the update never deletes or rewrites them.</p>\\n<div class='rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Belt and braces</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Before a big jump on a production site, take a database snapshot &mdash; Supabase does daily backups on paid plans, and you can trigger one on demand from the Supabase dashboard. Then run <code>npm run update -- --check</code> to see the pending list before you commit to it.</p>\\n</div>\\n\\n<h2 id='safety'>If something goes wrong</h2>\\n<ul class='space-y-2'>\\n <li><strong>Standalone projects:</strong> every framework file the update replaces is copied first into a timestamped folder under <code>.nextblock-backup/</code> in your project. Nothing is deleted, so files you added yourself are never removed.</li>\\n <li><strong>Git-backed installs:</strong> the update is an ordinary commit. <code>git log</code> shows it and <code>git revert</code> undoes it.</li>\\n <li><strong>A conflicted merge</strong> is aborted automatically &mdash; your working tree is left exactly as it was, with instructions printed for resolving it by hand.</li>\\n <li><strong>A failed migration</strong> rolls back. Fix the cause and re-run; nothing half-applied is left behind.</li>\\n</ul>\\n<p>If you have customised a file that NextBlock owns &mdash; something under <code>app/</code>, <code>components/</code> or <code>lib/</code> &mdash; the update will replace it and back up your version. Diff the backup afterwards to bring your change forward. Customisations that live in your own new files, in the CMS, or in <code>.env</code> are never affected.</p>\\n\\n<h2 id='knowing'>Knowing when there is something to update</h2>\\n<p>You do not have to poll. NextBlock checks in the background while you use the CMS and raises a dashboard banner when a newer version is published, telling you which version you are on and what is available. Administrators can also just run <code>npm run update -- --check</code> at any time.</p>\\n\\n<h2 id='faq'>Update FAQ</h2>\\n<h3>Will updating overwrite my content or settings?</h3>\\n<p>No. Content, media, users and settings live in your database; site configuration lives in your environment variables. The update touches application code, dependencies and schema structure only.</p>\\n<h3>Do I have to update every release?</h3>\\n<p>No, though staying close to the latest release keeps you on security fixes and makes each jump smaller. Updates apply in sequence, so skipping several versions still lands correctly.</p>\\n<h3>Can I run it in CI?</h3>\\n<p>Yes &mdash; <code>npm run update -- --yes</code> never prompts, and it exits non-zero if the schema step fails so a pipeline can catch it.</p>\\n<h3>What if my project has no database connection configured?</h3>\\n<p>Code and dependencies still update; the schema step is skipped with a warning telling you which environment variable to set. Re-run <code>npm run update -- --db-only</code> once it is configured.</p>\\n<h3>I am on the one-click Vercel deploy &mdash; do I need to run anything?</h3>\\n<p>No. That path is fully automatic. The command exists for when you want an update <em>now</em> rather than at midnight, or when you are working on a local clone.</p>\\n\\n<div class='rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5'>\\n <p class='mt-0 text-2xl font-semibold text-slate-900 dark:text-white'>One command, every install.</p>\\n <p class='text-sm text-slate-600 dark:text-slate-300'>New to NextBlock? Start with the install guide &mdash; then never think about upgrades again.</p>\\n <div class='mt-5 flex flex-wrap justify-center gap-3'>\\n <a href='/article/how-to-setup-nextblock' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Read the install guide</a>\\n <a href='https://github.com/nextblock-cms/nextblock' target='_blank' rel='noopener' class='inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50'>View on GitHub</a>\\n </div>\\n</div>\"}\n$nben$::jsonb, 0);\n END IF;\n\n ---------------------------------------------------------------------------\n -- French\n ---------------------------------------------------------------------------\n INSERT INTO public.posts (\n language_id, author_id, title, slug, label, excerpt, subtitle, status,\n published_at, meta_title, meta_description, feature_image_id, version,\n translation_group_id\n )\n SELECT\n 2, NULL,\n 'Les mises à jour de NextBlock : une seule commande, quelle que soit l''installation',\n 'comment-fonctionnent-les-mises-a-jour',\n 'Maintenance',\n 'Quelle que soit votre installation — Vercel en un clic, le CLI, Docker ou un git clone — une seule commande npm run update fait avancer ensemble le code, les dépendances et le schéma de base de données.',\n 'Synchronisation automatique sur Vercel, et une seule commande partout ailleurs : ce que fait npm run update, ce qu''il ne touche jamais, et comment revenir en arrière.',\n 'published', now(),\n 'Mettre à jour NextBlock — une commande pour toutes les installations',\n 'Mettez NextBlock à jour en une étape. npm run update récupère le nouveau code, installe les dépendances et applique les migrations en attente, sur Vercel, Docker, CLI et git clone.',\n v_image, 1, v_group\n WHERE NOT EXISTS (\n SELECT 1 FROM public.posts\n WHERE language_id = 2 AND slug = 'comment-fonctionnent-les-mises-a-jour'\n );\n\n SELECT id INTO v_fr_post\n FROM public.posts\n WHERE language_id = 2 AND slug = 'comment-fonctionnent-les-mises-a-jour'\n ORDER BY id\n LIMIT 1;\n\n IF v_fr_post IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM public.blocks WHERE post_id = v_fr_post\n ) THEN\n INSERT INTO public.blocks (post_id, language_id, block_type, content, \"order\")\n VALUES (v_fr_post, 2, 'text', $nbfr$\n{\"html_content\":\"<p class='text-lg leading-8 text-slate-700 dark:text-slate-300'>NextBlock &eacute;volue en continu &mdash; nouveaux blocs, corrections de l'&eacute;diteur, correctifs de s&eacute;curit&eacute;, et parfois une modification de la base de donn&eacute;es dont le nouveau code d&eacute;pend. Suivre tout cela supposait autrefois de savoir laquelle des quatre m&eacute;thodes d'installation vous aviez utilis&eacute;e. Ce n'est plus le cas. Tout projet NextBlock, quelle que soit sa cr&eacute;ation, comprend une seule commande :</p>\\n\\n<div class='my-10 overflow-hidden rounded-[2rem] border border-slate-800 bg-slate-950 shadow-2xl'>\\n <div class='flex items-center gap-2 border-b border-white/10 px-5 py-3'>\\n <span class='h-3 w-3 rounded-full bg-red-400/70'></span>\\n <span class='h-3 w-3 rounded-full bg-yellow-400/70'></span>\\n <span class='h-3 w-3 rounded-full bg-green-400/70'></span>\\n <span class='ml-3 text-xs font-mono text-slate-400'>votre projet</span>\\n </div>\\n <div class='px-6 py-8 text-center'>\\n <p class='mt-0 mb-2 font-mono text-2xl sm:text-3xl font-semibold text-emerald-300'>npm run update</p>\\n <p class='mb-0 text-sm text-slate-400'>Code &middot; d&eacute;pendances &middot; sch&eacute;ma de base de donn&eacute;es &mdash; dans cet ordre, en une seule &eacute;tape.</p>\\n </div>\\n</div>\\n\\n<p>La commande d&eacute;termine dans quel type d'installation elle s'ex&eacute;cute, choisit la bonne source pour le nouveau code, installe les d&eacute;pendances correspondantes, puis applique les migrations dont la nouvelle version a besoin. Si vous pr&eacute;f&eacute;rez regarder avant de sauter, <code>npm run update -- --check</code> indique exactement ce qui changerait sans rien modifier.</p>\\n\\n<h2 id='the-four-paths'>Les quatre installations et leurs mises &agrave; jour</h2>\\n<p class='text-slate-600 dark:text-slate-400'>Elles correspondent une &agrave; une aux quatre options du <a href='/article/comment-configurer-nextblock'>guide d'installation</a>. La commande est la m&ecirc;me partout ; seule la provenance du nouveau code change.</p>\\n\\n<div class='grid gap-5 md:grid-cols-2 my-8'>\\n <a href='#vercel' class='block rounded-[1.75rem] border border-blue-200 bg-blue-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 text-sm font-bold text-white'>1</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Enti&egrave;rement automatique</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>Vercel en un clic et forks GitHub</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Un workflow quotidien fusionne les nouveaut&eacute;s dans votre d&eacute;p&ocirc;t et Vercel red&eacute;ploie. Vous n'avez rien &agrave; faire.</p>\\n </a>\\n <a href='#docker' class='block rounded-[1.75rem] border border-amber-200 bg-amber-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-amber-500/20 dark:bg-amber-500/10'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-amber-500 text-sm font-bold text-white'>2</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-amber-700 dark:text-amber-200'>Une commande</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Docker</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Mettez &agrave; jour, puis reconstruisez la pile locale. Vos volumes Postgres et m&eacute;dias restent intacts.</p>\\n </a>\\n <a href='#cloud' class='block rounded-[1.75rem] border border-violet-200 bg-violet-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-violet-600 text-sm font-bold text-white'>3</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Une commande</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>npm create nextblock &rarr; Supabase</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Les nouveaux fichiers viennent de npm ; vos pages, routes et contenus ne sont pas touch&eacute;s.</p>\\n </a>\\n <a href='#clone' class='block rounded-[1.75rem] border border-emerald-200 bg-emerald-50/70 p-6 no-underline transition-shadow hover:shadow-lg dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <span class='flex h-9 w-9 items-center justify-center rounded-full bg-emerald-600 text-sm font-bold text-white'>4</span>\\n <p class='mt-4 mb-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Une commande</p>\\n <h3 class='mt-2 mb-2 text-xl font-semibold text-slate-900 dark:text-white'>git clone du monorepo</h3>\\n <p class='mb-0 text-sm leading-6 text-slate-600 dark:text-slate-300'>Un pull ou une fusion prot&eacute;g&eacute;e, puis les d&eacute;pendances et les migrations. Aucune &eacute;tape manuelle.</p>\\n </a>\\n</div>\\n\\n<h2 id='vercel'>1. Vercel en un clic et forks GitHub &mdash; sans intervention</h2>\\n<p>Ce chemin se met &agrave; jour tout seul. Lors du d&eacute;ploiement, NextBlock a cr&eacute;&eacute; un d&eacute;p&ocirc;t qui vous appartient ; l'&eacute;tape <strong>Connect GitHub</strong> du tableau de bord y installe un workflow qui s'ex&eacute;cute <strong>chaque jour &agrave; minuit UTC</strong> et peut aussi &ecirc;tre lanc&eacute; &agrave; la demande depuis l'onglet <strong>Actions</strong> de votre d&eacute;p&ocirc;t.</p>\\n<ol class='space-y-2'>\\n <li>Le workflow fusionne la derni&egrave;re version de NextBlock dans votre branche de d&eacute;ploiement.</li>\\n <li>Une fusion propre est pouss&eacute;e sur votre branche, ce qui d&eacute;clenche un d&eacute;ploiement Vercel normal.</li>\\n <li>Pendant ce build de production, NextBlock applique les migrations en attente <em>avant</em> de construire l'application &mdash; le nouveau code ne tourne donc jamais sur un ancien sch&eacute;ma.</li>\\n <li>En cas de conflit, rien n'est pouss&eacute;. Le workflow ouvre une issue GitHub et votre tableau de bord affiche une banni&egrave;re ambre qui pointe dessus. R&eacute;solvez, fermez l'issue, et la banni&egrave;re dispara&icirc;t d'elle-m&ecirc;me.</li>\\n</ol>\\n<div class='rounded-3xl border border-emerald-200 bg-emerald-50/80 p-6 my-8 dark:border-emerald-500/20 dark:bg-emerald-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700 dark:text-emerald-200'>Rendez le d&eacute;p&ocirc;t public</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Un d&eacute;p&ocirc;t public ne demande aucune configuration. Sur un d&eacute;p&ocirc;t priv&eacute;, ajoutez une variable d'environnement <code>NEXTBLOCK_GITHUB_TOKEN</code> avec un acc&egrave;s en lecture aux issues pour que la banni&egrave;re de conflit fonctionne &mdash; et sachez que l'offre gratuite Hobby de Vercel refuse de d&eacute;ployer automatiquement les commits automatis&eacute;s sur un d&eacute;p&ocirc;t priv&eacute;.</p>\\n</div>\\n<p>Vous travaillez sur un clone local de ce fork ? <code>npm run update</code> effectue la m&ecirc;me fusion sur votre machine, en ajoutant le d&eacute;p&ocirc;t <code>upstream</code> s'il manque, puis installe les d&eacute;pendances et applique les migrations.</p>\\n\\n<h2 id='docker'>2. npm create nextblock &rarr; Docker &mdash; mettre &agrave; jour puis reconstruire</h2>\\n<p>Depuis le dossier de votre projet :</p>\\n<pre><code>npm run update\\nnpm run docker:up</code></pre>\\n<p>La premi&egrave;re commande met &agrave; jour l'application, ses d&eacute;pendances et le sch&eacute;ma ; la seconde reconstruit et red&eacute;marre les conteneurs. Votre base de donn&eacute;es et vos m&eacute;dias vivent dans des volumes Docker et ne sont touch&eacute;s ni par l'une ni par l'autre &mdash; <code>docker:up</code> reconstruit des images, pas des donn&eacute;es.</p>\\n\\n<h2 id='cloud'>3. npm create nextblock &rarr; Supabase g&eacute;r&eacute; &mdash; une commande</h2>\\n<pre><code>npm run update\\nnpm run build\\nnpm start</code></pre>\\n<p>Votre projet est une application Next.js autonome : le nouveau code provient donc du paquet <code>create-nextblock</code> publi&eacute; sur npm &mdash; exactement l'artefact &agrave; partir duquel votre projet a &eacute;t&eacute; g&eacute;n&eacute;r&eacute;, versionn&eacute; en phase avec la release. NextBlock rafra&icirc;chit les fichiers qui lui appartiennent, fusionne les nouvelles versions de d&eacute;pendances dans votre <code>package.json</code>, lance <code>npm install</code>, puis applique les migrations.</p>\\n<div class='rounded-3xl border border-violet-200 bg-violet-50/80 p-6 my-8 dark:border-violet-500/20 dark:bg-violet-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-violet-700 dark:text-violet-200'>Vous d&eacute;ployez ce projet sur Vercel ?</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Lancez <code>npm run update</code> en local, validez le r&eacute;sultat et poussez. Votre build de production applique les migrations en attente au passage, exactement comme pour les installations en un clic.</p>\\n</div>\\n\\n<h2 id='clone'>4. Le monorepo clon&eacute; &mdash; une commande</h2>\\n<pre><code>npm run update</code></pre>\\n<p>Dans un clone du d&eacute;p&ocirc;t NextBlock, la commande met votre copie &agrave; jour, r&eacute;installe les d&eacute;pendances du workspace et applique les migrations en attente. Elle refuse de s'ex&eacute;cuter par-dessus des modifications non valid&eacute;es et vous explique comment les mettre de c&ocirc;t&eacute; : une mise &agrave; jour ne peut donc jamais faire dispara&icirc;tre du travail en cours. Si vous avez des commits locaux, elle s'arr&ecirc;te et vous oriente vers <code>git pull --rebase</code> plut&ocirc;t que de deviner.</p>\\n\\n<h2 id='what-it-does'>Ce que fait r&eacute;ellement <code>npm run update</code></h2>\\n<ol class='space-y-2'>\\n <li><strong>Identifie l'installation.</strong> Monorepo ou application autonome ; bas&eacute;e sur git ou sur npm ; Docker ou non.</li>\\n <li><strong>Met &agrave; jour le code</strong> depuis la bonne source &mdash; fusion git, pull en avance rapide, ou le paquet <code>create-nextblock</code> publi&eacute;.</li>\\n <li><strong>Installe les d&eacute;pendances</strong> avec <code>npm install</code>, pour que le code et les paquets qu'il importe avancent ensemble.</li>\\n <li><strong>Rafra&icirc;chit les fichiers de migration</strong> livr&eacute;s dans <code>@nextblock-cms/db</code>, afin que les derni&egrave;res &eacute;volutions du sch&eacute;ma soient sur le disque avant toute application.</li>\\n <li><strong>Applique les migrations en attente</strong>, en les listant d'abord et en demandant confirmation.</li>\\n <li><strong>Efface la banni&egrave;re de mise &agrave; jour</strong> du tableau de bord une fois la nouvelle version r&eacute;ellement en place.</li>\\n</ol>\\n\\n<h3>Options</h3>\\n<div class='overflow-x-auto my-6'>\\n<table class='w-full text-left text-sm'>\\n <thead><tr class='border-b border-slate-200 dark:border-white/10'><th class='py-3 pr-4 font-semibold'>Commande</th><th class='py-3 font-semibold'>Effet</th></tr></thead>\\n <tbody class='align-top'>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update</code></td><td class='py-3'>Code, d&eacute;pendances et sch&eacute;ma.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --check</code></td><td class='py-3'>Indique ce qui changerait. N'&eacute;crit rien.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --yes</code></td><td class='py-3'>Sans confirmation. Pratique en CI.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --db-only</code></td><td class='py-3'>Applique uniquement les migrations en attente.</td></tr>\\n <tr class='border-b border-slate-100 dark:border-white/5'><td class='py-3 pr-4'><code>npm run update -- --skip-db</code></td><td class='py-3'>Met &agrave; jour le code et les d&eacute;pendances, sans toucher &agrave; la base.</td></tr>\\n <tr><td class='py-3 pr-4'><code>npm run update -- --force</code></td><td class='py-3'>S'ex&eacute;cute m&ecirc;me si vous &ecirc;tes d&eacute;j&agrave; &agrave; jour.</td></tr>\\n </tbody>\\n</table>\\n</div>\\n\\n<h2 id='database'>Ce qui arrive &agrave; votre base de donn&eacute;es</h2>\\n<p>Les &eacute;volutions du sch&eacute;ma sont <strong>uniquement additives</strong>. NextBlock ne r&eacute;&eacute;crit ni ne rejoue jamais une migration d&eacute;j&agrave; appliqu&eacute;e : chacune est appliqu&eacute;e et enregistr&eacute;e dans la m&ecirc;me transaction, si bien qu'un &eacute;chec est annul&eacute; proprement et laisse la base exactement dans son &eacute;tat initial. Les migrations d&eacute;j&agrave; appliqu&eacute;es sont ignor&eacute;es par num&eacute;ro de version, ce qui rend une nouvelle ex&eacute;cution totalement s&ucirc;re.</p>\\n<p>Les migrations modifient la <em>structure</em> &mdash; tables, colonnes, index, permissions. Vos pages, articles, produits, m&eacute;dias et utilisateurs vous appartiennent : la mise &agrave; jour ne les supprime ni ne les r&eacute;&eacute;crit.</p>\\n<div class='rounded-3xl border border-blue-200 bg-blue-50/80 p-6 my-8 dark:border-blue-500/20 dark:bg-blue-500/10'>\\n <p class='mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-blue-700 dark:text-blue-200'>Par pr&eacute;caution</p>\\n <p class='mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200'>Avant un grand saut sur un site en production, prenez une sauvegarde de la base &mdash; Supabase en r&eacute;alise quotidiennement sur les offres payantes, et vous pouvez en d&eacute;clencher une &agrave; la demande depuis son tableau de bord. Lancez ensuite <code>npm run update -- --check</code> pour voir la liste des migrations en attente.</p>\\n</div>\\n\\n<h2 id='safety'>En cas de probl&egrave;me</h2>\\n<ul class='space-y-2'>\\n <li><strong>Projets autonomes :</strong> chaque fichier remplac&eacute; est d'abord copi&eacute; dans un dossier horodat&eacute; sous <code>.nextblock-backup/</code> dans votre projet. Rien n'est supprim&eacute; : les fichiers que vous avez ajout&eacute;s ne disparaissent jamais.</li>\\n <li><strong>Installations bas&eacute;es sur git :</strong> la mise &agrave; jour est un commit ordinaire. <code>git log</code> l'affiche et <code>git revert</code> l'annule.</li>\\n <li><strong>Une fusion en conflit</strong> est annul&eacute;e automatiquement &mdash; votre copie de travail reste intacte, avec les instructions pour r&eacute;soudre &agrave; la main.</li>\\n <li><strong>Une migration en &eacute;chec</strong> est annul&eacute;e. Corrigez la cause et relancez : rien ne reste &agrave; moiti&eacute; appliqu&eacute;.</li>\\n</ul>\\n<p>Si vous avez personnalis&eacute; un fichier appartenant &agrave; NextBlock &mdash; sous <code>app/</code>, <code>components/</code> ou <code>lib/</code> &mdash; la mise &agrave; jour le remplacera en sauvegardant votre version. Comparez ensuite la sauvegarde pour reporter votre modification. Les personnalisations qui vivent dans vos propres fichiers, dans le CMS ou dans <code>.env</code> ne sont jamais affect&eacute;es.</p>\\n\\n<h2 id='knowing'>Savoir qu'une mise &agrave; jour est disponible</h2>\\n<p>Inutile de surveiller. NextBlock v&eacute;rifie en arri&egrave;re-plan pendant que vous utilisez le CMS et affiche une banni&egrave;re sur le tableau de bord d&egrave;s qu'une version plus r&eacute;cente est publi&eacute;e, en indiquant votre version actuelle et celle disponible. Les administrateurs peuvent aussi lancer <code>npm run update -- --check</code> &agrave; tout moment.</p>\\n\\n<h2 id='faq'>FAQ des mises &agrave; jour</h2>\\n<h3>La mise &agrave; jour va-t-elle &eacute;craser mon contenu ou mes r&eacute;glages ?</h3>\\n<p>Non. Contenus, m&eacute;dias, utilisateurs et r&eacute;glages vivent dans votre base de donn&eacute;es ; la configuration du site vit dans vos variables d'environnement. La mise &agrave; jour ne touche que le code, les d&eacute;pendances et la structure du sch&eacute;ma.</p>\\n<h3>Dois-je installer chaque version ?</h3>\\n<p>Non, mais rester proche de la derni&egrave;re version vous garantit les correctifs de s&eacute;curit&eacute; et rend chaque saut plus petit. Les migrations s'appliquent dans l'ordre : sauter plusieurs versions fonctionne malgr&eacute; tout.</p>\\n<h3>Puis-je l'ex&eacute;cuter en CI ?</h3>\\n<p>Oui &mdash; <code>npm run update -- --yes</code> ne pose aucune question et renvoie un code d'erreur si l'&eacute;tape sch&eacute;ma &eacute;choue, pour qu'un pipeline puisse le d&eacute;tecter.</p>\\n<h3>Et si aucune connexion &agrave; la base n'est configur&eacute;e ?</h3>\\n<p>Le code et les d&eacute;pendances sont tout de m&ecirc;me mis &agrave; jour ; l'&eacute;tape sch&eacute;ma est ignor&eacute;e avec un avertissement indiquant la variable d'environnement &agrave; d&eacute;finir. Relancez ensuite <code>npm run update -- --db-only</code>.</p>\\n<h3>Je suis sur le d&eacute;ploiement Vercel en un clic &mdash; dois-je lancer quelque chose ?</h3>\\n<p>Non. Ce chemin est enti&egrave;rement automatique. La commande existe pour mettre &agrave; jour <em>tout de suite</em> plut&ocirc;t qu'&agrave; minuit, ou lorsque vous travaillez sur un clone local.</p>\\n\\n<div class='rounded-[2rem] border border-slate-200/80 bg-slate-50 p-8 my-12 text-center dark:border-white/10 dark:bg-white/5'>\\n <p class='mt-0 text-2xl font-semibold text-slate-900 dark:text-white'>Une commande, toutes les installations.</p>\\n <p class='text-sm text-slate-600 dark:text-slate-300'>Vous d&eacute;butez avec NextBlock ? Commencez par le guide d'installation &mdash; puis oubliez les mises &agrave; jour.</p>\\n <div class='mt-5 flex flex-wrap justify-center gap-3'>\\n <a href='/article/comment-configurer-nextblock' class='inline-flex items-center rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white no-underline shadow-lg hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200'>Lire le guide d'installation</a>\\n <a href='https://github.com/nextblock-cms/nextblock' target='_blank' rel='noopener' class='inline-flex items-center rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 no-underline hover:border-slate-500 dark:border-white/20 dark:text-slate-200 dark:hover:border-white/50'>Voir sur GitHub</a>\\n </div>\\n</div>\"}\n$nbfr$::jsonb, 0);\n END IF;\nEND\n$body$;\n\n-- Revision baseline for the two new posts.\n--\n-- 00000000000016 back-fills a 'snapshot' revision for every post that existed WHEN IT\n-- RAN. On a fresh install migrations run in order, so 016 executes before this file and\n-- these two posts would be the only ones in the CMS without a restore point. This is the\n-- same projection as 016 section 3b, scoped to the two slugs.\nINSERT INTO public.post_revisions (post_id, author_id, version, revision_type, content)\nSELECT\n po.id,\n NULL::uuid,\n po.version,\n 'snapshot'::public.revision_type,\n jsonb_build_object(\n 'meta', jsonb_build_object(\n 'title', po.title,\n 'slug', po.slug,\n 'language_id', po.language_id,\n 'status', po.status,\n 'meta_title', po.meta_title,\n 'meta_description', po.meta_description,\n 'custom_canonical', po.custom_canonical,\n 'published_at', to_char(po.published_at AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'),\n 'feature_image_id', po.feature_image_id,\n 'label', po.label,\n 'excerpt', po.excerpt,\n 'subtitle', po.subtitle\n ),\n 'blocks', COALESCE((\n SELECT jsonb_agg(\n jsonb_build_object(\n 'language_id', b.language_id,\n 'block_type', b.block_type,\n 'content', b.content,\n 'order', b.\"order\"\n ) ORDER BY b.\"order\" ASC, b.id ASC\n )\n FROM public.blocks b\n WHERE b.post_id = po.id\n ), '[]'::jsonb)\n )\n FROM public.posts po\n WHERE po.slug IN ('how-updating-works', 'comment-fonctionnent-les-mises-a-jour')\n AND NOT EXISTS (\n SELECT 1 FROM public.post_revisions r\n WHERE r.post_id = po.id\n AND r.revision_type = 'snapshot'\n AND r.version <= po.version\n )\nON CONFLICT (post_id, version) DO NOTHING;\n"
121
+ },
122
+ {
123
+ "version": "00000000000021",
124
+ "name": "00000000000021_updating_article_git_merge.sql",
125
+ "sql": "-- 00000000000021_updating_article_git_merge.sql\n-- Corrects the \"How Updating NextBlock Works\" article seeded in 00000000000020.\n--\n-- That version described the standalone update as \"replace the files and keep a backup\n-- under .nextblock-backup/\". The updater now performs a real git 3-way merge instead:\n-- both template versions are committed into the project's own object database under\n-- refs/nextblock/{base,head}, and the diff between them is applied with `git apply --3way`.\n-- A developer's edits to framework files are preserved and only genuine overlaps conflict.\n-- The copy-and-back-up path survives only as the fallback for a project with no git repo,\n-- no commits, or a dirty tree.\n--\n-- 020 is already applied everywhere and never replays, so the copy is corrected here as\n-- targeted, idempotent string replacements rather than a full re-write of the body: each\n-- replace() is a no-op once the old sentence is gone, so re-running changes nothing.\n-- Data-only; no schema change.\n\nDO $body$\nDECLARE\n v_en_post integer;\n v_fr_post integer;\nBEGIN\n SELECT id INTO v_en_post\n FROM public.posts WHERE language_id = 1 AND slug = 'how-updating-works'\n ORDER BY id LIMIT 1;\n\n SELECT id INTO v_fr_post\n FROM public.posts WHERE language_id = 2 AND slug = 'comment-fonctionnent-les-mises-a-jour'\n ORDER BY id LIMIT 1;\n\n ---------------------------------------------------------------------------\n -- English\n ---------------------------------------------------------------------------\n IF v_en_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(\n replace(\n content->>'html_content',\n -- 1. the \"if something goes wrong\" bullet\n '<li><strong>Standalone projects:</strong> every framework file the update replaces is copied first into a timestamped folder under <code>.nextblock-backup/</code> in your project. Nothing is deleted, so files you added yourself are never removed.</li>',\n '<li><strong>Standalone projects:</strong> the update is applied as a <strong>git 3-way merge</strong> into your working tree &mdash; nothing is committed for you. Review it with <code>git diff</code>, and undo the whole thing with <code>git reset --hard HEAD</code>. Nothing is ever deleted, so files you added yourself are never removed.</li>'\n ),\n -- 2. the \"you customised a framework file\" paragraph\n '<p>If you have customised a file that NextBlock owns &mdash; something under <code>app/</code>, <code>components/</code> or <code>lib/</code> &mdash; the update will replace it and back up your version. Diff the backup afterwards to bring your change forward. Customisations that live in your own new files, in the CMS, or in <code>.env</code> are never affected.</p>',\n '<p>If you have customised a file that NextBlock owns &mdash; something under <code>app/</code>, <code>components/</code> or <code>lib/</code> &mdash; <strong>your edit is kept</strong>. The update merges the upstream change into your version, and only a change that genuinely overlaps yours conflicts, with ordinary <code>&lt;&lt;&lt;&lt;&lt;&lt;&lt; ours</code> / <code>&gt;&gt;&gt;&gt;&gt;&gt;&gt; theirs</code> markers. List them with <code>git diff --name-only --diff-filter=U</code> and resolve with <code>git checkout --theirs</code> or <code>--ours</code>. Customisations in your own files, in the CMS, or in <code>.env</code> are never touched at all.</p>'\n ),\n -- 3. the managed-cloud section\n '<p>Your project is a standalone Next.js app, so new framework code is fetched from the published <code>create-nextblock</code> package on npm &mdash; the exact artifact your project was scaffolded from, versioned in lockstep with the release. NextBlock refreshes the files it owns, merges the new dependency versions into your <code>package.json</code>, runs <code>npm install</code>, and then applies migrations.</p>',\n '<p>Your project is a standalone Next.js app with no upstream to pull from, so new framework code comes from the published <code>create-nextblock</code> package on npm &mdash; the exact artifact your project was scaffolded from, versioned in lockstep with the release. NextBlock fetches both your current version and the new one, and applies the difference between them as a <strong>git 3-way merge</strong>, so the update behaves exactly like a <code>git pull</code>: files you never touched update silently, files you customised keep your changes. It then merges the new dependency versions into your <code>package.json</code>, runs <code>npm install</code>, and applies migrations.</p><p>This needs a git repository with at least one commit and a clean working tree &mdash; commit your work before updating. Without that there is nothing to merge against, so the files are copied instead and anything replaced is kept under <code>.nextblock-backup/</code>.</p>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_en_post\n AND block_type = 'text';\n END IF;\n\n ---------------------------------------------------------------------------\n -- French\n ---------------------------------------------------------------------------\n IF v_fr_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(\n replace(\n content->>'html_content',\n '<li><strong>Projets autonomes :</strong> chaque fichier remplac&eacute; est d''abord copi&eacute; dans un dossier horodat&eacute; sous <code>.nextblock-backup/</code> dans votre projet. Rien n''est supprim&eacute; : les fichiers que vous avez ajout&eacute;s ne disparaissent jamais.</li>',\n '<li><strong>Projets autonomes :</strong> la mise &agrave; jour est appliqu&eacute;e comme une <strong>fusion git &agrave; trois voies</strong> dans votre copie de travail &mdash; rien n''est valid&eacute; &agrave; votre place. Examinez-la avec <code>git diff</code>, et annulez tout avec <code>git reset --hard HEAD</code>. Rien n''est jamais supprim&eacute; : les fichiers que vous avez ajout&eacute;s ne disparaissent jamais.</li>'\n ),\n '<p>Si vous avez personnalis&eacute; un fichier appartenant &agrave; NextBlock &mdash; sous <code>app/</code>, <code>components/</code> ou <code>lib/</code> &mdash; la mise &agrave; jour le remplacera en sauvegardant votre version. Comparez ensuite la sauvegarde pour reporter votre modification. Les personnalisations qui vivent dans vos propres fichiers, dans le CMS ou dans <code>.env</code> ne sont jamais affect&eacute;es.</p>',\n '<p>Si vous avez personnalis&eacute; un fichier appartenant &agrave; NextBlock &mdash; sous <code>app/</code>, <code>components/</code> ou <code>lib/</code> &mdash; <strong>votre modification est conserv&eacute;e</strong>. La mise &agrave; jour fusionne le changement amont dans votre version, et seul un changement qui chevauche r&eacute;ellement le v&ocirc;tre entre en conflit, avec les marqueurs habituels <code>&lt;&lt;&lt;&lt;&lt;&lt;&lt; ours</code> / <code>&gt;&gt;&gt;&gt;&gt;&gt;&gt; theirs</code>. Listez-les avec <code>git diff --name-only --diff-filter=U</code> et r&eacute;solvez avec <code>git checkout --theirs</code> ou <code>--ours</code>. Les personnalisations dans vos propres fichiers, dans le CMS ou dans <code>.env</code> ne sont jamais touch&eacute;es.</p>'\n ),\n '<p>Votre projet est une application Next.js autonome : le nouveau code provient donc du paquet <code>create-nextblock</code> publi&eacute; sur npm &mdash; exactement l''artefact &agrave; partir duquel votre projet a &eacute;t&eacute; g&eacute;n&eacute;r&eacute;, versionn&eacute; en phase avec la release. NextBlock rafra&icirc;chit les fichiers qui lui appartiennent, fusionne les nouvelles versions de d&eacute;pendances dans votre <code>package.json</code>, lance <code>npm install</code>, puis applique les migrations.</p>',\n '<p>Votre projet est une application Next.js autonome sans d&eacute;p&ocirc;t amont &agrave; tirer : le nouveau code provient donc du paquet <code>create-nextblock</code> publi&eacute; sur npm &mdash; exactement l''artefact &agrave; partir duquel votre projet a &eacute;t&eacute; g&eacute;n&eacute;r&eacute;, versionn&eacute; en phase avec la release. NextBlock r&eacute;cup&egrave;re votre version actuelle et la nouvelle, puis applique la diff&eacute;rence entre les deux comme une <strong>fusion git &agrave; trois voies</strong> : la mise &agrave; jour se comporte donc exactement comme un <code>git pull</code> &mdash; les fichiers que vous n''avez jamais touch&eacute;s se mettent &agrave; jour silencieusement, ceux que vous avez personnalis&eacute;s conservent vos changements. Elle fusionne ensuite les nouvelles versions de d&eacute;pendances dans votre <code>package.json</code>, lance <code>npm install</code> et applique les migrations.</p><p>Cela n&eacute;cessite un d&eacute;p&ocirc;t git avec au moins un commit et une copie de travail propre &mdash; validez votre travail avant de mettre &agrave; jour. Sans cela il n''y a rien contre quoi fusionner : les fichiers sont alors copi&eacute;s et tout ce qui est remplac&eacute; est conserv&eacute; sous <code>.nextblock-backup/</code>.</p>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_fr_post\n AND block_type = 'text';\n END IF;\nEND\n$body$;\n"
126
+ },
127
+ {
128
+ "version": "00000000000022",
129
+ "name": "00000000000022_updating_article_accuracy.sql",
130
+ "sql": "-- 00000000000022_updating_article_accuracy.sql\n-- Three accuracy fixes to the \"How Updating NextBlock Works\" article\n-- (seeded in 00000000000020, first corrected in 00000000000021).\n--\n-- 0. The merge is performed with `git merge-file`, not `git apply --3way`. The latter\n-- implies --index: it stages its result (so `git diff` shows the developer nothing),\n-- requires every path to be tracked (one gitignored framework path aborted the whole\n-- update), and requires the worktree to match the index. merge-file touches no git\n-- state at all. Consequently the resolution commands 021 shipped are wrong: there are\n-- no index stages, so `git checkout --theirs/--ours` does not apply. The conflict\n-- markers are ordinary text; `git checkout -- <file>` discards one file's merge.\n--\n-- 1. \"A conflicted merge is aborted automatically\" was true of only ONE path. The\n-- monorepo/fork path does abort and restore the tree, because the merge belongs to\n-- upstream. The standalone path deliberately LEAVES the conflict in the working tree,\n-- because it is the developer's own repository and resolving it is the whole point.\n-- The bullet now states both.\n-- 2. The Docker section claimed `npm run update` refreshes the schema. It does not, by\n-- design: the self-hosted stack ships its own migration runner (the `migrate` service\n-- in docker-compose.yml, which tracks applied versions in a different table), so the\n-- updater stages the SQL and hands off to `npm run docker:up` rather than applying it\n-- twice through two different trackers.\n--\n-- Same targeted, idempotent replace() approach as 021 — a no-op once the old sentence is\n-- gone. Data-only; no schema change.\n\nDO $body$\nDECLARE\n v_en_post integer;\n v_fr_post integer;\nBEGIN\n SELECT id INTO v_en_post\n FROM public.posts WHERE language_id = 1 AND slug = 'how-updating-works'\n ORDER BY id LIMIT 1;\n\n SELECT id INTO v_fr_post\n FROM public.posts WHERE language_id = 2 AND slug = 'comment-fonctionnent-les-mises-a-jour'\n ORDER BY id LIMIT 1;\n\n IF v_en_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(\n replace(\n content->>'html_content',\n -- 021 shipped index-based resolution commands; the merge no longer\n -- uses the git index, so they do not apply.\n '<strong>your edit is kept</strong>. The update merges the upstream change into your version, and only a change that genuinely overlaps yours conflicts, with ordinary <code>&lt;&lt;&lt;&lt;&lt;&lt;&lt; ours</code> / <code>&gt;&gt;&gt;&gt;&gt;&gt;&gt; theirs</code> markers. List them with <code>git diff --name-only --diff-filter=U</code> and resolve with <code>git checkout --theirs</code> or <code>--ours</code>.',\n '<strong>your edit is kept</strong>. The update merges the upstream change into your version, and only a change that genuinely overlaps yours conflicts &mdash; the updater lists those files, and each one carries ordinary <code>&lt;&lt;&lt;&lt;&lt;&lt;&lt; your version</code> / <code>&gt;&gt;&gt;&gt;&gt;&gt;&gt; NextBlock</code> markers. Edit them as you would any conflict, or run <code>git checkout -- &lt;file&gt;</code> to discard the merge for that one file.'\n ),\n '<li><strong>A conflicted merge</strong> is aborted automatically &mdash; your working tree is left exactly as it was, with instructions printed for resolving it by hand.</li>',\n '<li><strong>A conflict</strong> behaves differently by install, on purpose. On a fork or clone the upstream merge is <em>aborted</em> and your working tree is left exactly as it was. On a standalone project the conflict is <em>left in place</em> for you to resolve &mdash; it is your own repository, and that is the point &mdash; and <code>git reset --hard HEAD</code> backs the whole update out.</li>'\n ),\n '<p>The first command refreshes the application, its dependencies and the schema; the second rebuilds and restarts the containers. Your database and media live in Docker volumes and are never touched by either step &mdash; <code>docker:up</code> rebuilds images, not data.</p>',\n '<p>The first command updates the application and its dependencies and stages the new migrations; the second rebuilds the containers <em>and applies those migrations</em>. The self-hosted stack runs its own migration service, so the updater hands the schema step to it rather than applying the same SQL through two different trackers. Your database and media live in Docker volumes and are never touched by either command &mdash; <code>docker:up</code> rebuilds images, not data.</p>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_en_post\n AND block_type = 'text';\n END IF;\n\n IF v_fr_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(\n replace(\n content->>'html_content',\n '<strong>votre modification est conserv&eacute;e</strong>. La mise &agrave; jour fusionne le changement amont dans votre version, et seul un changement qui chevauche r&eacute;ellement le v&ocirc;tre entre en conflit, avec les marqueurs habituels <code>&lt;&lt;&lt;&lt;&lt;&lt;&lt; ours</code> / <code>&gt;&gt;&gt;&gt;&gt;&gt;&gt; theirs</code>. Listez-les avec <code>git diff --name-only --diff-filter=U</code> et r&eacute;solvez avec <code>git checkout --theirs</code> ou <code>--ours</code>.',\n '<strong>votre modification est conserv&eacute;e</strong>. La mise &agrave; jour fusionne le changement amont dans votre version, et seul un changement qui chevauche r&eacute;ellement le v&ocirc;tre entre en conflit &mdash; la commande liste ces fichiers, et chacun porte les marqueurs habituels <code>&lt;&lt;&lt;&lt;&lt;&lt;&lt; your version</code> / <code>&gt;&gt;&gt;&gt;&gt;&gt;&gt; NextBlock</code>. Modifiez-les comme n''importe quel conflit, ou lancez <code>git checkout -- &lt;fichier&gt;</code> pour abandonner la fusion sur ce seul fichier.'\n ),\n '<li><strong>Une fusion en conflit</strong> est annul&eacute;e automatiquement &mdash; votre copie de travail reste intacte, avec les instructions pour r&eacute;soudre &agrave; la main.</li>',\n '<li><strong>Un conflit</strong> se comporte diff&eacute;remment selon l''installation, volontairement. Sur un fork ou un clone, la fusion amont est <em>annul&eacute;e</em> et votre copie de travail reste intacte. Sur un projet autonome, le conflit est <em>laiss&eacute; en place</em> pour que vous le r&eacute;solviez &mdash; c''est votre d&eacute;p&ocirc;t, et c''est tout l''int&eacute;r&ecirc;t &mdash; et <code>git reset --hard HEAD</code> annule toute la mise &agrave; jour.</li>'\n ),\n '<p>La premi&egrave;re commande met &agrave; jour l''application, ses d&eacute;pendances et le sch&eacute;ma ; la seconde reconstruit et red&eacute;marre les conteneurs. Votre base de donn&eacute;es et vos m&eacute;dias vivent dans des volumes Docker et ne sont touch&eacute;s ni par l''une ni par l''autre &mdash; <code>docker:up</code> reconstruit des images, pas des donn&eacute;es.</p>',\n '<p>La premi&egrave;re commande met &agrave; jour l''application et ses d&eacute;pendances et pr&eacute;pare les nouvelles migrations ; la seconde reconstruit les conteneurs <em>et applique ces migrations</em>. La pile auto-h&eacute;berg&eacute;e dispose de son propre service de migration : la mise &agrave; jour lui confie donc l''&eacute;tape sch&eacute;ma plut&ocirc;t que d''appliquer le m&ecirc;me SQL via deux suivis diff&eacute;rents. Votre base de donn&eacute;es et vos m&eacute;dias vivent dans des volumes Docker et ne sont touch&eacute;s par aucune des deux commandes &mdash; <code>docker:up</code> reconstruit des images, pas des donn&eacute;es.</p>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_fr_post\n AND block_type = 'text';\n END IF;\nEND\n$body$;\n"
131
+ },
132
+ {
133
+ "version": "00000000000023",
134
+ "name": "00000000000023_updating_article_layout_not_host.sql",
135
+ "sql": "-- 00000000000023_updating_article_layout_not_host.sql\n-- Corrects the most misleading claim in the \"How Updating NextBlock Works\" article\n-- (seeded 00000000000020, corrected in 021 and 022): that the automatic GitHub Action is\n-- about being deployed on Vercel.\n--\n-- It is not. The upstream-sync Action merges the NextBlock MONOREPO into the repository,\n-- so it only works where the repository IS the monorepo — a Vercel 1-click deploy, a\n-- GitHub fork, or a clone. A project scaffolded by `npm create nextblock` is the flattened\n-- standalone app (app/, components/, lib/ at the root); merging apps/ + libs/ + nx.json\n-- into it would wreck it. Pushing that project to GitHub and deploying it on Vercel does\n-- not change its layout, so it is still a `npm run update` install. Docker is orthogonal:\n-- it is how you RUN a project, not what shape the repository is.\n--\n-- Also documents that a merge conflict now holds the migration step back until the\n-- conflict is resolved, so the schema never moves ahead of undecided code.\n--\n-- Targeted, idempotent replace() as in 021/022. Data-only; no schema change.\n\nDO $body$\nDECLARE\n v_en_post integer;\n v_fr_post integer;\nBEGIN\n SELECT id INTO v_en_post\n FROM public.posts WHERE language_id = 1 AND slug = 'how-updating-works'\n ORDER BY id LIMIT 1;\n\n SELECT id INTO v_fr_post\n FROM public.posts WHERE language_id = 2 AND slug = 'comment-fonctionnent-les-mises-a-jour'\n ORDER BY id LIMIT 1;\n\n IF v_en_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(\n replace(\n content->>'html_content',\n -- 1. The section intro: say what actually qualifies.\n '<p>This path updates itself. When you deployed, NextBlock created a repository you own; the dashboard&rsquo;s <strong>Connect GitHub</strong> onboarding step installs a workflow into it that runs <strong>every day at midnight UTC</strong> and can also be triggered by hand from your repository&rsquo;s <strong>Actions</strong> tab.</p>',\n '<p>This path updates itself. When you deployed, NextBlock created a repository you own; the dashboard&rsquo;s <strong>Connect GitHub</strong> onboarding step installs a workflow into it that runs <strong>every day at midnight UTC</strong> and can also be triggered by hand from your repository&rsquo;s <strong>Actions</strong> tab.</p>\\n<div class=''rounded-3xl border border-slate-200 bg-slate-50 p-6 my-8 dark:border-white/10 dark:bg-white/5''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-slate-600 dark:text-slate-300''>What qualifies &mdash; it is the repository, not the host</p>\\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>The workflow merges the <strong>NextBlock monorepo</strong> into your repository, so it only works where your repository <em>is</em> that monorepo: a one-click deploy, a GitHub fork, or a clone. A project created with <code>npm create nextblock</code> is the flattened standalone app &mdash; <code>app/</code>, <code>components/</code> and <code>lib/</code> at the root &mdash; and merging <code>apps/</code>, <code>libs/</code> and <code>nx.json</code> into it would wreck it. Pushing that project to GitHub and deploying it on Vercel does not change its shape: it is still an <code>npm run update</code> install, and NextBlock will not offer it this workflow. Docker is a separate question entirely &mdash; that is how you <em>run</em> a project, not what shape its repository is.</p>\\n</div>'\n ),\n -- 2. The FAQ answer, which asked exactly the question this clarifies.\n '<h3>I am on the one-click Vercel deploy &mdash; do I need to run anything?</h3>\\n<p>No. That path is fully automatic. The command exists for when you want an update <em>now</em> rather than at midnight, or when you are working on a local clone.</p>',\n '<h3>I am on the one-click Vercel deploy &mdash; do I need to run anything?</h3>\\n<p>No. That path is fully automatic. The command exists for when you want an update <em>now</em> rather than at midnight, or when you are working on a local clone.</p>\\n<h3>I deployed to Vercel, but from <code>npm create nextblock</code>. Is that automatic too?</h3>\\n<p>No &mdash; and this is the distinction that catches people out. Automatic updates depend on your repository being the NextBlock <strong>monorepo</strong>, not on where the site is hosted. A project scaffolded by the CLI is the flattened standalone app whatever you deploy it to, so it updates with <code>npm run update</code>. You will not see the <strong>Connect GitHub</strong> step on that kind of install, because the workflow it installs would merge a completely different source tree into yours.</p>'\n ),\n -- 3. Conflicts hold the schema step.\n '<li><strong>A failed migration</strong> rolls back. Fix the cause and re-run; nothing half-applied is left behind.</li>',\n '<li><strong>A failed migration</strong> rolls back. Fix the cause and re-run; nothing half-applied is left behind.</li>\\n <li><strong>Unresolved conflicts hold the database back.</strong> If a merge left conflicts, the update finishes the code and dependency work but <em>stops before migrating</em> &mdash; your schema never moves ahead of code you have not finished deciding on. Resolve them and run <code>npm run update</code> again to apply the migrations, or walk away with <code>git reset --hard HEAD</code>; either way the database was never touched.</li>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_en_post\n AND block_type = 'text';\n END IF;\n\n IF v_fr_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(\n replace(\n content->>'html_content',\n '<p>Ce chemin se met &agrave; jour tout seul. Lors du d&eacute;ploiement, NextBlock a cr&eacute;&eacute; un d&eacute;p&ocirc;t qui vous appartient ; l''&eacute;tape <strong>Connect GitHub</strong> du tableau de bord y installe un workflow qui s''ex&eacute;cute <strong>chaque jour &agrave; minuit UTC</strong> et peut aussi &ecirc;tre lanc&eacute; &agrave; la demande depuis l''onglet <strong>Actions</strong> de votre d&eacute;p&ocirc;t.</p>',\n '<p>Ce chemin se met &agrave; jour tout seul. Lors du d&eacute;ploiement, NextBlock a cr&eacute;&eacute; un d&eacute;p&ocirc;t qui vous appartient ; l''&eacute;tape <strong>Connect GitHub</strong> du tableau de bord y installe un workflow qui s''ex&eacute;cute <strong>chaque jour &agrave; minuit UTC</strong> et peut aussi &ecirc;tre lanc&eacute; &agrave; la demande depuis l''onglet <strong>Actions</strong> de votre d&eacute;p&ocirc;t.</p>\\n<div class=''rounded-3xl border border-slate-200 bg-slate-50 p-6 my-8 dark:border-white/10 dark:bg-white/5''>\\n <p class=''mt-0 text-xs font-semibold uppercase tracking-[0.22em] text-slate-600 dark:text-slate-300''>Ce qui compte : le d&eacute;p&ocirc;t, pas l''h&eacute;bergeur</p>\\n <p class=''mt-3 mb-0 text-sm text-slate-700 dark:text-slate-200''>Le workflow fusionne le <strong>monorepo NextBlock</strong> dans votre d&eacute;p&ocirc;t : il ne fonctionne donc que si votre d&eacute;p&ocirc;t <em>est</em> ce monorepo &mdash; d&eacute;ploiement en un clic, fork GitHub ou clone. Un projet cr&eacute;&eacute; avec <code>npm create nextblock</code> est l''application autonome aplatie &mdash; <code>app/</code>, <code>components/</code> et <code>lib/</code> &agrave; la racine &mdash; et y fusionner <code>apps/</code>, <code>libs/</code> et <code>nx.json</code> le casserait. Pousser ce projet sur GitHub et le d&eacute;ployer sur Vercel ne change pas sa forme : il se met toujours &agrave; jour avec <code>npm run update</code>, et NextBlock ne lui proposera pas ce workflow. Docker est une tout autre question &mdash; c''est la fa&ccedil;on d''<em>ex&eacute;cuter</em> un projet, pas la forme de son d&eacute;p&ocirc;t.</p>\\n</div>'\n ),\n '<h3>Je suis sur le d&eacute;ploiement Vercel en un clic &mdash; dois-je lancer quelque chose ?</h3>\\n<p>Non. Ce chemin est enti&egrave;rement automatique. La commande existe pour mettre &agrave; jour <em>tout de suite</em> plut&ocirc;t qu''&agrave; minuit, ou lorsque vous travaillez sur un clone local.</p>',\n '<h3>Je suis sur le d&eacute;ploiement Vercel en un clic &mdash; dois-je lancer quelque chose ?</h3>\\n<p>Non. Ce chemin est enti&egrave;rement automatique. La commande existe pour mettre &agrave; jour <em>tout de suite</em> plut&ocirc;t qu''&agrave; minuit, ou lorsque vous travaillez sur un clone local.</p>\\n<h3>J''ai d&eacute;ploy&eacute; sur Vercel, mais depuis <code>npm create nextblock</code>. Est-ce automatique aussi ?</h3>\\n<p>Non &mdash; et c''est la distinction qui pi&egrave;ge le plus. Les mises &agrave; jour automatiques d&eacute;pendent du fait que votre d&eacute;p&ocirc;t soit le <strong>monorepo</strong> NextBlock, pas de l''endroit o&ugrave; le site est h&eacute;berg&eacute;. Un projet g&eacute;n&eacute;r&eacute; par le CLI reste l''application autonome aplatie, quel que soit l''h&eacute;bergeur : il se met &agrave; jour avec <code>npm run update</code>. L''&eacute;tape <strong>Connect GitHub</strong> ne s''affiche pas sur ce type d''installation, car le workflow qu''elle installe fusionnerait une arborescence totalement diff&eacute;rente dans la v&ocirc;tre.</p>'\n ),\n '<li><strong>Une migration en &eacute;chec</strong> est annul&eacute;e. Corrigez la cause et relancez : rien ne reste &agrave; moiti&eacute; appliqu&eacute;.</li>',\n '<li><strong>Une migration en &eacute;chec</strong> est annul&eacute;e. Corrigez la cause et relancez : rien ne reste &agrave; moiti&eacute; appliqu&eacute;.</li>\\n <li><strong>Les conflits non r&eacute;solus bloquent la base.</strong> Si une fusion a laiss&eacute; des conflits, la mise &agrave; jour termine le code et les d&eacute;pendances mais <em>s''arr&ecirc;te avant les migrations</em> &mdash; votre sch&eacute;ma ne prend jamais de l''avance sur un code que vous n''avez pas fini d''arbitrer. R&eacute;solvez-les puis relancez <code>npm run update</code> pour appliquer les migrations, ou abandonnez avec <code>git reset --hard HEAD</code> : dans les deux cas la base n''a jamais &eacute;t&eacute; touch&eacute;e.</li>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_fr_post\n AND block_type = 'text';\n END IF;\nEND\n$body$;\n"
136
+ },
137
+ {
138
+ "version": "00000000000024",
139
+ "name": "00000000000024_updating_article_newline_fix.sql",
140
+ "sql": "-- 00000000000024_updating_article_newline_fix.sql\n-- Repairs two mistakes made by 00000000000023 in the \"How Updating NextBlock Works\"\n-- article, and lands the FAQ entry that migration failed to insert.\n--\n-- 1. RENDERING BUG. 023 wrote `\\n` inside ordinary single-quoted SQL literals. With\n-- standard_conforming_strings on (the default), that is a literal backslash followed\n-- by 'n' — not a newline — so five visible \"\\n\" sequences were stored in the article\n-- body. Replaced here with real newlines via chr(10). Idempotent: once none remain,\n-- replace() is a no-op.\n--\n-- 2. SILENT NO-MATCH. 023's FAQ replacement targeted a string spanning `</h3>\\n<p>`, and\n-- for the same reason the literal never matched the real newline in the stored HTML, so\n-- the replacement quietly did nothing. Redone here by anchoring on the single-line <h3>\n-- alone and prepending the new entry — no newline in either the search or the\n-- replacement, which is the rule this file establishes for editing the article.\n--\n-- Data-only; no schema change.\n\nDO $body$\nDECLARE\n v_en_post integer;\n v_fr_post integer;\nBEGIN\n SELECT id INTO v_en_post\n FROM public.posts WHERE language_id = 1 AND slug = 'how-updating-works'\n ORDER BY id LIMIT 1;\n\n SELECT id INTO v_fr_post\n FROM public.posts WHERE language_id = 2 AND slug = 'comment-fonctionnent-les-mises-a-jour'\n ORDER BY id LIMIT 1;\n\n IF v_en_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n -- (1) literal backslash-n -> real newline\n replace(content->>'html_content', E'\\\\n', chr(10)),\n -- (2) the FAQ entry 023 failed to insert\n '<h3>I am on the one-click Vercel deploy &mdash; do I need to run anything?</h3>',\n '<h3>I deployed to Vercel, but from <code>npm create nextblock</code>. Is that automatic too?</h3><p>No &mdash; and this is the distinction that catches people out. Automatic updates depend on your repository being the NextBlock <strong>monorepo</strong>, not on where the site is hosted. A project scaffolded by the CLI is the flattened standalone app whatever you deploy it to, so it updates with <code>npm run update</code>. You will not see the <strong>Connect GitHub</strong> step on that kind of install, because the workflow it installs would merge a completely different source tree into yours.</p><h3>I am on the one-click Vercel deploy &mdash; do I need to run anything?</h3>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_en_post\n AND block_type = 'text';\n END IF;\n\n IF v_fr_post IS NOT NULL THEN\n UPDATE public.blocks\n SET content = jsonb_set(\n content,\n '{html_content}',\n to_jsonb(\n replace(\n replace(content->>'html_content', E'\\\\n', chr(10)),\n '<h3>Je suis sur le d&eacute;ploiement Vercel en un clic &mdash; dois-je lancer quelque chose ?</h3>',\n '<h3>J''ai d&eacute;ploy&eacute; sur Vercel, mais depuis <code>npm create nextblock</code>. Est-ce automatique aussi ?</h3><p>Non &mdash; et c''est la distinction qui pi&egrave;ge le plus. Les mises &agrave; jour automatiques d&eacute;pendent du fait que votre d&eacute;p&ocirc;t soit le <strong>monorepo</strong> NextBlock, pas de l''endroit o&ugrave; le site est h&eacute;berg&eacute;. Un projet g&eacute;n&eacute;r&eacute; par le CLI reste l''application autonome aplatie, quel que soit l''h&eacute;bergeur : il se met &agrave; jour avec <code>npm run update</code>. L''&eacute;tape <strong>Connect GitHub</strong> ne s''affiche pas sur ce type d''installation, car le workflow qu''elle installe fusionnerait une arborescence totalement diff&eacute;rente dans la v&ocirc;tre.</p><h3>Je suis sur le d&eacute;ploiement Vercel en un clic &mdash; dois-je lancer quelque chose ?</h3>'\n )\n )\n ),\n updated_at = now()\n WHERE post_id = v_fr_post\n AND block_type = 'text';\n END IF;\nEND\n$body$;\n"
141
+ },
142
+ {
143
+ "version": "00000000000025",
144
+ "name": "00000000000025_rebrand_nextblock_dev.sql",
145
+ "sql": "-- 00000000000025_rebrand_nextblock_dev.sql\n-- Domain rebrand: the retired .ca domain -> nextblock.dev.\n--\n-- Two halves to this change, because two populations need different treatment:\n--\n-- Fresh installs -- migrations 00000000000003_baseline_seed and\n-- 00000000000006/007/009_home_live_demo_promo* were rewritten in place\n-- to seed nextblock.dev directly. Safe to edit despite the append-only\n-- rule: Supabase tracks migration history by version string with no\n-- checksum, so an already-applied file is never re-read or replayed.\n-- Those four are seed/content only -- no schema, no constraints.\n--\n-- Existing installs -- prod, sandbox, and any deployed fork already seeded the old domain\n-- from the pre-rewrite versions of those files. They never replay, so\n-- this migration corrects their data forward.\n--\n-- Both paths converge on nextblock.dev. On a fresh install this migration matches nothing\n-- and is a no-op, which is also what makes it safe to re-run.\n--\n-- Also flips the sandbox demo account address. The account itself is created by\n-- apps/nextblock/app/api/cron/reset-sandbox/route.ts (now 'demo@nextblock.dev'); the legacy\n-- demo account is NOT removed here -- the reset route never deletes auth users and never\n-- truncates public.profiles, so it must be deleted by hand in the Supabase Auth dashboard\n-- or it survives as a working ADMIN.\n--\n-- NOTE: keep the word \"sandbox\" OUT of this filename -- generate-sandbox-reset.ts excludes any\n-- migration whose filename contains \"sandbox\" from the sandbox reset bundle.\n\nDO $body$\nDECLARE\n -- Assembled from two halves on purpose: it is the one string this rebrand is meant to\n -- erase, and spelling it out here would leave the repo-wide grep with a permanent hit\n -- inside the very migration that removes it. Resolves to the retired domain at runtime.\n legacy_domain constant text := 'nextblock' || '.ca';\nBEGIN\n -- 1. Seeded UI strings (e.g. the \"Purchase at ...\" link on the sandbox checkout panel).\n UPDATE public.translations\n SET translations = replace(translations::text, legacy_domain, 'nextblock.dev')::jsonb,\n updated_at = now()\n WHERE translations::text LIKE '%' || legacy_domain || '%';\n\n -- 2. Page/post content -- the home-page \"Live Demo\" promo carries the demo login address.\n UPDATE public.blocks\n SET content = replace(content::text, legacy_domain, 'nextblock.dev')::jsonb,\n updated_at = now()\n WHERE content::text LIKE '%' || legacy_domain || '%';\n\n -- 3. Site settings -- invoice branding on the sandbox carries the billing address.\n UPDATE public.site_settings\n SET value = replace(value::text, legacy_domain, 'nextblock.dev')::jsonb\n WHERE value::text LIKE '%' || legacy_domain || '%';\nEND\n$body$;\n"
146
+ },
147
+ {
148
+ "version": "00000000000026",
149
+ "name": "00000000000026_product_inquiries.sql",
150
+ "sql": "-- Purchase enquiries raised when the store cannot take payment.\n--\n-- A NextBlock store can be fully built — products, prices, images, pages — while its\n-- Stripe or Freemius credentials are still missing. Until this migration the only\n-- symptom was a shopper reaching checkout and receiving the payment provider's raw\n-- rejection (\"Invalid API Key provided: sk_test_*ummy\"), which tells them nothing and\n-- loses the sale outright.\n--\n-- The storefront now offers those shoppers an enquiry form in place of Add-to-Cart,\n-- and every submission lands here. The row is the product, not the email: SMTP is a\n-- separate piece of setup that the same half-configured store has usually also not\n-- done, so a notification that cannot be sent must never be the only record. The\n-- `email_delivered` flag records whether the owner was successfully notified; false\n-- means \"read this in the CMS, nobody got a mail about it\".\n--\n-- SECURITY POSTURE. Rows hold visitor-supplied PII (name, email, free text) plus a\n-- masked IP. There is deliberately NO anon policy of any kind: anonymous visitors\n-- neither read nor write this table directly. The public server action inserts with\n-- the service-role client after bot-protection and throttle checks, exactly as\n-- `privacy_consent_logs` does — that keeps the insert shape server-controlled and\n-- means a leaked anon key cannot enumerate or seed enquiries. ADMINs read; nobody\n-- else does, because these are sales leads and personal data.\n--\n-- `product_id` is a PLAIN uuid with NO foreign key, following site_script_revisions:\n-- an enquiry is evidence that someone wanted a product, and deleting the product\n-- should not delete or rewrite that evidence. `product_slug` and `product_title` are\n-- denormalised so a deleted product is still identifiable in the list.\n--\n-- Forward-only and idempotent.\n\nCREATE TABLE IF NOT EXISTS public.product_inquiries (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n -- No FK by design (see header). Null when the product was deleted after the fact.\n product_id uuid,\n product_slug text,\n product_title text,\n sender_name text NOT NULL,\n sender_email text NOT NULL,\n message text NOT NULL,\n -- Which language the visitor was browsing in, so the owner can reply in kind.\n locale text,\n ip_masked text,\n user_agent text,\n -- False when SMTP was unconfigured or the send failed: the CMS list is then the\n -- only place this enquiry exists.\n email_delivered boolean DEFAULT false NOT NULL,\n is_resolved boolean DEFAULT false NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT product_inquiries_pkey PRIMARY KEY (id),\n CONSTRAINT product_inquiries_sender_name_not_blank CHECK ((char_length(btrim(sender_name)) > 0)),\n CONSTRAINT product_inquiries_sender_email_not_blank CHECK ((char_length(btrim(sender_email)) > 0)),\n CONSTRAINT product_inquiries_message_not_blank CHECK ((char_length(btrim(message)) > 0))\n);\n\nCOMMENT ON TABLE public.product_inquiries IS 'Visitor purchase enquiries raised when the store cannot take payment. Written by the service role from a public server action; read by ADMINs only.';\n\nCOMMENT ON COLUMN public.product_inquiries.product_id IS 'Plain uuid, no FK: an enquiry outlives the product it was about.';\n\nCOMMENT ON COLUMN public.product_inquiries.ip_masked IS 'Partially masked IP (e.g. 203.0.113.x) - never store a full address. Also backs the per-IP submission throttle.';\n\nCOMMENT ON COLUMN public.product_inquiries.email_delivered IS 'False when the owner notification could not be sent (e.g. SMTP unconfigured); the stored row is then the only record.';\n\n-- Newest-first is the only listing order the CMS needs.\nCREATE INDEX IF NOT EXISTS product_inquiries_created_idx\n ON public.product_inquiries USING btree (created_at DESC);\n\n-- Backs the throttle lookup in the public server action: count recent rows per IP.\nCREATE INDEX IF NOT EXISTS product_inquiries_ip_created_idx\n ON public.product_inquiries USING btree (ip_masked, created_at DESC);\n\n-- Lets the CMS count outstanding enquiries without scanning resolved history.\nCREATE INDEX IF NOT EXISTS product_inquiries_unresolved_idx\n ON public.product_inquiries USING btree (created_at DESC)\n WHERE (is_resolved = false);\n\nALTER TABLE public.product_inquiries ENABLE ROW LEVEL SECURITY;\n\n-- anon is deliberately absent: the public path goes through the service role.\nGRANT SELECT, UPDATE ON TABLE public.product_inquiries TO authenticated;\nGRANT ALL ON TABLE public.product_inquiries TO service_role;\n\nDROP POLICY IF EXISTS product_inquiries_admin_read_policy ON public.product_inquiries;\nCREATE POLICY product_inquiries_admin_read_policy ON public.product_inquiries FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\n-- ADMINs may only flip the resolved flag; the enquiry content itself is a record.\nDROP POLICY IF EXISTS product_inquiries_admin_update_policy ON public.product_inquiries;\nCREATE POLICY product_inquiries_admin_update_policy ON public.product_inquiries FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS product_inquiries_service_role_policy ON public.product_inquiries;\nCREATE POLICY product_inquiries_service_role_policy ON public.product_inquiries TO service_role USING (true) WITH CHECK (true);\n\n-- Where enquiry notifications are sent. Kept in the PUBLIC settings row rather than the\n-- secret one because it is an address, not a credential — but it is never rendered to\n-- the storefront: the public form posts a product id and the server resolves the\n-- recipient. Empty string means \"fall back\", see resolveSellerContactEmail().\nINSERT INTO public.site_settings (key, value)\nVALUES ('store_contact', '{\"contactEmail\": \"\"}'::jsonb)\nON CONFLICT (key) DO NOTHING;\n\n-- Storefront copy for the enquiry flow. Public-facing, so it must be translatable;\n-- the components pass an English literal as the fallback, so an install that never\n-- runs this seed still renders correctly.\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES\n ('ecommerce.contact_seller', '{\"en\": \"Contact the seller\", \"fr\": \"Contacter le vendeur\"}', now(), now()),\n ('ecommerce.contact_seller_heading', '{\"en\": \"Interested in this product?\", \"fr\": \"Ce produit vous intéresse ?\"}', now(), now()),\n ('ecommerce.contact_seller_intro', '{\"en\": \"Online ordering isn''t available for this item yet. Send the seller a message and they''ll get back to you about buying it.\", \"fr\": \"La commande en ligne n''est pas encore disponible pour cet article. Envoyez un message au vendeur et il vous répondra au sujet de son achat.\"}', now(), now()),\n ('ecommerce.contact_seller_name', '{\"en\": \"Your name\", \"fr\": \"Votre nom\"}', now(), now()),\n ('ecommerce.contact_seller_email', '{\"en\": \"Your email\", \"fr\": \"Votre courriel\"}', now(), now()),\n ('ecommerce.contact_seller_message', '{\"en\": \"Message\", \"fr\": \"Message\"}', now(), now()),\n ('ecommerce.contact_seller_send', '{\"en\": \"Send message\", \"fr\": \"Envoyer le message\"}', now(), now()),\n ('ecommerce.contact_seller_sending', '{\"en\": \"Sending...\", \"fr\": \"Envoi...\"}', now(), now()),\n ('ecommerce.contact_seller_sent', '{\"en\": \"Thanks - your message has been sent to the seller. They''ll reply to the email address you gave.\", \"fr\": \"Merci - votre message a été envoyé au vendeur. Il répondra à l''adresse courriel que vous avez indiquée.\"}', now(), now()),\n ('ecommerce.contact_seller_error', '{\"en\": \"Sorry, your message couldn''t be sent. Please try again in a moment.\", \"fr\": \"Désolé, votre message n''a pas pu être envoyé. Veuillez réessayer dans un instant.\"}', now(), now()),\n ('ecommerce.contact_seller_invalid', '{\"en\": \"Please check your name, email address and message, then try again.\", \"fr\": \"Veuillez vérifier votre nom, votre adresse courriel et votre message, puis réessayer.\"}', now(), now()),\n ('ecommerce.contact_seller_throttled', '{\"en\": \"You''ve sent several messages already. Please wait a few minutes before sending another.\", \"fr\": \"Vous avez déjà envoyé plusieurs messages. Veuillez patienter quelques minutes avant d''en envoyer un autre.\"}', now(), now()),\n ('ecommerce.not_available_for_purchase', '{\"en\": \"Not available for online purchase\", \"fr\": \"Non disponible à l''achat en ligne\"}', now(), now()),\n ('ecommerce.checkout_payments_unavailable', '{\"en\": \"This store is not able to take payments right now. Please contact the seller to complete your purchase.\", \"fr\": \"Cette boutique ne peut pas accepter de paiements pour le moment. Veuillez contacter le vendeur pour finaliser votre achat.\"}', now(), now())\nON CONFLICT (key) DO NOTHING;\n"
151
+ },
152
+ {
153
+ "version": "00000000000027",
154
+ "name": "00000000000027_message_threads.sql",
155
+ "sql": "-- Private conversations with anonymous visitors, and the end of the exposed form address.\n--\n-- Two problems, one shape.\n--\n-- FIRST: a contact-form block stores its destination address in the block's own\n-- content, and `FormBlockRenderer` is a \"use client\" component that receives that\n-- content wholesale. Everything crossing that boundary is serialized into the RSC\n-- payload, so the shop owner's inbox is published in the markup of every page\n-- carrying a form. `form_endpoints` moves the address server-side and leaves behind a\n-- `form_key` — an opaque handle that grants nothing and is safe to serialize.\n--\n-- SECOND: a visitor who sends a message has nowhere to receive an answer. They are\n-- anonymous — `cms_interactions` cannot hold them, because its `user_id` is NOT NULL\n-- with a foreign key to `profiles`. So this is a separate lane: `message_threads` plus\n-- `thread_messages`, reached by a tokenised link rather than an account.\n--\n-- WHY REVIEWS AND COMMENTS ARE NOT HERE. They are already public, already tied to a\n-- registered account, and a staff answer to them is published content, not a private\n-- reply. That lane stays in `cms_interactions` and is handled by migration 28. One\n-- inbox reads both; the storage stays honest about the difference.\n--\n-- SECURITY POSTURE. Rows hold visitor PII (name, email, free text) and a masked IP.\n-- There is NO anon grant and NO anon policy: the tokenised visitor page verifies the\n-- token in application code and then reads with the service role, the same posture the\n-- MCP token route takes against `mcp_access_tokens`. ADMINs read; nobody else does.\n--\n-- `token_hash` stores only a SHA-256 of the visitor's token. The plaintext exists long\n-- enough to be placed in one outbound email and is never written down. A token is\n-- minted on the FIRST ADMIN REPLY, never at submission — a store that receives a\n-- hundred enquiries and answers three has three live credentials, not a hundred.\n--\n-- `source` is a text column with a CHECK rather than an enum, deliberately.\n-- `interaction_type` in this same schema is the cautionary tale: a two-value enum\n-- guarded by an exhaustive CHECK, where adding a third value needs ALTER TYPE ... ADD\n-- VALUE — and PostgreSQL forbids using a value added in the transaction that added it,\n-- which is exactly one migration file. A CHECK is replaceable in one statement.\n--\n-- Forward-only and idempotent.\n\n-- ---------------------------------------------------------------------------\n-- 1. form_endpoints — where a contact-form block's mail goes, and what its fields\n-- are called. `fields` is a SERVER-SIDE snapshot of the field manifest so the\n-- notification email renders labels the browser did not supply; the same rule\n-- the product-enquiry action follows when it looks the product title up itself.\n-- ---------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS public.form_endpoints (\n form_key uuid NOT NULL,\n label text DEFAULT 'Contact form' NOT NULL,\n -- NULL/empty means \"fall back to the resolver ladder\", like store_contact.\n recipient_email text,\n fields jsonb DEFAULT '[]'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT form_endpoints_pkey PRIMARY KEY (form_key),\n CONSTRAINT form_endpoints_label_not_blank CHECK ((char_length(btrim(label)) > 0)),\n CONSTRAINT form_endpoints_fields_array CHECK ((jsonb_typeof(fields) = 'array'))\n);\n\nCOMMENT ON TABLE public.form_endpoints IS 'Server-side destination and field manifest for a contact-form block, keyed by the non-secret form_key carried in block content. The address never reaches the browser.';\n\nCOMMENT ON COLUMN public.form_endpoints.form_key IS 'Non-secret opaque handle. Safe in the RSC payload: it grants nothing, it is only a lookup key.';\n\nCOMMENT ON COLUMN public.form_endpoints.fields IS 'Snapshot of [{temp_id,label,field_type}] written by the CMS editor, so labels in emails and the inbox are server-trusted.';\n\nDROP TRIGGER IF EXISTS set_form_endpoints_updated_at ON public.form_endpoints;\nCREATE TRIGGER set_form_endpoints_updated_at BEFORE UPDATE ON public.form_endpoints\n FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\n\nALTER TABLE public.form_endpoints ENABLE ROW LEVEL SECURITY;\n\n-- anon deliberately absent: the public submit path goes through the service role.\nGRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.form_endpoints TO authenticated;\nGRANT ALL ON TABLE public.form_endpoints TO service_role;\n\nDROP POLICY IF EXISTS form_endpoints_editor_read_policy ON public.form_endpoints;\nCREATE POLICY form_endpoints_editor_read_policy ON public.form_endpoints FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])));\n\nDROP POLICY IF EXISTS form_endpoints_admin_write_policy ON public.form_endpoints;\nCREATE POLICY form_endpoints_admin_write_policy ON public.form_endpoints FOR ALL TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS form_endpoints_service_role_policy ON public.form_endpoints;\nCREATE POLICY form_endpoints_service_role_policy ON public.form_endpoints TO service_role USING (true) WITH CHECK (true);\n\n-- ---------------------------------------------------------------------------\n-- 2. message_threads — the private-lane spine and the only home of a thread token.\n-- `subject_id` and `form_key` are PLAIN uuids with NO foreign key: a conversation\n-- outlives the product or the form block it started from.\n-- ---------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS public.message_threads (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n source text NOT NULL,\n -- product_inquiries.id when source = 'product_inquiry'.\n subject_id uuid,\n -- form_endpoints.form_key when source = 'contact_form'.\n form_key uuid,\n -- Denormalised so a deleted product or removed form block stays identifiable.\n subject_label text DEFAULT 'Message' NOT NULL,\n sender_name text,\n -- NULL when a contact form collected no email field. Such a thread can never be\n -- answered, and the CMS says so rather than failing silently at reply time.\n sender_email text,\n locale text,\n -- Submitted field map for a contact form: {temp_id: value}.\n fields jsonb DEFAULT '{}'::jsonb NOT NULL,\n status text DEFAULT 'open' NOT NULL,\n unread_for_admin boolean DEFAULT true NOT NULL,\n unread_for_visitor boolean DEFAULT false NOT NULL,\n -- SHA-256 hex of the visitor's token; NULL until the first admin reply.\n token_hash text,\n token_expires_at timestamp with time zone,\n token_revoked_at timestamp with time zone,\n token_last_used_at timestamp with time zone,\n last_message_at timestamp with time zone DEFAULT now() NOT NULL,\n ip_masked text,\n user_agent text,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT message_threads_pkey PRIMARY KEY (id),\n CONSTRAINT message_threads_token_hash_key UNIQUE (token_hash),\n CONSTRAINT message_threads_source_check\n CHECK ((source = ANY (ARRAY['product_inquiry'::text, 'contact_form'::text]))),\n CONSTRAINT message_threads_status_check\n CHECK ((status = ANY (ARRAY['open'::text, 'closed'::text]))),\n CONSTRAINT message_threads_subject_check CHECK (\n (((source = 'product_inquiry'::text) AND (subject_id IS NOT NULL)) OR\n ((source = 'contact_form'::text) AND (form_key IS NOT NULL)))),\n CONSTRAINT message_threads_fields_object CHECK ((jsonb_typeof(fields) = 'object'))\n);\n\nCOMMENT ON TABLE public.message_threads IS 'Private conversations with anonymous visitors (product enquiries and contact-form submissions). Written by the service role from public server actions; read by ADMINs only. Public reviews and comments are NOT here - they live in cms_interactions.';\n\nCOMMENT ON COLUMN public.message_threads.token_hash IS 'SHA-256 hex of the visitor thread token. The raw token is never stored; it is minted on the first admin reply and mailed once.';\n\nCOMMENT ON COLUMN public.message_threads.subject_id IS 'product_inquiries.id. Plain uuid, no FK: a conversation outlives the enquiry record it grew from.';\n\nCREATE INDEX IF NOT EXISTS message_threads_last_message_idx\n ON public.message_threads USING btree (last_message_at DESC);\n\nCREATE INDEX IF NOT EXISTS message_threads_source_last_idx\n ON public.message_threads USING btree (source, last_message_at DESC);\n\n-- Backs the CMS nav unread badge without scanning read history.\nCREATE INDEX IF NOT EXISTS message_threads_unread_idx\n ON public.message_threads USING btree (last_message_at DESC)\n WHERE (unread_for_admin = true);\n\n-- Idempotent backfill / dedupe of enquiry threads.\nCREATE INDEX IF NOT EXISTS message_threads_subject_idx\n ON public.message_threads USING btree (subject_id) WHERE (subject_id IS NOT NULL);\n\n-- No separate token index: message_threads_token_hash_key is already the unique\n-- btree the /thread lookup probes.\n\nDROP TRIGGER IF EXISTS set_message_threads_updated_at ON public.message_threads;\nCREATE TRIGGER set_message_threads_updated_at BEFORE UPDATE ON public.message_threads\n FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\n\nALTER TABLE public.message_threads ENABLE ROW LEVEL SECURITY;\n\nGRANT SELECT, UPDATE ON TABLE public.message_threads TO authenticated;\nGRANT ALL ON TABLE public.message_threads TO service_role;\n\nDROP POLICY IF EXISTS message_threads_admin_read_policy ON public.message_threads;\nCREATE POLICY message_threads_admin_read_policy ON public.message_threads FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS message_threads_admin_update_policy ON public.message_threads;\nCREATE POLICY message_threads_admin_update_policy ON public.message_threads FOR UPDATE TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)) WITH CHECK ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS message_threads_service_role_policy ON public.message_threads;\nCREATE POLICY message_threads_service_role_policy ON public.message_threads TO service_role USING (true) WITH CHECK (true);\n\n-- ---------------------------------------------------------------------------\n-- 3. thread_messages — one turn of a private conversation.\n-- ---------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS public.thread_messages (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n thread_id uuid NOT NULL,\n direction text NOT NULL,\n body text NOT NULL,\n -- profiles.id for an outbound reply. No FK: a departing admin must not take the\n -- conversation with them.\n author_id uuid,\n author_name text,\n -- False when SMTP was unconfigured or the send failed. sendEmail() throws when\n -- unconfigured, so this row - not the mail - is the record.\n email_delivered boolean DEFAULT false NOT NULL,\n email_error text,\n ip_masked text,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT thread_messages_pkey PRIMARY KEY (id),\n CONSTRAINT thread_messages_direction_check\n CHECK ((direction = ANY (ARRAY['inbound'::text, 'outbound'::text]))),\n CONSTRAINT thread_messages_body_not_blank CHECK ((char_length(btrim(body)) > 0))\n);\n\nCOMMENT ON TABLE public.thread_messages IS 'Turns of a private conversation. Content is append-only; only the delivery flags may change after insert.';\n\n-- A real foreign key here, unlike the outward pointers above: a message has no\n-- meaning without its thread.\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint\n WHERE conname = 'thread_messages_thread_id_fkey'\n AND conrelid = 'public.thread_messages'::regclass) THEN\n ALTER TABLE ONLY public.thread_messages\n ADD CONSTRAINT thread_messages_thread_id_fkey FOREIGN KEY (thread_id)\n REFERENCES public.message_threads(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\nCREATE INDEX IF NOT EXISTS thread_messages_thread_created_idx\n ON public.thread_messages USING btree (thread_id, created_at);\n\n-- Backs the per-IP reply throttle on the public thread page.\nCREATE INDEX IF NOT EXISTS thread_messages_ip_created_idx\n ON public.thread_messages USING btree (ip_masked, created_at DESC);\n\n-- Message TEXT is append-only — not the whole row, because email_delivered has to be\n-- settable after the send completes. The trigger blocks rewrites of the load-bearing\n-- columns only, and it binds the service role too, which otherwise bypasses RLS.\nCREATE OR REPLACE FUNCTION public.prevent_thread_message_rewrite() RETURNS trigger\n LANGUAGE plpgsql SET search_path = '' AS $$\nBEGIN\n IF TG_OP = 'DELETE' THEN\n RAISE EXCEPTION 'thread_messages is append-only; DELETE is not permitted'\n USING ERRCODE = 'restrict_violation';\n END IF;\n IF NEW.body IS DISTINCT FROM OLD.body\n OR NEW.direction IS DISTINCT FROM OLD.direction\n OR NEW.thread_id IS DISTINCT FROM OLD.thread_id\n OR NEW.created_at IS DISTINCT FROM OLD.created_at THEN\n RAISE EXCEPTION 'thread_messages content is append-only; only delivery flags may change'\n USING ERRCODE = 'restrict_violation';\n END IF;\n RETURN NEW;\nEND;\n$$;\n\nDROP TRIGGER IF EXISTS trg_thread_messages_append_only ON public.thread_messages;\nCREATE TRIGGER trg_thread_messages_append_only BEFORE UPDATE OR DELETE ON public.thread_messages\n FOR EACH ROW EXECUTE FUNCTION public.prevent_thread_message_rewrite();\n\n-- NB: the ON DELETE CASCADE above fires this trigger, so deleting a thread is\n-- impossible by design. \"Delete\" in the CMS means status='closed' plus a revoked\n-- token, and the UI says so.\n\nALTER TABLE public.thread_messages ENABLE ROW LEVEL SECURITY;\n\nGRANT SELECT ON TABLE public.thread_messages TO authenticated;\nGRANT ALL ON TABLE public.thread_messages TO service_role;\n\nDROP POLICY IF EXISTS thread_messages_admin_read_policy ON public.thread_messages;\nCREATE POLICY thread_messages_admin_read_policy ON public.thread_messages FOR SELECT TO authenticated USING ((( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS thread_messages_service_role_policy ON public.thread_messages;\nCREATE POLICY thread_messages_service_role_policy ON public.thread_messages TO service_role USING (true) WITH CHECK (true);\n\n-- ---------------------------------------------------------------------------\n-- 4. Backfill: every existing enquiry becomes a thread whose first inbound turn is\n-- the original message. product_inquiries is KEPT and kept written — it is the\n-- enquiry's own record and owns the ip_masked the submission throttle counts.\n-- The thread is a conversation ABOUT the enquiry, not a replacement for it.\n-- ---------------------------------------------------------------------------\nINSERT INTO public.message_threads\n (source, subject_id, subject_label, sender_name, sender_email, locale,\n ip_masked, user_agent, unread_for_admin, status, last_message_at, created_at)\nSELECT 'product_inquiry', i.id, COALESCE(i.product_title, 'Product enquiry'),\n i.sender_name, i.sender_email, i.locale, i.ip_masked, i.user_agent,\n NOT i.is_resolved, CASE WHEN i.is_resolved THEN 'closed' ELSE 'open' END,\n i.created_at, i.created_at\nFROM public.product_inquiries i\nWHERE NOT EXISTS (SELECT 1 FROM public.message_threads t WHERE t.subject_id = i.id);\n\nINSERT INTO public.thread_messages (thread_id, direction, body, email_delivered, ip_masked, created_at)\nSELECT t.id, 'inbound', i.message, i.email_delivered, i.ip_masked, i.created_at\nFROM public.product_inquiries i\nJOIN public.message_threads t ON t.subject_id = i.id\nWHERE NOT EXISTS (SELECT 1 FROM public.thread_messages m WHERE m.thread_id = t.id);\n\n-- ---------------------------------------------------------------------------\n-- 5. The form-block data migration.\n-- Rewrites every stored form block: mints a form_key, moves recipient_email into\n-- form_endpoints, snapshots the field manifest, and DELETES recipient_email from\n-- the jsonb. Three shapes have to be handled, because a form nested inside a\n-- section has no blocks row of its own — it lives in the section's content:\n-- a) blocks.content where block_type = 'form'\n-- b) blocks.content where block_type = 'section' (column_blocks, slides)\n-- c) content_drafts.blocks / product_drafts.blocks (jsonb arrays of snapshots)\n-- ---------------------------------------------------------------------------\nCREATE OR REPLACE FUNCTION public.nb_migrate_form_columns(p_cols jsonb)\nRETURNS jsonb LANGUAGE plpgsql VOLATILE SET search_path = '' AS $fn$\nDECLARE v_col jsonb; v_blk jsonb; v_new_cols jsonb := '[]'::jsonb; v_new_col jsonb;\nBEGIN\n FOR v_col IN SELECT * FROM jsonb_array_elements(p_cols) LOOP\n v_new_col := '[]'::jsonb;\n FOR v_blk IN SELECT * FROM jsonb_array_elements(coalesce(v_col, '[]'::jsonb)) LOOP\n v_new_col := v_new_col || jsonb_build_array(\n v_blk || jsonb_build_object('content', public.nb_migrate_form_content(v_blk->'content')));\n END LOOP;\n v_new_cols := v_new_cols || jsonb_build_array(v_new_col);\n END LOOP;\n RETURN v_new_cols;\nEND;\n$fn$;\n\nCREATE OR REPLACE FUNCTION public.nb_migrate_form_content(p_content jsonb)\nRETURNS jsonb LANGUAGE plpgsql VOLATILE SET search_path = '' AS $fn$\nDECLARE\n v_key uuid; v_email text; v_fields jsonb; v_slide jsonb; v_new_slides jsonb;\nBEGIN\n IF p_content IS NULL OR jsonb_typeof(p_content) <> 'object' THEN\n RETURN p_content;\n END IF;\n\n -- Leaf: this object IS a form block's content.\n IF (p_content ? 'recipient_email') AND (p_content ? 'fields') THEN\n v_key := gen_random_uuid();\n v_email := nullif(btrim(coalesce(p_content->>'recipient_email','')), '');\n SELECT coalesce(jsonb_agg(jsonb_build_object(\n 'temp_id', f->>'temp_id', 'label', f->>'label', 'field_type', f->>'field_type')), '[]'::jsonb)\n INTO v_fields\n FROM jsonb_array_elements(coalesce(p_content->'fields', '[]'::jsonb)) f;\n INSERT INTO public.form_endpoints (form_key, label, recipient_email, fields)\n VALUES (v_key, 'Contact form', v_email, v_fields)\n ON CONFLICT (form_key) DO NOTHING;\n RETURN (p_content - 'recipient_email') || jsonb_build_object('form_key', v_key::text);\n END IF;\n\n -- Container: a section's slides, each with column_blocks.\n IF (p_content ? 'slides') AND jsonb_typeof(p_content->'slides') = 'array' THEN\n v_new_slides := '[]'::jsonb;\n FOR v_slide IN SELECT * FROM jsonb_array_elements(p_content->'slides') LOOP\n v_new_slides := v_new_slides || jsonb_build_array(\n v_slide || jsonb_build_object('column_blocks',\n public.nb_migrate_form_columns(coalesce(v_slide->'column_blocks','[]'::jsonb))));\n END LOOP;\n p_content := p_content || jsonb_build_object('slides', v_new_slides);\n END IF;\n\n -- Container: a standard section's column_blocks.\n IF (p_content ? 'column_blocks') AND jsonb_typeof(p_content->'column_blocks') = 'array' THEN\n p_content := p_content || jsonb_build_object('column_blocks',\n public.nb_migrate_form_columns(p_content->'column_blocks'));\n END IF;\n\n RETURN p_content;\nEND;\n$fn$;\n\n-- The LIKE guard keeps this from rewriting (and bumping updated_at on) every block.\nUPDATE public.blocks\n SET content = public.nb_migrate_form_content(content)\n WHERE content::text LIKE '%recipient_email%';\n\nUPDATE public.content_drafts d\n SET blocks = (SELECT coalesce(jsonb_agg(b || jsonb_build_object(\n 'content', public.nb_migrate_form_content(b->'content'))), '[]'::jsonb)\n FROM jsonb_array_elements(d.blocks) b)\n WHERE d.blocks::text LIKE '%recipient_email%';\n\nUPDATE public.product_drafts d\n SET blocks = (SELECT coalesce(jsonb_agg(b || jsonb_build_object(\n 'content', public.nb_migrate_form_content(b->'content'))), '[]'::jsonb)\n FROM jsonb_array_elements(d.blocks) b)\n WHERE d.blocks::text LIKE '%recipient_email%';\n\n-- One-shot helpers. Dropping them keeps the public schema (and db:types) clean.\nDROP FUNCTION IF EXISTS public.nb_migrate_form_content(jsonb);\nDROP FUNCTION IF EXISTS public.nb_migrate_form_columns(jsonb);\n\n-- ---------------------------------------------------------------------------\n-- 6. Seeds. Thread-page copy is public-facing so it must be translatable; the\n-- components pass an English literal as the fallback, so an install that never\n-- runs this seed still renders correctly.\n-- ---------------------------------------------------------------------------\nINSERT INTO public.site_settings (key, value)\nVALUES ('forms_contact', '{\"contactEmail\": \"\"}'::jsonb)\nON CONFLICT (key) DO NOTHING;\n\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES\n ('thread.heading', '{\"en\": \"Your conversation\", \"fr\": \"Votre conversation\"}', now(), now()),\n ('thread.reply_label', '{\"en\": \"Write a reply\", \"fr\": \"Écrire une réponse\"}', now(), now()),\n ('thread.send', '{\"en\": \"Send reply\", \"fr\": \"Envoyer la réponse\"}', now(), now()),\n ('thread.sending', '{\"en\": \"Sending...\", \"fr\": \"Envoi...\"}', now(), now()),\n ('thread.sent', '{\"en\": \"Thanks - your reply has been sent.\", \"fr\": \"Merci - votre réponse a été envoyée.\"}', now(), now()),\n ('thread.error', '{\"en\": \"Sorry, your reply couldn''t be sent. Please try again in a moment.\", \"fr\": \"Désolé, votre réponse n''a pas pu être envoyée. Veuillez réessayer dans un instant.\"}', now(), now()),\n ('thread.throttled', '{\"en\": \"You''ve sent several replies already. Please wait a few minutes.\", \"fr\": \"Vous avez déjà envoyé plusieurs réponses. Veuillez patienter quelques minutes.\"}', now(), now()),\n ('thread.closed', '{\"en\": \"This conversation has been closed.\", \"fr\": \"Cette conversation est fermée.\"}', now(), now()),\n ('thread.invalid', '{\"en\": \"This link has expired or is no longer valid. If you still need help, please contact us again from our website.\", \"fr\": \"Ce lien a expiré ou n''est plus valide. Si vous avez encore besoin d''aide, contactez-nous de nouveau depuis notre site.\"}', now(), now()),\n ('thread.you', '{\"en\": \"You\", \"fr\": \"Vous\"}', now(), now()),\n ('forms.submission_stored', '{\"en\": \"Thanks - your message has been received.\", \"fr\": \"Merci - votre message a bien été reçu.\"}', now(), now())\nON CONFLICT (key) DO NOTHING;\n"
156
+ },
157
+ {
158
+ "version": "00000000000028",
159
+ "name": "00000000000028_interaction_replies.sql",
160
+ "sql": "-- Staff replies on reviews and post comments, and the indexes cms_interactions never had.\n--\n-- WHY THIS IS A DIFFERENT SHAPE FROM MIGRATION 27, AND WHY IT NEEDS NO TOKEN.\n-- `cms_interactions.user_id` is NOT NULL with a foreign key to `profiles`: every review\n-- and comment already comes from a signed-in account with a reachable address, and the\n-- content already renders publicly on /product/{slug} and /article/{slug}. A staff\n-- answer there is PUBLISHED CONTENT plus moderation, not a private conversation.\n-- Routing it through the tokenised thread page would hide a public reply behind a\n-- secret link. So: one inbox in the CMS, two storage models underneath.\n--\n-- THE ONE CONSTRAINT THAT DICTATES THE MODELLING.\n-- `check_rating_only_for_review` is an exhaustive OR over ('review','comment'):\n-- (type='review' AND rating IS NOT NULL AND rating BETWEEN 1 AND 5)\n-- OR (type='comment' AND rating IS NULL)\n-- A type='review' row with a NULL rating is rejected, so a reply to a review cannot be\n-- a 'review' row. Adding a 'reply' value to interaction_type would ALSO violate it —\n-- the OR covers no third value — and PostgreSQL forbids using an enum value in the\n-- transaction that added it, which is exactly one migration file.\n--\n-- THEREFORE a reply is a `type='comment'` row carrying the PARENT's target and a NULL\n-- rating: a combination the existing constraints already accept, unchanged. The bonus\n-- is decisive — `update_product_ratings()` aggregates only\n-- WHERE product_id = ? AND type='review' AND status='approved'\n-- so a reply can never move products.average_rating or products.total_reviews. The new\n-- CHECK below pins that invariant in the schema instead of trusting the action to\n-- remember it.\n--\n-- No new policies are needed. `cms_interactions_insert_policy` already admits an\n-- ADMIN/WRITER self-insert with status='approved'\n-- (auth.uid() = user_id AND (status='pending' OR role IN ('ADMIN','WRITER')))\n-- which is precisely a published staff reply.\n--\n-- Forward-only and idempotent.\n\nALTER TABLE public.cms_interactions ADD COLUMN IF NOT EXISTS parent_id uuid;\n\nCOMMENT ON COLUMN public.cms_interactions.parent_id IS 'Set on a staff reply: points at the review or comment being answered. Replies are always type=comment with a NULL rating, and carry the parent''s product_id/post_id so check_product_or_post still holds.';\n\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint\n WHERE conname = 'cms_interactions_parent_id_fkey'\n AND conrelid = 'public.cms_interactions'::regclass) THEN\n ALTER TABLE ONLY public.cms_interactions\n ADD CONSTRAINT cms_interactions_parent_id_fkey FOREIGN KEY (parent_id)\n REFERENCES public.cms_interactions(id) ON DELETE CASCADE;\n END IF;\nEND $rb$;\n\n-- One level only, never itself, no rating, always a comment. Keeps the reply\n-- invisible to the ratings trigger and to the existing storefront review query.\nDO $rb$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint\n WHERE conname = 'cms_interactions_reply_check'\n AND conrelid = 'public.cms_interactions'::regclass) THEN\n ALTER TABLE public.cms_interactions\n ADD CONSTRAINT cms_interactions_reply_check CHECK (\n ((parent_id IS NULL) OR\n ((parent_id <> id) AND (rating IS NULL) AND (type = 'comment'::public.interaction_type))));\n END IF;\nEND $rb$;\n\n-- Fetch every reply for a page of parents in one probe.\nCREATE INDEX IF NOT EXISTS cms_interactions_parent_idx\n ON public.cms_interactions USING btree (parent_id, created_at)\n WHERE (parent_id IS NOT NULL);\n\n-- cms_interactions has had ZERO secondary indexes since the baseline, while both\n-- public renderers filter on exactly these predicates on every product and article\n-- page. The inbox multiplies read volume on this table, so they go in now.\nCREATE INDEX IF NOT EXISTS cms_interactions_product_type_status_idx\n ON public.cms_interactions USING btree (product_id, type, status, created_at DESC)\n WHERE (product_id IS NOT NULL);\n\nCREATE INDEX IF NOT EXISTS cms_interactions_post_type_status_idx\n ON public.cms_interactions USING btree (post_id, type, status, created_at DESC)\n WHERE (post_id IS NOT NULL);\n\n-- Inbox listing: newest top-level items, replies excluded.\nCREATE INDEX IF NOT EXISTS cms_interactions_inbox_idx\n ON public.cms_interactions USING btree (created_at DESC)\n WHERE (parent_id IS NULL);\n\nINSERT INTO public.translations (key, translations, created_at, updated_at) VALUES\n ('interactions.staff_reply', '{\"en\": \"Reply from the team\", \"fr\": \"Réponse de l''équipe\"}', now(), now()),\n ('interactions.staff_badge', '{\"en\": \"Staff\", \"fr\": \"Équipe\"}', now(), now())\nON CONFLICT (key) DO NOTHING;\n"
161
+ },
162
+ {
163
+ "version": "00000000000029",
164
+ "name": "00000000000029_form_endpoints_default_empty.sql",
165
+ "sql": "-- A contact form has no address of its own by default.\n--\n-- The starter content ships a contact page addressed to `contact@example.com`, and\n-- migration 27 faithfully carried that placeholder into `form_endpoints`. Faithful was\n-- the wrong call. `example.com` is reserved by RFC 2606 precisely so it can never be\n-- registered, so the address is not merely unhelpful — it is guaranteed undeliverable,\n-- while looking like a real setting to every layer downstream. The visitor is thanked,\n-- the relay accepts the message, and nobody is ever notified. Nothing errors, so an\n-- install can run that way indefinitely.\n--\n-- Since the messaging system arrived, a per-form address is not something an operator\n-- needs to think about at all. Submissions are stored as threads and answered in\n-- CMS → Messages; the notification address is a single site-wide setting, and a form\n-- only carries its own address when someone deliberately wants that form routed\n-- elsewhere (a careers form to HR, say).\n--\n-- So the default becomes NULL, meaning \"use the site contact address\", which resolves\n-- to the address set in CMS → Messages and finally to the first admin's own login. An\n-- install therefore reaches a real human out of the box without configuring anything.\n--\n-- The sandbox is unaffected: `resolveFormRecipient` overrides everything with\n-- SANDBOX_CONTACT_EMAIL when NEXT_PUBLIC_IS_SANDBOX is set, so the hosted demo keeps\n-- routing to the operator's own inbox without storing an address here.\n--\n-- Forward-only and idempotent.\n\n-- Reserved domains from RFC 2606 / RFC 6761. Matching on the domain rather than the\n-- exact seeded string also catches an operator who typed their own placeholder.\nUPDATE public.form_endpoints\n SET recipient_email = NULL\n WHERE recipient_email IS NOT NULL\n AND (\n lower(recipient_email) LIKE '%@example.com'\n OR lower(recipient_email) LIKE '%@example.org'\n OR lower(recipient_email) LIKE '%@example.net'\n OR lower(recipient_email) LIKE '%@example.edu'\n OR lower(recipient_email) LIKE '%.example'\n OR lower(recipient_email) LIKE '%.invalid'\n OR lower(recipient_email) LIKE '%.test'\n OR lower(recipient_email) LIKE '%.localhost'\n OR lower(recipient_email) LIKE '%.local'\n );\n\nCOMMENT ON COLUMN public.form_endpoints.recipient_email IS 'Per-form override. NULL means \"use the site contact address\" (CMS -> Messages, falling back to the first admin), which is the default and the usual case.';\n\n-- Same reasoning for the site-wide rows: a placeholder there is worse than an empty\n-- value, because an empty one falls through to the first admin and actually arrives.\nUPDATE public.site_settings\n SET value = jsonb_set(value, '{contactEmail}', '\"\"'::jsonb)\n WHERE key IN ('forms_contact', 'store_contact')\n AND value ? 'contactEmail'\n AND lower(value->>'contactEmail') LIKE '%@example.%';\n"
166
+ },
167
+ {
168
+ "version": "00000000000030",
169
+ "name": "00000000000030_seo_redirects_and_robots.sql",
170
+ "sql": "-- SEO engine: managed 301/302 redirects and operator-configurable robots directives.\n--\n-- Two unrelated-looking things ship in one migration because they are the same\n-- feature from an operator's point of view: the `/cms/settings/seo` screen is where\n-- someone goes to say \"this URL moved\" and \"do not crawl that\". Splitting them\n-- across two migrations would only mean two files that must always be applied\n-- together.\n--\n-- WHY A TABLE AND NOT next.config.js redirects(). Redirects are content, not\n-- configuration. An editor who renames a page's slug needs the old URL to keep\n-- working immediately, without a redeploy and without touching source control.\n-- That rules out the build-time array; it has to be data.\n--\n-- WHY THE PROXY READS THIS WITH THE ANON KEY. apps/nextblock/proxy.ts (Next 16's\n-- renamed middleware) resolves redirects before rendering, and it holds a Supabase\n-- client built from the anon key. So the public SELECT policy below is load-bearing:\n-- without an explicit `TO authenticated, anon` grant the proxy's lookup would return\n-- zero rows for every anonymous visitor -- silently, with no error -- and no redirect\n-- would ever fire. Only is_active rows are exposed, so a half-written rule is never\n-- live.\n--\n-- WHY status_code IS AN integer AND NOT AN ENUM. The same reasoning migration 27\n-- recorded for `source`: a Postgres enum cannot be extended and used inside the same\n-- transaction, which is exactly the scope of one migration file. A CHECK constraint\n-- is replaceable in a single statement. 301 and 302 are the only two values the\n-- admin UI offers; 307/308 are deliberately not exposed, because their\n-- method-preserving semantics surprise operators who just want \"this page moved\".\n--\n-- LOOP SAFETY is enforced in application code (wouldCreateLoop in\n-- @nextblock-cms/utils, called by the admin server actions) rather than by a\n-- constraint, because detecting a cycle requires walking the whole table and a CHECK\n-- constraint can only see one row. The self-redirect case IS cheap to check per row,\n-- so that one is a constraint -- it is the cycle operators actually hit.\n--\n-- SECURITY POSTURE. A redirect can send every visitor of a path to an arbitrary\n-- external origin, which makes this table an open-redirect surface and a phishing\n-- lever. Writes are therefore ADMIN-only -- WRITER is deliberately excluded, matching\n-- site_scripts rather than the content tables. Reads are public but limited to active\n-- rows. The robots settings live in site_settings, whose existing read policy is\n-- already public for non-secret keys; nothing here is secret.\n--\n-- Forward-only and idempotent.\n\nCREATE TABLE IF NOT EXISTS public.cms_redirects (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n -- The incoming site-relative path to match, normalized by the application to a\n -- leading slash with no trailing slash (except root). Matching is exact: prefix\n -- and wildcard rules are deliberately not supported, because they are the usual\n -- way an operator builds an accidental loop.\n source_path text NOT NULL,\n -- Where to send the visitor. Either another site-relative path or a fully\n -- qualified https URL for an off-site move.\n destination_path text NOT NULL,\n -- 301 permanent (the SEO-meaningful one: search engines transfer ranking signals\n -- and browsers cache it aggressively) or 302 temporary.\n status_code integer DEFAULT 301 NOT NULL,\n is_active boolean DEFAULT true NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT cms_redirects_pkey PRIMARY KEY (id),\n -- One rule per source. This UNIQUE constraint also provides the index the proxy\n -- lookup relies on, so no separate plain index on source_path is needed.\n CONSTRAINT cms_redirects_source_path_key UNIQUE (source_path),\n CONSTRAINT cms_redirects_status_code_check\n CHECK ((status_code = ANY (ARRAY[301, 302]))),\n -- A source is always a path on this site; accepting an absolute URL here would\n -- silently never match, since the proxy only ever compares pathnames.\n CONSTRAINT cms_redirects_source_path_check\n CHECK ((source_path ~ '^/')),\n -- A destination is either a site-relative path or an https URL. Plain http is\n -- refused so a redirect can never downgrade a visitor to cleartext.\n CONSTRAINT cms_redirects_destination_path_check\n CHECK (((destination_path ~ '^/') OR (destination_path ~ '^https://'))),\n -- The one cycle a single row can express, and the one operators actually create.\n CONSTRAINT cms_redirects_no_self_redirect_check\n CHECK ((source_path <> destination_path))\n);\n\nCOMMENT ON TABLE public.cms_redirects IS\n 'Operator-managed 301/302 redirects resolved by apps/nextblock/proxy.ts before rendering. Only is_active rows are publicly readable; only ADMIN may write, because a redirect rule is an open-redirect surface.';\nCOMMENT ON COLUMN public.cms_redirects.source_path IS\n 'Exact site-relative path to match, normalized to a leading slash and no trailing slash (except root). No wildcards, by design.';\nCOMMENT ON COLUMN public.cms_redirects.destination_path IS\n 'Site-relative path or absolute https URL to send the visitor to.';\nCOMMENT ON COLUMN public.cms_redirects.status_code IS\n '301 permanent or 302 temporary. 307/308 are not offered by the admin UI.';\nCOMMENT ON COLUMN public.cms_redirects.is_active IS\n 'Only active rows are readable by anon, and only active rows are matched by the proxy.';\n\n-- The proxy loads the whole active set once per cache window rather than querying per\n-- request, so the hot query is \"all active rows\" and this partial index is what serves\n-- it. Carrying source_path in the index keeps that read index-only.\nCREATE INDEX IF NOT EXISTS cms_redirects_active_source_idx\n ON public.cms_redirects USING btree (source_path) WHERE (is_active);\n\nDROP TRIGGER IF EXISTS set_cms_redirects_updated_at ON public.cms_redirects;\nCREATE TRIGGER set_cms_redirects_updated_at\n BEFORE UPDATE ON public.cms_redirects\n FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();\n\nALTER TABLE public.cms_redirects ENABLE ROW LEVEL SECURITY;\n\nGRANT ALL ON TABLE public.cms_redirects TO anon;\nGRANT ALL ON TABLE public.cms_redirects TO authenticated;\nGRANT ALL ON TABLE public.cms_redirects TO service_role;\n\n-- The proxy runs as anon for a logged-out visitor, which is the overwhelming majority\n-- of traffic and the only traffic redirects really matter for. Without this policy the\n-- lookup returns zero rows and the feature is silently dead.\nDROP POLICY IF EXISTS \"Public read active redirects\" ON public.cms_redirects;\nCREATE POLICY \"Public read active redirects\" ON public.cms_redirects\n FOR SELECT TO authenticated, anon USING (is_active);\n\nDROP POLICY IF EXISTS \"Admins read all redirects\" ON public.cms_redirects;\nCREATE POLICY \"Admins read all redirects\" ON public.cms_redirects\n FOR SELECT TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins insert redirects\" ON public.cms_redirects;\nCREATE POLICY \"Admins insert redirects\" ON public.cms_redirects\n FOR INSERT TO authenticated\n WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins update redirects\" ON public.cms_redirects;\nCREATE POLICY \"Admins update redirects\" ON public.cms_redirects\n FOR UPDATE TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role))\n WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\nDROP POLICY IF EXISTS \"Admins delete redirects\" ON public.cms_redirects;\nCREATE POLICY \"Admins delete redirects\" ON public.cms_redirects\n FOR DELETE TO authenticated\n USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));\n\n-- Robots directives live in site_settings rather than a table of their own: there is\n-- exactly one robots.txt per install, so a dedicated table would only ever hold one\n-- row, and site_settings already carries the public-read / staff-write policy this\n-- needs. The stored shape matches `RobotsSettings` in @nextblock-cms/utils, and\n-- `normalizeRobotsSettings` tolerates a missing or malformed value -- so this seed is\n-- a convenience for the settings screen, not a correctness requirement for rendering.\n--\n-- ON CONFLICT DO NOTHING because an install that has already configured robots must\n-- not have its rules reset by a replay of this migration.\nINSERT INTO public.site_settings (key, value)\nVALUES (\n 'seo_robots_settings',\n '{\"customRules\": \"\", \"isIndexingEnabled\": true, \"sitemapEnabled\": true, \"userAgentRules\": [{\"allow\": [\"/\"], \"disallow\": [], \"userAgent\": \"*\"}]}'::jsonb\n)\nON CONFLICT (key) DO NOTHING;\n"
171
+ },
172
+ {
173
+ "version": "00000000000031",
174
+ "name": "00000000000031_seo_robots_settings_admin_only.sql",
175
+ "sql": "-- Restrict writes to the robots.txt settings row to ADMIN only.\n--\n-- `site_settings.seo_robots_settings` is the row that decides whether the whole site\n-- is crawlable: app/robots.ts reads it on every /robots.txt hit and serves a blanket\n-- `Disallow: /` when `isIndexingEnabled` is false. The CMS only offers that switch\n-- under /cms/settings/seo, and `saveRobotsSettings` re-checks for ADMIN before it\n-- writes — but RLS is the independent boundary, and it did not agree. The baseline\n-- write policies let ADMIN *or* WRITER write any key outside the sensitive array, so\n-- a WRITER holding a normal session could PATCH this row through PostgREST and take\n-- the entire site out of Google. That failure is quiet (nothing in the CMS shows it),\n-- slow to notice (search traffic decays over weeks) and hard to attribute after the\n-- fact, which is why the database has to refuse it rather than trusting the one\n-- server action that happens to guard it today.\n--\n-- The SELECT policy is deliberately NOT touched. This key must stay anon-READABLE:\n-- app/robots.ts reads it with the anon (SSG) client on every crawl, and adding the key\n-- to the read policy's sensitive array would make robots.txt fall back to its\n-- permissive defaults for every crawler. Only INSERT/UPDATE/DELETE move to ADMIN-only,\n-- matching the UI boundary — exactly the shape migration 00000000000011 used for\n-- language_detection_settings, which is anon-readable for the same reason.\n--\n-- Every key already present in each policy's array is preserved (dropping one would\n-- silently widen write access back to WRITER for that key); `seo_robots_settings` is\n-- appended to the three write policies only.\n\nDROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;\nCREATE POLICY site_settings_insert_policy ON public.site_settings FOR INSERT TO authenticated WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text, 'seo_robots_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text, 'seo_robots_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n\nDROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;\nCREATE POLICY site_settings_update_policy ON public.site_settings FOR UPDATE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text, 'seo_robots_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text, 'seo_robots_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)))) WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text, 'seo_robots_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text, 'seo_robots_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n\nDROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;\nCREATE POLICY site_settings_delete_policy ON public.site_settings FOR DELETE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text, 'seo_robots_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text, 'seo_robots_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));\n\n-- Forward-only and idempotent.\n"
176
+ }
177
+ ];