There’s a feeling you get the first time a total stranger uses something you built. Not a friend you talked into trying it, a real person, somewhere out there, who found your app and is using it right now. It’s one of the best feelings I know of, and it never gets old.
But to get that feeling, you have to be able to SEE it. You need to know people are showing up, and more than that, what they do once they arrive. That’s what analytics give you: a window into how real people use your thing.
The basic analytics your host provides (page views, visitor counts) are a great start, but they top out fast. They can’t tell you whether anyone clicked the button you sweated over, or which feature people love, or where they get stuck. For that you want custom analytics, tracking exactly the things YOU care about. There are great ready-made tools for this (PostHog, Plausible, etc), and if you’d rather just use one of those, that’s completely valid. The reason I choose to vibe code my own is full control and flexibility: you decide exactly what gets tracked, the data lives in your own database instead of a third party’s, and you tend to understand your app better for having built it.
This is the deep dive on the analytics piece of how to know your vibe coded app isn’t quietly on fire. That post maps the whole picture (analytics, logging, error reporting); here we actually build the analytics part.
This is the step by step guide. By the end you’ll have your own analytics pipeline: events flowing from your app into a database, and a dashboard where you can watch real people use what you made. I’ll explain each piece so you understand what’s happening, I’ll give you the exact things to ask Claude (or whatever agent you use) to build it, and I’ll flag the one security step you absolutely cannot skip. And because you’re vibe coding, we’ll keep it hands-off: once Claude is wired into your database, most of this is just asking it to do the work.
Let’s build your window.
The shape of it
Before we touch anything, here’s the whole picture, because it’s simpler than it sounds:
- Your app sends an event (a little note that says “someone clicked sign up” or “someone opened the dashboard”) to a database.
- The database, we’ll use Supabase, stores every event in a table.
- A dashboard reads those events back and shows you what’s happening.
That’s it. Your app sends, Supabase stores, your dashboard shows. Everything below is just wiring those three together.
Step 1: Spin up a Supabase project
Supabase is a backend you can use without being a backend engineer. For our purposes it’s a database with a generous free tier, which is all we need to start.
Head to supabase.com, sign up, and create a new project. It’ll ask for a project name and a database password (save that password somewhere safe). Give it a minute to finish setting up.
You don’t need Claude for this part, it’s mostly clicking through their setup. Once your project is ready, you’ve got an empty database waiting for data. Next, let’s hook Claude up to it so you can do the rest without touching the dashboard again.
Step 2: Connect Claude to your Supabase project
This is what makes everything below hands-off. Supabase is now an official Claude connector, so Claude can work with your database directly instead of handing you SQL to run yourself.
In Claude, turn on the Supabase connector and authorize your account. It pops open a browser login, the same as any “sign in with…” flow, so you never paste a key into the chat. Once it’s connected, you can ask Claude to create tables, set up security, and run queries in plain English, and it does the work for you. Helllll yeah! Now that’s what I’m talking about!
One heads-up worth understanding: this gives Claude admin-level access to your database, the kind that can build things but also delete them.
- While you’re making something new with no real users yet, this is low-risk. Go for it.
- Once real people and real data are in there, follow Supabase’s own advice: point Claude at a development copy or use read-only mode, and glance at what it’s doing instead of rubber-stamping.
- This admin access is a separate thing from the safety net we’ll set up for your app in a moment. Think of it as you holding the master key to set the place up, while your app runs on a locked-down key that obeys the rules.
Step 3: Have Claude create your events table
Every event your app sends needs somewhere to live. That’s a table, think of it like a spreadsheet where each row is one thing that happened.
Here’s a simple events table that covers most of what you’ll want:
- id: a unique id for each event (generated automatically)
- created_at: when it happened (also automatic)
- event_name: what happened, like
signup_clickedordashboard_opened - session_id: a random id for each visitor, so you can tell one person’s actions apart from another’s
- properties: a flexible field (a type called
jsonb) for extra details you want to attach, like which plan someone picked
Because Claude is connected to Supabase now, you don’t touch the dashboard or paste anything. Just tell it what you want:
“Using the Supabase tools, create a table called
eventswith these columns: id (uuid, primary key, auto-generated), created_at (timestamp, defaults to now), event_name (text), session_id (text), and properties (jsonb).”
Claude runs it against your database and reports back. So you know exactly what happened under the hood, here’s the SQL it ran:
create table events (
id uuid primary key default gen_random_uuid(),
created_at timestamptz default now(),
event_name text,
session_id text,
properties jsonb
);
NOTE: you don’t have to pass in the specific columns if you don’t want to. Claude can come up with it based on your app on the fly. This is just an example.
That’s the whole payoff of wiring Claude into Supabase: you describe what you want, Claude makes it real, and you never tab away to run anything. You can even do it from your phone! ;D
Step 4: Turn on Row Level Security (do not skip this)
Once again: DO NOT SKIP THIS. This is the step that protects you, and it’s the one thing that’s easy to miss while vibe coding. So here it is in bold: the table Claude just created starts with its security turned OFF. You have to turn it on.
Here’s why it matters. To send events from your app, your app needs a key, and that key (the publishable key) ships inside your app’s code where anyone can find it. That’s by design, it’s meant to be public. But it’s only safe if you’ve told your database what that key is allowed to do. Leave security off, and that public key becomes a skeleton key: anyone who finds it can read, change, or wipe your entire database. People have leaked their whole user list this exact way.
The fix is Row Level Security (RLS for short). It’s a set of rules for who can do what to your table. For analytics, the rules you want are simple:
- Anyone using your app CAN add a new event (so your tracking works).
- Nobody using the public key can READ events back (so a stranger can’t pull your data).
You’ll read your own data through a protected dashboard later, which we’ll handle separately.
Same hands-off move as before, just ask:
“Turn on Row Level Security for my
eventstable and add a policy that allows anonymous inserts but does NOT allow reading rows with the public key.”
Claude sets it up for you. Here’s what it’s putting in place, so you can see your safety net:
alter table events enable row level security;
create policy "Allow anonymous inserts"
on events for insert
to anon
with check (true);
That one policy lets your app write events all day. And because there’s no policy for reading, the public key can’t pull anything back out. Your data stays private.
Two more security rules while we’re here, both important:
- Supabase also gives you a secret key (it used to be called the service_role key). That one ignores all your rules and has full access. It belongs only in backend or server code your users never see. Never put the secret key in your front-end code or commit it to a public repo. The publishable key is the one that goes in your app.
- Don’t type a key into the chat. The thing to avoid is a key becoming a line in your chat history, sitting there in plain text. So don’t paste one into the conversation yourself. Letting Claude move keys around through the Supabase connector is fine, because they go straight from the connection into your project and never pass through the chat. (For this analytics setup it’s a non-issue anyway: your app only uses the publishable key, the one that’s public by design.)
Get this step right and the rest is the fun part. If keys and access control still feel fuzzy, I unpack the core security concepts here.
Step 5: Send events from your app
Now we wire your app up to Supabase so it starts sending those events. Here’s where vibe coding really shines: you don’t spell out the code, you describe the outcome you want and let Claude build it. It already knows your table, it just made it.
First, connect your app to your project:
“Connect my app to my Supabase project so it can send events to my
eventstable. Read the connection details from environment variables, don’t hardcode them.”
Claude pulls in the Supabase client library and sets up the connection. In JavaScript, which many vibe coded web apps use, that ends up looking something like:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY
)
That bit about environment variables matters: you want the URL and key stored outside your code, not typed directly into it. The exact naming depends on your setup, but Claude will sort that out. And since Claude is connected to Supabase, it can even pull your project URL and publishable key and drop them into your environment file for you, so you’re not copying those by hand either. (That’s the connector moving the key into your project, not you typing it into the chat, so it never lands in your history.)
Now the part you came for: telling it what to track:
“Track when someone clicks the signup button and save a metrics event for it.”
Claude will handle the wiring for you now that you have the infrastructure!
NOTE: Privacy is a concern here. Avoid storing personal information of your users. You can google PII and GDPR for EU users for more detail. Just a call out. Rule of thumb: keep things anonymous and if you wouldn’t want it leaked, don’t put it in your events table.
Step 6: Prove your events are firing
Here’s the sneaky thing about analytics: when an event fails to save, your app keeps working like nothing happened. No crash, no error on screen, the user never notices. Lovely for them, dangerous for you, because you can build a whole dashboard, sit back to watch the strangers roll in, and see nothing. Not because nobody came, but because the events were quietly failing the whole time.
So before you trust it, prove it works. The good news: it takes about two minutes.
The quick check. Trigger one of your events yourself. Click the button, sign up, do whatever fires an event you set up. Then confirm the row landed. Because Claude is connected to Supabase, the easiest way is to just ask:
“Check my events. Did a
signup_clickedevent land in the last few minutes? Show me the row.”
Claude queries your database and tells you. If the row’s there with the right event_name and a session_id, you’re in business. (You can also eyeball it in Supabase’s table editor, same thing, more clicking.)
If nothing landed, don’t panic, it’s almost always one of a few usual suspects, and Claude can chase it down for you. The most common culprit is your RLS policy not allowing the insert, followed by a wrong key or a table or column name that doesn’t match. Two things help Claude help you:
- Check your browser’s developer console. Your tracking quietly logs an error there whenever a save fails, so if something’s broken, the reason is probably sitting right there. Copy it.
- Hand it over: “My events aren’t landing in Supabase, here’s the console error: [paste it]. Check my table, my RLS policies, and my client setup, and fix whatever’s wrong.” Since Claude can see your database directly, it can usually spot the mismatch and patch it.
The permanent guard (optional, but you’ll thank yourself). The check above proves it works today. A test proves it still works after you’ve changed a dozen other things next month. It’s the same reason I go on about tests everywhere else: you really don’t want to discover your analytics died three weeks ago. Have Claude write you one:
“Write me a test that fires an event, confirms the row showed up in the database, then cleans up after itself. Run it against my development project, not real data.”
Now you’ve got a one-command way to confirm the whole pipeline works end to end, any time you want. Run it, see green, move on.
Once your events are firing and you’ve proven it, you’re ready for the fun part: looking at them. (Good lord, finallllly!)
Step 7: Build your dashboard
Your events are firing and you’ve confirmed it (Nice!). The last piece is a way to look at them. Your window.
You’ve got two easy options here, and the nice part is neither one makes you build a login or worry about who can see your data:
The quick way: Supabase’s built-in editor. Supabase’s own table editor shows you raw events right there, and its SQL editor lets you run a query like “how many signups this week” any time you’re curious. It’s private because it sits behind your Supabase login, and it’s zero effort. For a brand new app this might be all you need.
My personal favorite: a local dashboard. When you want something prettier, charts, totals, your most-used features at a glance, have Claude build you a dashboard that runs locally.
So what does “local” mean? It means it runs on your own computer, not out
on the internet. You start it up with a command when you want to check your
numbers, it opens in your browser at an address like localhost:3000 (an
address only your machine can reach). It’s not a website anyone can visit.
It’s just for you, on your machine.
That local-only part is the trick that makes this both easy and safe. There’s no public page, so there’s nothing to lock down and no login to build. And because the dashboard never leaves your computer or gets deployed anywhere, it’s perfectly fine to let it read your data with the powerful secret key. The key just stays put in a local env file on your machine (the kind you keep out of git), which is exactly where a secret key is supposed to live.
You could ask something like:
“Build me a small dashboard I can run locally on my own machine that reads from my
eventstable and shows total events per day, my most common event_names, and a count of unique visitors. Give me a command to start it up.”
And there it is. The moment that dashboard loads and you see real events from real people, that’s the window. A front row seat to strangers on the internet using the thing you built.
A few things to watch
You’re basically done. A handful of gotchas worth keeping in mind:
- RLS is the whole ballgame for safety. If you remember one thing, it’s that tables start with security off and you turn it on. Safe key plus RLS on is fine. Safe key plus RLS off is an open database.
- Keys in the right places. Publishable key in your app, secret key only in code that never reaches your users (your backend, or a local-only dashboard), and neither one typed into an AI chat.
- The public insert can be abused. Because anyone can find your publishable key, in theory someone could send junk events to your table. For a hobby project this basically never happens and isn’t worth the worry. If your app gets big, that’s the moment to ask Claude about rate limiting or routing events through a backend.
That’s your window
That’s the whole thing. Events flowing from your app, stored safely in Supabase, and a dashboard where you get to watch real people use what you built. The setup takes an afternoon, most of it is just asking Claude for each piece, and once it’s running it quietly hands you your new favorite website: your metrics dashboard :D
If you want the bigger picture of where I’d start as a new vibe coder, I wrote that up in how I’d approach learning to build apps today.
And if there’s something you’d want me to go deeper on next, let me know!
keep on vibin’!