◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-03-29Advanced

Push Notification Masterplan for Rork Apps — Segmented Delivery, A/B Testing, and Automation Pipelines

An advanced guide to push notifications in Rork apps. Learn how to implement user segmentation, A/B testing, automated lifecycle pipelines, and retention-focused delivery strategies.

Rork585push notifications11segmentationA/B testing3Firebase Cloud MessagingExpo Notificationsretention10automation8

✦ Premium Article

Setup and context — Why "Blast Everyone" Doesn't Work Anymore

Push notifications are one of the most powerful retention channels available to mobile app developers. But sending the same message to every user at the same time is a recipe for notification fatigue, rising opt-out rates, and diminishing returns.

Look at any top-performing app on the App Store or Google Play, and you'll find they all rely on segmented delivery based on user behavior and attributes, A/B testing to optimize messaging and timing, and event-driven automation pipelines to keep everything running without manual intervention.

In this guide, we'll walk through the architecture and implementation patterns for running production-grade push notifications in a Rork (React Native / Expo) app. This is aimed at developers who already have basic push notifications working — if you haven't set that up yet, start with our push notification fundamentals guide first.

Designing a Segmentation System — Delivering the Right Message to the Right User

Segmentation Dimensions

Effective segmented delivery starts with defining how you classify your users. Here are the three dimensions that work best in practice.

Attribute-based segments classify users by static properties captured at registration — language, region, plan tier, and signup date. These are simple to implement but offer limited personalization.

Behavior-based segments classify users by in-app actions — last activity date, session frequency, feature usage counts, and purchase history. Because they reflect what users are actually doing, they enable far more targeted messaging.

Lifecycle segments classify users by engagement stage — new, active, inactive, at-risk, and churned. Tailoring messages to each stage is the single most impactful thing you can do for retention.

Segment Management Table Design with Supabase

Let's design the database schema for managing segment data. Here's a Supabase (PostgreSQL) implementation:

-- User segment management table
CREATE TABLE user_segments (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
  -- Attribute segments
  locale TEXT DEFAULT 'ja',
  plan_type TEXT DEFAULT 'free', -- free / pro / premium
  registered_at TIMESTAMPTZ NOT NULL,
  -- Behavioral segments
  last_active_at TIMESTAMPTZ,
  session_count INTEGER DEFAULT 0,
  feature_usage JSONB DEFAULT '{}',
  -- Lifecycle segment (auto-calculated)
  lifecycle_stage TEXT GENERATED ALWAYS AS (
    CASE
      WHEN last_active_at IS NULL THEN 'new'
      WHEN last_active_at > NOW() - INTERVAL '3 days' THEN 'active'
      WHEN last_active_at > NOW() - INTERVAL '14 days' THEN 'inactive'
      WHEN last_active_at > NOW() - INTERVAL '30 days' THEN 'at_risk'
      ELSE 'churned'
    END
  ) STORED,
  -- Notification preferences
  push_token TEXT,
  push_enabled BOOLEAN DEFAULT true,
  quiet_hours_start TIME DEFAULT '22:00',
  quiet_hours_end TIME DEFAULT '08:00',
  timezone TEXT DEFAULT 'Asia/Tokyo',
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
 
-- Indexes for segment queries
CREATE INDEX idx_segments_lifecycle ON user_segments(lifecycle_stage);
CREATE INDEX idx_segments_plan ON user_segments(plan_type);
CREATE INDEX idx_segments_push ON user_segments(push_enabled) WHERE push_token IS NOT NULL;

The key design choice here is the lifecycle_stage column using PostgreSQL's GENERATED ALWAYS AS ... STORED. Whenever last_active_at is updated, the lifecycle stage is automatically recalculated. No application-side logic needed — the database always reflects the current state.

Tracking Segment Data from the Rork App

We need a hook that automatically records user activity and feeds the segment table in real time:

// hooks/useSegmentTracker.ts
import { useEffect, useCallback } from 'react';
import { AppState, AppStateStatus } from 'react-native';
import { supabase } from '@/lib/supabase';
import { useAuth } from '@/hooks/useAuth';
 
export function useSegmentTracker() {
  const { user } = useAuth();
 
  // Record a session whenever the app comes to foreground
  useEffect(() => {
    if (!user) return;
 
    const subscription = AppState.addEventListener(
      'change',
      (nextState: AppStateStatus) => {
        if (nextState === 'active') {
          updateActivity(user.id);
        }
      }
    );
 
    // Also record on initial mount
    updateActivity(user.id);
 
    return () => subscription.remove();
  }, [user]);
 
  const updateActivity = useCallback(async (userId: string) => {
    const { error } = await supabase
      .from('user_segments')
      .upsert(
        {
          user_id: userId,
          last_active_at: new Date().toISOString(),
          session_count: supabase.rpc('increment_session_count', {
            target_user_id: userId,
          }),
        },
        { onConflict: 'user_id' }
      );
 
    if (error) {
      console.warn('Segment update failed:', error.message);
    }
  }, []);
 
  // Helper to track individual feature usage
  const trackFeatureUsage = useCallback(
    async (featureName: string) => {
      if (!user) return;
 
      const { data: current } = await supabase
        .from('user_segments')
        .select('feature_usage')
        .eq('user_id', user.id)
        .single();
 
      const usage = (current?.feature_usage as Record<string, number>) || {};
      usage[featureName] = (usage[featureName] || 0) + 1;
 
      await supabase
        .from('user_segments')
        .update({ feature_usage: usage })
        .eq('user_id', user.id);
    },
    [user]
  );
 
  return { trackFeatureUsage };
}

Mount this hook in your app's root layout, and session data flows automatically into your segment table. Call trackFeatureUsage from specific screens or actions to build up per-feature usage counts.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Real-world numbers from operating apps with 50M+ cumulative downloads — segment design and opt-out rates
✦Shared notification infrastructure patterns for running multiple apps in parallel, with CPS-controlled dispatch
✦Continuous KPI monitoring and the improvement loop using BigQuery + Looker Studio
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⟐ Business2026-05-11
Building Fortune & Manifestation Apps with Rork — Daily Delivery Design That Survives Day Three
Fortune apps lose most users in three days. A Rork design for daily delivery, push timing, and widgets that turns the niche into a habit.
⬡ Dev Tools2026-10-15
When EXPO_PUBLIC_ Values Read Fine in Dev but Come Back undefined in Production: Telling the Spellings Apart
The API URL reads fine while developing and turns undefined once the app is built. The cause was how process.env was written. I lined up babel-preset-expo's output for dev and production and tabulated which spellings get replaced and which are left behind.
⬡ Dev Tools2026-10-13
Before You Ship: Go Looking for Your Own Keys Inside the Exported Expo Bundle
A key can be missing from your source and still be sitting inside the finished app. I ran expo export on Expo SDK 57, searched the bundle with grep and a short Node script, and recorded which secrets show up and which don't.