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

- Tool: App Features
- Last updated: July 2026

## 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.

## 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.

- **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.
- **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.
- **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.
- **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.
- **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.
- **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.

## Data model

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

```sql
-- User events (shared with analytics dashboard)
create table if not exists public.user_events (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete set null,
  event_type text not null,
  screen text,
  properties jsonb default '{}',
  created_at timestamptz not null default now()
);

create index if not exists user_events_created_idx
  on public.user_events (created_at desc);
create index if not exists user_events_type_created_idx
  on public.user_events (event_type, created_at desc);

-- Admin alerts table
create table public.admin_alerts (
  id uuid primary key default gen_random_uuid(),
  alert_type text not null,
  threshold_value numeric not null,
  actual_value numeric not null,
  triggered_at timestamptz not null default now(),
  dismissed_at timestamptz
);

create index admin_alerts_active_idx
  on public.admin_alerts (alert_type, dismissed_at)
  where dismissed_at is null;

-- User roles (may already exist from analytics-dashboard)
create table if not exists public.user_roles (
  user_id uuid references auth.users(id) on delete cascade primary key,
  role text not null default 'user' check (role in ('user', 'admin'))
);

-- RLS
alter table public.user_events enable row level security;
alter table public.admin_alerts enable row level security;
alter table public.user_roles enable row level security;

-- Users can insert their own events
create policy "Users can insert own events"
  on public.user_events for insert
  with check (auth.uid() = user_id);

-- Only admins can read all events (for the live feed)
create policy "Admins can read all events"
  on public.user_events for select
  using (
    exists (
      select 1 from public.user_roles
      where user_id = auth.uid() and role = 'admin'
    )
  );

-- Only admins can read and update alerts
create policy "Admins can manage alerts"
  on public.admin_alerts for all
  using (
    exists (
      select 1 from public.user_roles
      where user_id = auth.uid() and role = 'admin'
    )
  );

create policy "Users can read own role"
  on public.user_roles for select
  using (auth.uid() = user_id);

-- Threshold alert Edge Function (call from pg_cron every 60 seconds)
-- Deploy as supabase/functions/check-thresholds/index.ts
-- pg_cron schedule (enable pg_cron extension first in Dashboard -> Database -> Extensions):
-- select cron.schedule(
--   'check-thresholds-every-minute',
--   '* * * * *',
--   $$select net.http_post(
--     url := 'https://[your-project].supabase.co/functions/v1/check-thresholds',
--     headers := jsonb_build_object('Authorization', 'Bearer ' || '[service-role-key]'),
--     body := '{}'::jsonb
--   )$$
-- );
```

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 paths

### Flutterflow — fit 3/10, 8–12 hours

FlutterFlow can build the Realtime event feed and Presence-based live count via custom action blocks. Most of the dashboard requires Dart code — use this path only if the team is already on FlutterFlow and comfortable with custom code.

1. Create an AdminDashboard page in FlutterFlow; add a conditional visibility check using a Supabase Backend Query on user_roles — if role is not 'admin', navigate to the home page immediately
2. Add a custom Dart action block for the Supabase Realtime Presence subscription: join the 'live-users' channel with the current user's id, current_screen, device_type, and country; update a page state variable (presenceList) on every presenceStateChangedCallback
3. Add a Flutter AnimatedList widget for the event feed; wire it to a custom Dart action block that subscribes to postgres_changes INSERT events on user_events filtered to event_type=in.(page_view,purchase,crash,login); insert new events at index 0 of the list
4. Add a custom widget for the geographic map using the flutter_map package with OpenStreetMap tile layer; read coordinates from the presenceList state variable; add a CircleMarker for each presence entry with a pulsing animation via AnimationController
5. Add a Supabase Backend Query on admin_alerts to display active (dismissed_at IS NULL) alerts as a banner at the top of the screen; subscribe to a Realtime channel for new alert inserts to show banners in real time
6. In FlutterFlow Settings → App Details → iOS Permissions, add NSLocationWhenInUseUsageDescription if requesting GPS location for higher-accuracy mapping

Limitations:

- The geographic map, threshold alert banner, and Presence subscription all require custom Dart action blocks — this dashboard pushes FlutterFlow to its visual builder limits and a developer comfortable with Dart is needed for the custom action blocks
- FlutterFlow Pro plan ($70/month) is required to export and deploy custom Dart code
- The analytics dashboard view (historical charts, CSV export, TanStack Table) must be built separately — FlutterFlow's visual builder cannot combine a complex live dashboard and a historical analytics view on the same screen

### Lovable — fit 1/10, not recommended

Lovable builds web SPAs and cannot render a Flutter mobile dashboard. A Lovable-built web companion admin panel is possible for viewing the same Supabase data in a browser, but the mobile real-time experience described here cannot be built in Lovable.

1. If you need a web companion to the mobile dashboard, prompt Lovable to build an admin web panel subscribed to the same Supabase Realtime channels — this is a separate tool from the mobile dashboard, not a replacement

Starter prompt:

```
Build a web real-time admin dashboard companion. Use the Supabase service_role key (SUPABASE_SERVICE_KEY in Lovable Cloud Secrets) in an Edge Function called get-stats for admin queries. Subscribe to Supabase Realtime postgres_changes on user_events (INSERT): render new events as they arrive in an animated feed (newest at top). Show a live active-user KPI card with a pulsing green dot, reconciled every 10 seconds by counting distinct user_id in user_events from the last 5 minutes. Add an event-type filter dropdown (all, page_view, purchase, error). Display a threshold alert banner when error events in the last 5 minutes exceed 10. Gate access: query user_roles via Edge Function on mount; redirect non-admin users to a 403 page. Show a reconnecting banner when Realtime disconnects and resubscribe on reconnect. Handle states: connecting, live, reconnecting, and error.
```

Limitations:

- Lovable produces React web apps; this feature requires a Flutter mobile app
- A web-based admin panel viewing the same Supabase data is a valid parallel project but does not fulfill the mobile real-time dashboard use case

### Custom — fit 5/10, 3–5 weeks

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.

1. Design 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. Build 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. Build the Flutter admin app as a standalone Flutter module (not FlutterFlow) for full control over custom widgets, AnimationController curves, and StreamBuilder composition
4. Implement 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. Set 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

Limitations:

- 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

## Gotchas

- **Active user count drifts above the actual number** — 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** — 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** — 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** — 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** — 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

- Call channel.unsubscribe() in Flutter's dispose() AND in AppLifecycleState.detached — missing either one creates ghost Presence entries that inflate the live user count
- 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
- 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
- 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
- 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
- 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
- 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

## 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.

---

Source: https://www.rapidevelopers.com/app-features/real-time-user-insights-dashboard
© RapidDev — https://www.rapidevelopers.com/app-features/real-time-user-insights-dashboard
