Chapter Nine: Production RBAC
Transactions, Drivers, Schema Migrations & Idempotent Pipelines
Previously
Chapter Eight made access accountable. You can now model access policy as first-class data, trace every grant to a policy or an audited exception, and record reviews of the grants themselves. Chapter Nine connects the RBAC system to a real application.
The Situation
Amasoft’s RBAC system is live in TypeDB. The compliance team has self-service audit queries. Access policies are encoded as entities. Now the provisioning system needs to connect - granting and revoking access automatically as people join, leave, or change roles.
Transactions
Every TypeDB query runs inside a transaction. You’ve been using them all along: Studio’s auto mode handles that for you. But in a production application, you need to be aware of them.
TypeDB has three transaction types, which serve distinct purposes:
| Type | Queries | Notes |
|---|---|---|
read |
|
Cheapest, general usage |
write |
Reads, plus |
Also allows reads. Commit promptly |
schema |
Everything, including |
Deployment and migrations only. Can touch both data and schema. Blocks other writes while open |
|
⚠️ Transactions: best practices
|
Idempotent Provisioning using 'put'
The provisioning system runs on a schedule. If it runs twice, it should not create duplicate identities or duplicate access grants. TypeDB’s put keyword handles this:
# Idempotent identity provisioning
put
$_ isa full_time_employee,
has email "new.hire@amasoft.com",
has name "New Hire",
has status "active";
put inserts if the exact instance does not exist, and does nothing if it already does. Run the same script ten times — the database has one instance, not ten.
|
💡
put vs insertWhen you run a This is what makes The only cost is that |
# Idempotent access grant — provisioning always grants under policy
match
$i isa full_time_employee, has email "new.hire@amasoft.com";
$sam isa full_time_employee, has email "sam.rivera@amasoft.com";
$s isa engineering_platform, has name "GitShed";
$p isa access_policy, has name "Engineering Access";
put
$_ isa policy_access_grant, links (grantee: $i, system: $s, grantor: $sam, policy: $p),
has permission_level "read",
has start_datetime 2024-08-01T09:00:00;
The Access Check - The Most Important Query
This query runs thousands of times a day. An identity requests access to a system — does it have it?
match
$i isa identity, has email "alex.chen@amasoft.com",
has status "active";
$s isa payments_system;
{ $_ isa access_grant, links (grantee: $i, system: $s); }
or
{ $_ isa project_team_membership, links (member: $i, team: $t);
$_ isa access_grant, links (grantee: $t, system: $s); };
$i has name $name;
fetch { "has_access": $name };
|
⚠️ You cannot fetch an entity directly
|
|
💡 Identity status as an access check’s safety net
Revoking access properly means closing each grant with an |
One Identifier for Every Identity
In a production application, the query above would be templated: the application would inject the email address as a parameter. However, while it finds Alex by email, it can’t find the CI deploy bot - the heaviest user of this query. It has no email; its key is service_name.
But in TypeDB we can fix this using attribute subtyping! Let’s try introducing a super-attribute 'identity_name' that all identities own a subtype of. (Note: this query has a bug, and will fail.)
define
attribute identity_name @abstract, value string;
attribute email sub identity_name;
attribute service_name sub identity_name;
identity owns identity_name @key;
You should see an error like: "Cannot redeclare inherited value type string on email." TypeDB sees that identity_name, email and service_name are all explicitly value string - a redundant and error-prone state. You might be tempted to just remove value string from the supertype, but this would cause a different error - not all attribute types are keyable, so that would create a conflict with owns identity_name @key.
To fix this properly, we’re going to do a schema migration - defining the new concepts and undefining the ones that must go, all in one transaction:
define
attribute identity_name @abstract, value string;
attribute email sub identity_name;
attribute service_name sub identity_name;
identity owns identity_name @key;
end;
undefine
value string from email;
value string from service_name;
end;
Success! Now every identity has an identity_name of some variety.
|
💡 Atomic schema migrations
TypeDB is very strict over what is allowed in its schema at the end of a commit. This ensures consistent state whenever anyone opens any kind of transaction. It’s therefore common for schema migration scripts to consist of multiple queries that must be run atomically in a single transaction - |
|
⭐ TypeDB Advantage: Attributes can have subtypes too
Attribute subtyping allows us to express all of this in a single, simple query. |
match
$i isa identity, has identity_name "ci-deploy-bot",
has status "active";
$s isa engineering_platform;
{ $_ isa access_grant, links (grantee: $i, system: $s); }
or
{ $_ isa project_team_membership, links (member: $i, team: $t);
$_ isa access_grant, links (grantee: $t, system: $s); };
$i has name $name;
fetch { "has_access": $name };
For checking if access grants match policy, call the relevant function(s) directly:
match
$i isa identity, has identity_name "alex.chen@amasoft.com",
has status "active";
let $authorised in who_should_have_access_to_payments($i);
$authorised has name $name;
fetch { "policy_authorised": $name };
The result is empty (no access) or returns the identity (has access).
Production RBAC Principle — Never Delete History
In Chapter Three, you deleted Alex’s contractor record and all its relations when promoting them to full-time employee. In production, you’d want to retain historical records instead.
That’s out of scope of this exercise, but let’s look at our model for revoking an access grant:
|
🏢 Production RBAC systems do not delete history — they close it
When access is revoked, the |
Deprovision an identity by closing all their access grants:
# Close all active grants for a departing employee
match
$i isa identity, has identity_name "departing@amasoft.com";
$ag isa access_grant, links (grantee: $i);
{ not { $ag has end_datetime $_; }; }
or { $ag has end_datetime > 2024-12-31T17:00:00; };
insert
$ag has end_datetime 2024-12-31T17:00:00;
# Suspend the identity
match
$i isa identity, has identity_name "departing@amasoft.com";
update
$i has status "terminated";
|
💡 The 'update' keyword
When replacing the value of an already-attached attribute, use Behind the scenes, |
|
💡 Two steps for offboarding
We do both because the two steps represent distinct concepts. An identity’s grants represent their permissions. Status represents whether they are still active at all in Amasoft. From the perspective of an access check, status is a convenient safety net to check. |
The Architect’s Checklist — Taking This to Production
The schema you have built may form the foundation for a production-grade system, but if we were going to deploy for a real company, here are some natural extensions to consider:
|
💡 Natural extensions for production deployment
All of these follow the same TypeDB patterns you have already learned. |
Chapter Nine Challenge
|
🎯 Challenge: The Provisioning Pipeline
|
What You Can Do Now
You can now manage transactions correctly, write idempotent provisioning pipelines, run schema migrations, offboard employees, close access history without deleting it, and run the access check query at the heart of every RBAC system. You have completed RBAC In Business — and built a foundation for a production-grade system you could adopt for your own company.