create-bw-app 0.13.3 → 0.14.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 (86) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/app-manifest.mjs +4 -2
  4. package/src/constants.mjs +88 -11
  5. package/src/doctor.mjs +38 -0
  6. package/src/generator.mjs +75 -19
  7. package/template/base/app/(auth)/auth/confirmed/page.tsx +1 -0
  8. package/template/base/app/(auth)/auth/post-login/page.tsx +1 -0
  9. package/template/base/app/(auth)/auth-provider.tsx +25 -0
  10. package/template/base/app/(auth)/forgot-password/page.tsx +1 -0
  11. package/template/base/app/(auth)/invite/[invitationId]/invite-route-mount.tsx +10 -0
  12. package/template/base/app/(auth)/invite/[invitationId]/page.tsx +1 -0
  13. package/template/base/app/(auth)/layout.tsx +5 -0
  14. package/template/base/app/(auth)/login/page.tsx +1 -0
  15. package/template/base/app/(auth)/reset-password/page.tsx +1 -0
  16. package/template/base/app/(auth)/signup/page.tsx +1 -0
  17. package/template/base/app/(shell)/account/page.tsx +5 -0
  18. package/template/base/app/(shell)/dashboard/dashboard-live-mount.tsx +21 -0
  19. package/template/base/app/(shell)/dashboard/page.tsx +1 -0
  20. package/template/base/app/(shell)/layout.tsx +24 -0
  21. package/template/base/app/(shell)/shell-layout-client.tsx +134 -0
  22. package/template/base/app/api/account/route.ts +2 -0
  23. package/template/base/app/auth/callback/route.ts +1 -0
  24. package/template/base/app/auth/cleanup/route.ts +1 -0
  25. package/template/base/app/layout.tsx +8 -2
  26. package/template/base/config/env.ts +98 -0
  27. package/template/modules/marketing/app/(shell)/marketing/page.tsx +1 -0
  28. package/template/modules/marketing/app/api/marketing/_handlers.ts +144 -0
  29. package/template/modules/marketing/app/api/marketing/analytics/campaigns/[id]/route.ts +2 -0
  30. package/template/modules/marketing/app/api/marketing/analytics/overview/route.ts +2 -0
  31. package/template/modules/marketing/app/api/marketing/analytics/segments/[id]/route.ts +2 -0
  32. package/template/modules/marketing/app/api/marketing/campaigns/[id]/cancel/route.ts +2 -0
  33. package/template/modules/marketing/app/api/marketing/campaigns/[id]/recipients/route.ts +2 -0
  34. package/template/modules/marketing/app/api/marketing/campaigns/[id]/retry/route.ts +2 -0
  35. package/template/modules/marketing/app/api/marketing/campaigns/[id]/route.ts +2 -0
  36. package/template/modules/marketing/app/api/marketing/campaigns/[id]/schedule/route.ts +2 -0
  37. package/template/modules/marketing/app/api/marketing/campaigns/[id]/send/route.ts +2 -0
  38. package/template/modules/marketing/app/api/marketing/campaigns/[id]/test/route.ts +2 -0
  39. package/template/modules/marketing/app/api/marketing/campaigns/route.ts +2 -0
  40. package/template/modules/marketing/app/api/marketing/segments/[id]/preview/route.ts +2 -0
  41. package/template/modules/marketing/app/api/marketing/segments/[id]/route.ts +2 -0
  42. package/template/modules/marketing/app/api/marketing/segments/preview/route.ts +2 -0
  43. package/template/modules/marketing/app/api/marketing/segments/route.ts +2 -0
  44. package/template/modules/marketing/app/api/marketing/topics/route.ts +2 -0
  45. package/template/modules/marketing/app/api/marketing/unsubscribe/[token]/route.ts +2 -0
  46. package/template/modules/marketing/app/api/marketing/webhooks/resend/route.ts +2 -0
  47. package/template/modules/marketing/app/api/marketing/worker/route.ts +2 -0
  48. package/template/modules/marketing/app/api/marketing/workflows/[id]/activate/route.ts +2 -0
  49. package/template/modules/marketing/app/api/marketing/workflows/[id]/nodes/[nodeId]/route.ts +2 -0
  50. package/template/modules/marketing/app/api/marketing/workflows/[id]/nodes/route.ts +2 -0
  51. package/template/modules/marketing/app/api/marketing/workflows/[id]/pause/route.ts +2 -0
  52. package/template/modules/marketing/app/api/marketing/workflows/[id]/route.ts +2 -0
  53. package/template/modules/marketing/app/api/marketing/workflows/[id]/runs/route.ts +2 -0
  54. package/template/modules/marketing/app/api/marketing/workflows/route.ts +2 -0
  55. package/template/modules/projects/app/(shell)/projects/[id]/board/page.tsx +1 -0
  56. package/template/modules/projects/app/(shell)/projects/[id]/page.tsx +1 -0
  57. package/template/modules/projects/app/(shell)/projects/[id]/tasks/page.tsx +1 -0
  58. package/template/modules/projects/app/(shell)/projects/layout.tsx +5 -0
  59. package/template/modules/projects/app/(shell)/projects/page.tsx +1 -0
  60. package/template/modules/projects/app/(shell)/projects/projects-live-mounts.tsx +27 -0
  61. package/template/modules/projects/app/(shell)/projects/projects-server-mounts.tsx +54 -0
  62. package/template/modules/projects/app/api/projects/[id]/activity/route.ts +2 -0
  63. package/template/modules/projects/app/api/projects/[id]/links/[itemId]/route.ts +2 -0
  64. package/template/modules/projects/app/api/projects/[id]/links/route.ts +2 -0
  65. package/template/modules/projects/app/api/projects/[id]/members/route.ts +2 -0
  66. package/template/modules/projects/app/api/projects/[id]/milestones/[itemId]/route.ts +2 -0
  67. package/template/modules/projects/app/api/projects/[id]/milestones/route.ts +2 -0
  68. package/template/modules/projects/app/api/projects/[id]/route.ts +2 -0
  69. package/template/modules/projects/app/api/projects/[id]/tasks/[itemId]/route.ts +2 -0
  70. package/template/modules/projects/app/api/projects/[id]/tasks/route.ts +2 -0
  71. package/template/modules/projects/app/api/projects/_handlers.ts +86 -0
  72. package/template/modules/projects/app/api/projects/organizations/route.ts +2 -0
  73. package/template/modules/projects/app/api/projects/route.ts +2 -0
  74. package/template/modules/projects/app/api/projects/stats/route.ts +2 -0
  75. package/template/supabase/README.md +3 -0
  76. package/template/supabase/module-registry.json +6 -0
  77. package/template/supabase/modules/marketing/README.md +12 -0
  78. package/template/supabase/modules/marketing/migrations/20260725120000_marketing_v1.sql +118 -0
  79. package/template/supabase/modules/marketing/migrations/20260725130000_marketing_campaigns.sql +133 -0
  80. package/template/supabase/modules/marketing/migrations/20260726120000_marketing_segments.sql +41 -0
  81. package/template/supabase/modules/marketing/migrations/20260726150000_marketing_workflows.sql +174 -0
  82. package/template/supabase/modules/marketing/migrations/20260726170000_marketing_engine_safety.sql +68 -0
  83. /package/template/modules/admin/app/{admin → (shell)/admin}/users/page.tsx +0 -0
  84. /package/template/modules/crm/app/{crm → (shell)/crm}/layout.tsx +0 -0
  85. /package/template/modules/crm/app/{crm → (shell)/crm}/page.tsx +0 -0
  86. /package/template/modules/crm/app/{crm → (shell)/crm}/report/page.tsx +0 -0
