Skip to main content
RapidDev - Software Development Agency
App Featuresanalytics-admin20 min read

How to Add a Real-Time User Insights Dashboard (Copy-Paste Prompts)

A real-time user insights dashboard combines Supabase Realtime Presence (who is online now), a postgres_changes event feed (what they are doing), flutter_map with OpenStreetMap tiles (where they are), and a pg_cron threshold alert engine (when error rates spike). With FlutterFlow you can build a working version in 8–12 hours; a production-grade build is a 3–5 week custom project. Running cost is $0/month at 100 users and $25–50/month at 10K users.

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

Feature spec

Advanced

Category

analytics-admin

Build with AI

8–14 hours with FlutterFlow + Supabase Realtime

Custom build

3–5 weeks custom dev

Running cost

$0/mo up to 100 users · $25–50/mo at 10K users

Works on

Mobile

Everything it takes to ship a Real-Time User Insights Dashboard — parts, prompts, and real costs.

TL;DR

A real-time user insights dashboard combines Supabase Realtime Presence (who is online now), a postgres_changes event feed (what they are doing), flutter_map with OpenStreetMap tiles (where they are), and a pg_cron threshold alert engine (when error rates spike). With FlutterFlow you can build a working version in 8–12 hours; a production-grade build is a 3–5 week custom project. Running cost is $0/month at 100 users and $25–50/month at 10K users.

What a Real-Time User Insights Dashboard Actually Is

A real-time user insights dashboard is the admin screen that shows what is happening in the app right now — not yesterday's aggregate, but the current second. The three live data streams that make it work are: Supabase Realtime Presence (who is connected and on which screen), Supabase postgres_changes events on the user_events table (what actions are firing), and a pg_cron-driven threshold alert engine (when error or crash rates cross a critical value). On top of those streams, a flutter_map widget with OpenStreetMap tiles renders a geographic dot map of active users derived from ip-api.com or MaxMind GeoLite2 geolocation. The hardest engineering problem is not displaying the data — it is keeping the display stable under connection drops, preventing stale Presence counts, and making sure the threshold alert fires once rather than every 60 seconds.

What users consider table stakes in 2026

  • Live count of users currently in the app, updating without manual refresh, displayed with a pulsing green animation
  • Stream of recent events (page_view, purchase, crash, login) appearing as they happen in a scrolling feed, newest at the top
  • Per-user drill-down accessible by tapping a row in the event feed — shows current screen, session duration, device type, and country
  • Geographic dot map showing where active users are located right now, with a pulsing CircleMarker per user
  • In-app alert banner or push notification when a critical metric (error rate, crash count) crosses a configurable threshold
  • Dashboard accessible only to users with admin role — non-admins are navigated away immediately on page load

Anatomy of the Feature

Six components, four of which need custom Dart code blocks in FlutterFlow. The Presence tracker and event feed are the core; the geographic map and alert engine are advanced layers.

Layers:UIBackend

Realtime Presence tracker

Backend

Each app session joins a Supabase Realtime Presence channel named 'live-users' and calls channel.track({ user_id, current_screen, started_at, device_type, country }). The admin dashboard subscribes to the same channel and receives presenceStateChangedCallback events whenever a user joins or leaves. The active user list is built from the aggregate of all current presence payloads.

Note: Call channel.unsubscribe() in Flutter's dispose() method and in AppLifecycleState.detached to prevent stale presences. Set server-side heartbeat timeout to 15 seconds to evict killed apps within one cycle.

Live event feed

UI

Flutter StreamBuilder receiving Supabase Realtime postgres_changes INSERT events on the user_events table. New event rows slide into the top of a Flutter AnimatedList; each row shows event_type, a truncated user_id (first 8 characters), and a relative timestamp ('3 seconds ago'). A rate limiter caps UI updates to at most one per 500ms during burst traffic to prevent the feed from becoming unreadable.

Note: Subscribe to postgres_changes on user_events filtered to specific event types (event_type=in.(page_view,purchase,crash,login)) to reduce noise on the feed. Without the filter, low-value background events drown out meaningful activity.

Active user count KPI

UI

