Writing · Architecture · Security · AI agents
Deny Wins: How WardenAuth Authorizes Users and AI Agents

- Product
- wardenauthz.com
- Data
- Aurora PostgreSQL + DynamoDB
- Runtime
- ARM Lambda, Fargate hot path
- SDKs
- TypeScript, Go, Python
Every B2B app ends up building the same thing: roles, permissions, tenants, an admin screen to manage them, and an audit log for the customer who asks who changed what. Then AI agents arrive, and the question gets harder: what may an agent do when it acts for one of your users?
WardenAuth is my answer to both: one authorization API for your users and the AI agents that act in your app. This post explains how it decides.
One question, asked everywhere
Your code asks one thing: may this subject do this action on this resource, in this scope?
const { allowed } = await warden.access.hasAccess({
subjectId: 'maria@acme.test',
scopeId: 'eu-support',
resource: 'tickets',
action: 'reply',
})
if (!allowed) throw new Forbidden()The scope is never optional. Forgetting the tenant in one query is the classic multi-tenant bug, so the API does not let you leave it out.
Scopes that inherit
Tenants are a tree of scopes. An organization is the root scope and its workspaces are children. Roles can be defined at any level, and a role granted on the organization applies to every workspace below it. Data is partitioned by scope, and every query is filtered by it.
Permissions are resource:action pairs, such as tickets:reply or refunds:create, with wildcards when you need them. Roles group permissions, and policies attach roles to subjects: users, API keys and agents.
Deny wins
When an allow and a deny both apply, the deny wins, at every scope level. An explicit deny on a workspace beats an allow inherited from the organization. It is the rule people expect from a security product, and it makes the dangerous mistake, granting too much, harder to make.
Every answer says why
A bare true or false is useless when a customer asks why they cannot see something. Every check returns its reasons:
{
"allowed": true,
"matchedBy": ["role:support"],
"deniedBy": [],
"scopeChain": ["acme", "eu-support"]
}Which roles matched, which policies denied, and the chain of scopes that was walked. Support can answer the question without reading code, and so can the customer's own admin.
Agents get the same rules, capped by the user
AI agents usually reach tools through MCP servers: GitHub, Linear, Stripe or your own. WardenAuth sits in front of them as a gateway, and every tool call becomes an access check like any other. Tools are granted to roles, and an agent acting for a user never gets more than that user can do.
Sensitive tools can require a person. When a support agent tries to refund €120 through Stripe, the call waits for a reviewer, who approves it with a written reason. Every call leaves a signed receipt: what was asked, what was decided and why.
Two databases, on purpose
The data splits in two by how it is used. The relational core (scopes, roles, permissions, policies, SSO and SCIM configuration) lives in Aurora PostgreSQL, where relationships and constraints belong. The hot path and the append-only data (API keys, audit events, rate-limit counters, webhook deliveries) live in DynamoDB, keyed by scope.
Access checks are the one path where latency matters most, so they are served from Fargate with a direct PostgreSQL connection pool rather than through a Lambda and the Data API. The rest of the API runs on ARM Lambdas.
An audit log you can trust
Audit events are immutable and HMAC-signed. They go through SQS, so a burst never slows the API, into DynamoDB, and from there to an S3 archive with Object Lock for long-term retention. A dead-letter queue and a redriver make sure an event is retried, not dropped. API keys are stored as HMAC-SHA256 hashes with a server-side pepper.
How the code is organised
The backend follows a clean, domain-driven layout: the domain layer has no infrastructure imports, repository interfaces are defined in the domain and implemented in infrastructure, and dependencies are injected at cold start. The test suite has more than 3,800 tests. That discipline is what lets one person keep a security product moving quickly.
WardenAuth also handles authorization for Fabiana, my AI customer assistant. If you are about to build roles and permissions, or let agents loose on your tools, try the demo on wardenauthz.com: pick a role, act as the user, then let the agent try something risky.