@nextblock-cms/db 0.11.2 → 0.12.0

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 (59) hide show
  1. package/lib/supabase/server.d.ts +9 -0
  2. package/lib/supabase/types.d.ts +9 -0
  3. package/package.json +1 -1
  4. package/supabase/migrations/00000000000000_baseline_schema.sql +2454 -0
  5. package/supabase/migrations/00000000000001_baseline_constraints_and_indexes.sql +1071 -0
  6. package/supabase/migrations/00000000000002_baseline_security_and_grants.sql +1335 -0
  7. package/supabase/migrations/00000000000003_baseline_seed.sql +468 -0
  8. package/supabase/migrations/00000000000004_default_logo_email_safe.sql +24 -0
  9. package/supabase/migrations/00000000000005_add_custom_canonical.sql +13 -0
  10. package/supabase/migrations/00000000000006_home_live_demo_promo.sql +67 -0
  11. package/supabase/templates/confirmation.html +1 -1
  12. package/supabase/templates/email_change.html +1 -1
  13. package/supabase/templates/invite.html +1 -1
  14. package/supabase/templates/magic_link.html +1 -1
  15. package/supabase/templates/reauthentication.html +1 -1
  16. package/supabase/templates/recovery.html +1 -1
  17. package/supabase/migrations/00000000000000_setup_foundation_and_enums.sql +0 -40
  18. package/supabase/migrations/00000000000001_setup_cms_core.sql +0 -124
  19. package/supabase/migrations/00000000000002_setup_content_tables.sql +0 -138
  20. package/supabase/migrations/00000000000003_setup_catalog_and_licensing.sql +0 -160
  21. package/supabase/migrations/00000000000004_setup_fulfillment_shipping_taxes_and_currencies.sql +0 -797
  22. package/supabase/migrations/00000000000005_setup_functions_and_triggers.sql +0 -767
  23. package/supabase/migrations/00000000000006_setup_rls_and_grants.sql +0 -872
  24. package/supabase/migrations/00000000000007_setup_indexes.sql +0 -158
  25. package/supabase/migrations/00000000000008_seed_platform_defaults.sql +0 -155
  26. package/supabase/migrations/00000000000009_seed_translations.sql +0 -801
  27. package/supabase/migrations/00000000000010_seed_content_scaffold.sql +0 -1088
  28. package/supabase/migrations/00000000000011_setup_cortex_ai_settings.sql +0 -81
  29. package/supabase/migrations/00000000000012_setup_commerce_coupons.sql +0 -275
  30. package/supabase/migrations/00000000000013_setup_cortex_ai_db_mutation_audit.sql +0 -47
  31. package/supabase/migrations/00000000000014_setup_content_drafts.sql +0 -119
  32. package/supabase/migrations/00000000000015_setup_product_drafts.sql +0 -97
  33. package/supabase/migrations/00000000000016_add_feature_image_to_pages.sql +0 -8
  34. package/supabase/migrations/00000000000017_add_product_blocks.sql +0 -118
  35. package/supabase/migrations/00000000000018_setup_bot_protection_settings.sql +0 -77
  36. package/supabase/migrations/00000000000019_add_product_categories.sql +0 -64
  37. package/supabase/migrations/00000000000020_add_category_translations.sql +0 -10
  38. package/supabase/migrations/00000000000021_migrate_hero_blocks_to_sections.sql +0 -29
  39. package/supabase/migrations/00000000000022_seed_cortex_ai_guide_post.sql +0 -184
  40. package/supabase/migrations/00000000000023_setup_custom_block_definitions.sql +0 -183
  41. package/supabase/migrations/00000000000024_setup_ucp_cart_sessions.sql +0 -72
  42. package/supabase/migrations/00000000000025_add_sale_schedule_columns.sql +0 -406
  43. package/supabase/migrations/00000000000026_seed_on_sale_translation.sql +0 -9
  44. package/supabase/migrations/00000000000027_setup_privacy_and_mfa.sql +0 -479
  45. package/supabase/migrations/00000000000028_clear_advisor_warnings.sql +0 -93
  46. package/supabase/migrations/00000000000029_refresh_setup_article.sql +0 -252
  47. package/supabase/migrations/00000000000030_setup_system_configuration.sql +0 -80
  48. package/supabase/migrations/00000000000031_seed_footer_navigation.sql +0 -85
  49. package/supabase/migrations/00000000000032_neutralize_seeded_contact_emails.sql +0 -34
  50. package/supabase/migrations/00000000000033_setup_config_settings.sql +0 -99
  51. package/supabase/migrations/00000000000034_enable_staff_2fa_reminder_default.sql +0 -15
  52. package/supabase/migrations/00000000000035_reassert_advisor_fixes.sql +0 -33
  53. package/supabase/migrations/00000000000036_setup_system_alerts.sql +0 -72
  54. package/supabase/migrations/00000000000037_refresh_setup_article_install_paths.sql +0 -317
  55. package/supabase/migrations/00000000000038_seed_consent_banner_translations.sql +0 -126
  56. package/supabase/migrations/00000000000039_setup_interactions.sql +0 -153
  57. package/supabase/migrations/00000000000040_add_profiles_fk_to_interactions.sql +0 -8
  58. package/supabase/migrations/00000000000041_seed_interactions_translations.sql +0 -145
  59. package/supabase/migrations/00000000000042_seed_deploy_to_vercel_ctas.sql +0 -72
