Skip to main content
RapidDev - Software Development Agency
App Featurespersonalization-ux27 min read

How to Add Marketing Automation to Your App — Copy-Paste Prompts Inside

Marketing automation needs five pieces: a workflow definition stored as JSONB, a trigger engine watching your Supabase tables, a job queue with pg_cron scheduling, email via Resend and push via Firebase Cloud Messaging, and a visual builder canvas. With Lovable or V0 you can ship a working drip sequence in 6–12 hours. Supabase Pro ($25/mo) is required for pg_cron; Resend free tier covers 3,000 emails/month.

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Advanced

Category

personalization-ux

Build with AI

6–12 hours with Lovable or V0

Custom build

2–6 weeks custom dev

Running cost

$25–70/mo for 100–1K users (Supabase Pro + email)

Works on

WebMobile

Everything it takes to ship Marketing Automation — parts, prompts, and real costs.

TL;DR

Marketing automation needs five pieces: a workflow definition stored as JSONB, a trigger engine watching your Supabase tables, a job queue with pg_cron scheduling, email via Resend and push via Firebase Cloud Messaging, and a visual builder canvas. With Lovable or V0 you can ship a working drip sequence in 6–12 hours. Supabase Pro ($25/mo) is required for pg_cron; Resend free tier covers 3,000 emails/month.

What Marketing Automation Actually Is

Marketing automation is a system that watches what users do in your app and responds with timed, targeted actions — a welcome email sequence when someone signs up, a push notification when they've been inactive for 7 days, a discount when they complete their first purchase. The mechanics are: triggers (events that start a workflow), conditions (IF user.plan = 'free'), delay steps (wait 3 days), and actions (send email, send push, update a database field). Unlike a simple scheduled email, automation is user-specific and event-driven. The product decisions are which triggers matter for your business, how complex the branching needs to be, and whether you need a visual builder your team can operate without engineering support.

What users consider table stakes in 2026

  • Visual workflow builder showing the trigger-to-action chain as a connected node graph so non-engineers can read and edit it
  • Pre-built trigger types including user signed up, made first purchase, inactive for N days, and reached a milestone — configured via form fields without SQL
  • Email and push notification as action channels, each with a template editor showing a preview before activation
  • Audience segmentation filtering by user attributes (plan, country, signup date, last active) with AND/OR conditions
  • Delay and wait steps between actions with configurable duration in hours or days
  • Per-workflow analytics showing emails sent, open rate, click rate, and conversion alongside the workflow graph
  • Pause and resume individual workflows without deleting them or losing pending jobs in flight

Anatomy of the Feature

Seven components. The workflow builder canvas and the job queue are the hardest for AI tools to generate reliably — the canvas because React Flow node persistence is often stubbed, the queue because pg_cron and SELECT FOR UPDATE SKIP LOCKED are uncommon patterns that AI tools frequently get wrong.

Layers:UIDataBackendService

Workflow Builder Canvas

UI

A visual node graph built with React Flow (web, MIT license, zero cost) where each node represents a trigger, condition (IF/ELSE branch), delay step, or action. Nodes connect via edges to form the execution DAG. Drag to reposition, click to configure each node via a sidebar form. Serializes to JSONB on save via a Server Action.

Note: React Flow nodes and edges must be explicitly persisted to Supabase — React Flow stores them in component state by default. Add an onSave button that calls reactFlowInstance.toObject() and writes the result as the steps JSONB column. AI tools frequently miss this step.

Trigger Engine

Backend

A Supabase Edge Function subscribed to Supabase Realtime INSERT/UPDATE events on key tables (users, orders, sessions) or called by a pg_cron schedule. Evaluates the trigger_config JSONB from the workflow against the event payload — e.g. 'user.plan = free AND event = signed_up'. When conditions match, inserts a row into automation_jobs with scheduled_at = now() + delay.

Note: Time-based triggers (inactive for N days) require pg_cron to run a query against the users table on a schedule. Event-based triggers (user just purchased) use Supabase Realtime DB events. Both types feed the same automation_jobs queue.

Job Queue Table

Data

The automation_jobs table acts as a durable task queue. Each row represents one pending action for one user in one workflow: workflow_id, user_id, step_index, scheduled_at, status (pending/running/completed/failed), attempts, and last_error. A pg_cron job checks every minute for rows WHERE scheduled_at <= now() AND status = 'pending' and invokes the Edge Function processor.

Note: Use SELECT ... FOR UPDATE SKIP LOCKED when fetching the next job. This is the correct PostgreSQL pattern for job queues — it prevents two concurrent pg_cron invocations from double-processing the same job and sending duplicate emails.

