Skip to content

Self-Service Catalog

The self-service catalog is one place where people request things — a new laptop, “the CRM Sales role,” access to the Finance app — and where each request flows through the right approval and fulfillment. A single catalog spans every application, while each requester only ever sees the items they are allowed to see.

Every requestable item is the same kind of object regardless of what it delivers. “Request a service” and “request access” look and flow identically to the requester; the difference is only in how the item is fulfilled behind the scenes.

Each catalog item declares one fulfillment_type:

TypeWhat happens when requested
intake_formOpens a public/intake form — the request is a form submission
taskCreates a record from a template (e.g. a service_request), optionally firing a business rule
access_grantRequests a role/application/table grant, routed through approval, then granted on approval

Items are stored in system.catalog_items, carry a stable slug, belong to an owning application, and reuse the platform’s global form categories for grouping.

The catalog spans applications and lives at a clean, non-app-scoped root:

GET /api/catalog # cross-app discovery, grouped by category
POST /api/catalog/{slug}/request # request an item (dispatches on fulfillment_type)
GET /api/catalog/requests # the caller's own submitted requests, newest first

GET /api/catalog returns only the items the caller may see, already grouped by category — there is no client-side filtering to bypass.

Which items a person sees is decided by three tiers, layered on the platform’s existing authorization primitives rather than a separate rule language:

  1. public — shown to any authenticated caller.
  2. internal — shown only to callers who hold application-level read on the item’s own application.
  3. restricted — shown only to callers who hold a specific grant for that item.

Because visibility is computed from precomputed access — not an author-editable predicate — an item you cannot see is structurally unrequestable: the request endpoint re-runs the identical gate. And because fulfillment always runs through the platform’s normal authorized write path, the catalog can never widen access beyond what the underlying permissions already allow. For finer conditions (e.g. “US-region employees only”), model membership as a dynamic group grant rather than a bespoke rule.

An access_grant item ties the catalog to the platform’s approval engine. A typical item declares both the grant it confers and the approval it requires:

{
"fulfillment_type": "access_grant",
"fulfillment_config": {
"grant": {
"target_type": "role",
"application": "crm",
"role_name": "sales",
"max_duration": "P90D"
},
"approval": {
"approver_type": "role",
"approver_role": "crm-manager",
"mode": "any",
"due_in": "P2D"
}
}
}

The flow:

  1. A principal requests the item. An access-request record is created under the requester’s identity.
  2. The approval engine routes the request to the named approvers, with escalation, reminders, and one-click approve/reject links.
  3. On terminal approval, the platform writes the grant — assigning the role (or minting a request-scoped role for application/table grants) to the requester.
  4. If max_duration is set, the grant is stamped with an expiry and automatically revoked when it lapses; otherwise it stands until removed.
  5. Every grant and revocation is recorded in the audit log.

target_type may be role, application, or table. Two safeguards apply: an item can only be published for access its author has authority over, and an approver can only approve access they themselves hold — the superuser/operator role is never grantable through the catalog. Revocation also refuses to remove the last holder of an admin role.

Agents and service accounts are first-class principals, so an agent can both request and receive access using the same mechanics as a person. When an agent acts on behalf of a human, the request records the agent as the actor and the human as the sponsor.

GET /api/catalog/requests returns a caller’s own submitted requests in one uniform shape — access-grant requests unioned with intake-form submissions they created, newest first. Each query is scoped to the caller’s own principal, so no one sees anyone else’s requests.