Admin UI shell
Audience: operators who want to browse and edit the access-control model from a browser instead of the CLI or raw RPC.
Aperture ships a small embedded admin UI — a static, single-page frontend
served from the root of a running aperture serve. It is not a separate service:
the HTML, CSS, and JavaScript are compiled into the binary and mounted at /, so
starting the server is all it takes to reach the UI. Every screen is a thin client
over the same Twirp / HTTP surface; the shell holds no
access-control logic of its own — it decodes what the server returns and posts
mutations back through the facade.
What it is built from
The shell is deliberately dependency-light and builds with no Node pipeline. Its
assets are pre-built, committed blobs embedded via //go:embed all:static (see
internal/server/static.go), so the binary stays self-contained in dev and CI.
| Layer | Role in the shell |
|---|---|
| Alpine.js | Small reactive framework driving each screen’s component and the hash-routed navigation. |
| BERA design tokens | The house design-token system. The shell’s authoritative styling is the hand-authored static/css/bera.css, written from the token spec; it is not Tailwind/DaisyUI. |
| Tailwind CSS / DaisyUI | Vendored utility/component CSS carried for the domain screens. |
| Rete.js | A node-graph editing library. The Rules screen mounts a Rete canvas to edit rule blueprints; the bundle is loaded at runtime from /vendor/rete/rete.min.js. |
The full frontend lives under internal/server/static/ (index.html, css/,
js/, and vendor/). Vendored blobs (Alpine, Tailwind, DaisyUI) are pinned and
documented in internal/server/static/vendor/README.md. The Rete.js bundle is
the one asset regenerated by a manual make vendor-rete target — it is never
rebuilt in CI. Treat all of these as committed artifacts, not build outputs.
How to reach it
Start the server and open the listen address in a browser:
aperture serve # listens on :8080 by default
- Default address:
:8080(override with--addr). See theservecommand for all flags. - The UI is mounted last on the mux at
/. More specific routes — the Twirp path prefix,POST /check,GET /healthz— take precedence, so the frontend never shadows the API.
Auth expectation
Aperture’s authentication is external — the shell never issues credentials.
Every request the UI makes is routed through a single apiFetch wrapper that
attaches an Authorization: Bearer <token> header, and a 401 clears the token
and re-opens the sign-in affordance.
With the default dev authenticator (aperture serve with no --auth
override), the bearer token is the principal id: “signing in” through the UI
just names which principal your session presents. Switching to the oidc or
parsec adapters changes how that bearer is obtained, but the shell’s behaviour
is unchanged — it still only carries whatever token it holds. A global account
switcher in the top bar picks the active account shared by every screen; the
server scopes the account list to what the signed-in principal may see
(ListAccounts).
Screen tour
The left-hand nav routes by URL hash (#/crud, #/grants, …). Each screen is
one Alpine component in static/js/, and each drives a small set of Twirp RPCs
on aperture.ApertureService.
Model (CRUD) — #/crud
The model editor. Browse and mutate the core entities — principals, roles,
groups, permissions, object types, and accounts — plus account memberships. It
lists each entity type (ListPrincipals, ListRoles, ListGroups,
ListPermissions, ListObjectTypes, ListAccounts), creates/updates via the
matching Put*, and removes via Delete*; memberships use
PutMembership / DeleteMembership. It probes the viewer’s admin tier with the
open Check RPC and can Export the model.
Grants — #/grants
Bestowed grants, delegation, and grant templates. Lists and edits grants
(ListGrants, PutGrant, DeleteGrant), issues and rescinds authority
(Bestow, Revoke), and manages templates (ListTemplates, PutTemplate,
DeleteTemplate, ApplyTemplate) with a bulk path (BulkPutGrants). It uses
Check to resolve the viewer’s tier.
Graph — #/graph
A force-directed map of the whole model rendered on a <canvas>. It pulls the
entire declarative model with a single Export RPC (seeding an account via
ListAccounts first) and derives every node and edge — subjects, grants,
objects, memberships, roles, and reachability — from that one document, so the
picture always reflects the live store. The layout is a small self-contained
simulation; no external graph library is used here.
Rules — #/rules
The rule-blueprint editor. This is where Rete.js is used: rules are edited as
a node graph on a Rete canvas, then serialized to Aperture’s rule AST. It loads
and saves rules (ListRules, GetRule, PutRule), lists object types
(ListObjectTypes) and object identifiers (ObjectIdentifiers), and validates
or dry-runs an expression against the live model (ValidateRule,
EvaluateRule, Check).
What-if — #/whatif
A read-only decision simulator. Ask “would principal P be allowed action A on
object O?” and see both the verdict and the full explain trace — the expanded
subject set, every grant considered with its per-grant outcome, and the deciding
grant(s). It drives the open Check and Explain RPCs (no mutation), listing
accounts via ListAccounts. It runs a hypothetical against the live model
without writing anything.
Audit — #/audit
A queryable table over the append-only audit trail via the QueryAudit RPC.
Every mutation, impersonation, and delegation is always recorded; decisions are
sampled. Read access is tier-gated — a system-admin reads everything, an
account-admin reads only events scoped to their own account — and the screen
probes both tiers with Check (using ListAccounts for scope). A viewer with
neither tier is told why the table is empty.
Import / export (portability) — #/portability
Download the declarative model file or import one with a preview diff. It exports
the whole model (Export), and imports a supplied document (Import) after
resolving the viewer’s tier with Check and scoping with ListAccounts.
Related
- RPC / HTTP overview and the RPC reference — the surface every screen calls.
servecommand — flags, listen address, and auth adapter.- The service facade — the one code path behind every RPC the UI drives.