goappfw ~/auth-lite-v2

auth-lite-v2 — running in production, port 7772

One binary.
Every tenant.

This site is auth-lite-v2, the reference host of goappfw v2 — identity, tenancy, files, audit, a typed data plane, and an MCP endpoint for agents, all compiled into one Go process and mounted under this domain. No sidecars. No frontend deploy. Sign up below and a fresh tenant appears inside it. Try it: create a workspace, write a post, then point the same token at another tenant and watch it bounce.

auth-lite-v2 one process · one domain transport/ core/ data/ platform/ tenant portal /tenant/ui admin UI /admin/ui MCP endpoint /mcp · for agents openapi /api/openapi.json 1 process · 0 sidecars · 1 domain
// the tenancy contract, in four requests — on a loop
POST/api/identity/signup200tenant + owner + JWT, one round trip
GET/api/posts  X-Tenant-ID: own200your rows, your workspace
GET/api/posts  X-Tenant-ID: other403RequireTenantMatch — isolation holds
POST/api/identity/signin200user.signed_in → audit stream
run it yourself — sign up, then try the cross-tenant probe in the dashboard
01

Surfaces

Four doors into the same process — three for people, one for agents. Every one of them is served by the binary above: nothing behind a CDN, nothing deployed separately.

02

The data plane

Present a Bearer JWT and an X-Tenant-ID; the framework verifies the token into a tenant-bound actor and rejects any mismatch. Writes and reads go through the same routes the portal uses.

auth-lite — as a tenant
# create a record as tenant <tid> (JWT-bound: the token's Attrs["tenant"] must match)
curl -X POST /api/employees -H "X-Tenant-ID: $tid" \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{"firstName":"Ada","email":"ada@x.test","role":"eng"}'

# list — same tenant binding
curl /api/employees -H "X-Tenant-ID: $tid" -H "Authorization: Bearer <token>"

Or — act as a tenant with the admin token (D-18): it is admitted on the data plane as a tenant-impersonating identity, audited as <admin> as <tenant>.

auth-lite — admin, impersonating
# the admin token assumes <tid> (no JWT needed; audited). No X-Tenant-ID → 400.
curl -X POST /api/employees -H "X-Tenant-ID: $tid" \
  -H "Authorization: Bearer $APPFW_ADMIN_TOKEN" -H "Content-Type: application/json" \
  -d '{"firstName":"Ada","email":"ada@x.test"}'
03

Under the hood

The guarantees this example exists to demonstrate.

3.1

Auth is attached — tenant-bound RBAC

The data plane verifies a presented Bearer into a tenant-bound rbac.Actor; a mismatched tenant is denied (403). The admin surface admits the admin token or an admin-role JWT.

3.2

Host-owned identity mints the tokens

The portal's signup/signin (go-auth store + bcrypt) mints a JWT the framework then verifies — real production posture, not a dev shortcut. Users are tenant-local: credentials live inside each workspace's own DB, so login names the workspace before it names the user.

3.3

Tenancy is enforced, not decorative

RequireTenantMatch binds every data-plane call to the token's tenant. The portal ships a live probe you can run with your own token to watch the 403 fire. The admin's records browser is the sanctioned cross-tenant surface — and its writes go through the normal data plane, audited.

3.4

Agents get the same guarantees

The MCP tools at /mcp are generated from the same registry the REST plane serves, so an agent can never reach a collection the API doesn't expose. The endpoint is admin-gated (an unauthenticated call is a 401) and every tool call lands in the audit stream like any other write — no side door.

04

Dev login surrogate

off in prod

The example's /api/dev/mint endpoint is disabled here (APPFW_DEV_MINT unset). Present a real token minted by your own login path over the same HMAC secret — or sign up for one.