Flutter Text widget fed by a stream of Presence join/leave events counting the current member map size. Displayed alongside a pulsing green dot animation built with Flutter's AnimationController (scale 0.8 → 1.2, repeat). A 10-second polling fallback (Timer.periodic) queries the Supabase REST API for a server-side count reconciliation in case the Realtime stream misses a leave event.

Note: The polling fallback is critical — mobile network changes (WiFi → LTE) silently drop the Presence channel without triggering the leave callback. The reconciliation keeps the count accurate within one polling interval.

Geographic active-user map

UI

flutter_map package with OpenStreetMap tiles renders a full-screen or card-embedded map. Each active user's Presence payload includes a country and optionally lat/lon from ip-api.com geolocation. Pulsing CircleMarker widgets placed at each coordinate. Tapping a marker shows a popup with user_id, current_screen, and device_type.

Note: ip-api.com returns CDN or VPN exit node locations for corporate and privacy-focused users. Display geolocation as approximate country/city with a 'Location is approximate' disclaimer. For higher accuracy, request explicit GPS permission from users via the location package.

Threshold alert engine

Backend

Supabase Edge Function (check_thresholds) called by a pg_cron job every 60 seconds. The function queries error event count in the last 5 minutes from user_events; if above the configured threshold (e.g. 10 crash events in 5 minutes), it inserts a row into admin_alerts and deduplicates with WHERE NOT EXISTS (SELECT 1 FROM admin_alerts WHERE alert_type = $1 AND dismissed_at IS NULL). The admin_alerts insert triggers a Supabase Realtime broadcast to the dashboard. Optionally sends a Slack Incoming Webhook and an FCM push to the admin's device.

Note: The deduplication check is non-negotiable — without it, the admin receives a new alert every 60 seconds while the condition persists. An alert is re-armed only after the admin explicitly dismisses it.

Role guard

Backend

FlutterFlow conditional page visibility checks the user_roles table via a Supabase Backend Query on dashboard page load. If the current user's role is not 'admin', the user is immediately navigated to the app's home page. A second RLS policy on user_events restricts SELECT to admin-role users so direct Supabase queries also fail for non-admins.

Note: Page visibility conditionals in FlutterFlow can be bypassed by deep-linking directly to the page URL. Always back the UI guard with an RLS policy on the data tables.

The data model

Three tables plus the user_roles table shared with the analytics dashboard. Run this in the Supabase SQL Editor:

schema.sql
1-- User events (shared with analytics dashboard)
2create table if not exists public.user_events (
3 id uuid primary key default gen_random_uuid(),
4 user_id uuid references auth.users(id) on delete set null,
5 event_type text not null,
6 screen text,
7 properties jsonb default '{}',
8 created_at timestamptz not null default now()
9);
10
11create index if not exists user_events_created_idx
12 on public.user_events (created_at desc);
13create index if not exists user_events_type_created_idx
14 on public.user_events (event_type, created_at desc);
15
16-- Admin alerts table
17create table public.admin_alerts (
18 id uuid primary key default gen_random_uuid(),
19 alert_type text not null,
20 threshold_value numeric not null,
21 actual_value numeric not null,
22 triggered_at timestamptz not null default now(),
23 dismissed_at timestamptz
24);
25
26create index admin_alerts_active_idx
27 on public.admin_alerts (alert_type, dismissed_at)
28 where dismissed_at is null;
29
30-- User roles (may already exist from analytics-dashboard)
31create table if not exists public.user_roles (
32 user_id uuid references auth.users(id) on delete cascade primary key,
33 role text not null default 'user' check (role in ('user', 'admin'))
34);
35
36-- RLS
37alter table public.user_events enable row level security;
38alter table public.admin_alerts enable row level security;
39alter table public.user_roles enable row level security;
40
41-- Users can insert their own events
42create policy "Users can insert own events"
43 on public.user_events for insert
44 with check (auth.uid() = user_id);
45
46-- Only admins can read all events (for the live feed)
47create policy "Admins can read all events"
48 on public.user_events for select
49 using (
50 exists (
51 select 1 from public.user_roles
52 where user_id = auth.uid() and role = 'admin'
53 )
54 );
55
56-- Only admins can read and update alerts
57create policy "Admins can manage alerts"
58 on public.admin_alerts for all
59 using (
60 exists (
61 select 1 from public.user_roles
62 where user_id = auth.uid() and role = 'admin'
63 )
64 );
65
66create policy "Users can read own role"
67 on public.user_roles for select
68 using (auth.uid() = user_id);
69
70-- Threshold alert Edge Function (call from pg_cron every 60 seconds)
71-- Deploy as supabase/functions/check-thresholds/index.ts
72-- pg_cron schedule (enable pg_cron extension first in Dashboard -> Database -> Extensions):
73-- select cron.schedule(
74-- 'check-thresholds-every-minute',
75-- '* * * * *',
76-- $$select net.http_post(
77-- url := 'https://[your-project].supabase.co/functions/v1/check-thresholds',
78-- headers := jsonb_build_object('Authorization', 'Bearer ' || '[service-role-key]'),
79-- body := '{}'::jsonb
80-- )$$
81-- );

