Skip to main content

Command Palette

Search for a command to run...

Don't build an admin panel yet. Save five SQL queries instead.

Updated
•6 min read•View as Markdown

Every SaaS I've started had the same week two. The product barely works, there are maybe a dozen users, and I'm building an admin dashboard. Tables, filters, a little search bar, a "view as user" button. It feels responsible. It's mostly procrastination with a login screen.

My opinion now: until you have real customers and someone other than you needs to look at the data, you don't need an admin panel. You need five saved SQL queries, a read-only database user, and the discipline to run them.

Why the admin panel is a trap this early

An admin panel is a second product. It has its own auth, its own bugs, its own UI that goes stale every time your schema changes. And at 12 users you don't even know which questions you'll be asking next month, so you end up building screens for the questions you imagine, not the ones you actually have.

The worse part is that it feels like progress. You can demo it. You can't demo a text file of queries. But the text file answers more questions, and it takes an afternoon instead of two weeks.

Step zero: a read-only user so you don't wreck prod

Before anything else, make a separate Postgres role that can only read. On Postgres 14 or newer this is short:

CREATE ROLE founder_readonly WITH LOGIN PASSWORD 'use-a-real-one';
GRANT pg_read_all_data TO founder_readonly;
ALTER ROLE founder_readonly SET default_transaction_read_only = on;
ALTER ROLE founder_readonly SET statement_timeout = '15s';

pg_read_all_data gives SELECT on everything. The read-only default and the timeout are there for the day you're tired and run something dumb. One honest catch: it doesn't bypass row level security, so if you use RLS (hello Supabase), check that your queries actually return rows and aren't silently empty.

Point your SQL client at this user and keep your write credentials somewhere you have to go looking for them. Most "I deleted a customer" stories start with a client that was connected as the owner role.

The five queries I'd save on day one

Your table names will be different. The questions shouldn't be.

1. Who signed up yesterday, and from where?

SELECT email, created_at, signup_source
FROM users
WHERE created_at >= current_date - 1
ORDER BY created_at DESC;

If you don't store a source, start now. Even a plain UTM param saved on signup beats guessing which post worked.

2. Who signed up but never did the one thing?

Pick the single action that means someone got value. Created a project, sent an invoice, connected a repo. Then:

SELECT u.email, u.created_at
FROM users u
LEFT JOIN projects p ON p.user_id = u.id
WHERE p.id IS NULL
  AND u.created_at < now() - interval '2 days'
ORDER BY u.created_at DESC;

This is the most useful query you'll own. Every name on that list is a person you can email personally and ask what got in the way. I wrote more about picking the few metrics that matter before 100 users if you're unsure which action to pick.

3. Who used to be active and went quiet?

SELECT u.email, max(e.created_at) AS last_seen
FROM users u
JOIN events e ON e.user_id = u.id
GROUP BY u.email
HAVING max(e.created_at) < now() - interval '14 days'
ORDER BY last_seen DESC;

No events table? Use whatever has a timestamp a user touches, like last_login or updated_at on their main object. Rough is fine.

4. Money: who's paying, and whose payment failed?

If billing lives in Stripe, don't rebuild it in SQL. Keep a plain query for your own subscriptions table (status, plan, period end) and look at failed payments in Stripe directly. The goal is to know every Monday who is past due, not to have a pretty MRR chart.

5. Everything about one customer, by email.

SELECT u.*,
  (SELECT count(*) FROM projects p WHERE p.user_id = u.id) AS projects,
  (SELECT max(created_at) FROM events e WHERE e.user_id = u.id) AS last_seen
FROM users u
WHERE u.email = 'someone@example.com';

This is the support query. When someone writes in angry, you paste their email and see their whole story in one row before you reply.

Where to keep them

Boring answer: a queries/ folder in your repo, one .sql file per question, with a comment on top saying what it answers. They get versioned with the schema, so when you rename a column the broken query shows up in the same PR.

For running them, use whatever you already open daily. TablePlus, DBeaver, psql, the Supabase SQL editor. If you really want charts, self-hosted Metabase on the read-only user is fine, but notice that's still not an admin panel you maintain.

Writes stay manual, and slow on purpose

Some days you will need to change data. Refund someone, extend a trial, fix a broken record. Do it in a transaction, with the owner credentials you had to go find, and look at the row count before you commit:

BEGIN;
UPDATE subscriptions SET trial_ends_at = trial_ends_at + interval '7 days'
WHERE user_id = 482;
-- read the "UPDATE 1" line. If it says anything else, ROLLBACK.
COMMIT;

Write down every manual write in a notes file with the date. That file is your future admin panel spec.

When you actually should build one

I start building internal screens when one of these is true:

  • A non-technical person (support hire, cofounder, VA) needs to look things up without SQL.
  • The same manual write shows up in my notes file three or more times a week.
  • I keep pasting customer data into chat to answer questions, which is a privacy problem, not a tooling one.

Until then, the queries are enough. When the quiet-user list from query 3 starts growing, that's usually a sign to look at who is leaving and why, not at your dashboard. I argued the same about reading churn when you have under 50 customers, where the percentage lies and the names tell you everything.

Build the product. Save the queries. Run them every Monday. The admin panel can wait for the problem it's supposed to solve.

M
Mohan12h ago

Mostly procrastination with a login screen" is painfully accurate. I'm a solo founder on Supabase and the pull to build a dashboard is real. The activation-gap query is the one I'd steal first. For a free product with no payments, it's really the main health metric: signed up but never finished the one thing they came for. Did you find the read-only role plays nicely with Supabase's SQL editor, or do you run these from psql/TablePlus instead?