Admin

An auto-admin back-office that discovers your api's entities at runtime (capability map) and renders a live, reactive, permission-gated CRUD view per entity — DataTable + AutoForm + provenance + undo, assembled from shipped primitives as editable template code.

The back-office, done for you — without a sealed generated cage. The admin reads your api's capability map (/_voltro/inspect/manifest) at runtime and renders a live, reactive view for every entity it exposes: a <DataTable> over the list query and an <AutoForm> over the create mutation, columns and fields straight from the descriptors' schemas. Point it at any Voltro api and the sidebar + pages adapt — there's no per-entity code. And it's editable template code you own, not a runtime feature (the lesson of the schema-driven-UI graveyard). Template id: frontend-admin.

Scaffold

voltro create-project acme --api=api-backend --web=frontend-admin
voltro dev   # the admin discovers api-backend's entities

Sign in at /login (a demo cookie gate) → /admin.

How discovery works

The sidebar and every entity page are driven by two @voltro/client exports:

import { useCapabilityManifest, deriveEntityAdmins } from '@voltro/client'

const { manifest } = useCapabilityManifest('app')   // one-shot fetch of the capability map
const entities = manifest ? deriveEntityAdmins(manifest) : []
// each entity: { table, columns, listTag?, createTag?, updateTag?, deleteTag?, createScope, writeScope, deleteScope }

deriveEntityAdmins joins each user table to the procedures that actually serve it — the query whose source is the table (→ the list <DataTable>), the mutations whose target is {table, op} (→ create/edit/delete). So the admin binds only to procedures that exist; it never guesses tags by naming convention.

Per-entity CRUD

// src/pages/admin/[entity].tsx (abridged)
const canCreate = useCan(spec.createScope)         // <table>:create
{spec.createTag && canCreate ? (
  <AutoForm api="app" mutation={spec.createTag} submitLabel={`Add ${spec.table}`} />
) : null}

{spec.listTag ? (
  <DataTable api="app" query={spec.listTag} rowActions={rowActions} />
) : null}

The list is a live subscription — a create from the form (or anyone, in another tab) appears without a refetch. Each row's actions include a delete (gated by useCan(spec.deleteScope), run via useMutation(spec.deleteTag)) and a provenance drawer (useProvenance) answering "why is this row here?".

Permission gating

Write affordances are hidden via useCan on conventional per-entity scopes (<table>:create, <table>:delete). <PermissionProvider> in the admin layout feeds the current subject's scopes; the demo seeds admin:full (bypass) — swap it for your session's real scopes:

const { data } = useSubscription<{ scopes: string[] }>('app', 'auth.session')
<PermissionProvider scopes={data?.scopes ?? []}>…</PermissionProvider>

This is UX gating, not enforcement — the api's own permission() guards remain the real authorization boundary. Row-level field visibility (vs. action-level) is a future step.

Undo / redo

The topbar's undo bar wraps the framework's built-in undo (useUndoLog('app') → the __voltro.undo.* built-ins): revert the last change across the whole admin, server-persisted so it survives reload. It requires undo capture on (VOLTRO_UNDO, on by default in dev); if you run with it off, delete the UndoBar lines.

What ships

apps/acme/web/
├── app.config.ts            # type:web, apis:{ app } — the api the admin introspects
└── src/
    ├── config.ts            # APP_NAME
    ├── lib/{auth,admin}.ts  # demo cookie gate + demo scopes (both swap for real)
    └── pages/
        ├── (marketing)/     # silent group — public landing + /login
        └── admin/           # /admin SSR gate (layout loader → RedirectError)
            ├── layout.tsx    # auth gate + capability-map nav + PermissionProvider + undo bar
            ├── index.tsx     # entity overview cards
            ├── [entity].tsx  # the per-entity CRUD binding
            └── error|loading|not-found.tsx

Make it yours

Everything under src/pages/admin/ is yours. Swap the cookie gate (lib/auth.ts) + demo scopes (lib/admin.ts) for real auth; replace the generic [entity].tsx with a hand-built page for any entity that needs more than CRUD.

When to use frontend-admin vs. the other web templates

You want… Pick
A back-office over your api's entities, done for you frontend-admin
An authenticated app shell (public + gated dashboard), no backend frontend-dashboard
The reactive end-to-end loop (one entity, hand-wired) frontend-app

Pairs well with

  • Any api template — the admin adapts to whatever entities + procedures it exposes. api-data-advanced (authors + books) shows it rendering related entities; api-rbac supplies real scopes for the gating.

Anti-patterns

  • Shipping the demo admin:full scopes to production. That bypasses every useCan gate. Feed the subject's real scopes to <PermissionProvider>.
  • Treating useCan as authorization. It hides buttons; the server's permission() guard is the real gate. A hidden action is still callable over rpc by a crafted client.
  • Assuming a naming convention. The admin binds to discovered tags, not <table>.create-style guesses — so it works even when your procedures are named differently.