Your users live in Supabase. Your app tables, your auth, your row-level security, all in a Postgres database you own and can open a SQL editor against right now. Your product analytics? Those events live in PostHog or Mixpanel, in a cloud you do not own, priced by the event, and they go quietly wrong every time you ship. You fix the code the next morning. The data from the day it broke is already gone.
Sentry catches runtime errors. Skene catches broken telemetry.
This is a walkthrough of how Skene puts product analytics back where the rest of your app already lives: in your own Supabase project, in an Analytics Bucket you own. How the events get there, how the map gets built from your repo and your schema, and how a check on every pull request keeps the whole thing from rotting.
Key takeaways
- Your product analytics can live in the same Supabase project as everything else: raw events land in an Analytics Bucket as Iceberg/Parquet, backed by Storage you own, never copied to a vendor.
- Skene adds the tracking you're missing and validates the events you already have (your PostHog and Mixpanel calls included) on every pull request, so the data stays complete and stops going quietly wrong.
- The check runs as a GitHub Action on every PR, reads your Supabase schema read-only (the PR's own preview branch when you use Supabase Branches), and comments the moment a write breaks, while the data is still recoverable.
- Once the events are trustworthy, you query the paths, cohorts, and features that drive revenue over MCP (from your coding agent), instead of babysitting another dashboard.
- Analytics is not retroactive. You can fix the code tomorrow. The data you never recorded today is gone for good.
The warehouse is a bucket in your own Supabase
Start with the part that matters most if you already run on Supabase: the events do not leave.
Skene exposes a first-party ingest endpoint. You call skene.track from your app, and the event is written into your own Supabase project as an Analytics Bucket table, in Iceberg format on Parquet files. No second warehouse to stand up. No proprietary store you rent and cannot query. The raw rows sit in Storage you already own, next to the tables that back the rest of your product.
Your posthog.capture and mixpanel.track calls keep feeding the dashboards your team likes. skene.track lands a first-party copy of those events in your own Supabase, so the raw data is yours too. Keep the dashboard, own the events underneath it.
Here is the path an event takes:
Your app
│ skene.track("team_invite_sent", { team_id })
▼
POST /api/v1/track (API key, analytics_write scope)
│
▼
Buffer (Postgres or Redis) idempotency key dedupes client retries
│
▼
Cron worker → DuckDB loader
│
▼
Analytics Bucket in your Supabase (Iceberg / Parquet: default.events)
│
▼
Rollups for the fast overlay → the lifecycle map
A few things worth knowing about that pipeline:
- Idempotency keys on ingest mean a client that retries a request does not double-count. One event, one row.
- The buffer has two backends. Low-volume workspaces buffer in Postgres. High-volume ones point at Redis. Same client call either way.
- DuckDB does the batch load into Iceberg on your Analytics Bucket. Columnar storage for high-cardinality event properties, without you standing up a separate cluster to run it.
- Rollups keep the map fast. The canvas reads pre-aggregated counts, not a raw Parquet scan on every page load. Your analysts can still query the Iceberg table directly when they want row-level detail.
The skene.track call is the same shape you already reach for:
skene.track("team_invite_completed", {
team_id: team.id,
inviter_id: user.id,
invitee_count: invites.length,
})
Own the raw events. Own the aggregates. Point whatever dashboard you already use at the same Supabase project. Nothing about this asks you to trust a black box with your data.
The map: what your app can already measure
Custom events are one signal. The other one is already sitting in your schema.
Open Journey, connect your GitHub repo and your linked Supabase project, and run Analyze. Two passes run and merge. A code pass reads the repository and finds the events your app already records, whether that's a posthog.capture, a mixpanel.track, or a first-party write, and traces the wrappers they route through. A schema pass inspects your Supabase tables, columns, and relationships. The result is a lifecycle map: stages, milestones, and for each milestone the evidence behind it.
The scan knows the call shapes you actually use:
posthog.capture("checkout_completed", { plan: "pro" })
mixpanel.track("Team Invite Sent", { team_size: 5 })
trackEvent("signup_completed", { source: "google" }) // your own wrapper
That last line is the one that trips most tools. Codebases route everything through a local trackEvent helper, so Skene follows the wrapper back to the call it actually makes and maps the event to the milestone it supports. And it reads all of this from your Git repository, not from a PostHog or Mixpanel API. Git tells it what should fire after the next merge, which is exactly what you want to compare against when a dashboard goes quiet.
A milestone can be backed by a recorded event (a PostHog or Mixpanel call, or a first-party write), by a Supabase table (say public.teams with a created_at window), or by both on the same node. The detail panel exposes cohort SQL against your own project, a plain COUNT(*) on the table that backs a milestone, with optional state filters. The whole map is written to a journey.yaml in your repo, so it lives in version control next to the code it describes.
Why derive it from the repo and the schema every time, instead of a doc? Because a static event list rots on day one. A coding agent refactors a file, an event moves, a column changes shape, and the markdown table in your wiki has no idea. The map Skene shows you is rebuilt from what is actually on disk and in your database, not from what someone wrote down six months ago.
Live Flows: watch the real path before you name events
Sometimes you do not know the right event names yet. You have a hunch about where people drop, but naming the events up front is guessing.
Live Flows adds passive capture for exactly that moment. You paste a small snippet (around 5 KB, one scoped public key) into your app shell, and it records clicks, navigations, and form submits into an interaction buffer. Out of that, Skene draws a directed graph of the real paths through your product, with exit points and heatmaps. No event taxonomy required on day one. You watch what people actually do, then decide which events are worth naming.
The privacy model is deliberate and narrow. The capture script reports element fingerprints only: tag name, id, data-testid, aria-label, the first 60 characters of visible text, a short CSS path, and click coordinates. It never reports form values, input contents, cookies, or localStorage. The public key cannot be widened beyond its capture scope. Adding a field that carried personal data would be a regression, not a feature.
Keeping it honest on every pull request
Here is the part nobody else is doing, and it is the reason any of the numbers above are worth trusting.
The write that records an event, a posthog.capture, a mixpanel.track, or an insert into your own table, is a side effect. Your coding agent is good at logic and indifferent to side effects. It refactors a checkout flow or an onboarding step, the line that records the event rides along with the change, gets renamed or dropped, and the pull request still passes review because the feature works. Nothing turns red. The events just stop arriving. You find out three sprints later when a chart looks wrong, and by then the gap in the history is permanent.
So Skene runs as a GitHub Action on every pull request. It indexes the events your app records, reads your Supabase schema read-only for the tables those events land in, and on each PR compares the new state against that baseline.
If you use Supabase Branches, that baseline is the PR's own preview branch, not stale main. Open a PR that adds a migration and renames the event that writes to the changed table, and Skene checks the two against each other on that branch, before either one reaches production. The schema change and the tracking change get reviewed together, in the one place they actually meet.
When a change removes a write, renames it, or hands your schema a payload it cannot store, Skene comments on the PR and names the specifics:

The GitHub App comments the moment a tracked event changes, names the milestone it affects, and offers the fix as a one-click suggestion.
You fix it in the same commit, while the data is still recoverable, instead of discovering it in a flat dashboard weeks later. The comment names the file, the event, and the milestone on Journey it touches, and it comes with a one-click suggested change. Prefer a full restore? Reply /skene fix and the bot opens the PR for you.
Two more things make the check hold up in practice:
- Status Check is the lighter re-verification. A full Analyze is expensive, so Status Check re-scans the repo and schema, re-stamps the tracking status on
journey.yaml, and uses semantic matching when an event name drifts slightly (team_invite_sentbecominginvite_sent_to_team) without rerunning the whole pipeline. Run it after a big refactor or before a board meeting. - The gates are type-aware. An event-backed milestone and a table-backed milestone verify differently. A database milestone whose schema is unavailable on a given PR does not fail CI, so a frontend-only change never trips a false red.
The same comparator runs in four places: as an MCP server inside Cursor, Claude Code, or Codex before you commit; as a GitHub Action in CI; over an HTTP validation API; and as a one-time repo audit from the CLI. Pick the checkpoint that matches your workflow. It is the same check either way, and it always sits outside the agent that wrote the code.

The same check from inside your coding agent over MCP, run against the working-tree diff before anything is pushed.
Then the part that pays: query it over MCP
Trustworthy events are the precondition. What you do with them is the point.
Because the data is complete and lives in Supabase, Skene exposes it as growth intelligence over its MCP server. You ask your coding agent which paths convert in the fewest steps, which cohorts actually pay, and which features to reinforce or cut, and it answers from your own events. Tracking Planner takes a scenario in plain language ("how do we measure activation for team invites?") and returns the gaps: which signals already exist, which are net-new, grouped by acquisition, activation, retention, revenue, and referral. When the gap is a missing event, it hands you the exact skene.track call to add.
That value layer is live today. The one piece still on the roadmap is a turnkey, in-product dollar-revenue view. Composing dollar-weighted ROI in your own agent from your own revenue numbers already works. The one-click report of it is what is next.
What Skene is not
Clarity here keeps the wrong buyer away.
| Skene is not | What that means |
|---|---|
| A dashboard you babysit | PostHog and Mixpanel stay your product-analytics surfaces. Skene keeps the calls that feed them honest, holds a trustworthy copy of the events in your own Supabase, and answers questions over MCP. Point any dashboard at that Supabase and Skene feeds it. It does not replace the tools you already use. |
| A CDP or a data copy | Your events stay in your database. Skene owns the gate and the record of what every event is for, not a copy of your data. |
| A coding agent | It does not write your features. It validates the tracking those agents touch. The check is the seat, not the author. |
A day you can picture
You add a question to a milestone: did the team owner invite a member within seven days of creating the workspace? Analyze finds the team_created write, finds the invite_sent write, and notices your teams table has no completion column. Tracking Planner flags the missing invite_accepted event and hands you the call to add it. You paste the Live Flows snippet and watch people hit the invite screen but drop on "copy link" instead of "send email," so you fire the event on link copy too.
Next sprint, an agent refactors onboarding and renames invite_sent. Before it merges, the PR comment names the event, the file, and the milestone that still points at the old name. You reply /skene fix. The funnel stays intact, and the number you report at the end of the quarter is one you can actually defend.
Working with design partners
We are building Skene with a small group of design partners: teams shipping fast on Supabase with coding agents and no dedicated analytics person, where the founder owns the metric and cannot fully trust it. If that is you, and you have watched a dashboard go flat after a refactor, we would like to talk.
Try it
Connect your repo and your Supabase project read-only, run the first Analyze against the code you already have, and add the GitHub Action. It takes about as long as your CI does. Read the docs or open the repository to get started.
Continue with Skene
How Skene works
Skene reads the event writes in your code and checks them against your Supabase schema on every PR.
Skene vs. analytics tools
Compare keeping your telemetry in your own Supabase to shipping events to hosted product analytics platforms.
Pricing
Open-source core is free. Paid plans start at $29/month with a 1.5M token budget.