@@ -0,0 +1,2 @@
1
+ export const dynamic = "force-dynamic";
2
+ export { projectDashboardGet as GET, projectPatch as PATCH, projectDelete as DELETE } from "../_handlers";
@@ -0,0 +1,2 @@
1
+ export const dynamic = "force-dynamic";
2
+ export { projectTaskPatch as PATCH, projectTaskDelete as DELETE } from "../../../_handlers";
@@ -0,0 +1,2 @@
1
+ export const dynamic = "force-dynamic";
2
+ export { projectTasksPost as POST } from "../../_handlers";
@@ -0,0 +1,86 @@
1
+ import {
2
+ handleProjectsActivityGetRequest,
3
+ handleProjectsAssignableProfilesGetRequest,
4
+ handleProjectsDashboardGetRequest,
5
+ handleProjectsDeleteRequest,
6
+ handleProjectsGetRequest,
7
+ handleProjectsLinksDeleteRequest,
8
+ handleProjectsLinksPatchRequest,
9
+ handleProjectsLinksPostRequest,
10
+ handleProjectsMembersPutRequest,
11
+ handleProjectsMilestonesDeleteRequest,
12
+ handleProjectsMilestonesPatchRequest,
13
+ handleProjectsMilestonesPostRequest,
14
+ handleProjectsOrganizationsGetRequest,
15
+ handleProjectsOrganizationsPostRequest,
16
+ handleProjectsPatchRequest,
17
+ handleProjectsPostRequest,
18
+ handleProjectsStatsGetRequest,
19
+ handleProjectsTasksDeleteRequest,
20
+ handleProjectsTasksPatchRequest,
21
+ handleProjectsTasksPostRequest,
22
+ } from "@brightweblabs/module-projects";
23
+
24
+ type IdRouteContext = { params: Promise<{ id: string }> };
25
+ type ItemRouteContext = { params: Promise<{ id: string; itemId: string }> };
26
+
27
+ export function projectsGet(request: Request) {
28
+ return handleProjectsGetRequest(request);
29
+ }
30
+ export function projectsPost(request: Request) {
31
+ return handleProjectsPostRequest(request);
32
+ }
33
+ export function projectsStatsGet(request: Request) {
34
+ return handleProjectsStatsGetRequest(request);
35
+ }
36
+ export function projectsOrganizationsGet(request: Request) {
37
+ return handleProjectsOrganizationsGetRequest(request);
38
+ }
39
+ export function projectsOrganizationsPost(request: Request) {
40
+ return handleProjectsOrganizationsPostRequest(request);
41
+ }
42
+ export function projectDashboardGet(request: Request, context: IdRouteContext) {
43
+ return handleProjectsDashboardGetRequest(request, context);
44
+ }
45
+ export function projectPatch(request: Request, context: IdRouteContext) {
46
+ return handleProjectsPatchRequest(request, context);
47
+ }
48
+ export function projectDelete(request: Request, context: IdRouteContext) {
49
+ return handleProjectsDeleteRequest(request, context);
50
+ }
51
+ export function projectActivityGet(request: Request, context: IdRouteContext) {
52
+ return handleProjectsActivityGetRequest(request, context);
53
+ }
54
+ export function projectMembersGet(request: Request, context: IdRouteContext) {
55
+ return handleProjectsAssignableProfilesGetRequest(request, context);
56
+ }
57
+ export function projectMembersPut(request: Request, context: IdRouteContext) {
58
+ return handleProjectsMembersPutRequest(request, context);
59
+ }
60
+ export function projectLinksPost(request: Request, context: IdRouteContext) {
61
+ return handleProjectsLinksPostRequest(request, context);
62
+ }
63
+ export function projectLinkPatch(request: Request, context: ItemRouteContext) {
64
+ return handleProjectsLinksPatchRequest(request, context);
65
+ }
66
+ export function projectLinkDelete(request: Request, context: ItemRouteContext) {
67
+ return handleProjectsLinksDeleteRequest(request, context);
68
+ }
69
+ export function projectMilestonesPost(request: Request, context: IdRouteContext) {
70
+ return handleProjectsMilestonesPostRequest(request, context);
71
+ }
72
+ export function projectMilestonePatch(request: Request, context: ItemRouteContext) {
73
+ return handleProjectsMilestonesPatchRequest(request, context);
74
+ }
75
+ export function projectMilestoneDelete(request: Request, context: ItemRouteContext) {
76
+ return handleProjectsMilestonesDeleteRequest(request, context);
77
+ }
78
+ export function projectTasksPost(request: Request, context: IdRouteContext) {
79
+ return handleProjectsTasksPostRequest(request, context);
80
+ }
81
+ export function projectTaskPatch(request: Request, context: ItemRouteContext) {
82
+ return handleProjectsTasksPatchRequest(request, context);
83
+ }
84
+ export function projectTaskDelete(request: Request, context: ItemRouteContext) {
85
+ return handleProjectsTasksDeleteRequest(request, context);
86
+ }
@@ -0,0 +1,2 @@
1
+ export const dynamic = "force-dynamic";
2
+ export { projectsOrganizationsGet as GET, projectsOrganizationsPost as POST } from "../_handlers";
@@ -0,0 +1,2 @@
1
+ export const dynamic = "force-dynamic";
2
+ export { projectsGet as GET, projectsPost as POST } from "./_handlers";
@@ -0,0 +1,2 @@
1
+ export const dynamic = "force-dynamic";
2
+ export { projectsStatsGet as GET } from "../_handlers";
@@ -10,6 +10,7 @@ This directory is the scaffold-owned home for the Brightweb shared database modu
10
10
  - `modules/core`: always-on platform foundations
