Operations surface

Admin Dashboard

One operator, one interface, the whole shop

Next.js 16React 19Supabase SSRRealtimeRecharts

The admin is not a separate app. It is a set of routes inside the same Next.js build as the storefront, gated by an admin_users RLS check and a small server-side helper.

Technical narrative

This is where the archive goes deeper than the homepage: architecture choices, operating constraints, and the decisions that make the product maintainable over time.

Route map

The admin is organized by what the owner actually does each week, not by database table.

Mobile read

One operations spine

Catalog health, order queues, and action items live in one admin path.

Routine work is grouped

Teams can move between product edits, fulfillment checks, and reporting without context loss.

Permission boundaries are visible

Role-sensitive areas stay distinct so the interface can grow without becoming a junk drawer.

/adminoverview: catalog health, order queue, action items
├─ /catalogproduct & inventory management
│ ├─ /productsgrid + edit/delete
│ ├─ /products/[id]metadata, images, tags, pricing
│ ├─ /stockinventory grid
│ └─ /healthdata quality checks
├─ /merchandisingstorefront curation
│ ├─ /promotionsfeatured collections, events
│ ├─ /fresh-shelfnew arrivals
│ └─ /homepagehero carousel, tiles
├─ /ordersreceived → ready → fulfilled (Realtime)
├─ /inboxcontact form + notification routing
├─ /inventoryCSV import with dry-run
└─ /automationtaxonomy migrations
Scroll the detail view if you want the full map. Every route matches a real operating question: what sold, what's out, what arrived, who wrote in.

Access model

Every admin page calls requireAdminAccess on the server before rendering. It verifies the Supabase session, checks membership in admin_users, and redirects to /login on 401 or / on 403. The same admin_users row is what RLS policies reference to permit write access. There is no duplicate allowlist.

ts
// bt-liquor/src/lib/admin-access.ts
export async function requireAdminAccess(
  supabase: SupabaseClient,
): Promise<AdminUser> {
  const { data: { user } } = await supabase.auth.getUser();
  if (!user) redirect("/login");

  const { data: admin } = await supabase
    .from("admin_users")
    .select("user_id")
    .eq("user_id", user.id)
    .maybeSingle();

  if (!admin) redirect("/");
  return { userId: user.id };
}
One function, called at the top of every admin server component and server action.

bt-liquor/src/lib/admin-access.ts

Realtime order queue

Pickup orders move through three states: received, ready, fulfilled. The owner needs to see new orders land without refreshing. A Supabase Realtime subscription on the orders table drives the queue; tab counts update live.

ts
// orders-shell.tsx (client)
useEffect(() => {
  const channel = supabase
    .channel("admin-orders")
    .on(
      "postgres_changes",
      { event: "*", schema: "public", table: "orders" },
      () => refreshQueue(),
    )
    .subscribe();
  return () => { supabase.removeChannel(channel); };
}, []);
No polling, no websocket plumbing to maintain; Supabase handles the transport.

CSV inventory import

Stock updates come from a distributor spreadsheet, not an API. The import route accepts a CSV, parses it with a small custom parser (quoted fields, BOM, CRLF), maps each row's external SKU to an internal product_variants.id, and reports matched/unmatched/price-change counts as a dry run.

bt-liquor/src/lib/csv-import.tsparser + SKU reconciliation + diff reporting

Catalog health

An /admin/catalog/health route surfaces structural problems: products live on the storefront with no image, featured products with zero stock, variants with inactive products, subtype values that fell outside the current CHECK constraint. It is the honest dashboard that keeps the pretty dashboard honest.

Design system reuse

Admin shares the same Tailwind tokens, typography, and motion presets as the storefront. Density is tuned up for operational speed with tighter row heights, stickier headers, and keyboard-friendly forms, but there is no separate component library. A fix to the product card ripples through both surfaces at once.