Heads up: Supabase Realtime Presence does not use the database — it is channel-scoped in memory on the Realtime server. No table needed for Presence data. The admin_alerts table is the persistent record of threshold breaches; the Realtime broadcast is the live notification.

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:

Custom development is the correct path for a production-grade real-time admin dashboard. Full control over Supabase Realtime channel design, geographic pipeline, and threshold alert rules — and the only path that reliably handles 50+ concurrent admin viewers.

Step by step

  1. 1Design the Presence channel architecture: shard by region or feature area if expecting >500 concurrent users — a single global Presence channel hits Supabase Pro's 500-connection limit at high scale
  2. 2Build the geographic pipeline: for higher accuracy than ip-api.com, integrate MaxMind GeoLite2 (free with attribution) as an on-device lookup using a locally bundled GeoIP2 database updated monthly; this avoids the 45-request/minute ip-api.com free-tier limit entirely
  3. 3Build the Flutter admin app as a standalone Flutter module (not FlutterFlow) for full control over custom widgets, AnimationController curves, and StreamBuilder composition
  4. 4Implement the event feed rate limiter as a client-side Dart StreamTransformer that throttles updates to one per 500ms and coalesces burst events into a count badge ('12 new events') when the feed falls behind
  5. 5Set up the threshold alert pipeline: pg_cron calls the Edge Function; Edge Function sends Slack webhook + FCM push to all admin FCM tokens from a user_fcm_tokens table; deduplication prevents repeat alerts while condition persists

Where this path bites

  • Supabase Realtime Presence minimum latency is approximately 100–300ms — not suitable for sub-100ms financial trading or safety-critical monitoring applications
  • At 50+ concurrent admin viewers of the same Presence channel, Supabase Pro is required and channel partitioning must be designed upfront
  • Historical event replay mode (rewinding the last hour of activity) requires a dedicated event streaming architecture like Kafka or Tinybird — Supabase alone cannot serve this without significant query tuning

Third-party services you'll need

The base dashboard runs on Supabase Realtime and free geolocation. Costs scale with concurrent connection count and geolocation accuracy requirements.

ServiceWhat it doesFree tierPaid from
Supabase RealtimePresence channel for online user count and current screen; postgres_changes subscription for the live event feed; admin_alerts channel for threshold notificationsFree: 200 concurrent connectionsPro $25/mo: up to 500 concurrent connections
ip-api.comIP geolocation for Presence payload country and coordinates to power the geographic mapFree: 45 requests/minute (no API key required)Pro $15/mo for higher rate limits (approx)
MaxMind GeoLite2Alternative on-device IP geolocation database — avoids rate limits; bundled with the appFree with attribution and MaxMind account registrationGeoIP2 City commercial license pricing on request
Firebase Cloud MessagingPush notification to admin's device when a threshold alert triggersFree up to 250K messages/monthFree (no paid tier for FCM)
Slack Incoming WebhooksAlert the operations team in Slack when error rates or crash counts breach a thresholdFree (no limit on Incoming Webhooks)Free

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

$0/mo

Supabase free Realtime (200 connections) easily covers 100 concurrent users plus admin viewers. ip-api.com free tier handles geolocation. FCM free. Slack webhooks free.

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.

Active user count drifts above the actual number