11
11
  - `modules/admin`: RBAC and privileged governance behavior
12
12
  - `modules/crm`: organizations, CRM contacts, and invitation flows
13
+ - `modules/marketing`: consent, campaigns, segments, analytics, and workflows
13
14
  - `modules/projects`: project and work-management data
14
15
  - `clients/<client-slug>`: true client-only schema deltas plus the client stack plan
15
16
  - `clients/<client-slug>`: client stack metadata and client-only migrations for this project
@@ -40,6 +41,7 @@ Create a new shared module migration:
40
41
  pnpm db:new core profile_notification_cursor
41
42
  pnpm db:new admin role_change_guard
42
43
  pnpm db:new crm organization_invite_expiry
44
+ pnpm db:new marketing campaign_delivery_guard
43
45
  pnpm db:new projects task_due_date_index
44
46
  ```
45
47
 
@@ -58,5 +60,6 @@ Projects created from the published scaffold should use the Supabase files in th
58
60
  - `supabase/modules/core/README.md`
59
61
  - `supabase/modules/admin/README.md`
60
62
  - `supabase/modules/crm/README.md`
63
+ - `supabase/modules/marketing/README.md`
61
64
  - `supabase/modules/projects/README.md`
62
65
  - `supabase/clients/README.md`
@@ -24,6 +24,12 @@
24
24
  "dependsOn": ["core", "admin", "orgs"],
25
25
  "description": "CRM contacts, status tracking, and organization integration."
26
26
  },
27
+ "marketing": {
28
+ "label": "Marketing",
29
+ "path": "supabase/modules/marketing/migrations",
30
+ "dependsOn": ["core", "admin", "orgs", "crm"],
31
+ "description": "Consent-first marketing topics, subscriptions, campaigns, segments, analytics, and workflows."
32
+ },
27
33
  "projects": {
28
34
  "label": "Projects",
29
35
  "path": "supabase/modules/projects/migrations",
@@ -0,0 +1,12 @@
1
+ # Marketing Migrations
2
+
3
+ `marketing` owns consent, campaign delivery, segmentation, analytics, and workflow automation data.
4
+
5
+ ## Dependency
6
+
7
+ Depends on:
8
+
9
+ - `core`
10
+ - `admin`
11
+ - `orgs`
12
+ - `crm`
@@ -0,0 +1,118 @@
1
+ -- Brightweb Marketing v1: consent, contact preferences, and suppression.
2
+
3
+ CREATE TABLE IF NOT EXISTS public.marketing_topics (
4
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
5
+ slug text NOT NULL UNIQUE,
6
+ label text NOT NULL,
7
+ description text,
8
+ is_active boolean NOT NULL DEFAULT true,
9
+ position integer NOT NULL DEFAULT 0,
10
+ created_at timestamptz NOT NULL DEFAULT now(),
11
+ updated_at timestamptz NOT NULL DEFAULT now()
12
+ );
13
+
14
+ INSERT INTO public.marketing_topics (slug, label, description, position)
15
+ VALUES
16
+ ('newsletter', 'Newsletter', 'News, stories, and practical updates from BrightWeb.', 0),
17
+ ('product-updates', 'Product updates & events', 'Product announcements, releases, and events.', 10)
18
+ ON CONFLICT (slug) DO NOTHING;
19
+
20
+ CREATE TABLE IF NOT EXISTS public.marketing_subscriptions (
21
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
22
+ contact_id uuid NOT NULL REFERENCES public.crm_contacts(id) ON DELETE CASCADE,
23
+ topic_id uuid NOT NULL REFERENCES public.marketing_topics(id) ON DELETE CASCADE,
24
+ status text NOT NULL DEFAULT 'subscribed',
25
+ consent_source text,
26
+ consent_at timestamptz,
27
+ unsubscribed_at timestamptz,
28
+ created_at timestamptz NOT NULL DEFAULT now(),
29
+ updated_at timestamptz NOT NULL DEFAULT now(),
30
+ CONSTRAINT marketing_subscriptions_status_check
31
+ CHECK (status IN ('subscribed', 'unsubscribed')),
32
+ CONSTRAINT marketing_subscriptions_contact_topic_unique
33
+ UNIQUE (contact_id, topic_id)
34
+ );
35
+
36
+ CREATE INDEX IF NOT EXISTS idx_marketing_subscriptions_contact_id
37
+ ON public.marketing_subscriptions (contact_id);
38
+
39
+ CREATE INDEX IF NOT EXISTS idx_marketing_subscriptions_topic_id
40
+ ON public.marketing_subscriptions (topic_id);
41
+
42
+ CREATE TABLE IF NOT EXISTS public.marketing_contact_settings (
43
+ contact_id uuid PRIMARY KEY REFERENCES public.crm_contacts(id) ON DELETE CASCADE,
44
+ unsubscribe_token uuid NOT NULL DEFAULT gen_random_uuid() UNIQUE,
45
+ preferred_language text,
46
+ created_at timestamptz NOT NULL DEFAULT now(),
47
+ updated_at timestamptz NOT NULL DEFAULT now()
48
+ );
49
+
50
+ CREATE TABLE IF NOT EXISTS public.marketing_suppressions (
51
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
52
+ email text NOT NULL UNIQUE,
53
+ reason text NOT NULL,
54
+ source text,
55
+ created_at timestamptz NOT NULL DEFAULT now(),
56
+ CONSTRAINT marketing_suppressions_email_lowercase_check
57
+ CHECK (email = lower(trim(email))),
58
+ CONSTRAINT marketing_suppressions_reason_check
59
+ CHECK (reason IN ('bounced', 'complained', 'unsubscribed_all', 'manual'))
60
+ );
61
+
62
+ CREATE INDEX IF NOT EXISTS idx_marketing_suppressions_email
63
+ ON public.marketing_suppressions (email);
64
+
65
+ CREATE OR REPLACE FUNCTION public.set_marketing_updated_at()
66
+ RETURNS trigger
67
+ LANGUAGE plpgsql
68
+ SECURITY DEFINER
69
+ SET search_path = public
70
+ AS $$
71
+ BEGIN
72
+ NEW.updated_at := now();
73
+ RETURN NEW;
74
+ END;
75
+ $$;
76
+
77
+ DROP TRIGGER IF EXISTS set_marketing_topics_updated_at ON public.marketing_topics;
78
+ CREATE TRIGGER set_marketing_topics_updated_at
79
+ BEFORE UPDATE ON public.marketing_topics
80
+ FOR EACH ROW
81
+ EXECUTE FUNCTION public.set_marketing_updated_at();
82
+
83
+ DROP TRIGGER IF EXISTS set_marketing_subscriptions_updated_at ON public.marketing_subscriptions;
84
+ CREATE TRIGGER set_marketing_subscriptions_updated_at
85
+ BEFORE UPDATE ON public.marketing_subscriptions
86
+ FOR EACH ROW
87
+ EXECUTE FUNCTION public.set_marketing_updated_at();
88
+
89
+ DROP TRIGGER IF EXISTS set_marketing_contact_settings_updated_at ON public.marketing_contact_settings;
90
+ CREATE TRIGGER set_marketing_contact_settings_updated_at
91
+ BEFORE UPDATE ON public.marketing_contact_settings
92
+ FOR EACH ROW
93
+ EXECUTE FUNCTION public.set_marketing_updated_at();
94
+
95
+ ALTER TABLE public.marketing_topics ENABLE ROW LEVEL SECURITY;
96
+ ALTER TABLE public.marketing_subscriptions ENABLE ROW LEVEL SECURITY;
97
+ ALTER TABLE public.marketing_contact_settings ENABLE ROW LEVEL SECURITY;
98
+ ALTER TABLE public.marketing_suppressions ENABLE ROW LEVEL SECURITY;
99
+
100
+ DROP POLICY IF EXISTS "Service role manages marketing topics" ON public.marketing_topics;
101
+ CREATE POLICY "Service role manages marketing topics"
102
+ ON public.marketing_topics FOR ALL TO service_role
103
+ USING (true) WITH CHECK (true);
104
+
105
+ DROP POLICY IF EXISTS "Service role manages marketing subscriptions" ON public.marketing_subscriptions;
106
+ CREATE POLICY "Service role manages marketing subscriptions"
107
+ ON public.marketing_subscriptions FOR ALL TO service_role
108
+ USING (true) WITH CHECK (true);
109
+
110
+ DROP POLICY IF EXISTS "Service role manages marketing contact settings" ON public.marketing_contact_settings;
111
+ CREATE POLICY "Service role manages marketing contact settings"
112
+ ON public.marketing_contact_settings FOR ALL TO service_role
113
+ USING (true) WITH CHECK (true);
114
+
115
+ DROP POLICY IF EXISTS "Service role manages marketing suppressions" ON public.marketing_suppressions;
116
+ CREATE POLICY "Service role manages marketing suppressions"
117
+ ON public.marketing_suppressions FOR ALL TO service_role
118
+ USING (true) WITH CHECK (true);
@@ -0,0 +1,133 @@
1
+ create table if not exists public.marketing_campaigns (
2
+ id uuid primary key default gen_random_uuid(),
3
+ name text not null,
4
+ subject text not null,
5
+ preheader text,
6
+ from_name text,
7
+ from_email text,
8
+ topic_id uuid not null references public.marketing_topics(id),
9
+ body_html text,
10
+ body_text text,
11
+ body_json jsonb,
12
+ status text not null default 'draft'
13
+ check (status in ('draft', 'scheduled', 'sending', 'sent', 'canceled', 'failed')),
14
+ scheduled_at timestamptz,
15
+ sent_at timestamptz,
16
+ batch_size integer not null default 100 check (batch_size > 0),
17
+ rate_per_minute integer check (rate_per_minute is null or rate_per_minute > 0),
18
+ total_recipients integer not null default 0,
19
+ sent_count integer not null default 0,
20
+ failed_count integer not null default 0,
21
+ created_by_profile_id uuid references public.profiles(id),
22
+ created_at timestamptz not null default now(),
23
+ updated_at timestamptz not null default now()
24
+ );
25
+
26
+ create table if not exists public.marketing_campaign_recipients (
27
+ id uuid primary key default gen_random_uuid(),
28
+ campaign_id uuid not null references public.marketing_campaigns(id) on delete cascade,
29
+ contact_id uuid references public.crm_contacts(id) on delete set null,
30
+ email text not null,
31
+ status text not null default 'queued'
32
+ check (status in ('queued', 'sending', 'sent', 'failed', 'suppressed', 'skipped')),
33
+ attempt_count integer not null default 0,
34
+ next_attempt_at timestamptz,
35
+ provider_message_id text,
36
+ error text,
37
+ sent_at timestamptz,
38
+ created_at timestamptz not null default now(),
39
+ updated_at timestamptz not null default now(),
40
+ unique (campaign_id, contact_id)
41
+ );
42
+
43
+ create index if not exists marketing_campaign_recipients_campaign_status_idx
44
+ on public.marketing_campaign_recipients (campaign_id, status);
45
+ create index if not exists marketing_campaign_recipients_worker_claim_idx
46
+ on public.marketing_campaign_recipients (status, next_attempt_at);
47
+ create index if not exists marketing_campaign_recipients_provider_message_idx
48
+ on public.marketing_campaign_recipients (provider_message_id)
49
+ where provider_message_id is not null;
50
+
51
+ create table if not exists public.marketing_message_events (
52
+ id uuid primary key default gen_random_uuid(),
53
+ campaign_id uuid references public.marketing_campaigns(id) on delete set null,
54
+ recipient_id uuid references public.marketing_campaign_recipients(id) on delete set null,
55
+ contact_id uuid,
56
+ provider text not null default 'resend',
57
+ event_type text not null,
58
+ provider_event_id text,
59
+ payload jsonb,
60
+ occurred_at timestamptz not null default now(),
61
+ created_at timestamptz not null default now()
62
+ );
63
+
64
+ create unique index if not exists marketing_message_events_provider_event_unique_idx
65
+ on public.marketing_message_events (provider, provider_event_id)
66
+ where provider_event_id is not null;
67
+
68
+ alter table public.marketing_campaigns enable row level security;
69
+ alter table public.marketing_campaign_recipients enable row level security;
70
+ alter table public.marketing_message_events enable row level security;
71
+
72
+ drop policy if exists "service role manages marketing campaigns" on public.marketing_campaigns;
73
+ create policy "service role manages marketing campaigns"
74
+ on public.marketing_campaigns for all
75
+ using (auth.role() = 'service_role')
76
+ with check (auth.role() = 'service_role');
77
+
78
+ drop policy if exists "service role manages marketing campaign recipients"
79
+ on public.marketing_campaign_recipients;
80
+ create policy "service role manages marketing campaign recipients"
81
+ on public.marketing_campaign_recipients for all
82
+ using (auth.role() = 'service_role')
83
+ with check (auth.role() = 'service_role');
84
+
85
+ drop policy if exists "service role manages marketing message events"
86
+ on public.marketing_message_events;
87
+ create policy "service role manages marketing message events"
88
+ on public.marketing_message_events for all
89
+ using (auth.role() = 'service_role')
90
+ with check (auth.role() = 'service_role');
91
+
92
+ drop trigger if exists marketing_campaigns_set_updated_at on public.marketing_campaigns;
93
+ create trigger marketing_campaigns_set_updated_at
94
+ before update on public.marketing_campaigns
95
+ for each row execute function public.set_marketing_updated_at();
96
+
97
+ drop trigger if exists marketing_campaign_recipients_set_updated_at
98
+ on public.marketing_campaign_recipients;
99
+ create trigger marketing_campaign_recipients_set_updated_at
100
+ before update on public.marketing_campaign_recipients
101
+ for each row execute function public.set_marketing_updated_at();
102
+
103
+ create or replace function public.claim_marketing_campaign_recipients(
104
+ target_campaign_id uuid,
105
+ claim_limit integer,
106
+ claim_time timestamptz default now()
107
+ )
108
+ returns setof public.marketing_campaign_recipients
109
+ language sql
110
+ security definer
111
+ set search_path = public
112
+ as $$
113
+ update public.marketing_campaign_recipients
114
+ set status = 'sending',
115
+ attempt_count = attempt_count + 1,
116
+ updated_at = claim_time
117
+ where id in (
118
+ select id
119
+ from public.marketing_campaign_recipients
120
+ where campaign_id = target_campaign_id
121
+ and status = 'queued'
122
+ and (next_attempt_at is null or next_attempt_at <= claim_time)
123
+ order by created_at, id
124
+ for update skip locked
125
+ limit greatest(1, least(claim_limit, 1000))
126
+ )
127
+ returning *;
128
+ $$;
129
+
130
+ revoke all on function public.claim_marketing_campaign_recipients(uuid, integer, timestamptz)
131
+ from public, anon, authenticated;
132
+ grant execute on function public.claim_marketing_campaign_recipients(uuid, integer, timestamptz)
133
+ to service_role;
@@ -0,0 +1,41 @@
1
+ -- Saved CRM audience filters for consent-first marketing campaigns.
2
+ create table if not exists public.marketing_segments (
3
+ id uuid primary key default gen_random_uuid(),
4
+ name text not null,
5
+ description text,
6
+ rule_json jsonb not null default '{}'::jsonb,
7
+ created_by_profile_id uuid references public.profiles(id) on delete set null,
8
+ created_at timestamptz not null default now(),
9
+ updated_at timestamptz not null default now()
10
+ );
11
+
12
+ alter table public.marketing_campaigns
13
+ add column if not exists segment_id uuid
14
+ references public.marketing_segments(id) on delete set null;
15
+
16
+ create index if not exists marketing_segments_created_at_idx
17
+ on public.marketing_segments (created_at desc);
18
+
19
+ create index if not exists marketing_campaigns_segment_id_idx
20
+ on public.marketing_campaigns (segment_id)
21
+ where segment_id is not null;
22
+
23
+ create index if not exists marketing_message_events_engagement_idx
24
+ on public.marketing_message_events (event_type, occurred_at desc, contact_id)
25
+ where contact_id is not null;
26
+
27
+ drop trigger if exists set_marketing_segments_updated_at on public.marketing_segments;
28
+ create trigger set_marketing_segments_updated_at
29
+ before update on public.marketing_segments
30
+ for each row execute function public.set_marketing_updated_at();
31
+
32
+ alter table public.marketing_segments enable row level security;
33
+
34
+ drop policy if exists "Service role manages marketing segments"
35
+ on public.marketing_segments;
36
+ create policy "Service role manages marketing segments"
37
+ on public.marketing_segments for all to service_role
38
+ using (true) with check (true);
39
+
40
+ revoke all on table public.marketing_segments from anon, authenticated;
41
+ grant all on table public.marketing_segments to service_role;
@@ -0,0 +1,174 @@
1
+ create table if not exists public.marketing_workflows (
2
+ id uuid primary key default gen_random_uuid(),
3
+ name text not null,
4
+ description text,
5
+ trigger_type text not null
6
+ check (trigger_type in (
7
+ 'contact_subscribed',
8
+ 'crm_status_changed',
9
+ 'project_status_changed',
10
+ 'form_submitted'
11
+ )),
12
+ trigger_config jsonb not null default '{}'::jsonb,
13
+ status text not null default 'draft'
14
+ check (status in ('draft', 'active', 'paused')),
15
+ created_by_profile_id uuid references public.profiles(id),
16
+ created_at timestamptz not null default now(),
17
+ updated_at timestamptz not null default now()
18
+ );
19
+
20
+ create table if not exists public.marketing_workflow_nodes (
21
+ id uuid primary key default gen_random_uuid(),
22
+ workflow_id uuid not null
23
+ references public.marketing_workflows(id) on delete cascade,
24
+ position integer not null,
25
+ node_type text not null
26
+ check (node_type in ('send_email', 'wait', 'add_tag')),
27
+ config jsonb not null default '{}'::jsonb,
28
+ created_at timestamptz not null default now(),
29
+ updated_at timestamptz not null default now(),
30
+ unique (workflow_id, position)
31
+ );
32
+
33
+ create index if not exists marketing_workflow_nodes_workflow_position_idx
34
+ on public.marketing_workflow_nodes (workflow_id, position);
35
+
36
+ create table if not exists public.marketing_workflow_runs (
37
+ id uuid primary key default gen_random_uuid(),
38
+ workflow_id uuid not null
39
+ references public.marketing_workflows(id) on delete cascade,
40
+ contact_id uuid references public.crm_contacts(id) on delete cascade,
41
+ status text not null default 'active'
42
+ check (status in ('active', 'completed', 'failed', 'canceled')),
43
+ current_position integer not null default 0,
44
+ next_run_at timestamptz,
45
+ context jsonb,
46
+ started_at timestamptz not null default now(),
47
+ completed_at timestamptz,
48
+ created_at timestamptz not null default now(),
49
+ updated_at timestamptz not null default now(),
50
+ unique (workflow_id, contact_id)
51
+ );
52
+
53
+ create index if not exists marketing_workflow_runs_due_idx
54
+ on public.marketing_workflow_runs (status, next_run_at);
55
+
56
+ create table if not exists public.marketing_workflow_step_runs (
57
+ id uuid primary key default gen_random_uuid(),
58
+ run_id uuid not null
59
+ references public.marketing_workflow_runs(id) on delete cascade,
60
+ node_id uuid references public.marketing_workflow_nodes(id) on delete set null,
61
+ status text not null check (status in ('done', 'failed', 'skipped')),
62
+ error text,
63
+ executed_at timestamptz not null default now()
64
+ );
65
+
66
+ create table if not exists public.marketing_contact_tags (
67
+ id uuid primary key default gen_random_uuid(),
68
+ contact_id uuid not null references public.crm_contacts(id) on delete cascade,
69
+ tag text not null,
70
+ created_at timestamptz not null default now(),
71
+ unique (contact_id, tag)
72
+ );
73
+
74
+ create table if not exists public.marketing_worker_cursors (
75
+ key text primary key,
76
+ cursor timestamptz
77
+ );
78
+
79
+ alter table public.marketing_workflows enable row level security;
80
+ alter table public.marketing_workflow_nodes enable row level security;
81
+ alter table public.marketing_workflow_runs enable row level security;
82
+ alter table public.marketing_workflow_step_runs enable row level security;
83
+ alter table public.marketing_contact_tags enable row level security;
84
+ alter table public.marketing_worker_cursors enable row level security;
85
+
86
+ drop policy if exists "service role manages marketing workflows"
87
+ on public.marketing_workflows;
88
+ create policy "service role manages marketing workflows"
89
+ on public.marketing_workflows for all
90
+ using (auth.role() = 'service_role')
91
+ with check (auth.role() = 'service_role');
92
+
93
+ drop policy if exists "service role manages marketing workflow nodes"
94
+ on public.marketing_workflow_nodes;
95
+ create policy "service role manages marketing workflow nodes"
96
+ on public.marketing_workflow_nodes for all
97
+ using (auth.role() = 'service_role')
98
+ with check (auth.role() = 'service_role');
99
+
100
+ drop policy if exists "service role manages marketing workflow runs"
101
+ on public.marketing_workflow_runs;
102
+ create policy "service role manages marketing workflow runs"
103
+ on public.marketing_workflow_runs for all
104
+ using (auth.role() = 'service_role')
105
+ with check (auth.role() = 'service_role');
106
+
107
+ drop policy if exists "service role manages marketing workflow step runs"
108
+ on public.marketing_workflow_step_runs;
109
+ create policy "service role manages marketing workflow step runs"
110
+ on public.marketing_workflow_step_runs for all
111
+ using (auth.role() = 'service_role')
112
+ with check (auth.role() = 'service_role');
113
+
114
+ drop policy if exists "service role manages marketing contact tags"
115
+ on public.marketing_contact_tags;
116
+ create policy "service role manages marketing contact tags"
117
+ on public.marketing_contact_tags for all
118
+ using (auth.role() = 'service_role')
119
+ with check (auth.role() = 'service_role');
120
+
121
+ drop policy if exists "service role manages marketing worker cursors"
122
+ on public.marketing_worker_cursors;
123
+ create policy "service role manages marketing worker cursors"
124
+ on public.marketing_worker_cursors for all
125
+ using (auth.role() = 'service_role')
126
+ with check (auth.role() = 'service_role');
127
+
128
+ drop trigger if exists marketing_workflows_set_updated_at
129
+ on public.marketing_workflows;
130
+ create trigger marketing_workflows_set_updated_at
131
+ before update on public.marketing_workflows
132
+ for each row execute function public.set_marketing_updated_at();
133
+
134
+ drop trigger if exists marketing_workflow_nodes_set_updated_at
135
+ on public.marketing_workflow_nodes;
136
+ create trigger marketing_workflow_nodes_set_updated_at
137
+ before update on public.marketing_workflow_nodes
138
+ for each row execute function public.set_marketing_updated_at();
139
+
140
+ drop trigger if exists marketing_workflow_runs_set_updated_at
141
+ on public.marketing_workflow_runs;
142
+ create trigger marketing_workflow_runs_set_updated_at
143
+ before update on public.marketing_workflow_runs
144
+ for each row execute function public.set_marketing_updated_at();
145
+
146
+ create or replace function public.claim_marketing_workflow_runs(
147
+ claim_limit integer,
148
+ claim_time timestamptz default now()
149
+ )
150
+ returns setof public.marketing_workflow_runs
151
+ language sql
152
+ security definer
153
+ set search_path = public
154
+ as $$
155
+ update public.marketing_workflow_runs
156
+ set next_run_at = claim_time + interval '5 minutes',
157
+ updated_at = claim_time
158
+ where id in (
159
+ select id
160
+ from public.marketing_workflow_runs
161
+ where status = 'active'
162
+ and next_run_at is not null
163
+ and next_run_at <= claim_time
164
+ order by next_run_at, created_at, id
165
+ for update skip locked
166
+ limit greatest(1, least(claim_limit, 100))
167
+ )
168
+ returning *;
169
+ $$;
170
+
171
+ revoke all on function public.claim_marketing_workflow_runs(integer, timestamptz)
172
+ from public, anon, authenticated;
173
+ grant execute on function public.claim_marketing_workflow_runs(integer, timestamptz)
174
+ to service_role;