Referenta

Admin App

Local-only Next.js app for managing things in hosted dev and prod Supabase that the dashboard intentionally doesn't expose — with a two-tier dev→prod promotion flow and an audit trail.

apps/admin is a small Next.js app that manages configuration data that lives in Supabase but isn't safe to expose in the customer-facing dashboard — currently the Assistant model whitelist and user management (/users: search, create, edit, delete, and trial-reset users across hosted dev/prod), and any future cross-environment toggles. It is deliberately local-only: there is no deploy target, and the layout throws at runtime if NODE_ENV === "production" outside of a next build phase. The whole point is to keep the surface narrow and not be reachable from the internet.

It runs on port 3069 and is started with:

pnpm --filter admin dev

Why it's a separate app

The dashboard is wired to a single Supabase project per environment: locally it points at 127.0.0.1:54321, in CI / preview / staging / production it points at the matching hosted project. Admin tooling needs the opposite shape — it must read and write to hosted dev and hosted prod from your laptop, side by side, so an operator can stage a change in dev, eyeball it, and then promote the same change to prod with a visible diff. Putting that capability inside apps/dashboard would either pollute its env namespace or force conditional UI that's easy to leak. Splitting the surface fixes both problems.

apps/admin must never be deployed. The layout has a guard that throws unless NODE_ENV !== "production" (or we're inside a next build). Don't add it to Vercel projects, don't add it to pnpm dev, don't ship it.

Two-tier state model

The Assistant model whitelist keeps three layers of state:

  • Client local — what the operator is editing in the browser.
  • Hosted dev — the staged truth, written by "Save".
  • Hosted prod — the live truth, only updated by an explicit "Apply to prod" action.

The promotion flow is deliberately friction-y: client edits land in dev first, then a modal shows a precise per-row diff against prod, the operator confirms, and only then does the write hit prod. That gives you a chance to spot mistakes before they go live, and produces a meaningful audit row (see Audit log).

/users doesn't use this staged pattern — it switches between hosted dev and hosted prod directly and writes CRUD operations straight to whichever one is selected, with no client-local diff or promotion step. Mutations against prod still land in the same audit log.

Environment setup

Admin reads two parallel sets of Supabase credentials — one for hosted dev, one for hosted prod — plus the operator's email for audit attribution. The project URLs go through Referenta's custom domains rather than the raw *.supabase.co hosts, so they're not secret and live in apps/admin/.env; the service-role keys and operator email are secrets and live in apps/admin/.env.local (gitignored):

# apps/admin/.env
SUPABASE_DEV_URL="https://sb-dev.referenta.de"
SUPABASE_PROD_URL="https://sb.referenta.de"

# apps/admin/.env.local
SUPABASE_DEV_SERVICE_ROLE_KEY=...
SUPABASE_PROD_SERVICE_ROLE_KEY=...

# Operator email (auto-populated from git config when present)
ADMIN_OPERATOR_EMAIL=you@referenta.de

pnpm --filter admin dev runs tsx scripts/bootstrap-env.ts first, which reads your git user email and writes ADMIN_OPERATOR_EMAIL into .env.local if it's missing. Service-role keys are not auto-fetched — pull them from the Supabase dashboard for each project once and keep them out of files that get synced anywhere.

The service-role keys grant unrestricted access to each Supabase project. Keep apps/admin/.env.local on your machine only; it is gitignored, but don't paste those keys into Slack, shared notes, or chat with cloud assistants.

Dev → prod promotion flow

The day-to-day operator flow for the model whitelist:

Edit in the browser

Open the admin page (e.g. /assistant/models), make changes. Changes are kept as a client-local state diff against hosted dev — nothing is written until you press Save.

Save to hosted dev

Pressing Save writes the diff to the hosted dev Supabase project and shows a toast with the +N adds / -N removes counts. State now reads from dev as the source of truth; further edits compare against this.

Review the prod diff

The "Apply to prod" button glows red / yellow / green based on how far hosted prod has drifted from hosted dev. Clicking it opens a modal with a per-row checkbox diff against prod, plus a pre-flight schema probe that flags any table-missing / column-missing issues before the write.

Confirm and apply

Tick the rows to promote (default: all), confirm. The selected ops are written to the hosted prod Supabase project. Failed rows surface inline with a retry-failed handler. Success / partial / failed status is recorded to the audit log.

Audit log

Every "apply to prod" action writes a row into admin_apply_log on the prod project. The table is declared in supabase/schemas/90_admin.sql:

CREATE TABLE "public"."admin_apply_log" (
    "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    "operator_email" text NOT NULL,
    "section" text NOT NULL,
    "resource_table" text NOT NULL,
    "op_count" integer NOT NULL,
    "op_summary" text NOT NULL,
    "details" jsonb NOT NULL,
    "status" text NOT NULL CHECK (status IN ('success', 'partial', 'failed')),
    "error_details" jsonb,
    "applied_at" timestamptz NOT NULL DEFAULT now()
);

Inspect it with any prod-credentialed Supabase client, or with supabase db remote ... against the prod project ref. The audit write is best-effort — if it fails, the apply still succeeds and a warning logs to the admin console — because losing the actual prod change to a flaky audit insert would be the wrong tradeoff.

Build and lint hygiene

  • The runtime guard skips during a next build phase (NEXT_PHASE === "phase-production-build") so type-checking and build-time page collection still pass. Don't relax this guard further.
  • There is no nested apps/admin/biome.json — root config wins. Don't reintroduce one; it conflicts with lefthook + biome at the workspace level.
  • React, Next, and Sonner versions in apps/admin/package.json must match packages/ui. A mismatched React or Sonner copy will produce a runtime singleton split (toasts silently fail to render).

On this page