Problem
The dynamic admin shell at /admin/* is wonderful for the long tail of schemas, but the four schemas that govern tenancy and identity — Organization, User, TenantMembership, Role — are the universal control surface for every SchemaForge product, and they're the most painful to operate through the generic CRUD form.
Today, to put a new user into an org as an admin, an operator has to:
- Open
/admin/User → click "Create" → fill in email, display_name, role_rank, roles[] (manual array), password_hash (?!), organization (paste a ULID).
- Open
/admin/TenantMembership → click "Create" → paste the User ULID into user, type \"Organization\" into tenant_type, paste the Organization ULID into tenant_id, paste a Role ULID into role.
Two ULID copy-pastes per user just to onboard them. There is no concept of "invite by email" — the operator has to mint a password_hash manually or coordinate out-of-band.
For a SchemaForge product that's resold (the Engage case: federal contracting CRM sold to multiple firms), this is the first thing a customer admin tries to do and the first thing that makes the platform feel hand-built.
Requested
Promote the four tenancy schemas from "dynamic CRUD" to first-class admin flows in /admin/*. None of this needs new schemas — it's UX on top of what's already there.
1. /admin/users — invite + manage
- Invite by email: form takes
email, display_name, an org picker (filtered to the orgs the operator can write to), a role picker. Backend mints a one-time invitation token, emails it (or returns it for copy-paste in dev), creates the User + TenantMembership row in one transaction.
- Reset password: per-row action that forces a password change on next login.
- Role assignment: real role picker, not a comma-separated text input. Show role badges with their
role_rank.
- Hide
password_hash from the create/edit form entirely (it's already @hidden in the schema — the admin shell should respect that even on the generic form).
2. /admin/orgs (or /admin/organizations) — tenant root management
- Create Org: today already works but should auto-bootstrap a TenantMembership for the operator (or a configurable initial admin user) so the org isn't immediately stranded.
- Members tab: per-org view showing TenantMemberships + Role with inline add/remove. No raw ULIDs visible.
- Empty-state: when there are zero non-platform-admin users in an org, show "Invite your first user" instead of an empty table.
3. /admin/memberships — TenantMembership as a relationship, not a row
- Picker-driven create: user combobox (search by email), org combobox (search by name), role combobox. Tenant_type defaults to
Organization and is hidden unless multiple tenant roots exist in the schema.
- Bulk add: "add users X, Y, Z to org Q with role R."
- Per-user view: when drilling into a User, show their TenantMemberships as a list with org name + role, not a relation field of ULIDs.
4. /admin/roles — role catalogue with rank visualization
- Show roles ordered by
role_rank, with a visual hierarchy.
- Block deletion of a role that's referenced by any TenantMembership.
- Surface which schemas'
@access annotations reference each role.
5. Cross-cutting: tenancy chrome
Why This Belongs in SchemaForge, Not Each Product
Every SchemaForge product needs this same UX. Today each product either:
- ships the generic CRUD form (rough — see above), or
- reimplements user/org provisioning in its own curated pages (Engage was about to do this — and would have duplicated logic that the platform should own).
Centralizing it means every customer of every SchemaForge product gets the same polished onboarding experience, and the schemas stay the single source of truth.
Relationship to #70
#70 (auth: /auth/me + /auth/switch-tenant) is the data plane for the active-tenant chrome in #5 above. This issue is the UI surface; #70 is the API contract that lights it up.
Use Case
Engage (Govcraft/engage) is being prepared for resale to federal contractors. The first thing a customer admin does after their org is provisioned is invite their BD team. Today that's 2 form pages, 3 ULID copy-pastes per user, and a manually-minted password_hash. That's a deal-breaker for a resold product.
Problem
The dynamic admin shell at
/admin/*is wonderful for the long tail of schemas, but the four schemas that govern tenancy and identity —Organization,User,TenantMembership,Role— are the universal control surface for every SchemaForge product, and they're the most painful to operate through the generic CRUD form.Today, to put a new user into an org as an admin, an operator has to:
/admin/User→ click "Create" → fill inemail,display_name,role_rank,roles[](manual array),password_hash(?!),organization(paste a ULID)./admin/TenantMembership→ click "Create" → paste the User ULID intouser, type\"Organization\"intotenant_type, paste the Organization ULID intotenant_id, paste a Role ULID intorole.Two ULID copy-pastes per user just to onboard them. There is no concept of "invite by email" — the operator has to mint a
password_hashmanually or coordinate out-of-band.For a SchemaForge product that's resold (the Engage case: federal contracting CRM sold to multiple firms), this is the first thing a customer admin tries to do and the first thing that makes the platform feel hand-built.
Requested
Promote the four tenancy schemas from "dynamic CRUD" to first-class admin flows in
/admin/*. None of this needs new schemas — it's UX on top of what's already there.1.
/admin/users— invite + manageemail,display_name, an org picker (filtered to the orgs the operator can write to), a role picker. Backend mints a one-time invitation token, emails it (or returns it for copy-paste in dev), creates the User + TenantMembership row in one transaction.role_rank.password_hashfrom the create/edit form entirely (it's already@hiddenin the schema — the admin shell should respect that even on the generic form).2.
/admin/orgs(or/admin/organizations) — tenant root management3.
/admin/memberships— TenantMembership as a relationship, not a rowOrganizationand is hidden unless multiple tenant roots exist in the schema.4.
/admin/roles— role catalogue with rank visualizationrole_rank, with a visual hierarchy.@accessannotations reference each role.5. Cross-cutting: tenancy chrome
active_tenantandtenant_chain(depends on auth: expose /auth/me principal endpoint (user_id, tenant_chain, active_tenant) and /auth/switch-tenant #70 —/auth/me+/auth/switch-tenant).Why This Belongs in SchemaForge, Not Each Product
Every SchemaForge product needs this same UX. Today each product either:
Centralizing it means every customer of every SchemaForge product gets the same polished onboarding experience, and the schemas stay the single source of truth.
Relationship to #70
#70 (auth:
/auth/me+/auth/switch-tenant) is the data plane for the active-tenant chrome in #5 above. This issue is the UI surface; #70 is the API contract that lights it up.Use Case
Engage (Govcraft/engage) is being prepared for resale to federal contractors. The first thing a customer admin does after their org is provisioned is invite their BD team. Today that's 2 form pages, 3 ULID copy-pastes per user, and a manually-minted
password_hash. That's a deal-breaker for a resold product.