@@ -0,0 +1,24 @@
1
+ -- Swap the seeded DEFAULT site logo from the WebP asset to a bundled PNG.
2
+ --
3
+ -- WebP (and AVIF) don't render in Outlook and several other email clients, so the default
4
+ -- logo shown in transactional emails was a broken image out of the box. The PNG is a
5
+ -- /public bundled asset (registered in resolveMediaUrl's BUNDLED_PUBLIC_MEDIA_KEYS), so it
6
+ -- resolves to `<site>/images/nextblock-logo-button-tiny.png` and renders everywhere.
7
+ --
8
+ -- Forward-only, data-only (no schema change), idempotent, and GUARDED so it never
9
+ -- overrides a logo an operator has since chosen: it only repoints the seeded default logo
10
+ -- row while that row still points at the seeded WebP media.
11
+
12
+ INSERT INTO public.media (
13
+ id, uploader_id, file_name, object_key, file_type, size_bytes, description,
14
+ width, height, blur_data_url, variants, file_path, folder, created_at, updated_at
15
+ ) VALUES (
16
+ 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d', NULL, 'nextblock-logo-button-tiny.png',
17
+ 'images/nextblock-logo-button-tiny.png', 'image/png', 10000, 'Default site logo',
18
+ NULL, NULL, NULL, NULL, NULL, NULL, now(), now()
19
+ ) ON CONFLICT DO NOTHING;
20
+
21
+ UPDATE public.logos
22
+ SET media_id = 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d'
23
+ WHERE id = 'd45ef03d-27fc-4099-8b46-1cdf82d658d2'
24
+ AND media_id = 'ea6fdaf5-f8de-416c-9f2b-b689b7a4ed38';
@@ -0,0 +1,13 @@
1
+ -- Add a nullable `custom_canonical` column to pages, posts and products.
2
+ --
3
+ -- This is the per-content manual canonical override used by each route's
4
+ -- generateMetadata(): when set, it wins over the default self-referencing
5
+ -- `<site_url>/<slug>` canonical (see app/lib/seo.ts buildCanonicalUrl). NULL/empty
6
+ -- keeps the self-referencing default, so existing rows are unaffected.
7
+ --
8
+ -- Forward-only and idempotent (ADD COLUMN IF NOT EXISTS). No backfill: a NULL value
9
+ -- is the "use the default canonical" sentinel.
10
+
11
+ ALTER TABLE public.pages ADD COLUMN IF NOT EXISTS custom_canonical text;
12
+ ALTER TABLE public.posts ADD COLUMN IF NOT EXISTS custom_canonical text;
13
+ ALTER TABLE public.products ADD COLUMN IF NOT EXISTS custom_canonical text;
@@ -0,0 +1,67 @@
1
+ -- 00000000000006_home_sandbox_promo.sql
2
+ -- Adds a "Live Demo" promo section to the English (slug 'home') and French
3
+ -- (slug 'accueil') home pages that drives visitors to the public sandbox at
4
+ -- https://cms.nextblock.dev -- production (nextblock.dev) never mentioned it before.
5
+ --
6
+ -- The section is inserted right after the hero (order 1); existing home-page blocks
7
+ -- shift down by one so relative order is preserved. Each block carries a sentinel
8
+ -- comment (nb-sandbox-promo) inside its HTML so the sandbox reset route can strip it
9
+ -- there: a CTA pointing at the sandbox is pointless *inside* the sandbox itself.
10
+ --
11
+ -- Forward-only and idempotent: the NOT EXISTS sentinel guard makes a re-run a no-op,
12
+ -- and page lookups are by (language code, slug) so explicit IDs never collide with
13
+ -- user-created blocks on a live database.
14
+
15
+ DO $body$
16
+ DECLARE
17
+ v_en_lang integer;
18
+ v_fr_lang integer;
19
+ v_en_page integer;
20
+ v_fr_page integer;
21
+ BEGIN
22
+ SELECT id INTO v_en_lang FROM public.languages WHERE code = 'en' LIMIT 1;
23
+ SELECT id INTO v_fr_lang FROM public.languages WHERE code = 'fr' LIMIT 1;
24
+
25
+ -- English home page
26
+ SELECT id INTO v_en_page
27
+ FROM public.pages
28
+ WHERE slug = 'home' AND language_id = v_en_lang
29
+ ORDER BY id
30
+ LIMIT 1;
31
+
32
+ IF v_en_page IS NOT NULL AND NOT EXISTS (
33
+ SELECT 1 FROM public.blocks
34
+ WHERE page_id = v_en_page AND content::text LIKE '%nb-sandbox-promo%'
35
+ ) THEN
36
+ UPDATE public.blocks
37
+ SET "order" = "order" + 1
38
+ WHERE page_id = v_en_page AND "order" >= 1;
39
+
40
+ INSERT INTO public.blocks (page_id, language_id, block_type, content, "order")
41
+ VALUES (v_en_page, v_en_lang, 'section', $en$
42
+ {"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.ca</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>"}}]]}
43
+ $en$::jsonb, 1);
44
+ END IF;
45
+
46
+ -- French home page
47
+ SELECT id INTO v_fr_page
48
+ FROM public.pages
49
+ WHERE slug = 'accueil' AND language_id = v_fr_lang
50
+ ORDER BY id
51
+ LIMIT 1;
52
+
53
+ IF v_fr_page IS NOT NULL AND NOT EXISTS (
54
+ SELECT 1 FROM public.blocks
55
+ WHERE page_id = v_fr_page AND content::text LIKE '%nb-sandbox-promo%'
56
+ ) THEN
57
+ UPDATE public.blocks
58
+ SET "order" = "order" + 1
59
+ WHERE page_id = v_fr_page AND "order" >= 1;
60
+
61
+ INSERT INTO public.blocks (page_id, language_id, block_type, content, "order")
62
+ VALUES (v_fr_page, v_fr_lang, 'section', $fr$
63
+ {"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.ca</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>"}}]]}
64
+ $fr$::jsonb, 1);
65
+ END IF;
66
+ END
67
+ $body$;
@@ -12,7 +12,7 @@
12
12
  <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px;background:#ffffff;border:1px solid #e2e8f0;border-radius:20px;overflow:hidden;">
