Skip to main content
RapidDev - Software Development Agency
Lovable PromptsAnalyticsIntermediate

Build Product Analytics in Lovable

A self-hosted product-analytics dashboard with an event ingest endpoint, funnel and retention queries on a Postgres events table, and Recharts dashboards — for teams with owned-data requirements who have read the buy-vs-build math and still want to build.

Time to MVP

~1 day

Credits

~100-180 credits for full chain

Difficulty

Intermediate

Cloud features

3

TL;DR

The one-paragraph version before you dive in.

Before pasting anything: PostHog and Mixpanel each offer 1 million events per month free and give you funnels, retention, and session replays out of the box. Build in Lovable only if you have specific owned-data or regulated-industry requirements. If you still want to build, this kit gives you an event ingest endpoint, funnel and retention queries on Postgres, and Recharts dashboards — with the GIN indexes and warehouse export path that keep it viable past 100K events per month.

Setup checklist

Complete these steps in Lovable before pasting the starter prompt — takes about 5 minutes.

Cloud tab settings

Database

The events table with three mandatory indexes is the core of this build — without the GIN index on properties jsonb, funnel queries time out past 50K rows.

  1. 1Click the + button next to Preview to open the Cloud tab
  2. 2Click Database — the starter prompt creates all 4 tables and the mandatory indexes via migration
  3. 3After the migration runs, verify the indexes exist: Cloud tab → Database → Indexes, look for events_name_date_idx, events_user_date_idx, and events_properties_gin_idx

Auth

Email+password for the internal team viewing dashboards. The ingest endpoint is public (authenticated by API key in the request body, not by Supabase Auth).

  1. 1Cloud tab → Users & Auth
  2. 2Enable Email sign-in (default — already on)
  3. 3Disable public sign-up: set 'Allow new users to sign up' to OFF — you'll invite team members manually
  4. 4Invite team members via Cloud tab → Users & Auth → Invite user

Edge Functions

The ingest endpoint and query functions run as Edge Functions so the ingest path is a public HTTPS URL your product app can POST to.

  1. 1Cloud tab → Edge Functions — leave empty; the follow-up prompts generate ingest and query-funnel
  2. 2After generating ingest, add the first API key in /settings so your product can start sending events

Preflight checklist

  • You're in a fresh Lovable project (Vite + React + TypeScript + Tailwind + shadcn/ui already scaffolded)
  • You're on Pro $25/mo — the full chain is 100-180 credits, fitting within Pro's allowance with one possible top-up
  • You have read the BUILD_VS_BUY math: PostHog gives 1M events/month free with funnels, retention, and replays. Build here only if you have owned-data, on-prem, or regulated-industry requirements, or if analytics IS your product
  • Credit estimates are extrapolated from internal-dashboard + Recharts cousin patterns

The starter prompt — paste this first

Copy this. Paste it into Lovable Agent Mode (the default chat at the bottom-left of the editor). Hit send.

lovable-agent-mode.txt
~50-80 credits
Build a self-hosted product analytics dashboard. Stack assumptions: React + Vite + TypeScript + Tailwind + shadcn/ui already scaffolded by Lovable, React Router for routes, Supabase JS client against Lovable Cloud. This is an internal tool — only the ingest endpoint is public.

## Database schema (create one migration)

Create four tables in the public schema:

1. `events` — id (bigserial pk), user_id (uuid nullable — null for anonymous events), session_id (text), name (text not null), properties (jsonb not null default '{}'::jsonb), occurred_at (timestamptz not null default now()), received_at (timestamptz not null default now()), ingestion_source (text).

2. `sessions` — id (text pk — client-generated UUID), user_id (uuid), started_at (timestamptz default now()), last_event_at (timestamptz), event_count (int default 0), country (text), browser (text), device_type (text).

