Ship/Now

What is wired · 2 min

Admin

A read-only view of what is happening, gated on a list of emails rather than a column anyone could write.

/admin shows orders, users and revenue. It exists so you can answer "did that payment go through" without opening the Stripe dashboard, and it deliberately stops there.

Who gets in

ADMIN_EMAILS=you@example.com,cofounder@example.com

An environment variable, not an is_admin column. The reasoning is short: a column on a table a user can update is a column a user can set. Even where the policy currently forbids it, that is one careless update policy away from being a privilege escalation, and it would look like a normal line of code in review.

Changing who is an admin means a deploy. That is the right amount of friction for the list of people who can see everyone's data.

const user = await requireUser();
const admins = (process.env.ADMIN_EMAILS ?? "").split(",").map((s) => s.trim());
if (!admins.includes(user.email)) notFound();

notFound() rather than a redirect, so the page does not confirm it exists to somebody probing.

Why it is read-only

An admin panel that can edit rows needs an audit trail, a confirmation step and a way to undo, or it is a foot-gun with a nice font. Refunds belong in Stripe, where they already trigger your webhook. Data fixes belong in the Supabase SQL editor, where they are logged.

If you do add a write action, put it behind a server action that re-checks the admin list. Do not trust the page having rendered.

Extending it

The page is a single Server Component doing a handful of queries with supabaseAdmin(). Add a query, add a table, keep it boring. It is the one screen where a chart is worth less than a list of the last twenty rows.

Something wrong or missing on this page? Tell us.