13
13
  <tr>
14
14
  <td style="padding:32px 32px 8px;text-align:center;">
15
- <img src="https://nextblock.dev/images/nextblock-logo-small.webp" alt="Site logo" width="88" style="display:block;margin:0 auto 20px;" />
15
+ <img src="{{ .SiteURL }}/api/brand/email-logo" alt="Site logo" width="150" style="display:block;margin:0 auto 20px;max-width:150px;height:auto;border:0;" />
16
16
  <p style="margin:0 0 12px;font-size:12px;letter-spacing:0.16em;text-transform:uppercase;color:#64748b;">Email confirmation</p>
17
17
  <h1 style="margin:0;font-size:30px;line-height:1.2;">Confirm your email</h1>
18
18
  </td>
@@ -12,7 +12,7 @@
12
12
  <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px;background:#ffffff;border:1px solid #e2e8f0;border-radius:20px;overflow:hidden;">
13
13
  <tr>
14
14
  <td style="padding:32px 32px 8px;text-align:center;">
15
- <img src="https://nextblock.dev/images/nextblock-logo-small.webp" alt="Site logo" width="88" style="display:block;margin:0 auto 20px;" />
15
+ <img src="{{ .SiteURL }}/api/brand/email-logo" alt="Site logo" width="150" style="display:block;margin:0 auto 20px;max-width:150px;height:auto;border:0;" />
16
16
  <p style="margin:0 0 12px;font-size:12px;letter-spacing:0.16em;text-transform:uppercase;color:#64748b;">Email change</p>
17
17
  <h1 style="margin:0;font-size:30px;line-height:1.2;">Confirm your new email</h1>
18
18
  </td>
@@ -12,7 +12,7 @@
12
12
  <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px;background:#ffffff;border:1px solid #e2e8f0;border-radius:20px;overflow:hidden;">
13
13
  <tr>
14
14
  <td style="padding:32px 32px 8px;text-align:center;">
15
- <img src="https://nextblock.dev/images/nextblock-logo-small.webp" alt="Site logo" width="88" style="display:block;margin:0 auto 20px;" />
15
+ <img src="{{ .SiteURL }}/api/brand/email-logo" alt="Site logo" width="150" style="display:block;margin:0 auto 20px;max-width:150px;height:auto;border:0;" />
16
16
  <p style="margin:0 0 12px;font-size:12px;letter-spacing:0.16em;text-transform:uppercase;color:#64748b;">Invitation</p>
17
17
  <h1 style="margin:0;font-size:30px;line-height:1.2;">You have been invited</h1>
18
18
  </td>
@@ -12,7 +12,7 @@
12
12
  <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px;background:#ffffff;border:1px solid #e2e8f0;border-radius:20px;overflow:hidden;">
13
13
  <tr>
14
14
  <td style="padding:32px 32px 8px;text-align:center;">
15
- <img src="https://nextblock.dev/images/nextblock-logo-small.webp" alt="Site logo" width="88" style="display:block;margin:0 auto 20px;" />
15
+ <img src="{{ .SiteURL }}/api/brand/email-logo" alt="Site logo" width="150" style="display:block;margin:0 auto 20px;max-width:150px;height:auto;border:0;" />
16
16
  <p style="margin:0 0 12px;font-size:12px;letter-spacing:0.16em;text-transform:uppercase;color:#64748b;">Passwordless sign-in</p>