Email Action

Service

The Edge Function reads the email step's template and recipient from the workflow context and calls Resend's API (resend.com). Resend free tier: 3,000 emails/month. Open tracking via a 1-pixel tracking URL that must resolve to a stable public endpoint (not a preview URL). Click tracking via a redirect URL logged to analytics_events.

Note: Open and click tracking require a stable public URL — they will silently fail on Lovable preview domains or V0 sandbox URLs. Deploy to production before testing analytics.

Push Notification Action

Service

Firebase Cloud Messaging (FCM) for native mobile apps (Flutter, React Native) and web push. The Edge Function fetches the user's device token from a user_devices table and calls the FCM API with the notification payload. FCM is free with no volume limit. For Expo-based apps, use Expo Push Notifications API (free tier: 1,000 notifications/month; paid from $29/month approx).

Note: Device tokens expire or rotate when users reinstall the app. Handle FCM's 'registration-token-not-registered' error response by deleting the stale token from user_devices rather than retrying.

Audience Segment Builder

UI

A filter UI built with shadcn/ui components (or Flutter custom widgets for mobile admin panels) allowing AND/OR conditions on user attributes: plan, country, signup_date, last_active_at. Each condition is a row: attribute, operator (equals, greater than, contains), value. The segment definition is stored as JSONB and evaluated by a Supabase RPC function (evaluate_segment) at trigger time.

Note: Store the segment as a portable JSONB definition rather than a SQL fragment. The RPC function translates JSONB conditions into a parameterized query against the users table — this prevents SQL injection and keeps segment logic portable.

Workflow Analytics

Data

The analytics_events table records every automation event: trigger_fired, email_sent, email_opened (pixel callback), link_clicked (redirect callback), push_delivered, push_opened, and conversion. Aggregated per workflow for the analytics dashboard showing open rate, click rate, and conversion funnel. Supabase Realtime enables live counters on the workflow detail page.

Note: Email open tracking relies on a 1-pixel transparent image hosted at a public URL. The URL encodes the job_id in the path — when the email client loads the pixel, your Edge Function logs the email_opened event. Most email clients block tracking pixels by default; treat open rate as directional, not precise.

The data model

Three tables: workflow definitions, the job queue, and analytics events. Run this in the Supabase SQL Editor — it includes the table structure, RLS policies, and the index that makes the job queue performant:

schema.sql
1-- Workflow definitions
2CREATE TABLE public.automation_workflows (
3 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
4 name text NOT NULL,
5 trigger_config jsonb NOT NULL DEFAULT '{}',
6 steps jsonb NOT NULL DEFAULT '[]',
7 is_active bool DEFAULT false NOT NULL,
8 created_by uuid REFERENCES auth.users(id),
9 created_at timestamptz DEFAULT now() NOT NULL,
10 updated_at timestamptz DEFAULT now() NOT NULL
11);
12
13ALTER TABLE public.automation_workflows ENABLE ROW LEVEL SECURITY;
14
15CREATE POLICY "Workflow owners can manage their workflows"
16 ON public.automation_workflows
17 USING (created_by = auth.uid());
18
19-- Job queue
20CREATE TABLE public.automation_jobs (
21 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
22 workflow_id uuid REFERENCES public.automation_workflows(id) ON DELETE CASCADE NOT NULL,
23 user_id uuid REFERENCES auth.users(id) NOT NULL,
24 step_index int DEFAULT 0 NOT NULL,
25 scheduled_at timestamptz NOT NULL,
26 status text CHECK (status IN ('pending', 'running', 'completed', 'failed')) DEFAULT 'pending' NOT NULL,
27 attempts int DEFAULT 0 NOT NULL,
28 last_error text,
29 created_at timestamptz DEFAULT now() NOT NULL
30);
31
32-- Critical index: pg_cron polls this constantly
33CREATE INDEX automation_jobs_due_idx
34 ON public.automation_jobs (scheduled_at, status)
35 WHERE status = 'pending';
36
37ALTER TABLE public.automation_jobs ENABLE ROW LEVEL SECURITY;
38
39CREATE POLICY "Service role manages jobs"
40 ON public.automation_jobs
41 USING (auth.role() = 'service_role');
42
43CREATE POLICY "Workflow owners can view jobs"
44 ON public.automation_jobs FOR SELECT
45 USING (
46 EXISTS (
47 SELECT 1 FROM public.automation_workflows w
48 WHERE w.id = workflow_id
49 AND w.created_by = auth.uid()
50 )
51 );
52
53-- Analytics events
54CREATE TABLE public.automation_analytics (
55 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
56 workflow_id uuid REFERENCES public.automation_workflows(id) ON DELETE CASCADE NOT NULL,
57 job_id uuid REFERENCES public.automation_jobs(id),
58 event_type text NOT NULL,
59 user_id uuid REFERENCES auth.users(id),
60 metadata jsonb DEFAULT '{}',
61 occurred_at timestamptz DEFAULT now() NOT NULL
62);
63
64CREATE INDEX automation_analytics_workflow_idx
65 ON public.automation_analytics (workflow_id, occurred_at DESC);
66
67ALTER TABLE public.automation_analytics ENABLE ROW LEVEL SECURITY;
68
69CREATE POLICY "Workflow owners can view analytics"
70 ON public.automation_analytics FOR SELECT
71 USING (
72 EXISTS (
73 SELECT 1 FROM public.automation_workflows w
74 WHERE w.id = workflow_id
75 AND w.created_by = auth.uid()
76 )
77 );
78
79CREATE POLICY "Service role writes analytics"
80 ON public.automation_analytics FOR INSERT
81 WITH CHECK (auth.role() = 'service_role');
82
83-- User email preferences (unsubscribe tracking)
84CREATE TABLE public.user_email_preferences (
85 user_id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
86 unsubscribed bool DEFAULT false NOT NULL,
87 unsubscribed_at timestamptz
88);
89
90ALTER TABLE public.user_email_preferences ENABLE ROW LEVEL SECURITY;
91
92CREATE POLICY "Users manage their own preferences"
93 ON public.user_email_preferences
94 USING (user_id = auth.uid());