3. `api_keys` — id (uuid pk default gen_random_uuid()), name (text not null), key_hash (text not null unique — store SHA-256 hash, never the raw key), is_active (bool default true), created_by (uuid references auth.users(id)), created_at (timestamptz default now()), last_used_at (timestamptz).

4. `dashboards` — id (uuid pk default gen_random_uuid()), owner_id (uuid not null references auth.users(id)), name (text not null), layout (jsonb default '{}'::jsonb), is_shared (bool default false), created_at (timestamptz default now()).

5. `profiles` — id (uuid pk references auth.users(id) on delete cascade), full_name (text), role (text not null default 'viewer' check (role in ('viewer','editor','admin'))).

## Mandatory indexes (create these in the same migration)

CREATE INDEX IF NOT EXISTS events_name_date_idx ON events (name, occurred_at DESC);
CREATE INDEX IF NOT EXISTS events_user_date_idx ON events (user_id, occurred_at DESC);
CREATE INDEX IF NOT EXISTS events_properties_gin_idx ON events USING GIN (properties);

These three indexes are NOT optional — without them, funnel queries on 100K+ rows will time out.

## Postgres RPC functions (create in the migration)

- `track_event(p_name text, p_user_id uuid, p_session_id text, p_props jsonb)` SECURITY DEFINER — INSERT into events. Used by the ingest Edge Function.
- `event_count_by_day(p_event_name text, p_days int)` SECURITY DEFINER — returns table(day date, count bigint): SELECT occurred_at::date as day, count(*) FROM events WHERE name = p_event_name AND occurred_at > now() - make_interval(days => p_days) GROUP BY day ORDER BY day.
- `unique_users_by_day(p_event_name text, p_days int)` SECURITY DEFINER — same but COUNT(DISTINCT user_id).

## RLS policies

Enable RLS on all five tables.
- events: no SELECT policy for users (read via RPCs only); INSERT via track_event RPC only.
- sessions: admin/team SELECT via has_role_analytics().
- api_keys: SELECT/DELETE for owner or admin; INSERT for admin.
- dashboards: SELECT for owner OR is_shared=true; INSERT/UPDATE/DELETE for owner.
- profiles: per-user SELECT/UPDATE own row; admin SELECT all.

Create SECURITY DEFINER function `has_role_analytics()` returns boolean: RETURN EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role IN ('viewer','editor','admin'));

Grant EXECUTE on track_event, event_count_by_day, unique_users_by_day to anon (ingest calls these without user auth).

## Layout

Create `src/layouts/AppLayout.tsx` — dark mode (className='dark' on html element). Left sidebar (200px): logo, nav links (Overview, Events, Funnels, Retention, Dashboards, Settings). Top bar: date range picker (shadcn Popover + Calendar, global filter — defaults to last 7 days). Main scroll area: router outlet.

## Pages and routes