17
17
  <h1 style="margin:0;font-size:30px;line-height:1.2;">Your sign-in link is ready</h1>
18
18
  </td>
@@ -12,7 +12,7 @@
12
12
  <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px;background:#ffffff;border:1px solid #e2e8f0;border-radius:20px;overflow:hidden;">
13
13
  <tr>
14
14
  <td style="padding:32px 32px 8px;text-align:center;">
15
- <img src="https://nextblock.dev/images/nextblock-logo-small.webp" alt="Site logo" width="88" style="display:block;margin:0 auto 20px;" />
15
+ <img src="{{ .SiteURL }}/api/brand/email-logo" alt="Site logo" width="150" style="display:block;margin:0 auto 20px;max-width:150px;height:auto;border:0;" />
16
16
  <p style="margin:0 0 12px;font-size:12px;letter-spacing:0.16em;text-transform:uppercase;color:#64748b;">Security check</p>
17
17
  <h1 style="margin:0;font-size:30px;line-height:1.2;">Confirm it's really you</h1>
18
18
  </td>
@@ -12,7 +12,7 @@
12
12
  <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px;background:#ffffff;border:1px solid #e2e8f0;border-radius:20px;overflow:hidden;">
13
13
  <tr>
14
14
  <td style="padding:32px 32px 8px;text-align:center;">
15
- <img src="https://nextblock.dev/images/nextblock-logo-small.webp" alt="Site logo" width="88" style="display:block;margin:0 auto 20px;" />
15
+ <img src="{{ .SiteURL }}/api/brand/email-logo" alt="Site logo" width="150" style="display:block;margin:0 auto 20px;max-width:150px;height:auto;border:0;" />
16
16
  <p style="margin:0 0 12px;font-size:12px;letter-spacing:0.16em;text-transform:uppercase;color:#64748b;">Password recovery</p>
17
17
  <h1 style="margin:0;font-size:30px;line-height:1.2;">Reset your password</h1>
18
18
  </td>