Heads up: The partial index on automation_jobs WHERE status = 'pending' keeps pg_cron's every-minute poll fast even with thousands of historical completed jobs. The user_email_preferences table is required before going live — any automation sending email must check this table before calling Resend to stay compliant with CAN-SPAM.

Build it — pick your path

Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.

Hand-built by developersFit for this feature:

Full custom development is the right path when you need A/B test nodes, CAN-SPAM/GDPR compliance infrastructure, white-label automation sold to your own customers, or custom behavioral event triggers beyond standard DB events.

Step by step

  1. 1Implement A/B test nodes in the React Flow canvas: a split node that routes a configurable percentage of the audience to variant A vs variant B email templates; track conversions per variant in analytics_events
  2. 2Build CAN-SPAM/GDPR compliance infrastructure: suppression lists, double-opt-in confirmation flows, automatic unsubscribe handling from Resend's webhook, and 30-day user data deletion workflows triggered by account deletion
  3. 3Implement custom behavioral event tracking for in-app events not captured in standard DB changes: video watched 80% of duration, feature used 3 times, upgrade modal dismissed twice — requires a client-side event SDK calling trigger-workflow
  4. 4Build multi-tenant workflow isolation for white-label scenarios where each of your customers manages their own automation rules with separate template libraries and analytics
  5. 5Set up custom email sending domain with Resend (DKIM, SPF, DMARC DNS records) for deliverability — takes 1–2 days and requires DNS access to your sending domain

Where this path bites

  • Deliverability setup (custom domain, DKIM/SPF/DMARC DNS records) takes 1–2 days alone and requires DNS access separate from the application code
  • Multi-tenant workflow isolation adds significant schema complexity — each customer needs isolated workflow definitions, job queues, analytics, and suppression lists with strict RLS boundaries

Third-party services you'll need

Marketing automation requires three external services — email, push, and the scheduler. All have usable free tiers for early-stage apps:

ServiceWhat it doesFree tierPaid from
ResendTransactional email sending for email action steps; open and click tracking via webhooks; unsubscribe webhook for automatic preference updates3,000 emails/month, 1 custom domain$20/mo for 50,000 emails (approx)
Firebase Cloud Messaging (FCM)Push notification delivery to Android and iOS devices; no volume limit; requires Firebase project setupFree, no volume limitFree
Supabase pg_cronScheduled every-minute polling of automation_jobs queue to trigger the process-jobs Edge Function; required for any time-delayed automation stepNot available on free tier$25/mo (Supabase Pro, includes pg_cron)
React FlowOpen-source node graph library for the visual workflow builder canvas; MIT licenseFree (MIT license)Free
Expo Push NotificationsAlternative push channel for Expo-based React Native apps; simpler setup than raw FCM for Expo projects1,000 notifications/monthFrom $29/mo (approx)

Swipe the table sideways to see pricing.

What it costs to run

Drag through the tiers to see how your monthly bill scales with users — no surprises later.

Estimated monthly running cost

$35/mo