Create these routes in src/App.tsx:
- /overview (Overview.tsx) — 4 KPI cards (Events today, Unique users today, Top event name by count, Most-used browser from sessions). Below: a Recharts LineChart of total events per day for the last 7 days using event_count_by_day RPC.
- /events (Events.tsx) — two columns. Left: list of distinct event names with count badge (SELECT name, count(*) FROM events GROUP BY name ORDER BY count DESC LIMIT 50). Right: when a name is selected, show EventStreamRow list of the last 50 raw events with that name (user_id, session_id, occurred_at, properties as a pretty-printed JSON accordion).
- /funnels (Funnels.tsx) — FunnelBuilder component: up to 5 step inputs (event name selects from the event name list), a date range override, a 'Calculate' button. Shows a vertical step-by-step funnel with conversion % between each step (stub the calculation for now — return mock data, real implementation in follow-up #2).
- /retention (Retention.tsx) — CohortGrid component placeholder: a 12x8 grid of empty cells with a 'Coming in follow-up' message.
- /dashboards (Dashboards.tsx) — dashboard list with Create button (creates a new dashboards row) and each card links to /dashboards/:id. /dashboards/:id shows the saved layout.config as editable placeholder cards.
- /settings (Settings.tsx) — two tabs: API Keys (list of api_keys with revoke button; Create Key form that generates a random key, shows it once, stores only the SHA-256 hash), Team (list of profiles with role selector for admin to promote/demote).
- /login (Login.tsx) — email+password.

## Key components

KPICard (number + label + trend arrow + color: emerald-500 for positive, rose-500 for negative), ChartCard (Recharts wrapper: accepts an RPC function + chart type, handles loading/error states), EventStreamRow (event name chip + user_id truncated + time ago + collapsible properties JSON), FunnelBuilder (step inputs + Calculate button + conversion funnel bars), CohortGrid (12 column grid, cell color intensity proportional to retention %).

## Styling

Dark mode throughout. Accent color emerald-500 for positive metrics, rose-500 for drops. Dense data display: compact table rows (py-1.5). Event name chips use a muted background. Top-right date range picker is always visible in the header.

What this prompt generates

  • Creates one SQL migration with 5 tables, 3 mandatory indexes on events, aggregation RPCs, and RLS policies
  • Scaffolds dark-mode AppLayout with sidebar and global date range picker
  • Generates all 7 pages with stubbed funnel calculation and placeholder retention grid
  • Builds KPICard, ChartCard, EventStreamRow, FunnelBuilder, CohortGrid components
  • Sets up /settings with API key management (hashed storage, show-once reveal)

Paste into: Lovable Agent Mode (the default chat at the bottom-left of the editor)

Expected output

What Lovable will generate after the starter prompt runs successfully.

Files
supabase/migrations/0001_analytics_schema.sql

5 tables + 3 mandatory indexes + track_event RPC + aggregation RPCs + RLS

src/layouts/AppLayout.tsx

Dark sidebar layout with global date range picker in header

src/components/KPICard.tsx

Metric card with number, trend arrow, emerald/rose color

src/components/ChartCard.tsx

Recharts wrapper with loading/error state handling

src/components/EventStreamRow.tsx

Event name + user_id + time + collapsible properties JSON

src/components/FunnelBuilder.tsx

Step event selector + Calculate button + conversion bars

src/components/CohortGrid.tsx

12x8 retention heatmap grid

src/pages/Overview.tsx

4 KPI cards + events/day line chart

src/pages/Events.tsx

Event name list + raw event stream

src/pages/Funnels.tsx

Funnel builder UI (stubbed calculation)

src/pages/Retention.tsx

Cohort grid placeholder

src/pages/Dashboards.tsx

Dashboard list and /dashboards/:id page

src/pages/Settings.tsx

API key management and team role management

supabase/functions/ingest/index.ts

Public event ingest endpoint with API key validation and batched INSERT

Routes
/overview

KPI cards and events per day line chart

/events

Event name list with raw event stream

/funnels

Multi-step funnel builder with conversion rates

/retention

Cohort retention heatmap grid

/dashboards

Saved dashboard list

/dashboards/:id

Individual saved dashboard

/settings

API key management and team roles

DB Tables
events
ColumnType
idbigserial primary key
user_iduuid nullable
session_idtext
nametext not null
propertiesjsonb default {}
occurred_attimestamptz not null default now()

RLS: No direct SELECT for users; read via RPCs. INSERT via track_event RPC only. GIN index on properties is mandatory.

sessions
ColumnType
idtext primary key
user_iduuid
event_countint default 0
browsertext
device_typetext

RLS: Analytics team SELECT via has_role_analytics()

api_keys
ColumnType
iduuid primary key
nametext not null
key_hashtext not null unique
is_activebool default true

RLS: Admin SELECT/DELETE; INSERT for admin only

dashboards
ColumnType
iduuid primary key
owner_iduuid references auth.users(id)
layoutjsonb
is_sharedbool default false

RLS: SELECT for owner OR is_shared=true; write for owner only

profiles
ColumnType
iduuid primary key references auth.users(id)
roletext check in (viewer,editor,admin)

RLS: Per-user read/write own; admin read all

Components
KPICard

Metric number with trend indicator and emerald/rose color

ChartCard

Recharts wrapper with loading and error states

EventStreamRow

Single event with collapsible properties JSON

FunnelBuilder

Step selector, Calculate button, conversion funnel bars

CohortGrid

Heatmap grid for cohort retention rates

Follow-up prompts

Paste these into Agent Mode one by one, in order, after the starter prompt finishes.

1

Build the ingest API endpoint with JS SDK snippet

Working public ingest endpoint with API key validation, rate limiting, and a copy-pasteable JS tracker snippet

~45-60 credits
prompt
Implement the ingest Edge Function at supabase/functions/ingest/index.ts.

The function accepts POST with body {api_key: string, events: [{name, user_id?, session_id?, properties?, occurred_at?}]}. It should:
1. Validate the API key: hash the provided api_key with SHA-256 (use Web Crypto API available in Deno), look up api_keys.key_hash = hashed, is_active = true. If not found, return 401 {error: 'Invalid API key'}. Update last_used_at on success.
2. Batch INSERT all events in one statement using the track_event RPC or a direct INSERT (SECURITY DEFINER context).
3. Return 200 {received: events.length}.
4. Rate limit: maintain a key in memory (or a postgres table api_key_usage) — reject if >1000 events per minute per key with 429.

In /settings, add the 'Get Started' section: a copy-pasteable JS snippet:
```
// Add to your app
const analytics = {
  track(name, properties = {}) {
    fetch('https://your-app.lovable.app/api/ingest', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({api_key: 'YOUR_API_KEY', events: [{name, properties, occurred_at: new Date().toISOString()}]})
    });
  }
};

analytics.track('page_view', {path: window.location.pathname});
analytics.track('button_click', {button: 'sign_up_cta'});
```

Replace 'your-app.lovable.app' with the actual deployed URL and 'YOUR_API_KEY' with a freshly generated API key from /settings.

When to use: First thing after the starter prompt — nothing else works until events are flowing in

2

Build real funnel queries with multi-step conversion math

Real multi-step funnel with sequential conversion math and configurable time window

~55-70 credits
prompt
Implement the funnel calculation in FunnelBuilder.tsx and a new Postgres RPC `calculate_funnel`.

The RPC signature: `calculate_funnel(p_steps text[], p_start_at timestamptz, p_end_at timestamptz, p_window_days int DEFAULT 7)` returns table(step_index int, step_name text, user_count bigint, conversion_rate numeric).

The RPC implements a sequential funnel: for step 1, count DISTINCT user_id where name = p_steps[1] and occurred_at between p_start_at and p_end_at. For step 2, count DISTINCT user_id who appear in step 1 AND have name = p_steps[2] occurring within p_window_days of their step 1 event. Repeat for each step. Use CTEs:

WITH step1 AS (SELECT DISTINCT user_id, MIN(occurred_at) as first_at FROM events WHERE name = p_steps[1] AND occurred_at BETWEEN p_start_at AND p_end_at GROUP BY user_id),
step2 AS (SELECT DISTINCT e.user_id FROM events e JOIN step1 s ON s.user_id = e.user_id WHERE e.name = p_steps[2] AND e.occurred_at BETWEEN s.first_at AND s.first_at + make_interval(days => p_window_days))...

In FunnelBuilder, replace the mock data with a supabase.rpc('calculate_funnel', {p_steps, p_start_at, p_end_at}) call. Show each step as a horizontal bar (width = conversion_rate %). Show absolute user count and % below each bar. Show drop-off % between steps in a rose-500 label.

When to use: Once ingest is working and you have at least 1,000 events to query

3

Add cohort retention grid

Cohort retention heatmap showing what % of each week's users returned in subsequent weeks

~50-60 credits
prompt
Implement the cohort retention grid in CohortGrid.tsx and a new Postgres RPC `calculate_retention`.

The RPC: `calculate_retention(p_signup_event text DEFAULT 'sign_up', p_return_event text DEFAULT 'page_view', p_weeks int DEFAULT 8)` returns table(cohort_week date, week_number int, retained_users bigint, cohort_size bigint, retention_rate numeric).

Implementation: group users by the week of their first p_signup_event occurrence (cohort_week). For each cohort, count users who also triggered p_return_event exactly N weeks later. Return one row per (cohort_week, week_number) pair.

In CohortGrid.tsx, render a table where:
- Rows = cohort weeks (most recent at top)
- Columns = Week 0 (baseline = 100%), Week 1, Week 2, ... Week N
- Each cell = retention_rate formatted as X.X%
- Cell background color: interpolate from emerald-100 (100%) to transparent (0%) based on retention_rate value
- Row header = cohort week date + cohort_size users
- Click a cell to see the user list (query raw events for those users in that week)

Grant EXECUTE on calculate_retention to authenticated.

When to use: When you have at least 4 weeks of event data and want to understand user return rates

4

Set up BigQuery warehouse export for events beyond 100K/month

Hourly BigQuery export with Postgres pruning — keeps the source DB fast while archiving full history in the warehouse

~65-80 credits
prompt
Add a BigQuery export pipeline for when your events table grows past 100K rows/month.

Create a new Edge Function at supabase/functions/export-to-warehouse/index.ts. The function:
1. Queries events where occurred_at > (SELECT MAX(exported_at) FROM warehouse_sync_log). Create a new table `warehouse_sync_log` (last_exported_event_id bigint, exported_at timestamptz, rows_exported int).
2. Streams events in batches of 1000 to BigQuery using the BigQuery REST API (POST to https://bigquery.googleapis.com/bigquery/v2/projects/{project}/datasets/{dataset}/tables/{table}/insertAll). Authenticate with a service account key stored as GOOGLE_SERVICE_ACCOUNT_JSON in Cloud Secrets.
3. After successful export, DELETES events older than 30 days from Postgres (keeping only a rolling 30-day window in Postgres for fast queries). Inserts a warehouse_sync_log row.
4. Returns {exported_rows, deleted_rows}.

Schedule this function via Cloud tab → Edge Functions → export-to-warehouse → Schedule to run hourly.

In /settings, add a 'Warehouse' tab showing the last sync timestamp and row count from warehouse_sync_log, with a 'Sync now' button that calls the function on demand.

Before running this prompt: create a BigQuery dataset in Google Cloud Console, create a service account with BigQuery Data Editor role, download the JSON key, and paste it into Cloud Secrets as GOOGLE_SERVICE_ACCOUNT_JSON.

When to use: When events table crosses 100K rows or Supabase storage approaches 300MB

5

Add Lovable AI natural-language chart queries

Natural-language SQL generation via Lovable AI (Gemini 3 Flash) with safe query execution and dashboard save

~70-85 credits
prompt
Add an 'Ask anything' natural-language query input to /dashboards.

In /dashboards, add a text input with placeholder 'Ask: What were the top events last week?' and a Submit button. On submit, call the Lovable AI connector (Cloud tab → AI → Gemini 3 Flash, which is the default model) with the user's question and this system context:

'You are a SQL analyst for a product analytics database. The main table is `events` with columns: id (bigserial), user_id (uuid), session_id (text), name (text), properties (jsonb), occurred_at (timestamptz). Generate a read-only SELECT SQL query answering the user's question. Return ONLY the SQL with no explanation or markdown fencing. Always include an ORDER BY and LIMIT 100.'

Pass the generated SQL to a new Edge Function `run-query` that:
1. Validates the SQL is a SELECT statement (reject UPDATE/DELETE/DROP with 400).
2. Executes via supabase.rpc or a prepared statement against the events table.
3. Returns up to 100 rows as JSON.

The /dashboards/:id page renders the result as either a DataTable (if columns > 3) or a Recharts BarChart (if 2 columns: name + count). Add a 'Save to dashboard' button that saves the query + result type to dashboards.layout as a named card.

When to use: Once funnel and retention are working and your team wants ad-hoc questions without writing SQL

6

Add Slack or Resend metric drop alerts

Automated metric drop alerts via Slack webhook or Resend email with WoW comparison and deduplication

~40-50 credits
prompt
Add alerting when a key metric drops. In /dashboards, let users define alert rules: 'Alert if [event_name] count drops more than [X]% week-over-week.'

Create an `alert_rules` table: id (uuid pk), name (text), event_name (text), threshold_pct (int not null), channel (text check in ('slack','email')), destination (text — Slack webhook URL or email address), is_active (bool default true), owner_id (uuid references auth.users(id)).

Create a scheduled Edge Function at supabase/functions/check-alerts/index.ts running every hour:
1. Fetch all active alert_rules.
2. For each rule, call event_count_by_day for the past 7 days and the 7 days before that.
3. Compare this week's total to last week's total. If the drop percentage > threshold_pct, fire the alert.
4. For Slack: POST {text: 'Alert: [event_name] is down X% WoW ([this week] vs [last week])'} to destination (a Slack Incoming Webhook URL).
5. For email: call Resend with the same message to destination.
6. Record each fired alert in an `alert_history` table (rule_id, fired_at, this_week_count, last_week_count, drop_pct) to avoid re-alerting on the same drop.

In /dashboards, add an 'Alerts' tab with the alert_rules list and a 'Create alert' form.

When to use: When stakeholders ask 'why didn't anyone tell me sign-ups dropped last week?'

Common errors

Real error strings you'll see. Find yours, paste the fix prompt.

Failed to insert event / slow inserts after 50K rows

You skipped the three mandatory indexes on the events table. Without them, every INSERT triggers a full table scan for conflict checking, and SELECT queries time out on jsonb aggregations.

Manual fix

Open Cloud tab → Database → SQL Editor and run: CREATE INDEX IF NOT EXISTS events_name_date_idx ON events (name, occurred_at DESC); CREATE INDEX IF NOT EXISTS events_user_date_idx ON events (user_id, occurred_at DESC); CREATE INDEX IF NOT EXISTS events_properties_gin_idx ON events USING GIN (properties); These are included in the starter migration, but confirm they exist in Cloud tab → Database → Indexes.

permission denied for table events from ingest endpoint

RLS is enabled on events, the ingest Edge Function runs without elevated privileges, and no INSERT policy exists for the anon or authenticated path.

Fix — paste into Lovable Agent Mode
Make the track_event RPC SECURITY DEFINER (already in the starter migration). Have the ingest Edge Function call supabase.rpc('track_event', {...}) instead of inserting directly. Grant EXECUTE on track_event to anon: GRANT EXECUTE ON FUNCTION track_event(text, uuid, text, jsonb) TO anon;
Funnel query times out after dataset grows past 500K rows

The sequential WHERE EXISTS subqueries in calculate_funnel do full table scans even with indexes, because the join pattern doesn't use the index efficiently at high cardinality.

Fix — paste into Lovable Agent Mode
Add a materialized view event_user_first_seen (user_id, event_name, first_occurred_at): CREATE MATERIALIZED VIEW event_user_first_seen AS SELECT DISTINCT ON (user_id, name) user_id, name as event_name, occurred_at as first_occurred_at FROM events ORDER BY user_id, name, occurred_at; REFRESH MATERIALIZED VIEW event_user_first_seen; Rewrite calculate_funnel to JOIN this materialized view instead of scanning events. Refresh the view hourly via a scheduled Edge Function.
API key visible in browser DevTools Network tab

The ingest endpoint is called from the browser with the API key in the request body — anyone who opens DevTools can see and copy the key to spam your endpoint.

Fix — paste into Lovable Agent Mode
Create two key tiers: a public ingest key (rate-limited to 1000 events/min/key, allowed from any CORS origin, can ONLY call ingest) and a server-side admin key (full access, never exposed to browsers). Document this in /settings with a warning banner on public keys. Rate limiting is already in follow-up #1's ingest function — make sure it's deployed.
Dashboard charts show no data even though ingest is working

The date range picker default is 'today' in UTC, but events have occurred_at in a different timezone offset. A 3pm event in US Eastern time is stored as 8pm UTC — if the date range is today in UTC, events from before 8pm UTC on the US East Coast fall into the previous day's bucket.

Fix — paste into Lovable Agent Mode
Standardize on UTC for occurred_at storage (already correct if you used default now()). In the date range picker, add timezone selection (default UTC, allow user to switch). Pass the selected timezone to RPCs as a parameter and convert range bounds: WHERE occurred_at >= p_start_at AT TIME ZONE p_tz.
Supabase free tier exceeded / 500MB DB warning after two months

You stored all events in Postgres without pruning. At 100K events/month with ~1KB per event (with jsonb properties), you hit 500MB in about 5 months. With rich properties, faster.

Fix — paste into Lovable Agent Mode
This is the 'stop building, reconsider' moment. Either: (a) set up the BigQuery export (follow-up #4) and add DELETE FROM events WHERE occurred_at < now() - interval '30 days' to the scheduled job; or (b) stop the custom build and integrate PostHog (1M events/month free, funnels and retention included). Option (b) is often the right call at this point.

Cost reality

What this build actually costs — no surprises on your card.

Recommended Lovable plan

Pro $25/mo. The full chain is 100-180 credits fitting within one month's Pro allowance plus possibly one top-up. BUT: compare this cost against integrating PostHog (free for 1M events/month) before committing.

Monthly run cost breakdown

~100-180 credits (starter ~50-80, six follow-ups ~200-250 if you do them all; most teams do 3 follow-ups for ~120 total). All estimates extrapolated from internal-dashboard + Recharts cousin patterns. total
ItemCost
Lovable Pro

Can drop to Free after the build is stable

$25/mo
Lovable Cloud / Supabase Free

Up to ~100K events/month at ~1KB/event before hitting 500MB storage limit

$0/mo
Supabase Pro (when needed)

When you cross 500MB DB or need point-in-time recovery

$25/mo
BigQuery

Only needed once you set up the warehouse export at 100K+ events/month

$0/mo storage up to 10GB, $6.25/TB queried
Custom domain$10-15/yr

Scaling notes: 100K events per month is the soft ceiling on Supabase Free (500MB DB fills in ~5 months at 1KB/event). 1M events per month is the hard ceiling where Supabase Pro plus warehouse export becomes mandatory — and that is also when PostHog's free tier starts looking free by comparison. Set yourself a tripwire: if Supabase storage crosses 200MB or you spend more than 20 hours per month maintaining the system, migrate to PostHog.

Production checklist

Steps to take before you share the URL with real users.

Indexes (Day One)

  • Verify all three mandatory indexes exist before sending any real events

    Cloud tab → Database → Indexes — confirm events_name_date_idx, events_user_date_idx, and events_properties_gin_idx all appear. If any are missing, run the CREATE INDEX statements from the common errors section. Missing indexes will cause funnel queries to time out once you have more than 50K events.

API Key Security

  • Never expose a server-side admin key in client code

    In /settings, mark public ingest keys clearly as 'Public — can be seen in browser'. Create a separate 'Private — server only' key tier for keys that have read access to dashboards. Document this in your internal team wiki so no one accidentally pastes an admin key into client-side JS.

Data Retention

  • Set up a 30-day Postgres retention policy before crossing 200MB

    In Cloud tab → Database → SQL Editor, create a scheduled function or add to your existing hourly Edge Function: DELETE FROM events WHERE occurred_at < now() - interval '30 days'; This keeps the hot table lean for fast queries while the warehouse holds the full archive.

Monitoring

  • Set up a Supabase storage size alert

    Create an alert_rules row (if you built follow-up #6) watching a synthetic 'db_storage_mb' metric, or check manually monthly: Cloud tab → Database → Overview shows current storage usage. Set a calendar reminder to check when you expect to cross 300MB.

Frequently asked questions

Should I just use PostHog or Mixpanel instead?

Probably yes. PostHog gives you 1 million events per month free and includes funnels, retention curves, session replays, feature flags, and A/B testing — all things you would spend weeks rebuilding here. Mixpanel also offers 1 million events per month free with excellent funnel and retention UI. Both integrate in about an hour. Build in Lovable only if: (a) your event volume is genuinely under 100K per month and you want full data ownership on a Postgres instance you control, (b) you're in a regulated industry (healthcare, finance, government) that cannot send behavioral data to a US third-party SaaS, or (c) analytics IS your product and you're building a niche vertical analytics tool to sell to others. If none of those apply, stop reading this kit and go to posthog.com.

How do I track events from a non-Lovable app?

The ingest Edge Function is a public HTTPS endpoint that accepts JSON. You can call it from any stack — Next.js, React Native, a Python backend, a mobile app, or even a curl command. Follow-up #1 generates a copy-pasteable JavaScript snippet for browser apps. For server-side tracking (important for events you don't want to trust the client with, like 'payment_succeeded'), call the endpoint from your backend server, not the browser. Never include your server-side admin API key in client-side code.

Can I do session replay like PostHog does?

Not with this kit. Session replay requires capturing every DOM mutation, mouse movement, and scroll event at very high frequency — typically 10-100 events per second — and reconstructing them as a video-like playback. The data volume alone would overwhelm the Supabase free tier in hours. PostHog's session replay is built on a specialized storage backend. If you need session replay, use PostHog or Hotjar — this is not a category where self-building makes sense.

What about A/B testing and feature flags?

This kit does not include A/B testing or feature flags. Those are significant features that require a flag evaluation SDK (served fast from an edge, not queried from Postgres), assignment logic, and statistical significance calculations. PostHog's feature flags and A/B testing are free up to 1M events/month and integrate in 10 minutes. If you need feature flags today, use PostHog or LaunchDarkly — do not build them from scratch.

How do I move my Supabase events to BigQuery?

Follow-up #4 in this kit sets up an hourly scheduled Edge Function that streams events to BigQuery using the BigQuery REST API and prunes Postgres to a 30-day rolling window. You need a Google Cloud project, a BigQuery dataset, and a service account JSON key with BigQuery Data Editor role — all created in the Google Cloud Console before running the follow-up prompt. BigQuery is free for storage up to 10GB and $6.25/TB queried (most analytics dashboards cost pennies per month to run).

How do I make sure my Postgres doesn't melt on funnel queries?

Three things in order of impact: (1) The three mandatory indexes from the starter migration are non-negotiable — without the GIN index on properties, any jsonb query scans every row. (2) Add the materialized view from follow-up #2 to pre-aggregate first-seen events per user per event type. (3) If you cross 500K events in Postgres, move to the BigQuery export and run heavy funnel queries there. Postgres aggregation on a 1M-row events table with jsonb filtering will be slow regardless of indexes — the right tool at that scale is a columnar warehouse.

Can RapidDev build a custom analytics platform for my vertical?

If your build outgrows this prompt kit and you need custom architecture, RapidDev builds production-grade Lovable apps at $13K-$25K — book a free 30-minute consultation at rapidevelopers.com.

Need a production-grade version?

RapidDev builds production-grade Lovable apps at $13K–$25K. Book a free 30-minute consultation.

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.