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.
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/app-manifest.mjs +4 -2
- package/src/constants.mjs +88 -11
- package/src/doctor.mjs +38 -0
- package/src/generator.mjs +75 -19
- package/template/base/app/(auth)/auth/confirmed/page.tsx +1 -0
- package/template/base/app/(auth)/auth/post-login/page.tsx +1 -0
- package/template/base/app/(auth)/auth-provider.tsx +25 -0
- package/template/base/app/(auth)/forgot-password/page.tsx +1 -0
- package/template/base/app/(auth)/invite/[invitationId]/invite-route-mount.tsx +10 -0
- package/template/base/app/(auth)/invite/[invitationId]/page.tsx +1 -0
- package/template/base/app/(auth)/layout.tsx +5 -0
- package/template/base/app/(auth)/login/page.tsx +1 -0
- package/template/base/app/(auth)/reset-password/page.tsx +1 -0
- package/template/base/app/(auth)/signup/page.tsx +1 -0
- package/template/base/app/(shell)/account/page.tsx +5 -0
- package/template/base/app/(shell)/dashboard/dashboard-live-mount.tsx +21 -0
- package/template/base/app/(shell)/dashboard/page.tsx +1 -0
- package/template/base/app/(shell)/layout.tsx +24 -0
- package/template/base/app/(shell)/shell-layout-client.tsx +134 -0
- package/template/base/app/api/account/route.ts +2 -0
- package/template/base/app/auth/callback/route.ts +1 -0
- package/template/base/app/auth/cleanup/route.ts +1 -0
- package/template/base/app/layout.tsx +8 -2
- package/template/base/config/env.ts +98 -0
- package/template/modules/marketing/app/(shell)/marketing/page.tsx +1 -0
- package/template/modules/marketing/app/api/marketing/_handlers.ts +144 -0
- package/template/modules/marketing/app/api/marketing/analytics/campaigns/[id]/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/analytics/overview/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/analytics/segments/[id]/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/campaigns/[id]/cancel/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/campaigns/[id]/recipients/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/campaigns/[id]/retry/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/campaigns/[id]/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/campaigns/[id]/schedule/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/campaigns/[id]/send/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/campaigns/[id]/test/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/campaigns/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/segments/[id]/preview/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/segments/[id]/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/segments/preview/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/segments/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/topics/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/unsubscribe/[token]/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/webhooks/resend/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/worker/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/workflows/[id]/activate/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/workflows/[id]/nodes/[nodeId]/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/workflows/[id]/nodes/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/workflows/[id]/pause/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/workflows/[id]/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/workflows/[id]/runs/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/workflows/route.ts +2 -0
- package/template/modules/projects/app/(shell)/projects/[id]/board/page.tsx +1 -0
- package/template/modules/projects/app/(shell)/projects/[id]/page.tsx +1 -0
- package/template/modules/projects/app/(shell)/projects/[id]/tasks/page.tsx +1 -0
- package/template/modules/projects/app/(shell)/projects/layout.tsx +5 -0
- package/template/modules/projects/app/(shell)/projects/page.tsx +1 -0
- package/template/modules/projects/app/(shell)/projects/projects-live-mounts.tsx +27 -0
- package/template/modules/projects/app/(shell)/projects/projects-server-mounts.tsx +54 -0
- package/template/modules/projects/app/api/projects/[id]/activity/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/links/[itemId]/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/links/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/members/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/milestones/[itemId]/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/milestones/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/tasks/[itemId]/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/tasks/route.ts +2 -0
- package/template/modules/projects/app/api/projects/_handlers.ts +86 -0
- package/template/modules/projects/app/api/projects/organizations/route.ts +2 -0
- package/template/modules/projects/app/api/projects/route.ts +2 -0
- package/template/modules/projects/app/api/projects/stats/route.ts +2 -0
- package/template/supabase/README.md +3 -0
- package/template/supabase/module-registry.json +6 -0
- package/template/supabase/modules/marketing/README.md +12 -0
- package/template/supabase/modules/marketing/migrations/20260725120000_marketing_v1.sql +118 -0
- package/template/supabase/modules/marketing/migrations/20260725130000_marketing_campaigns.sql +133 -0
- package/template/supabase/modules/marketing/migrations/20260726120000_marketing_segments.sql +41 -0
- package/template/supabase/modules/marketing/migrations/20260726150000_marketing_workflows.sql +174 -0
- package/template/supabase/modules/marketing/migrations/20260726170000_marketing_engine_safety.sql +68 -0
- /package/template/modules/admin/app/{admin → (shell)/admin}/users/page.tsx +0 -0
- /package/template/modules/crm/app/{crm → (shell)/crm}/layout.tsx +0 -0
- /package/template/modules/crm/app/{crm → (shell)/crm}/page.tsx +0 -0
- /package/template/modules/crm/app/{crm → (shell)/crm}/report/page.tsx +0 -0
|
@@ -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
|
+
}
|
|
@@ -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,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;
|