Supabase Pro ($25/mo) required for pg_cron. Resend free tier covers 3,000 emails — sufficient for welcome sequences at this scale. FCM push is free. The $35 floor is driven entirely by pg_cron requiring the Pro plan; without pg_cron you can't run time-delayed automations.

Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.

What breaks when AI tools build this

The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.

Email open pixel fires but the analytics event is never recorded

Symptom: The tracking pixel URL is generated as a path on the Lovable preview domain or V0 sandbox URL, neither of which can receive inbound HTTP requests from email clients. When a user's email client loads the pixel, the request hits a dead URL and silently fails — open rate shows 0% for all workflows.

Fix: Deploy to the published Lovable URL or Vercel before enabling open tracking. The track-email-open endpoint must be a stable HTTPS URL accessible from the public internet. Test by sending yourself a test email, opening it, and checking the automation_analytics table in Supabase for the email_opened event within 30 seconds.

pg_cron fires but automation_jobs are not processed — Edge Function returns 500

Symptom: pg_cron calls the Edge Function via an HTTP request. If the Edge Function URL is wrong, the service role key in the cron SQL is stale, or the Edge Function itself has an unhandled runtime error, pg_cron silently marks the invocation as failed. The job queue fills up with pending jobs while workflows appear active.

Fix: Check SELECT * FROM cron.job_run_details ORDER BY start_time DESC LIMIT 10 in the Supabase SQL Editor to see pg_cron invocation errors. Verify the Edge Function URL format is https://<project-ref>.supabase.co/functions/v1/<function-name> and the Authorization header uses the current service role key from Supabase Dashboard → Project Settings → API.

Users receive duplicate emails from the same automation step

Symptom: When pg_cron fires every minute, if the process-jobs Edge Function takes longer than 60 seconds to process a batch, the next cron invocation picks up the same 'pending' job before the first invocation has marked it as 'running'. Both invocations execute the email step, sending the same email twice to the same user.

Fix: Use SELECT ... FOR UPDATE SKIP LOCKED when fetching the next job from automation_jobs. This PostgreSQL pattern locks the row for the first invocation and causes the second invocation to skip it entirely. Verify the SQL contains FOR UPDATE SKIP LOCKED in your Edge Function — this is the single most important correctness requirement for a job queue.

React Flow canvas looks correct but workflow node connections don't save to Supabase

Symptom: V0 generates a React Flow canvas that correctly renders nodes and edges in the browser, but stores them only in React component state (reactFlowInstance internal state). Navigating away from the canvas page loses all node positions and connections. The steps JSONB column in automation_workflows never receives a write.

Fix: Add an explicit Save button that calls reactFlowInstance.toObject() to get the current {nodes, edges} state and writes it to the automation_workflows.steps column via a Server Action or Supabase client UPDATE. On page load, initialize React Flow with the stored steps value using defaultNodes and defaultEdges props. The prompt must explicitly request this persistence step — V0 frequently omits it.

Automation continues sending emails to users who unsubscribed

Symptom: The process-jobs Edge Function calls Resend without checking whether the recipient has unsubscribed. Users who clicked the unsubscribe link in a previous email continue receiving future automation steps, creating a CAN-SPAM compliance violation and degrading deliverability reputation.

Fix: Add a user_email_preferences table with an unsubscribed boolean column. Before every Resend API call in the Edge Function, query this table for the recipient's user_id. If unsubscribed = true, skip the send, log an automation_analytics event with event_type = email_skipped_unsubscribed, and advance to the next step. Also register Resend's unsubscribe webhook to auto-update this table when users click Resend's native unsubscribe link.

Best practices

1

Always use SELECT ... FOR UPDATE SKIP LOCKED in your job queue processor — it is the only correct pattern for preventing duplicate email sends from concurrent pg_cron invocations

2

Check user_email_preferences.unsubscribed before every email send in the Edge Function, not only at workflow enrollment — users can unsubscribe mid-sequence

3

Set up a dedicated sending domain in Resend with DKIM and SPF records before going live with more than 500 emails/month — shared-domain sending damages deliverability reputation

4

Initialize React Flow from the stored steps JSONB on canvas load and add an explicit Save button — never rely on React component state as the source of truth for workflow definitions

5

Test automation workflows with a 1-minute delay step before going live so you can verify the end-to-end flow in a few minutes rather than waiting hours

6

Implement exponential backoff for failed jobs (2^attempts minutes, max 3 attempts) to avoid hammering a temporarily unavailable email provider and filling pg_cron logs with errors

7

Add a workflow deactivation check in process-jobs so disabling a workflow cancels all pending jobs rather than letting the queue drain over hours

