Most APIs treat permissions as something that happens after the code has already decided what it wants to do.
You write:
deleteUser(userId)
Enter fullscreen mode Exit fullscreen mode
Then somewhere inside the function:
if (!user.canDelete) {
throw ForbiddenError();
}
Enter fullscreen mode Exit fullscreen mode
It works.
Until the system gets bigger.
Then permissions start appearing everywhere.
Controllers check roles.
Services check ownership.
Middleware checks tokens.
Database queries add tenant filters.
Background jobs bypass some checks.
Admin endpoints introduce special cases.
Eventually, permission logic becomes scattered across the architecture.
And that’s when a dangerous idea emerges:
What if permissions weren’t just runtime checks?
What if the API itself encoded permissions into its types?
Instead of thinking:
operation + permission check
Enter fullscreen mode Exit fullscreen mode
we could think:
permission + operation = valid program
Enter fullscreen mode Exit fullscreen mode
This changes the way we design APIs.
The compiler, type checker, or API boundary can become part of the authorization system.
Instead of merely asking:
“Does this user have permission?”
we can design systems where the software has enough information to make certain unauthorized operations difficult — or impossible — to express.
This is where API design starts touching type theory, capability-based security, Rust, domain-driven design, and distributed systems.
And I think it’s one of the most interesting directions for backend architecture.
1. The Traditional Permission Model
Consider a simple API:
DELETE /users/42
Enter fullscreen mode Exit fullscreen mode
The server receives the request.
It authenticates the caller.
Then it checks:
Is this user an administrator?
Enter fullscreen mode Exit fullscreen mode
The architecture might look like:
Client
|
v
API
|
v
Authentication
|
v
Authorization
|
v
Business Logic
|
v
Database
Enter fullscreen mode Exit fullscreen mode
This is normal.
But notice something.
The business operation exists independently from the permission.
We have:
deleteUser()
Enter fullscreen mode Exit fullscreen mode
and separately:
canDeleteUser()
Enter fullscreen mode Exit fullscreen mode
The programmer has to remember to connect them.
That creates a dangerous class of bugs:
deleteUser()
Enter fullscreen mode Exit fullscreen mode
gets called from somewhere that forgot:
canDeleteUser()
Enter fullscreen mode Exit fullscreen mode
Maybe a new endpoint.
Maybe a background job.
Maybe an internal service.
Maybe an administrative script.
The permission system becomes dependent on developer discipline.
And developer discipline doesn’t scale very well.
2. Permissions Are Usually Metadata
Many systems represent permissions as strings:
"user.read"
"user.write"
"user.delete"
Enter fullscreen mode Exit fullscreen mode
Then we might write:
if (user.permissions.includes("user.delete")) {
deleteUser(id);
}
Enter fullscreen mode Exit fullscreen mode
This is convenient.
But strings have almost no compile-time meaning.
The compiler doesn’t know that:
"user.delete"
Enter fullscreen mode Exit fullscreen mode
is related to:
deleteUser()
Enter fullscreen mode Exit fullscreen mode
You could accidentally write:
"user.deleet"
Enter fullscreen mode Exit fullscreen mode
and the compiler might happily accept it.
You could also call:
deleteUser()
Enter fullscreen mode Exit fullscreen mode
without checking the permission.
The relationship between authorization and behavior exists only in the programmer’s head.
That’s the problem.
3. What If Permission Became a Type?
Imagine we introduce:
struct ReadUser;
struct WriteUser;
struct DeleteUser;
Enter fullscreen mode Exit fullscreen mode
Now we could define an operation requiring a specific capability:
fn delete_user(
permission: DeleteUser,
user_id: UserId,
) -> Result<(), Error> {
// ...
}
Enter fullscreen mode Exit fullscreen mode
The interesting thing isn’t the struct itself.
The interesting thing is:
Where does
DeleteUsercome from?
It should only be constructed if the authorization layer proves that the caller has the required permission.
Conceptually:
Authentication
|
v
Authorization
|
v
DeleteUser capability
|
v
delete_user()
Enter fullscreen mode Exit fullscreen mode
Now the permission isn’t merely a boolean.
It is a value with a type.
And the function cannot execute without it.
4. The Compiler Becomes Part of the Security Boundary
Consider:
fn delete_user(
capability: DeleteUser,
id: UserId,
) {
// delete
}
Enter fullscreen mode Exit fullscreen mode
Some other code tries:
delete_user(UserId(42));
Enter fullscreen mode Exit fullscreen mode
It doesn’t compile.
The function requires:
DeleteUser
Enter fullscreen mode Exit fullscreen mode
not:
UserId
Enter fullscreen mode Exit fullscreen mode
The compiler is effectively saying:
You haven’t demonstrated that you possess the capability required to perform this operation.
This is a profound shift.
Authorization is no longer only:
runtime decision
Enter fullscreen mode Exit fullscreen mode
It becomes partially:
compile-time structure
Enter fullscreen mode Exit fullscreen mode
Of course, authentication and authorization against a real user still happen at runtime.
The key is that the result of authorization can be represented as a strongly typed capability.
5. Capability-Based Thinking
This idea is closely related to capability-based security.
Instead of saying:
“This user has permission to delete users.”
we give the program an object representing the authority:
DeleteUserCapability
Enter fullscreen mode Exit fullscreen mode
Possession of that object means:
you are authorized to perform this operation
Enter fullscreen mode Exit fullscreen mode
So:
User identity
|
v
Authorization policy
|
v
Capability
|
v
Operation
Enter fullscreen mode Exit fullscreen mode
The capability becomes a token of authority.
This is different from passing around:
role = "admin"
Enter fullscreen mode Exit fullscreen mode
because a role is broad.
A capability can be extremely specific.
For example:
DeleteUserCapability<User42>
Enter fullscreen mode Exit fullscreen mode
or:
WriteDocumentCapability<Document17>
Enter fullscreen mode Exit fullscreen mode
Now authorization can become much more granular.
6. Ownership Can Become Part of the Type
Imagine a multi-tenant application.
We have:
Tenant A
Tenant B
Enter fullscreen mode Exit fullscreen mode
and documents:
Document 1 -> Tenant A
Document 2 -> Tenant B
Enter fullscreen mode Exit fullscreen mode
A common security bug looks like:
GET /documents/2
Enter fullscreen mode Exit fullscreen mode
where the current user belongs to Tenant A.
The application forgets to add:
WHERE tenant_id = current_tenant
Enter fullscreen mode Exit fullscreen mode
Now Tenant A can see Tenant B’s data.
Instead, imagine:
struct TenantId<T>;
struct Document<Tenant> {
id: DocumentId,
tenant: PhantomData<Tenant>,
}
Enter fullscreen mode Exit fullscreen mode
The type system can begin carrying tenant context.
Conceptually:
Document<TenantA>
Enter fullscreen mode Exit fullscreen mode
is not the same type as:
Document<TenantB>
Enter fullscreen mode Exit fullscreen mode
Now we can design operations that require the correct tenant capability.
This is where type systems become interesting for security.
7. Permissioned APIs in TypeScript
TypeScript isn’t as strict as Rust, but we can still model permissions.
For example:
type Permission =
| "user.read"
| "user.write"
| "user.delete";
Enter fullscreen mode Exit fullscreen mode
Now we can define:
type Authorized<P extends Permission> = {
permission: P;
};
Enter fullscreen mode Exit fullscreen mode
Then:
function deleteUser(
auth: Authorized<"user.delete">,
userId: string
) {
// ...
}
Enter fullscreen mode Exit fullscreen mode
The caller needs a value proving that it has:
"user.delete"
Enter fullscreen mode Exit fullscreen mode
We can then build an authorization function:
function authorize<P extends Permission>(
token: Token,
permission: P
): Authorized<P> | null {
// runtime authorization
}
Enter fullscreen mode Exit fullscreen mode
Usage:
const capability =
authorize(token, "user.delete");
if (capability) {
deleteUser(capability, userId);
}
Enter fullscreen mode Exit fullscreen mode
Now authorization has become part of the function’s type contract.
8. Rust Makes This Even More Interesting
Rust’s type system is particularly useful for this pattern.
Consider:
struct ReadUser;
struct WriteUser;
struct DeleteUser;
Enter fullscreen mode Exit fullscreen mode
Then:
struct Capability<P> {
_permission: std::marker::PhantomData<P>,
}
Enter fullscreen mode Exit fullscreen mode
Our API might expose:
fn read_user(
_: Capability<ReadUser>,
id: UserId,
) -> User {
// ...
}
Enter fullscreen mode Exit fullscreen mode
and:
fn delete_user(
_: Capability<DeleteUser>,
id: UserId,
) -> Result<(), Error> {
// ...
}
Enter fullscreen mode Exit fullscreen mode
Now these operations have explicit authority requirements.
The function signature tells the truth.
Reading requires:
Capability<ReadUser>
Enter fullscreen mode Exit fullscreen mode
Deleting requires:
Capability<DeleteUser>
Enter fullscreen mode Exit fullscreen mode
The permission isn’t hidden inside the implementation.
It is part of the API.
9. This Changes API Documentation
Traditional API documentation might say:
DELETE /users/{id}
Requires admin role.
Enter fullscreen mode Exit fullscreen mode
A capability-oriented API might conceptually describe:
delete_user(
DeleteUserCapability,
UserId
)
Enter fullscreen mode Exit fullscreen mode
The required authority is visible in the interface itself.
This is powerful because the API becomes self-describing.
The type signature communicates:
what you can do
+
what authority you need
Enter fullscreen mode Exit fullscreen mode
That’s much harder to misunderstand.
10. Permissions Can Form a Type Hierarchy
Permissions aren’t always independent.
Suppose:
Admin
Enter fullscreen mode Exit fullscreen mode
implies:
ReadUser
WriteUser
DeleteUser
Enter fullscreen mode Exit fullscreen mode
We could model capabilities hierarchically.
Conceptually:
AdminCapability
|
+----> ReadUser
+----> WriteUser
+----> DeleteUser
Enter fullscreen mode Exit fullscreen mode
An administrator can derive narrower capabilities.
This is similar to how access tokens can be scoped.
Instead of giving every part of the application a giant:
Admin
Enter fullscreen mode Exit fullscreen mode
object, we could derive:
DeleteUser
Enter fullscreen mode Exit fullscreen mode
and pass only that capability to the component that needs it.
This follows a security principle that I really like:
Give code the minimum authority it needs.
11. Principle of Least Authority
Imagine a payment service.
It needs to charge customers.
It doesn’t need:
deleteUser
readPayroll
modifyInventory
Enter fullscreen mode Exit fullscreen mode
So instead of giving it:
Admin
Enter fullscreen mode Exit fullscreen mode
give it:
ChargePayment
Enter fullscreen mode Exit fullscreen mode
Now the architecture becomes:
Payment Service
|
v
ChargePaymentCapability
Enter fullscreen mode Exit fullscreen mode
If the service is compromised, its authority is limited.
This is much safer than:
Payment Service
|
v
SuperAdmin
Enter fullscreen mode Exit fullscreen mode
The type system helps communicate that boundary.
12. Capabilities Should Be Unforgeable
There is an important detail.
If anyone can simply write:
Capability::<DeleteUser>::new()
Enter fullscreen mode Exit fullscreen mode
then the system is useless.
The constructor must be protected.
For example:
pub struct DeleteUser {
private: (),
}
Enter fullscreen mode Exit fullscreen mode
Only the authorization subsystem can create it.
Conceptually:
External Code
|
X
Cannot construct capability
Enter fullscreen mode Exit fullscreen mode
but:
Authorization Layer
|
v
Creates capability
Enter fullscreen mode Exit fullscreen mode
This creates a controlled authority flow.
The capability becomes a proof.
13. Authorization as Proof
This is perhaps the deepest idea.
Instead of:
if authorized {
perform_operation();
}
Enter fullscreen mode Exit fullscreen mode
we can think:
authorization()
|
v
proof
|
v
operation(proof)
Enter fullscreen mode Exit fullscreen mode
The proof says:
The authorization system has established that this operation is allowed.
This resembles ideas from type theory and formal methods.
The type becomes a statement about what is valid.
For example:
DeleteUserCapability
Enter fullscreen mode Exit fullscreen mode
can be interpreted as:
“This code possesses authority to perform user deletion.”
The function doesn’t need to repeatedly ask:
Are you allowed?
Enter fullscreen mode Exit fullscreen mode
It already requires the proof.
14. But Runtime Authorization Still Exists
We shouldn’t misunderstand this.
A type system cannot know:
which user is logged in
Enter fullscreen mode Exit fullscreen mode
at compile time.
It cannot know:
whether Alice revoked Bob's access five seconds ago
Enter fullscreen mode Exit fullscreen mode
It cannot know:
whether this JWT has expired
Enter fullscreen mode Exit fullscreen mode
Those are runtime facts.
So the architecture becomes:
Runtime
|
| authenticate
v
Authorization
|
| establish capability
v
Type-safe business logic
Enter fullscreen mode Exit fullscreen mode
The runtime layer establishes authority.
The type system ensures that authority is required where appropriate.
This is a powerful combination.
15. The API Boundary Is Still Untrusted
Suppose a client sends:
DELETE /users/42
Enter fullscreen mode Exit fullscreen mode
The server cannot trust the client to provide:
DeleteUserCapability
Enter fullscreen mode Exit fullscreen mode
The capability exists internally.
The external request is merely a request.
The server authenticates:
Who are you?
Enter fullscreen mode Exit fullscreen mode
Then authorizes:
Are you allowed to delete user 42?
Enter fullscreen mode Exit fullscreen mode
Then creates:
DeleteUserCapability
Enter fullscreen mode Exit fullscreen mode
Then invokes:
delete_user(capability, 42)
Enter fullscreen mode Exit fullscreen mode
The architecture is:
HTTP Request
|
v
Authentication
|
v
Authorization
|
v
Capability
|
v
Typed Business Logic
Enter fullscreen mode Exit fullscreen mode
This is the important distinction between external permissions and internal authority.
16. Resource-Level Permissions
Global permissions aren’t always enough.
Consider:
document.read
Enter fullscreen mode Exit fullscreen mode
A user might have permission to read documents generally, but not every document.
We need:
document.read(document_id)
Enter fullscreen mode Exit fullscreen mode
Now the capability could carry resource identity:
struct ReadDocument {
document_id: DocumentId,
}
Enter fullscreen mode Exit fullscreen mode
Then:
fn read_document(
capability: ReadDocument
) -> Document {
// ...
}
Enter fullscreen mode Exit fullscreen mode
The capability doesn’t just prove:
You can read documents.
It proves:
You can read this specific document.
That is much stronger.
17. Ownership Can Be Encoded
Imagine:
struct Owned<T, Owner> {
value: T,
_owner: PhantomData<Owner>,
}
Enter fullscreen mode Exit fullscreen mode
Conceptually:
Owned<Document, TenantA>
Enter fullscreen mode Exit fullscreen mode
cannot be casually passed where:
Owned<Document, TenantB>
Enter fullscreen mode Exit fullscreen mode
is required.
This allows the type system to encode relationships that would otherwise be represented only as runtime conditions.
It doesn’t mean every application should implement a highly complex type-level security system.
But for critical systems, the idea is incredibly powerful.
18. Permissions and State Machines
Permissions can also depend on state.
Imagine an order:
PENDING
PAID
SHIPPED
DELIVERED
CANCELLED
Enter fullscreen mode Exit fullscreen mode
Not every operation is valid in every state.
For example:
cancel()
Enter fullscreen mode Exit fullscreen mode
may only be allowed when:
PENDING
Enter fullscreen mode Exit fullscreen mode
We can model this with types:
struct PendingOrder;
struct PaidOrder;
struct ShippedOrder;
Enter fullscreen mode Exit fullscreen mode
Then:
fn cancel(
order: Order<PendingOrder>,
capability: CancelOrder,
) -> CancelledOrder {
// ...
}
Enter fullscreen mode Exit fullscreen mode
But:
fn cancel(
order: Order<ShippedOrder>,
capability: CancelOrder,
)
Enter fullscreen mode Exit fullscreen mode
could be impossible to compile.
Now we have combined:
state
+
permissions
Enter fullscreen mode Exit fullscreen mode
inside the type system.
This is extremely interesting.
The API doesn’t just say:
“You need permission.”
It says:
“You need permission and the resource must be in the correct state.”
19. APIs Become State Machines
Traditional CRUD:
POST
GET
PATCH
DELETE
Enter fullscreen mode Exit fullscreen mode
Type-driven API:
CreateOrder
|
v
PendingOrder
|
+----> PayOrder
|
v
PaidOrder
|
+----> ShipOrder
|
v
ShippedOrder
Enter fullscreen mode Exit fullscreen mode
Each transition can require a capability.
PayOrderCapability
ShipOrderCapability
CancelOrderCapability
Enter fullscreen mode Exit fullscreen mode
Now the API becomes a typed state machine.
This is much closer to the actual business domain.
20. Temporal Permissions
Permissions can also expire.
Imagine:
DeployToProduction
Enter fullscreen mode Exit fullscreen mode
is granted for 30 minutes.
The type system cannot directly encode wall-clock time in an ordinary runtime application.
But the capability object can carry an expiration:
struct DeployCapability {
expires_at: Instant,
}
Enter fullscreen mode Exit fullscreen mode
The operation checks:
if capability.expires_at < now() {
return Err(Error::ExpiredCapability);
}
Enter fullscreen mode Exit fullscreen mode
Now we have:
type-level authority
+
runtime temporal validity
Enter fullscreen mode Exit fullscreen mode
Again, the type says:
This is a deployment capability.
The runtime says:
This particular capability is still valid.
21. Permission Types Can Improve Service Boundaries
Imagine a large backend:
User Service
Order Service
Payment Service
Inventory Service
Enter fullscreen mode Exit fullscreen mode
Instead of passing around:
UserContext
Enter fullscreen mode Exit fullscreen mode
everywhere, we can pass narrower capabilities.
For example:
Order Service
|
+---- ReadCustomer
|
+---- CreateOrder
|
+---- ReadInventory
Enter fullscreen mode Exit fullscreen mode
The Payment Service gets:
ChargePayment
Enter fullscreen mode Exit fullscreen mode
but not:
DeleteCustomer
Enter fullscreen mode Exit fullscreen mode
This makes service dependencies explicit.
You can inspect the function signatures and understand authority flow.
That’s valuable for architecture reviews.
22. Permissions Become Architecture
This is the bigger idea.
Most organizations treat authorization as:
security layer
Enter fullscreen mode Exit fullscreen mode
separate from:
architecture
Enter fullscreen mode Exit fullscreen mode
But if permissions are part of the type system, authority becomes visible in the architecture itself.
For example:
fn process_payment(
charge: ChargePayment,
order: Order
)
Enter fullscreen mode Exit fullscreen mode
immediately tells you:
This function has financial authority.
And:
fn read_customer(
capability: ReadCustomer,
customer_id: CustomerId
)
Enter fullscreen mode Exit fullscreen mode
tells you:
This function can access customer data.
Security stops being invisible middleware.
It becomes part of the program’s structure.
23. This Is Especially Interesting in Rust
Rust’s ownership model makes this pattern particularly natural.
Rust already teaches developers:
Who owns this?
Who can mutate this?
How long can this reference live?
Enter fullscreen mode Exit fullscreen mode
Adding capabilities extends the philosophy:
Who owns this?
Who can mutate this?
Who is authorized to perform this operation?
Enter fullscreen mode Exit fullscreen mode
Imagine:
fn update_document(
capability: WriteDocument,
document: &mut Document,
) {
// ...
}
Enter fullscreen mode Exit fullscreen mode
Now both memory authority and business authority appear in the signature.
That’s a fascinating convergence.
The language isn’t only checking memory safety.
It can help express authority boundaries.
24. Distributed Systems Make This Even More Interesting
Suppose Service A authorizes a user.
Then it calls Service B.
How does Service B know what authority the caller has?
We could propagate capability information.
But we should be careful.
We cannot simply serialize an internal capability object and trust it.
Instead, we might issue a signed authorization token:
{
"subject": "user-42",
"permissions": [
"order.read",
"order.create"
],
"expires_at": "..."
}
Enter fullscreen mode Exit fullscreen mode
Service B validates the token.
Then internally creates:
OrderReadCapability
OrderCreateCapability
Enter fullscreen mode Exit fullscreen mode
Now the architecture becomes:
Service A
|
| signed authorization
v
Service B
|
| validate
v
Typed capability
|
v
Business logic
Enter fullscreen mode Exit fullscreen mode
This bridges distributed authorization with type-safe local authority.
25. The Capability Should Be Narrow
One of the biggest mistakes would be creating:
SuperAdminCapability
Enter fullscreen mode Exit fullscreen mode
and passing it everywhere.
Then we’ve simply reinvented roles.
The goal is to create narrow capabilities.
Instead of:
Admin
Enter fullscreen mode Exit fullscreen mode
use:
ReadUser
WriteUser
DeleteUser
CreateOrder
ChargePayment
RefundPayment
Enter fullscreen mode Exit fullscreen mode
Then compose them only when necessary.
This gives us a more precise security model.
26. Designing the API Around Permissions
A useful architecture might look like:
HTTP Request
|
v
+----------------+
| Authentication |
+----------------+
|
v
+----------------+
| Authorization |
+----------------+
|
capability created
|
v
+----------------+
| Typed API |
+----------------+
|
v
+----------------+
| Domain Logic |
+----------------+
|
v
+----------------+
| Database |
+----------------+
Enter fullscreen mode Exit fullscreen mode
The key transition is:
authorization result
|
v
typed authority
Enter fullscreen mode Exit fullscreen mode
That is where the architecture changes.
27. What Happens When Permissions Change?
Suppose Alice has:
WriteDocument
Enter fullscreen mode Exit fullscreen mode
and then an administrator revokes her access.
An existing capability might still exist in memory.
This means capabilities should have carefully defined lifetimes.
For short-lived operations, this may be perfectly fine.
For long-lived processes, we need:
expiration
revocation checks
short leases
token rotation
Enter fullscreen mode Exit fullscreen mode
This is why type safety doesn’t eliminate runtime security.
It complements it.
28. Don’t Turn Everything Into a Type
There is also a danger here.
If every permission becomes a type and every resource relationship becomes a generic parameter, your code can become unreadable.
You might end up with:
Api<
Tenant<
Auth<
Resource<
Permission<
State<
Version<T>
>
>
>
>
>
>
Enter fullscreen mode Exit fullscreen mode
Congratulations.
You have created a programming language inside your API.
Type systems are powerful.
But complexity is also a security risk.
The goal isn’t:
Put everything into types.
The goal is:
Use types where they make important invariants obvious and difficult to violate.
29. A Practical Hybrid Model
A realistic system might divide responsibilities like this:
Compile time
Enforce:
operation requires capability
valid state transitions
resource ownership abstractions
safe authority propagation
Enter fullscreen mode Exit fullscreen mode
Runtime
Enforce:
identity
token validity
expiration
revocation
tenant membership
business policy
resource existence
contextual conditions
Enter fullscreen mode Exit fullscreen mode
The architecture becomes:
Compile Time
|
+-------------+-------------+
| |
Type Safety API Contracts
| |
+-------------+-------------+
|
v
Runtime
|
+-------------+-------------+
| | |
Identity Policy State
| | |
+-------------+-------------+
|
v
Authorization
|
v
Capability
|
v
Operation
Enter fullscreen mode Exit fullscreen mode
This is a much more realistic approach.
30. The API Signature Becomes a Security Document
This is probably my favorite part.
Consider:
fn refund_payment(
capability: RefundPayment,
payment: Payment
) -> Result<Refund, Error>
Enter fullscreen mode Exit fullscreen mode
That function signature tells us something important.
It says:
Refunds require authority.
Compare it to:
fn refund_payment(
payment: Payment
) -> Result<Refund, Error>
Enter fullscreen mode Exit fullscreen mode
Now the permission requirement is invisible.
A developer reading the first version immediately sees the security boundary.
The type signature becomes documentation.
But better than documentation:
the compiler can enforce the requirement.
Conclusion: Make Illegal Authority Hard to Express
The traditional API security model is:
Request
|
v
Check permission
|
v
Execute
Enter fullscreen mode Exit fullscreen mode
A type-driven security model becomes:
Request
|
v
Authenticate
|
v
Authorize
|
v
Create capability
|
v
Typed operation
Enter fullscreen mode Exit fullscreen mode
The difference looks small.
Architecturally, it is enormous.
Instead of scattering:
if hasPermission(...)
Enter fullscreen mode Exit fullscreen mode
throughout your application, you make authority part of the interfaces between components.
Instead of passing:
role = "admin"
Enter fullscreen mode Exit fullscreen mode
everywhere, you pass the narrow capability required for the specific operation.
Instead of treating permissions as strings, you can represent them as types.
Instead of allowing every piece of code to attempt every operation and hoping authorization was checked somewhere, you design APIs where sensitive operations require explicit authority.
The result is not perfect security.
A type system cannot know who logged in five seconds ago.
It cannot know whether an administrator revoked someone’s access.
It cannot know whether a token has expired.
It cannot replace runtime authorization.
But it can do something incredibly valuable:
It can make the architecture communicate what authority is required.
And it can make certain classes of mistakes harder to express.
That’s the deeper lesson.
Good type systems don’t merely prevent invalid data.
They can encode valid states, valid transitions, ownership, capabilities, and authority.
And when those ideas reach API design, the API becomes more than a collection of endpoints.
It becomes a security boundary that is visible in the code itself.
The most interesting question isn’t:
“How do I check whether this user has permission?”
It’s:
“Can I design this API so that unauthorized behavior is difficult to express in the first place?”
That is a very different way of thinking about backend architecture.
And perhaps the future of secure APIs isn’t simply better authorization middleware.
Maybe it’s APIs where permission is part of the type system itself.