Symptom: Supabase Presence does not clean up stale entries when the Flutter app is killed by the OS (force-quit, low-memory eviction, or background suspension). The leave event never fires; the admin dashboard counts the ghost session until the server-side Presence timeout clears it — which defaults to 30 seconds but can be longer.

Fix: Reduce the server heartbeat timeout to 15 seconds in the Supabase Realtime channel config. Call channel.unsubscribe() explicitly in Flutter's dispose() method and in the AppLifecycleState.detached lifecycle handler. Add a 10-second polling fallback (Timer.periodic) that reconciles the displayed count against a server-side count query — this catches any sessions that slip past the Presence eviction.

Live event feed stops updating after 5–10 minutes

Symptom: The Supabase Realtime postgres_changes subscription on user_events is silently dropped when the mobile network switches between WiFi and LTE. Flutter does not automatically reconnect the subscription; the StreamBuilder continues to render the last received state without indicating a connection problem.

Fix: Listen to supabase.client.realtime.connectionStateChangeStream. When the state transitions to connected after a previous disconnection, re-subscribe to the postgres_changes channel. Show a 'Reconnecting...' banner in the feed UI while the connection is in a non-connected state so the admin knows the feed is paused, not empty.

Geographic map shows all users in the same location

Symptom: ip-api.com returns the CDN edge node or corporate VPN exit IP address rather than the user's actual location. Entire groups of users from the same company appear as a single dot on the map, often in a data center location like Ashburn, Virginia.

Fix: Display geolocation as approximate region (country and city) with a disclaimer tooltip: 'Location is approximate and based on network IP, not GPS.' For applications where user location is meaningful (local services, event check-in), request explicit GPS permission via Flutter's location package and store lat/lon in the Presence payload as opt-in data.

Dashboard crashes on Presence payload with null fields

Symptom: On cold start, the Flutter app joins the Presence channel before the navigation stack has populated current_screen. The presence payload contains current_screen: null. The admin dashboard widget attempts to call .length on a null String, causing a null check operator used on a null value crash.

Fix: Guard every Presence payload field in the admin dashboard: final screen = payload['current_screen'] as String? ?? 'Starting...'; final country = payload['country'] as String? ?? 'Unknown'; Never assume a Presence payload field is non-null. Add a validation step in the app-side track() call that only fires after the first route has been pushed to the Navigator.

Threshold alert fires every 60 seconds for the same condition

Symptom: The pg_cron job calls the Edge Function every 60 seconds. The Edge Function checks whether error count exceeds the threshold and inserts a new admin_alerts row each time the condition is true. When an error spike lasts 5 minutes, the admin receives 5 identical alerts and the admin_alerts table accumulates duplicate rows.

Fix: Add a deduplication WHERE NOT EXISTS clause in the Edge Function's INSERT statement: INSERT INTO admin_alerts (alert_type, threshold_value, actual_value) SELECT $1, $2, $3 WHERE NOT EXISTS (SELECT 1 FROM admin_alerts WHERE alert_type = $1 AND dismissed_at IS NULL). An alert re-arms only after the admin dismisses it — set dismissed_at to now() via a tap on the in-app banner.

Best practices

1

Call channel.unsubscribe() in Flutter's dispose() AND in AppLifecycleState.detached — missing either one creates ghost Presence entries that inflate the live user count

2

Guard every Presence payload field for nullability — the app joins the Presence channel on cold start before current_screen is populated, and a single null field crash takes the dashboard offline

3

Filter the postgres_changes event feed to specific event types (event_type=in.(page_view,purchase,crash,login)) — without filtering, background system events flood the feed and make it unreadable

4

Add a deduplication WHERE NOT EXISTS check to every admin_alerts INSERT — without it, a 5-minute error spike generates 5 identical push notifications and Slack messages

5

Build a reconnection handler on the connectionStateChangeStream — mobile network changes silently drop Realtime subscriptions without triggering an error, and the feed appears frozen without a reconnection banner

6

Display geolocation as approximate country/city with a disclaimer — ip-api.com returns CDN or VPN exit node coordinates, not device GPS; overclaiming location accuracy erodes trust

7

Implement a client-side rate limiter (at most one UI update per 500ms) for the live event feed during burst traffic — without it, rapid database writes cause the AnimatedList to thrash and the dashboard becomes unusable