8

Log every automation action (email sent, push delivered, step skipped) to automation_analytics from the start — retroactively adding analytics to a running automation system requires schema migrations and data backfills

When You Need Custom Development

AI tools handle linear drip sequences and basic conditional branching reliably. Several patterns require custom development:

  • A/B testing nodes required — splitting audiences between two email variants, tracking conversion per variant, and declaring a winner based on statistical significance within the same visual canvas
  • CAN-SPAM/GDPR compliance infrastructure needed at scale: suppression lists, double-opt-in confirmation flows, automatic data deletion workflows, and audit logs that satisfy legal review
  • Custom behavioral event triggers based on in-app actions not captured in standard Supabase table events — video watched 80% of duration, feature used 3 times, upgrade modal dismissed twice — requires a client-side event SDK and a custom trigger ingestion pipeline
  • White-label marketing automation sold to your own customers: multi-tenant workflow isolation with separate template libraries, analytics dashboards, and suppression lists per customer, each with their own Resend sending domain

RapidDev builds these features for production

Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.

Get a fixed-price quote

Frequently asked questions

What's the difference between a trigger and an action in a workflow?

A trigger is the event that starts the workflow — 'user signed up', 'user made first purchase', 'user inactive for 7 days'. An action is what the workflow does in response — 'send email', 'send push notification', 'update user attribute'. Between them you can add conditions (if user.plan = free) and delay steps (wait 3 days). A workflow always starts with exactly one trigger and can have multiple actions in sequence.

Can I pause a workflow without deleting it?

Yes — set is_active = false on the automation_workflows row. This stops the trigger engine from enqueuing new jobs for that workflow. Jobs already in the queue (status = pending) should also be cancelled when you pause — add a workflow_active check in your process-jobs Edge Function that marks pending jobs as cancelled rather than executing them. This prevents emails from being sent from a workflow you've paused.

How do I handle users who unsubscribe?

Store unsubscribe status in a user_email_preferences table with an unsubscribed boolean and unsubscribed_at timestamp. Check this table before every email send in your Edge Function — if unsubscribed = true, skip the send and log the skip event in analytics. Register Resend's unsubscribe webhook to auto-update this table when users click Resend's native footer unsubscribe link. The unsubscribe page itself is a simple route that requires only the user_id in the URL — no authentication needed.

How do I test an automation workflow before going live?

Create a test workflow with a short delay step (1 minute instead of 3 days). Use a test email address you control as the recipient. Deploy to your staging environment (Vercel preview URL or Lovable published URL), enroll your test user account in the workflow by calling trigger-workflow directly, and wait for pg_cron to pick up the job. Check automation_jobs in Supabase to confirm the job moves from pending to running to completed. Only then change the delay back to the production value and activate for real users.

Can I send SMS messages as well as email and push?

SMS is not covered in this build kit because it requires Twilio at $0.0079/message — at 1,000 users receiving 5 automation SMS messages each, that's $39.50 per campaign. Add SMS as a separate action type in your workflow steps JSONB and an Edge Function that calls the Twilio Messages API with the user's phone number from your users table. Make sure you collect explicit SMS marketing consent separately from email consent.

How do I track whether an email was opened?

Embed a 1x1 transparent PNG image in the email body at a URL like https://yourapp.com/api/automation/track-email-open?job_id={job_id}. When the email client loads the image, your API route logs an email_opened event in automation_analytics. Important caveat: Apple Mail Privacy Protection (iOS 15+) pre-fetches all images, inflating open rates by 40–60%. Treat email open rate as a directional engagement signal, not a precise count. Click-through rate on real links is a more reliable conversion metric.

What happens if an email fails to send?

The process-jobs Edge Function increments the attempts counter on the automation_jobs row and sets scheduled_at to now() + exponential backoff (2 minutes after first failure, 4 after second, 8 after third). After 3 failed attempts, the job status is set to 'failed' and the last_error column stores the error message from Resend. The workflow dashboard should surface failed jobs so you can investigate — common causes are expired API keys, invalid recipient email addresses, and Resend rate limits.

Can I segment users by their in-app behavior?

Yes, but behavioral segmentation requires logging those behaviors to your database first. Standard database events (user signed up, order created) are captured automatically by Supabase Realtime. Custom behaviors (feature used 3 times, video watched 80%) need explicit event logging — your app code calls trigger-workflow or writes to a user_events table when the behavior occurs. Once in the database, these values become queryable attributes in your segment filter builder.

RapidDev

Need this feature production-ready?

RapidDev builds marketing automation into real apps — auth, database, payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.