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 entitiesSign 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.tsxMake 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-rbacsupplies real scopes for the gating.
Anti-patterns
- Shipping the demo
admin:fullscopes to production. That bypasses everyuseCangate. Feed the subject's real scopes to<PermissionProvider>. - Treating
useCanas authorization. It hides buttons; the server'spermission()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.