When You Need Custom Development

FlutterFlow with custom Dart actions is an adequate starting point for a single-admin internal tool. These scenarios require fully custom engineering:

  • Real-time dashboard must ingest events from multiple platforms simultaneously (iOS, Android, web) with a consistent event schema — requires a unified event bus, not separate per-platform Supabase Realtime subscriptions
  • 50 or more concurrent admin viewers will access the same Presence channel — Supabase Pro supports 500 concurrent connections total, and a single high-traffic Presence channel can exhaust that budget; channel partitioning by region requires custom architecture
  • Historical event replay mode is needed — rewinding the past hour of user activity requires a dedicated event streaming architecture (Kafka, Tinybird) because Supabase alone cannot serve time-windowed replay at low latency
  • Sub-100ms event latency is required for safety-critical or financial monitoring — Supabase Realtime's typical latency is 100–300ms; purpose-built streaming infrastructure is needed below that threshold

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

How many concurrent users can Supabase Realtime Presence handle before it degrades?

Supabase free tier supports 200 concurrent Realtime connections; Pro supports 500. At 10,000 active users, not all of them are online simultaneously — typical peak concurrency for a consumer app is 5–15% of the daily active user count. 10,000 DAU means roughly 500–1,500 peak concurrent sessions, which exceeds Pro's single-channel limit. At that scale, shard the Presence channel by region (e.g. 'live-users-us', 'live-users-eu') and aggregate the counts server-side.

Can I show real-time data alongside historical charts on the same screen?

Yes. Use two separate data sources on the same screen: the Realtime Presence channel and postgres_changes subscription feed the live widgets, while a set of Supabase aggregation view queries power the historical charts. In Flutter, compose them in a CustomScrollView with SliverToBoxAdapter widgets — the historical charts render at the top as fixed content, and the live event feed is a SliverList below that streams new items into its top.

How do I prevent the live event feed from overwhelming the UI during traffic spikes?

Add a client-side StreamTransformer in Dart that throttles the postgres_changes stream to emit at most one update per 500ms. During a burst, coalesce multiple incoming events into a single batch update and show a badge ('8 new events') rather than animating each one individually. In Flutter: stream.transform(ThrottleStreamTransformer(Duration(milliseconds: 500))). This keeps the AnimatedList from thrashing during peak load.

Can I filter the live feed to show only specific event types?

Yes, at two levels. First, in the Supabase Realtime subscription filter: .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'user_events', filter: 'event_type=in.(purchase,crash,login)' }, callback). This reduces the payload delivered to the dashboard. Second, add UI-level filter chips (All, Purchases, Errors) that toggle visibility on the already-received events without re-subscribing. Combining both reduces bandwidth and keeps the UI responsive.

How do I alert myself when error rates spike — even if I'm not looking at the dashboard?

The threshold alert engine handles this: a pg_cron job calls a Supabase Edge Function every 60 seconds, the function checks crash event count in the last 5 minutes, and if above threshold it sends a Slack Incoming Webhook (free) and an FCM push notification to your admin device. The FCM push arrives on your phone even when the dashboard app is closed. Make sure to store your admin device's FCM token in a user_fcm_tokens table when the app starts.

Can I drill down from a live event to the full session replay?

Not with this stack alone — Supabase Realtime Presence tracks current_screen and session metadata, not a full recording of every user interaction. For full session replay, integrate a dedicated tool like PostHog (open-source, self-hostable, $0 for under 1M events/month on the cloud) or LogRocket. PostHog can receive the same user_events you are already logging and replay sessions by correlating events with screen recordings captured by its Flutter SDK.

How do I handle the dashboard when the real-time connection drops?

Listen to supabase.client.realtime.connectionStateChangeStream in your dashboard widget. When the state changes to connecting after a previous connected state, show a yellow 'Reconnecting...' banner above the event feed and stop animating the active user count. When connected fires again, re-subscribe to both the Presence channel and the postgres_changes subscription and clear the banner. Never silently show stale data — the admin should always know whether the live feed is current.

RapidDev

Need this feature production-ready?

RapidDev builds a real-time user insights dashboard 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.