@@ -1,40 +0,0 @@
1
- -- 00000000000000_setup_foundation_and_enums.sql
2
- -- Consolidated migration preserving original statement order within grouped sections.
3
-
4
- -- 00000000000000_setup_schema_privileges.sql
5
- -- Foundation privileges for the public schema.
6
-
7
- GRANT USAGE ON SCHEMA public TO postgres;
8
- GRANT USAGE ON SCHEMA public TO anon;
9
- GRANT USAGE ON SCHEMA public TO authenticated;
10
- GRANT USAGE ON SCHEMA public TO service_role;
11
-
12
- -- 00000000000001_setup_auth_and_content_enums.sql
13
- -- Core enums used by auth and content tables.
14
-
15
- DO $$
16
- BEGIN
17
- IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN
18
- CREATE TYPE public.user_role AS ENUM ('ADMIN', 'WRITER', 'USER');
19
- END IF;
20
-
21
- IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'page_status') THEN
22
- CREATE TYPE public.page_status AS ENUM ('draft', 'published', 'archived');
23
- END IF;
24
- END
25
- $$;
26
-
27
- -- 00000000000002_setup_navigation_and_revision_enums.sql
28
- -- Supporting enums for navigation and revision history.
29
-
30
- DO $$
31
- BEGIN
32
- IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'menu_location') THEN
33
- CREATE TYPE public.menu_location AS ENUM ('HEADER', 'FOOTER', 'SIDEBAR');
34
- END IF;
35
-
36
- IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'revision_type') THEN
37
- CREATE TYPE public.revision_type AS ENUM ('snapshot', 'diff');
38
- END IF;
39
- END
40
- $$;
@@ -1,124 +0,0 @@
1
- -- 00000000000001_setup_cms_core.sql
2
- -- Consolidated migration preserving original statement order within grouped sections.
3
-
4
- -- 00000000000003_setup_site_settings_and_profiles.sql
5
- -- Global settings, user profiles, and saved addresses.
6
-
7
- CREATE TABLE public.site_settings (
8
- key text PRIMARY KEY,
9
- value jsonb
10
- );
11
-
12
- COMMENT ON TABLE public.site_settings IS 'Key-value store for global site settings.';
13
-
14
- CREATE TABLE public.profiles (
15
- id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
16
- updated_at timestamptz,
17
- full_name text,
18
- avatar_url text,
19
- website text,
20
- github_username text,
21
- phone text,
22
- role public.user_role NOT NULL DEFAULT 'USER'
23
- );
24
-
25
- COMMENT ON TABLE public.profiles IS 'Profile information for each user, extending auth.users.';
26
- COMMENT ON COLUMN public.profiles.id IS 'References auth.users.id';
27
- COMMENT ON COLUMN public.profiles.role IS 'User role for RBAC.';
28
-
29
- CREATE TABLE public.user_addresses (
30
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
31
- user_id uuid NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
32
- address_type text NOT NULL CHECK (address_type IN ('billing', 'shipping')),
33
- is_default boolean NOT NULL DEFAULT false,
34
- recipient_name text,
35
- company_name text,
36
- line1 text,
37
- line2 text,
38
- city text,
39
- state text,
40
- postal_code text,
41
- country_code text,
42
- created_at timestamptz NOT NULL DEFAULT now(),
43
- updated_at timestamptz NOT NULL DEFAULT now()
44
- );
45
-
46
- COMMENT ON COLUMN public.user_addresses.company_name IS
47
- 'Optional company or organization name for the address.';
48
-
49
- CREATE UNIQUE INDEX idx_user_addresses_one_default_per_type
50
- ON public.user_addresses (user_id, address_type)
51
- WHERE is_default = true;
52
-
53
- -- 00000000000004_setup_languages_and_media.sql
54
- -- Core locale and media tables.
55
-
56
- CREATE TABLE public.languages (
57
- id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
58
- code text NOT NULL UNIQUE,
59
- name text NOT NULL,
60
- is_default boolean NOT NULL DEFAULT false,
61
- is_active boolean DEFAULT true,
62
- created_at timestamptz NOT NULL DEFAULT now(),
63
- updated_at timestamptz NOT NULL DEFAULT now()
64
- );
65
-
66
- COMMENT ON TABLE public.languages IS 'Stores supported languages for the CMS.';
67
- COMMENT ON COLUMN public.languages.code IS 'BCP 47 language code.';
68
-
69
- CREATE UNIQUE INDEX ensure_single_default_language_idx
70
- ON public.languages (is_default)
71
- WHERE is_default = true;
72
-
73
- CREATE TABLE public.media (
74
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
75
- uploader_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
76
- file_name text NOT NULL,
77
- object_key text NOT NULL UNIQUE,
78
- file_type text,
79
- size_bytes bigint,
80
- description text,
81
- width integer,
82
- height integer,
83
- blur_data_url text,
84
- variants jsonb,
85
- file_path text,
86
- folder text,
87
- created_at timestamptz NOT NULL DEFAULT now(),
88
- updated_at timestamptz NOT NULL DEFAULT now()
89
- );
90
-
91
- COMMENT ON TABLE public.media IS 'Stores information about uploaded media assets.';
92
- COMMENT ON COLUMN public.media.object_key IS 'Unique key (path) in Cloudflare R2.';
93
- COMMENT ON COLUMN public.media.width IS 'Width of the image in pixels.';
94
- COMMENT ON COLUMN public.media.height IS 'Height of the image in pixels.';
95
- COMMENT ON COLUMN public.media.blur_data_url IS 'Base64 encoded string for image blur placeholders.';
96
- COMMENT ON COLUMN public.media.variants IS 'Array of image variant objects.';
97
- COMMENT ON COLUMN public.media.file_path IS 'Full path to the file in the storage bucket.';
98
- COMMENT ON COLUMN public.media.folder IS 'Folder path prefix for the R2 object.';
99
-
100
- -- 00000000000005_setup_translations_and_branding.sql
101
- -- Shared translation storage and logo metadata.
102
-
103
- CREATE TABLE public.translations (
104
- key text PRIMARY KEY,
105
- translations jsonb NOT NULL,
106
- created_at timestamptz NOT NULL DEFAULT now(),
107
- updated_at timestamptz NOT NULL DEFAULT now()
108
- );
109
-
110
- COMMENT ON COLUMN public.translations.key IS
111
- 'A unique, slugified identifier (e.g., "sign_in_button_text").';
112
- COMMENT ON COLUMN public.translations.translations IS
113
- 'Stores translations as key-value pairs (e.g., {"en": "Sign In", "fr": "S''inscrire"}).';
114
-
115
- CREATE TABLE public.logos (
116
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
117
- name text NOT NULL,
118
- media_id uuid REFERENCES public.media(id) ON DELETE SET NULL,
119
- created_at timestamptz NOT NULL DEFAULT now()
120
- );
121
-
122
- COMMENT ON TABLE public.logos IS 'Stores company and brand logos.';
123
- COMMENT ON COLUMN public.logos.name IS 'The name of the brand or company for the logo.';
124
- COMMENT ON COLUMN public.logos.media_id IS 'Foreign key to the media table for the logo image.';
@@ -1,138 +0,0 @@
1
- -- 00000000000002_setup_content_tables.sql
2
- -- Consolidated migration preserving original statement order within grouped sections.
3
-
4
- -- 00000000000006_setup_pages_and_posts.sql
5
- -- Page and post records.
6
-
7
- CREATE TABLE public.posts (
8
- id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
9
- language_id bigint NOT NULL REFERENCES public.languages(id) ON DELETE CASCADE,
10
- author_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
11
- title text NOT NULL,
12
- slug text NOT NULL,
13
- label text,
14
- excerpt text,
15
- subtitle text,
16
- status public.page_status NOT NULL DEFAULT 'draft',
17
- published_at timestamptz,
18
- meta_title text,
19
- meta_description text,
20
- feature_image_id uuid REFERENCES public.media(id) ON DELETE SET NULL,
21
- version integer NOT NULL DEFAULT 1,
22
- translation_group_id uuid NOT NULL DEFAULT gen_random_uuid(),
23
- created_at timestamptz NOT NULL DEFAULT now(),
24
- updated_at timestamptz NOT NULL DEFAULT now(),
25
- CONSTRAINT posts_language_id_slug_key UNIQUE (language_id, slug)
26
- );
27
-
28
- COMMENT ON TABLE public.posts IS 'Stores blog posts or news articles.';
29
- COMMENT ON COLUMN public.posts.slug IS 'URL-friendly identifier, unique per language.';
30
- COMMENT ON COLUMN public.posts.label IS
31
- 'Short editorial label rendered as a pill on post hero and article cards.';
32
- COMMENT ON COLUMN public.posts.excerpt IS
33
- 'Short editorial summary used in post metadata rows and post cards.';
34
- COMMENT ON COLUMN public.posts.subtitle IS
35
- 'Longer deck shown under the post title.';
36
- COMMENT ON COLUMN public.posts.feature_image_id IS
37
- 'ID of the media item to be used as the feature image.';
38
- COMMENT ON COLUMN public.posts.version IS 'Monotonic version number for hybrid revisions.';
39
- COMMENT ON COLUMN public.posts.translation_group_id IS
40
- 'Groups different language versions of the same conceptual post.';
41
-
42
- CREATE TABLE public.pages (
43
- id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
44
- language_id bigint NOT NULL REFERENCES public.languages(id) ON DELETE CASCADE,
45
- author_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
46
- title text NOT NULL,
47
- slug text NOT NULL,
48
- status public.page_status NOT NULL DEFAULT 'draft',
49
- meta_title text,
50
- meta_description text,
51
- version integer NOT NULL DEFAULT 1,
52
- translation_group_id uuid NOT NULL DEFAULT gen_random_uuid(),
53
- created_at timestamptz NOT NULL DEFAULT now(),
54
- updated_at timestamptz NOT NULL DEFAULT now(),
55
- CONSTRAINT pages_language_id_slug_key UNIQUE (language_id, slug)
56
- );
57
-
58
- COMMENT ON TABLE public.pages IS 'Stores static pages for the website.';
59
- COMMENT ON COLUMN public.pages.slug IS 'URL-friendly identifier, unique per language.';
60
- COMMENT ON COLUMN public.pages.version IS 'Monotonic version number for hybrid revisions.';
61
- COMMENT ON COLUMN public.pages.translation_group_id IS
62
- 'Groups different language versions of the same conceptual page.';
63
-
64
- -- 00000000000007_setup_blocks_and_navigation.sql
65
- -- Content blocks and navigation trees.
66
-
67
- CREATE TABLE public.blocks (
68
- id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
69
- page_id bigint REFERENCES public.pages(id) ON DELETE CASCADE,
70
- post_id bigint REFERENCES public.posts(id) ON DELETE CASCADE,
71
- language_id bigint NOT NULL REFERENCES public.languages(id) ON DELETE CASCADE,
72
- block_type text NOT NULL,
73
- content jsonb,
74
- "order" integer NOT NULL DEFAULT 0,
75
- created_at timestamptz NOT NULL DEFAULT now(),
76
- updated_at timestamptz NOT NULL DEFAULT now(),
77
- CONSTRAINT check_exactly_one_parent CHECK (
78
- (page_id IS NOT NULL AND post_id IS NULL)
79
- OR (post_id IS NOT NULL AND page_id IS NULL)
80
- )
81
- );
82
-
83
- COMMENT ON TABLE public.blocks IS 'Stores content blocks for pages and posts.';
84
- COMMENT ON COLUMN public.blocks.block_type IS
85
- 'Type of the block, e.g., "text", "image".';
86
- COMMENT ON COLUMN public.blocks.content IS 'JSONB content specific to the block_type.';
87
- COMMENT ON COLUMN public.blocks.order IS 'Sort order of the block.';
88
-
89
- CREATE TABLE public.navigation_items (
90
- id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
91
- language_id bigint NOT NULL REFERENCES public.languages(id) ON DELETE CASCADE,
92
- menu_key public.menu_location NOT NULL,
93
- label text NOT NULL,
94
- url text NOT NULL,
95
- parent_id bigint REFERENCES public.navigation_items(id) ON DELETE CASCADE,
96
- "order" integer NOT NULL DEFAULT 0,
97
- page_id bigint REFERENCES public.pages(id) ON DELETE SET NULL,
98
- translation_group_id uuid NOT NULL DEFAULT gen_random_uuid(),
99
- created_at timestamptz NOT NULL DEFAULT now(),
100
- updated_at timestamptz NOT NULL DEFAULT now()
101
- );
102
-
103
- COMMENT ON TABLE public.navigation_items IS 'Stores navigation menu items.';
104
- COMMENT ON COLUMN public.navigation_items.menu_key IS
105
- 'Identifies the menu this item belongs to.';
106
-
107
- -- 00000000000008_setup_revisions.sql
108
- -- Page and post revision history.
109
-
110
- CREATE TABLE public.page_revisions (
111
- id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
112
- page_id bigint NOT NULL REFERENCES public.pages(id) ON DELETE CASCADE,
113
- author_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
114
- version integer NOT NULL,
115
- revision_type public.revision_type NOT NULL,
116
- content jsonb NOT NULL,
117
- created_at timestamptz NOT NULL DEFAULT now(),
118
- CONSTRAINT page_revisions_page_version_key UNIQUE (page_id, version)
119
- );
120
-
121
- COMMENT ON TABLE public.page_revisions IS 'Hybrid (snapshot/diff) revisions for pages.';
122
- COMMENT ON COLUMN public.page_revisions.content IS
123
- 'If snapshot: full content; if diff: JSON Patch array.';
124
-
125
- CREATE TABLE public.post_revisions (
126
- id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
127
- post_id bigint NOT NULL REFERENCES public.posts(id) ON DELETE CASCADE,
128
- author_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
129
- version integer NOT NULL,
130
- revision_type public.revision_type NOT NULL,
131
- content jsonb NOT NULL,
132
- created_at timestamptz NOT NULL DEFAULT now(),
133
- CONSTRAINT post_revisions_post_version_key UNIQUE (post_id, version)
134
- );
135
-
136
- COMMENT ON TABLE public.post_revisions IS 'Hybrid (snapshot/diff) revisions for posts.';
137
- COMMENT ON COLUMN public.post_revisions.content IS
138
- 'If snapshot: full content; if diff: JSON Patch array.';
@@ -1,160 +0,0 @@
1
- -- 00000000000003_setup_catalog_and_licensing.sql
2
- -- Consolidated migration preserving original statement order within grouped sections.
3
-
4
- -- 00000000000009_setup_products_and_media.sql
5
- -- Core product catalog tables.
6
-
7
- CREATE TABLE public.products (
8
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
9
- language_id bigint NOT NULL REFERENCES public.languages(id) ON DELETE CASCADE,
10
- translation_group_id uuid NOT NULL DEFAULT gen_random_uuid(),
11
- sku text NOT NULL,
12
- title text NOT NULL,
13
- slug text NOT NULL,
14
- product_type text NOT NULL CHECK (product_type IN ('physical', 'digital')),
15
- payment_provider text NOT NULL CHECK (payment_provider IN ('stripe', 'freemius')),
16
- price integer NOT NULL,
17
- prices jsonb NOT NULL DEFAULT '{}'::jsonb,
18
- sale_price integer,
19
- sale_prices jsonb,
20
- stock integer DEFAULT 0,
21
- status text NOT NULL CHECK (status IN ('draft', 'active', 'archived')) DEFAULT 'draft',
22
- meta_title text,
23
- meta_description text,
24
- short_description text,
25
- description_json jsonb,
26
- metadata jsonb,
27
- freemius_plan_id text,
28
- freemius_product_id text,
29
- trial_period_days integer NOT NULL DEFAULT 0 CHECK (trial_period_days >= 0),
30
- trial_requires_payment_method boolean NOT NULL DEFAULT false,
31
- upc text,
32
- is_taxable boolean NOT NULL DEFAULT true,
33
- created_at timestamptz DEFAULT now(),
34
- updated_at timestamptz DEFAULT now(),
35
- CONSTRAINT products_type_provider_consistency_check CHECK (
36
- (product_type = 'physical' AND payment_provider = 'stripe')
37
- OR (product_type = 'digital' AND payment_provider = 'freemius')
38
- ),
39
- CONSTRAINT products_language_id_slug_key UNIQUE (language_id, slug),
40
- CONSTRAINT products_language_id_sku_key UNIQUE (language_id, sku)
41
- );
42
-
43
- COMMENT ON COLUMN public.products.is_taxable IS
44
- 'When true, this product participates in Stripe tax calculation.';
45
- COMMENT ON COLUMN public.products.prices IS
46
- 'Regular prices by ISO 4217 code in the smallest currency unit.';
47
- COMMENT ON COLUMN public.products.sale_prices IS
48
- 'Sale prices by ISO 4217 code in the smallest currency unit.';
49
-
50
- CREATE TABLE public.product_media (
51
- product_id uuid NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,
52
- media_id uuid NOT NULL REFERENCES public.media(id) ON DELETE CASCADE,
53
- sort_order integer DEFAULT 0,
54
- PRIMARY KEY (product_id, media_id)
55
- );
56
-
57
- -- 00000000000010_setup_product_variants_and_attributes.sql
58
- -- Variant attributes, terms, and sellable variants.
59
-
60
- CREATE TABLE public.product_attributes (
61
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
62
- name text NOT NULL,
63
- slug text NOT NULL UNIQUE,
64
- name_translations jsonb NOT NULL DEFAULT '{}'::jsonb,
65
- created_at timestamptz DEFAULT now(),
66
- updated_at timestamptz DEFAULT now()
67
- );
68
-
69
- CREATE TABLE public.product_attribute_terms (
70
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
71
- attribute_id uuid NOT NULL REFERENCES public.product_attributes(id) ON DELETE CASCADE,
72
- value text NOT NULL,
73
- slug text NOT NULL,
74
- sort_order integer NOT NULL DEFAULT 0,
75
- value_translations jsonb NOT NULL DEFAULT '{}'::jsonb,
76
- created_at timestamptz DEFAULT now(),
77
- updated_at timestamptz DEFAULT now(),
78
- CONSTRAINT product_attribute_terms_attribute_id_slug_key UNIQUE (attribute_id, slug)
79
- );
80
-
81
- CREATE TABLE public.product_variants (
82
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
83
- product_id uuid NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,
84
- sku text NOT NULL,
85
- price_adjustment integer NOT NULL DEFAULT 0,
86
- price integer NOT NULL DEFAULT 0,
87
- prices jsonb NOT NULL DEFAULT '{}'::jsonb,
88
- sale_price integer,
89
- sale_prices jsonb,
90
- stock_quantity integer NOT NULL DEFAULT 0,
91
- upc text,
92
- main_media_id uuid REFERENCES public.media(id) ON DELETE SET NULL,
93
- created_at timestamptz DEFAULT now(),
94
- updated_at timestamptz DEFAULT now(),
95
- CONSTRAINT product_variants_product_id_sku_key UNIQUE (product_id, sku)
96
- );
97
-
98
- COMMENT ON COLUMN public.product_variants.prices IS
99
- 'Variant regular prices by ISO 4217 code in the smallest currency unit.';
100
- COMMENT ON COLUMN public.product_variants.sale_prices IS
101
- 'Variant sale prices by ISO 4217 code in the smallest currency unit.';
102
-
103
- CREATE TABLE public.inventory_items (
104
- sku text PRIMARY KEY,
105
- quantity integer NOT NULL DEFAULT 0 CHECK (quantity >= 0),
106
- created_at timestamptz NOT NULL DEFAULT now(),
107
- updated_at timestamptz NOT NULL DEFAULT now()
108
- );
109
-
110
- COMMENT ON TABLE public.inventory_items IS
111
- 'Source-of-truth inventory records keyed by sellable SKU.';
112
- COMMENT ON COLUMN public.inventory_items.sku IS
113
- 'Global sellable SKU. Matching products or variants share inventory.';
114
- COMMENT ON COLUMN public.inventory_items.quantity IS
115
- 'Available quantity for this SKU.';
116
-
117
- CREATE TABLE public.variant_attribute_mapping (
118
- variant_id uuid NOT NULL REFERENCES public.product_variants(id) ON DELETE CASCADE,
119
- attribute_term_id uuid NOT NULL REFERENCES public.product_attribute_terms(id) ON DELETE CASCADE,
120
- PRIMARY KEY (variant_id, attribute_term_id)
121
- );
122
-
123
- -- 00000000000011_setup_licensing_and_freemius.sql
124
- -- Package activations and Freemius plan metadata.
125
-
126
- CREATE TABLE public.package_activations (
127
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
128
- license_key text NOT NULL,
129
- instance_name text NOT NULL,
130
- package_id text NOT NULL,
131
- status text NOT NULL DEFAULT 'active',
132
- meta jsonb DEFAULT '{}'::jsonb,
133
- last_validated_at timestamptz DEFAULT now(),
134
- created_at timestamptz DEFAULT now(),
135
- UNIQUE (license_key, package_id)
136
- );
137
-
138
- CREATE TABLE public.freemius_plans (
139
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
140
- product_id uuid NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,
141
- name text NOT NULL,
142
- title text,
143
- created_at timestamptz NOT NULL DEFAULT now(),
144
- updated_at timestamptz NOT NULL DEFAULT now()
145
- );
146
-
147
- CREATE TABLE public.freemius_pricing (
148
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
149
- plan_id uuid NOT NULL REFERENCES public.freemius_plans(id) ON DELETE CASCADE,
150
- api_monthly_price numeric,
151
- api_annual_price numeric,
152
- api_lifetime_price numeric,
153
- override_monthly_price numeric,
154
- override_annual_price numeric,
155
- override_lifetime_price numeric,
156
- license_quota integer,
157
- is_active boolean NOT NULL DEFAULT true,
158
- created_at timestamptz NOT NULL DEFAULT now(),
159
- updated_at timestamptz NOT NULL DEFAULT now()
160
- );