Access Policies: AWS-IAM-Style Permissions for Members and API Keys
Roles and API key scopes were too coarse for real teams. Access Policies add AWS-IAM-style Allow and Deny statements over resourceType:verb actions, attachable to any member or API key, evaluated on every org-scoped API request alongside the existing role and scope checks.
Four Roles and Three Scopes Only Go So Far
Until now, access in Hookbase came in two coarse shapes. Members got one of four roles (owner, admin, member, viewer), and API keys got some combination of three scopes (read, write, delete). That covers a small team. It stops working when the requests get specific:
- "This contractor can edit transforms, but must never delete a source."
- "Our CI key needs to create tunnels and nothing else."
- "Nobody except the owner should be able to reveal signing secrets."
None of those can be expressed as a role or a scope. Getting close meant either giving someone more access than they needed or building a workaround outside Hookbase.
Access Policies fix that. They're modelled on AWS IAM: a policy is a list of Allow and Deny statements over named actions, and you attach it to a member or an API key.
What's New
- Policy documents with Allow and Deny statements over
resourceType:verbactions, with*wildcards on either side (sources:*,*:read,*:*) - A built-in policy for every role and every key. Each role has a built-in policy that reproduces exactly what that role could already do, and each API key gets one derived from its scopes, so nothing changed for existing users on day one
- Attach custom policies to members or API keys, from the new Access tab in Settings or directly when you create an API key
- A searchable action picker in the policy editor, with a description for every action
- Evaluated on every org-scoped API route that has been mapped to an action
- Guard rails against privilege escalation: you can't grant what you don't hold, built-in policies can't be attached, and lifting a restriction on an admin or owner takes an owner
- Audit logged: create, update, delete, attach and detach all write
iam_policy.*entries to the audit log
Actions
Every action is a resourceType:verb pair, and the vocabulary is derived from the API's own route map, so each action corresponds to real endpoints. Resource types cover the whole product: sources, destinations, routes, transforms, filters, schemas, events, deliveries, deliveryClusters, cronJobs, apiPollers, tunnels, webhookApplications, webhookEndpoints, webhookSubscriptions, outboundMessages, apiKeys, alertRules, redactionPolicies, portalBranding, ssoConfig, billing and more.
Verbs go well beyond CRUD, which is the point. A few examples:
sources:revealSecretandsources:rotateSecretare separate fromsources:updatedeliveries:replayis separate fromdeliveries:readdestinations:testandwebhookEndpoints:testcover the "send a real test request" buttonscronJobs:triggerandapiPollers:triggercover "run it now"transforms:generatecovers AI transform generation
The full catalog, with a description for every action, is served at GET /api/organizations/:orgId/iam-policies/actions-catalog. The policy editor's picker uses the same endpoint.
A Policy Document
{
"version": "2026-01-01",
"statements": [
{
"sid": "TransformAuthoring",
"effect": "Allow",
"actions": ["transforms:*", "sources:read", "events:read"]
},
{
"sid": "NoSecretsNoDeletes",
"effect": "Deny",
"actions": ["sources:revealSecret", "sources:delete", "destinations:delete"]
}
]
}
Evaluation follows the same rules as AWS:
- An explicit Deny in any policy that applies to the principal wins. Always.
- Otherwise, any matching Allow grants the action.
- Otherwise the action is denied.
The schema is strict on purpose. A statement with a stray Resource, Principal or Condition key, copied from an AWS policy, is rejected instead of silently ignored. Policies apply at the resource-type level, not to individual resource IDs, and if an unknown key were dropped quietly, you could believe you had scoped a grant to one source when it actually covered the whole org. Action strings must be registered pairs, so a typo like source:read fails validation.
How Policies Combine with Roles and Scopes
Every principal starts from a built-in policy. A member's comes from their role (Owner (built-in), Admin (built-in) and so on). An API key's is generated from its scopes, so a key with read and write gets exactly the actions those scopes allowed before. Custom policies you attach are added to that set, and the evaluation rules above run over all of them.
The policy check is additive to the existing checks. The role and scope gates still run on each route, and a request has to pass both. In practice:
- Deny statements always take effect. Attaching a Deny is the dependable way to narrow a member or key below their role or scopes.
- For members, an Allow can't lift someone above their role. A viewer with an Allow for
sources:createis still blocked by the route's role check. - For API keys, an Allow can add actions on routes that don't check a scope. Routes that still check one, mostly deletes, also need the key to hold that scope.
- Some actions are human-only.
apiKeys:create, everybilling:*action and everyssoConfig:*action are behind routes that reject API keys outright, so no policy can give a key access to them.
Guard Rails
A permissions system is only as safe as the controls on who can change it. Creating, editing, attaching and detaching policies requires a signed-in admin or owner. API keys can't call these endpoints, whatever their scopes or policies. On top of that:
- Permission boundary. You can't create, edit or attach a policy that Allows anything you don't hold yourself. The API expands wildcards to concrete actions, checks each one against your own effective permissions, and returns a 403 with code
BEYOND_ACTOR_PERMISSIONSlisting the offending actions. - Built-in policies can't be attached (
MANAGED_POLICY_ATTACH), so attaching the owner policy can't become a backdoor role change. They can't be edited or deleted either. - Only an owner can remove a restriction from an admin or owner. Detaching or editing a policy attached to yourself, a peer admin or the owner returns a 403 unless you're the owner, because removing a Deny widens access.
- A policy that's still attached can't be deleted. Detach it first (
POLICY_HAS_ATTACHMENTS).
One limit to know about: a Deny attached to a member is not inherited by API keys that member creates. If you're restricting someone, Deny apiKeys:create as well.
Plans
Built-in policies apply to everyone. On every plan, the Access tab shows exactly which actions each role and scope grants. Creating, editing and attaching custom policies requires Pro or Business.
Deleting and detaching are not plan-gated, on purpose. A custom policy that's already attached keeps applying after a downgrade, so you can always revoke it.
Try It
Open Settings → Access and click New policy. Give it a name, add statements, and pick actions from the searchable list. Then open the policy's Attachments to attach it to a member or an API key. If you'd rather write the JSON yourself, the editor has a JSON mode too. When you create an API key, you can also attach custom policies right in the create dialog, alongside its scopes.
Everything in the UI is also available through the API, under /api/organizations/:orgId/iam-policies:
GET /iam-policies list built-in and custom policies
POST /iam-policies create { name, description?, document }
PATCH /iam-policies/:policyId replace { name, description?, document }
DELETE /iam-policies/:policyId
GET /iam-policies/:policyId/attachments
POST /iam-policies/:policyId/attachments { principalType: "member" | "api_key", principalId }
DELETE /iam-policies/:policyId/attachments/:attachmentId
GET /iam-policies/members/:memberId/policies
GET /iam-policies/api-keys/:apiKeyId/policies
Remember that the write endpoints need a signed-in session, not an API key. For background on keys and scopes, see the API keys reference, and see plans for what each tier includes.