Feature spec
AdvancedCategory
notifications-alerts
Build with AI
6-10 hours with Lovable or FlutterFlow
Custom build
2-4 weeks custom dev
Running cost
$0-25/mo up to 100 users · $25-125/mo at 1,000 users
Works on
Everything it takes to ship Smart Notifications Based on User Behavior — parts, prompts, and real costs.
Smart behavior-based notifications need four layers: an event tracker that records what each user does in the app, a scoring engine that ranks notification candidates by recency and frequency, a suppressor that blocks sends when a user has been ignoring messages, and a send-time optimizer that delivers at the hour the user historically opens. With Lovable or FlutterFlow you can ship a working v1 in 6-10 hours for $0-25/month at 100 users. This is an Advanced feature — plan the scoring pipeline before generating code.
What Smart Behavior-Based Notifications Actually Are
Standard push notifications are broadcast messages — the same text goes to every user at the same time. Smart behavior-based notifications flip this: the app watches what each user does (views, searches, favorites, cart additions, completions), builds a behavioral profile per user, and uses that profile to decide what to send, when to send it, and whether to send at all. A user who has viewed a specific product three times in two days gets a personalized nudge about that product; a user who has ignored the last five notifications gets nothing until they re-engage. The technical components are an event ingestion table, an hourly aggregation job, a scoring function, a suppressor that enforces fatigue limits, and a send-time optimizer that learns each user's most active hour. This is not a feature to generate without a plan — the multiple Edge Functions and SQL aggregations are where AI tools are most likely to produce plausible-looking but broken code.
What users consider table stakes in 2026
- Notifications reference the specific entity the user interacted with — product name, article title, or saved item — never generic 'Don't forget your cart' copy
- Users who ignore repeated notifications receive fewer messages automatically, not more, with sends pausing after 5 consecutive unread notifications
- Notification timing adapts per user — a user who opens the app every morning at 7 AM receives their nudge at 7 AM, not at 2 PM when no one responds
- Users can see a plain-language reason for why they received a notification (because you viewed this item 3 times) in the notification preference screen
- Opt-out by notification category (promotional, re-engagement, cart recovery) is available without disabling all notifications
- New users with no behavioral history receive a sensible default (10 AM local time, standard copy) until enough events accumulate to personalize
Anatomy of the Feature
Seven components organized across four layers. The UI components are straightforward. The data pipeline — aggregation, scoring, suppression, send-time optimization — is where AI tools generate incorrect logic most often. Read each component before prompting.
User Event Tracker
DataSupabase table user_events (id uuid, user_id uuid, event_type text, entity_id uuid, entity_type text, metadata jsonb, created_at timestamptz). Populated by client-side calls on every tracked user action: view, search, favorite, add-to-cart, complete. Flutter uses a Custom Action or Dart function that POSTs to a Supabase Edge Function on each interaction. Web clients use supabase.from('user_events').insert() directly with the anon key.
Note: Index on (user_id, event_type, created_at) is critical — the hourly aggregation query scans this table by user and time window. Without the index, the aggregation query degrades linearly with event volume.
Behavior Aggregation Job
BackendSupabase pg_cron job running hourly via a Supabase Edge Function. Reads user_events for the past 7 days grouped by user_id, aggregates event counts by event_type, and upserts into user_behavior_summary (user_id uuid PRIMARY KEY, preferred_hour int, events_last_7d int, last_active_at timestamptz, updated_at timestamptz). The preferred_hour column is updated from notification_opens — the hour-of-day with the highest open count per user.
Note: GROUP BY user_id in the aggregation SQL is mandatory — a missing GROUP BY causes all users to share a global average, making the scoring engine identical for everyone.
Notification Scoring Engine
BackendSQL function or Supabase Edge Function that evaluates each candidate notification per user using a weighted score: (event_recency_score * 0.4) + (event_frequency_score * 0.3) + (notification_history_penalty * 0.2) + (user_activity_score * 0.1). Output is a score 0-100 per user per notification type, stored in notification_candidates (id, user_id, type, variant, score, scheduled_for, sent_at, opened_at, suppressed bool). Candidates scoring above a threshold (e.g., 50) are scheduled for delivery.
Note: Start with a simpler scoring heuristic (events_last_7d > 2 AND last_active_at > NOW() - INTERVAL '3 days') before building a weighted formula — simpler rules are easier to debug and often perform better.
Send-Time Optimizer
DataTracks notification_opens (user_id, opened_at timestamptz) and aggregates EXTRACT(HOUR FROM opened_at) to find preferred_hour — the hour-of-day with the most opens for each user. This value is stored in user_behavior_summary.preferred_hour and used by the notification sender to schedule delivery via pg_cron or a delayed send. New users default to preferred_hour = 10 (10 AM local) until enough opens accumulate.
Note: Preferred hour should be stored in UTC and converted to the user's local timezone at send time using the user's stored timezone_preference (e.g., 'America/New_York').
Notification Suppressor
BackendBefore any notification send, checks notification_fatigue table: (user_id uuid PRIMARY KEY, sent_24h int DEFAULT 0, ignored_streak int DEFAULT 0, last_reset timestamptz). If sent_24h >= 3 OR ignored_streak >= 5, the candidate is marked suppressed = true in notification_candidates and skipped. ignored_streak increments when a notification is sent but opened_at remains NULL after 24 hours. A separate reset job checks: if user has any app_open event in the last 7 days and ignored_streak > 0, reset to 0.
Note: The suppressor reset job is the most commonly forgotten component. Without it, re-engaged users who previously ignored notifications stay suppressed indefinitely.
FCM Sender
Servicefirebase_messaging (Flutter) or firebase-admin SDK (Supabase Edge Function) sends FCM push notifications to stored tokens. The notification payload is dynamically constructed from the user's event data — 'You've saved 3 items, complete your purchase' rather than 'Don't forget your cart'. The entity_id and entity_type from user_events are used to look up the entity name for personalization. FCM tokens stored in push_tokens table.
Note: Personalized copy requires a JOIN from notification_candidates to the relevant entity table (products, articles, etc.) at send time — this JOIN must be included in the sender function.
A/B Test Layer
Datanotification_variants table stores 2-3 message variants per notification type (e.g., 'You left something behind' vs 'Complete your order for {product_name}'). Variant assignment: user_id::text % 2 (or % 3 for three variants) gives a stable, consistent assignment without a separate assignment table. Open and convert rates per variant are tracked via opened_at and conversion_event in notification_candidates.
Note: Stable variant assignment is essential — without it, the same user sees different messages on different days and A/B results are meaningless. Store the assigned variant in user_behavior_summary so it is consistent across sends.
The data model
Run this in the Supabase SQL editor. Creates the event tracking, behavior summary, notification pipeline, and fatigue management tables with RLS. This schema is the foundation — wire the Edge Functions to it.
1-- User event tracking table2create table public.user_events (3 id uuid default gen_random_uuid() primary key,4 user_id uuid references auth.users not null,5 event_type text not null, -- 'view', 'search', 'favorite', 'add_to_cart', 'complete'6 entity_id uuid,7 entity_type text, -- 'product', 'article', 'course', etc.8 metadata jsonb,9 created_at timestamptz default now()10);1112alter table public.user_events enable row level security;1314create policy "Users can insert own events"15 on public.user_events for insert16 with check (auth.uid() = user_id);1718create policy "Service role can read all events"19 on public.user_events for select20 to service_role using (true);2122-- Critical index for per-user aggregation performance23create index user_events_user_type_time_idx24 on public.user_events (user_id, event_type, created_at desc);2526-- Behavior summary: upserted hourly by aggregation job27create table public.user_behavior_summary (28 user_id uuid primary key references auth.users,29 preferred_hour int default 10, -- 0-23, defaults to 10am30 preferred_variant text default 'A', -- stable A/B assignment31 events_last_7d int default 0,32 last_active_at timestamptz,33 updated_at timestamptz default now()34);3536alter table public.user_behavior_summary enable row level security;3738create policy "Users can read own behavior summary"39 on public.user_behavior_summary for select40 using (auth.uid() = user_id);4142create policy "Service role can manage behavior summaries"43 on public.user_behavior_summary for all44 to service_role using (true);4546-- Notification candidates: scored and scheduled sends47create table public.notification_candidates (48 id uuid default gen_random_uuid() primary key,49 user_id uuid references auth.users not null,50 type text not null,51 variant text,52 score int,53 scheduled_for timestamptz,54 sent_at timestamptz,55 opened_at timestamptz,56 suppressed bool default false,57 suppression_reason text58);5960alter table public.notification_candidates enable row level security;6162create policy "Users can read own notification candidates"63 on public.notification_candidates for select64 using (auth.uid() = user_id);6566create policy "Service role can manage candidates"67 on public.notification_candidates for all68 to service_role using (true);6970create index notification_candidates_user_sent_idx71 on public.notification_candidates (user_id, sent_at desc);7273-- Fatigue tracking74create table public.notification_fatigue (75 user_id uuid primary key references auth.users,76 sent_24h int default 0,77 ignored_streak int default 0,78 last_reset timestamptz default now()79);8081alter table public.notification_fatigue enable row level security;8283create policy "Service role manages fatigue records"84 on public.notification_fatigue for all85 to service_role using (true);8687-- Notification variants definition table88create table public.notification_variants (89 id uuid default gen_random_uuid() primary key,90 type text not null,91 variant text not null, -- 'A', 'B', 'C'92 title_template text not null,93 body_template text not null, -- supports {{entity_name}} placeholder94 is_active bool default true,95 unique(type, variant)96);9798alter table public.notification_variants enable row level security;99100create policy "Authenticated users can read variants"101 on public.notification_variants for select102 to authenticated using (true);Heads up: The index on user_events (user_id, event_type, created_at desc) is non-optional — the hourly aggregation query filters by all three columns and will timeout on tables with more than 10K rows without it.
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.
Production-grade behavioral notification platform: event ingestion via Segment or Amplitude, cohort-based targeting via PostHog feature flags, campaign orchestration via Customer.io or Braze with built-in suppression and A/B testing. Right for funded products with 10K+ active users.
Step by step
- 1Event pipeline: Segment (free tier 1K MTU/mo; Team $120/mo for 10K MTU — approx) ingests all user events and forwards to Amplitude for behavioral cohort building
- 2Cohort targeting: PostHog feature flags define notification audience segments (active-last-7d, cart-abandoners, power-users) without custom SQL
- 3Campaign orchestration: Customer.io (Essentials $100/mo for 5K contacts — approx) handles suppression, send-time optimization, A/B testing, and channel selection natively — no custom suppressor code needed
- 4Delivery: Customer.io sends FCM push via its native mobile connector; SMS fallback via Twilio for high-value segments
- 5Analytics: open rate, conversion rate, revenue attributed per variant tracked in Customer.io campaign dashboard
Where this path bites
- Braze starts at approximately $50,000/year — appropriate only for enterprise-scale products
- Customer.io at $100/mo (approx) and Segment at $120/mo (approx) add $220+/mo in service costs before infrastructure; validate that notification revenue uplift justifies the spend
- Segment and Customer.io introduce vendor lock-in for behavioral data and campaign logic — migration is expensive
Third-party services you'll need
The core self-built pipeline uses only Supabase and FCM. Third-party behavioral platforms are optional and suitable only when the custom pipeline becomes a bottleneck at scale.
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Firebase Cloud Messaging | FCM push delivery to iOS and Android devices; zero per-message cost | Free for all push delivery | Free; Blaze plan required if using Cloud Functions to send (pay-as-you-go) |
| Supabase pg_cron | Schedules hourly behavior aggregation and daily re-engagement reset jobs | Not available on free tier | Included in Supabase Pro $25/mo |
| Customer.io | Behavioral messaging platform with built-in suppression, send-time optimization, and A/B testing (replaces custom scoring pipeline) | No meaningful free tier | Essentials $100/mo for up to 5,000 contacts (approx) |
| Segment | Event ingestion pipeline that forwards behavioral data to analytics and notification platforms | Free for 1,000 MTU/mo | Team $120/mo for 10,000 MTU (approx) |
| Amplitude | Product analytics with behavioral cohort building for notification audience targeting | Free for 1 million events/mo | Growth plan from $995/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
Supabase Pro $25/mo for pg_cron and Edge Function execution. FCM free. user_events table grows quickly — at 10 events/user/day, 100 users generate 30,000 rows/month, easily within Supabase free DB storage. Edge Function invocations within free limits.
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.
Scoring engine runs but every user gets the same score
Symptom: The behavior aggregation query is missing GROUP BY user_id. Instead of computing per-user event counts, it sums events across all users and assigns the global average to every row. Every user gets an identical score and identical notifications — the behavioral personalization is completely absent even though the pipeline appears to run correctly.
Fix: Verify GROUP BY user_id in the aggregation SQL before anything else. Add an index on user_events(user_id, event_type, created_at) to make per-user grouping fast at scale. After fixing the query, check user_behavior_summary rows manually — each user_id should have a distinct events_last_7d count.
Send-time optimizer sends all notifications at the same hour because preferred_hour is NULL
Symptom: New users have no notification_opens history. preferred_hour is computed from opened_at timestamps but remains NULL until a user has opened at least one notification. The scoring function doesn't handle the NULL case and the scheduler falls through to an unhandled default, sending all new-user notifications at midnight UTC — the worst possible time.
Fix: Set preferred_hour DEFAULT 10 in user_behavior_summary and handle the NULL case in the optimizer: COALESCE(preferred_hour, 10). Populate preferred_hour on first app open from the device timezone using Flutter's DateTime.now().timeZoneOffset, converting local 10 AM to UTC.
Suppression logic never resets — re-engaged users stay blocked permanently
Symptom: ignored_streak only resets when a notification is opened. But if a user is suppressed (ignored_streak >= 5), no notifications are sent — so they can never open one — so ignored_streak never resets. The user re-downloads the app, starts using it again, generates fresh events, but never receives a notification because they are permanently stuck in the suppressed state.
Fix: Add a separate daily reset job that runs independently of sends: UPDATE notification_fatigue SET ignored_streak = 0 WHERE ignored_streak > 0 AND user_id IN (SELECT user_id FROM user_events WHERE created_at > NOW() - INTERVAL '7 days'). This job checks raw app activity, not notification history, to identify re-engaged users.
A/B test shows both variants being sent to the same user
Symptom: Variant assignment is computed as user_id % 2 on every scoring run, which is stable for a given user_id. However, if the notification_candidates table is queried without filtering by the stored variant — for example, if the sender function re-computes the variant rather than reading it from the candidates row — a schema change, UUID format difference, or modulo edge case can produce different variant assignments on different sends.
Fix: Compute variant assignment once and store it in user_behavior_summary.preferred_variant on first notification creation. All subsequent scoring and sending functions read preferred_variant from this column instead of recalculating. This guarantees stable assignment regardless of query path.
Best practices
Break this feature into five explicit phases before generating any code: event tracking, aggregation, scoring, suppression, and sending — AI tools that attempt all five at once produce incomplete wiring between components
Enforce a hard cap of 3 notifications per user per 24 hours at the database level via notification_fatigue.sent_24h, not just in application code — this prevents bugs in the scoring engine from spamming users
Index user_events on (user_id, event_type, created_at desc) before writing the aggregation query — this is the highest-impact performance decision for this feature at any scale
Default preferred_hour to 10 (10 AM local) for new users and update it only after a user has at least 3 notification opens — early data from 1-2 opens is too noisy to determine genuine preference
Make dynamic notification copy specific to the actual entity the user interacted with — reference the product name, article title, or saved item; generic 'don't forget your cart' copy has 3-5x lower open rates
Store the full notification_candidates record including suppressed = true rows and their suppression_reason — this is the only way to diagnose why a specific user never receives notifications
Add a user-facing 'why did I get this?' explanation in the notification preference screen showing the event history that triggered the score — transparency reduces opt-out rates
Plan a retention policy for user_events from the start: at 10 events/user/day, a 10K user app generates 100M rows per year; delete events older than 90 days via a weekly pg_cron job
When You Need Custom Development
Lovable and FlutterFlow can build a functional behavioral notification pipeline for early-stage products. You outgrow them when scale, compliance, or multi-channel orchestration become requirements:
- App has 50K+ active users where hourly Supabase Edge Function aggregation becomes a database bottleneck — the aggregation query touches the full user_events table and locks rows at this scale, requiring a dedicated event streaming pipeline
- Regulatory compliance (GDPR, CCPA) requires explicit consent logging per behavior-based notification with a record of which behavioral data was used to personalize each send — a standard reminder_log table is not sufficient for this audit trail
- Multi-channel orchestration is required where the behavioral score determines whether to send push, email, SMS, or in-app message — and lower-priority channels activate only when higher-priority ones fail
- CRM integration needed to trigger notifications from sales pipeline events in Salesforce or HubSpot, not just in-app behavioral events
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
Frequently asked questions
What user actions should I track to personalize notifications?
Track the actions that signal intent: view (user looked at something), favorite or save (user expressed interest), add-to-cart or start (user began a flow), and complete or purchase (user finished). Five event types is enough to build useful behavioral scores. Track entity_id and entity_type with each event so you know which specific product, article, or course to reference in the notification copy.
How do I avoid annoying users with too many notifications?
Hard cap at 3 notifications per user per 24 hours in the notification_fatigue table, and pause all sends after 5 consecutive unread notifications (ignored_streak >= 5). These two rules eliminate the most common annoyance pattern — re-engagement campaigns that fire repeatedly at users who have disengaged. The 5-notification pause should last until the user opens the app again, not for a fixed time period.
What is the minimum number of users needed to make behavioral targeting worthwhile?
Behavioral targeting starts producing measurably better open rates at around 200-500 active users. Below that, there is not enough signal per user to distinguish individual behavior patterns from noise, and the aggregation query overhead is not justified. For early-stage apps under 500 users, a well-timed push based on a single event type (cart abandonment 2 hours after add-to-cart) outperforms a full scoring pipeline.
Can I use this for re-engagement campaigns to bring back inactive users?
Yes, but only if the suppressor reset job is working correctly. Re-engagement specifically targets users whose last_active_at is more than 7 days ago. The catch is: if these users were also previously suppressed (ignored_streak >= 5), your reset job must clear the ignored_streak when they return to the app — otherwise they will never receive a re-engagement notification regardless of your campaign logic.
How do I measure whether smart notifications are actually improving engagement?
Track open rate (opened_at IS NOT NULL / sent_at IS NOT NULL) and conversion rate (did the user perform the target action within 24 hours of opening the notification) per notification type and variant in the notification_candidates table. Compare these metrics against a control group using your A/B variant assignment. A 10-15% improvement in open rate over a broadcast baseline is a realistic initial target.
What is the difference between smart notifications and a standard push blast?
A standard push blast sends the same message to all users at the same time from a marketing tool. Smart behavioral notifications are generated per-user based on their individual in-app activity, sent at the time they are most likely to open the app, suppressed if they have been ignoring recent messages, and worded to reference the specific thing they were doing. The technical cost is significantly higher — event tracking, aggregation, scoring — but open rates are typically 3-5x higher than broadcast.
Can I test two different notification messages to see which performs better?
Yes, using the A/B variant assignment in user_behavior_summary. Assign users to Variant A or B based on user_id::text % 2 — this is stable for each user across all sends. Define each variant's title and body template in the notification_variants table. Track opened_at per variant in notification_candidates and compare open rates after accumulating at least 100 opens per variant before drawing conclusions.
Do I need a third-party tool or can I build this in Supabase?
You can build the full pipeline in Supabase: event tracking in a table, aggregation via pg_cron, scoring in an Edge Function, suppression checks in the same function, and FCM delivery via firebase-admin. This is the right choice for products under 10K active users. Above that, Customer.io or Braze handle suppression, send-time optimization, and A/B testing natively at a cost of $100-$50,000/month (approx) depending on the platform — the tradeoff is infrastructure cost versus engineering time.
Need this feature production-ready?
RapidDev builds smart notifications based on user behavior into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.