Entitlement Assistant · Data
- Lab
- Cloudflare
- Authorization
- AI Agents
This part is the information viewpoint: the data, a subsystem at a time, every table with every column, then the lifecycles the data moves through. It starts with the model at a glance and the rules every table keeps.


- Model at a glance — 50 tables, each with its keys
- Customer identity — the people and organizations served
- Internal identity — employees and system identities
- Account — every account, its version, its holds
- Product — features, plans and add-ons
- Access — roles and permissions
- Entitlement — subscriptions, add-ons, grants
- Policy — the policy set, in versions
- Support — cases, change requests, approvals
- Attribution — the audit chain
- State machines — five lifecycles, the change request’s first
1. Model at a glance
The model has 50 tables: 49 across nine subsystems — authorization has none, since it decides from the others’ data — and one the application holds, the outbox. Figure 8 shows every table with its keys alone; each subsystem’s section below draws its tables again with every column.


Figure 8 — The model at a glance: every table and its keys
Every table keeps the same rules. Ids are UUIDv7, stored as PostgreSQL’s own uuid and shown with a
prefix, so they sort by time; they are not secrets, since every access is authorized. Nothing is
deleted: the application’s role has no DELETE, so an identity ends by its status and a grant or a
case by a timestamp and a reason, and its history stays.
The database holds the structure, and the code the rules. Every status and kind is a text column whose
check lists its values, so a misspelt state cannot be stored; some reasons are free text — a change
request’s status reason, an assignment’s reason, a grant’s reason. Reference data is keyed by readable
keys: kebab-case for products, features, plans and policies, such as grants-up-to-14-days, and
dotted for permissions, such as account.read.
What an account is entitled to is never stored. It is computed when asked, by one building block, from the plan of the subscription’s current term, its add-ons and its active grants, so it cannot drift from them. Foreign keys point down the layer order; the audit chain alone refers back, by soft reference, so it depends on nothing it records.
Two constraints carry the support rules into the database. A change request is bound to its case’s subject by a composite foreign key — the case and the account together — so a request can never name an account its case is not about. An approval is one to one with its change request: its key is unique, though the shape would allow many.
The schema allows a little more than the code does, and says so here rather than leave it to be found. A case assignment may end as reassigned, though no code reassigns a case; a grant may end because its subscription ended, though no code ends it so; and nothing in the schema stops two activations of the policy set, or two grants of one permission to an employee, being current at once.
The application’s one table is the outbox: what a use case asks of the approval workflow, written in the use case’s own transaction and delivered after it commits.
| table | holds |
|---|---|
outbox_message | a message to the approval workflow, delivered after commit |
2. Customer identity
Customer identity holds the people and organizations a provider serves. A customer is an organization or an individual, never both, and an individual belongs to at most one organization at a time, for as long as its membership lasts. Figure 9 draws its 4 tables, every column.
A customer’s kind is part of the key an organization or an individual refers to it by, so a row of one cannot stand for a customer of the other kind; a person’s email is stored normalized, once, and a membership ends with a reason rather than disappearing.
2.1 Customer identity's tables
2.1 Customer identity’s tables
The tables, and what each holds.


Figure 9 — Customer identity: its tables
| table | holds |
customer | every customer, an organization or an individual, and its status |
organization | an organization’s legal name and primary domain |
individual | a person’s name and normalized email |
organization_membership | a person’s membership of an organization, while it lasts |
3. Internal identity
Internal identity holds the provider’s own identities: its employees, with their department, job function and level, and its system identities, which act where no person clicks. An employee’s password hash is replaced, never overwritten. Figure 10 draws its 6 tables, every column.
An employee’s kind and a system identity’s are held the same way, in the key, so an internal account knows which it belongs to. The department, job function and level are what give an employee their birthright roles in access, without anyone assigning them.
3.1 Internal identity's tables
3.1 Internal identity’s tables
The tables, and what each holds.


Figure 10 — Internal identity: its tables
| table | holds |
department | the provider’s departments, by key |
job_function | the provider’s job functions, by key |
internal_identity | every internal identity, an employee or a system, and its status |
employee | an employee’s department, job function, level and name |
system_identity | a system identity’s name and description; grant expiry and the approval process are its rows |
employee_credential | an employee’s password hash, replaced rather than overwritten |
4. Account
Account holds every account, customer or internal, with its status and a version that each applied change advances. A hold suspends an account until it is released: a held account is refused every action but reading it, auditing it, cancelling a request and handling its case. Figure 11 draws its 4 tables, every column.
The version is what keeps a change honest: a change request records the version it was previewed at, and applying it checks the account still has it. Every hold records who placed it and who released it, so a suspension’s history stays readable.
4.1 Account's tables
4.1 Account’s tables
The tables, and what each holds.


Figure 11 — Account: its tables
| table | holds |
account | every account, customer or internal, its status and its version |
internal_account | an internal account and the identity it belongs to |
account_hold | a hold that suspends an account, placed and released |
customer_account | a customer account, its customer and its category |
5. Product
Product holds the catalog: products and their features, the operations each feature allows, plans on ladders, add-ons, and which features and quotas each plan and add-on includes. Figure 12 draws its 9 tables, every column.
A plan’s rank on its ladder is what would make a change an upgrade or a downgrade — changing a plan is not built — and an add-on joins only the plans it is eligible for. A bundle’s quota is what a plan or an add-on adds to a feature that counts, such as seats.
5.1 Product's tables
5.1 Product’s tables
The tables, and what each holds.


Figure 12 — Product: its tables
| table | holds |
product | the products, by key |
feature | a product’s features, and a metered unit where one counts |
feature_operation | the operations a feature allows |
plan_ladder | an ordered ladder of plans |
plan | a plan, its ladder and its rank on it |
addon | the add-ons, by key |
plan_feature_bundle | the features a plan includes, and their quotas |
addon_feature_bundle | the features an add-on includes, and their quotas |
addon_plan_eligibility | the plans an add-on may join |
6. Access
Access holds roles and permissions. A permission is an operation on an internal resource or on a feature; roles give permissions to employees and to customers; a department, function and level give birthright roles at once. Figure 13 draws its 8 tables, every column.
A permission and the role that gives it name the same kind of principal, employee or customer, checked by keys, so an employee role cannot carry a customer’s permission. An assignment to an employee keeps its reason, its giver, its expiry and its end.
6.1 Access's tables
6.1 Access’s tables
The tables, and what each holds.


Figure 13 — Access: its tables
| table | holds |
resource_operation | the operations on internal resources |
permission | a permission: an operation on a resource or on a feature |
role | a role, for employees or for customers |
role_permission | the permissions a role gives |
birthright_role_assignment | the roles a department, function and level give at once |
internal_role_assignment | a role given to an employee, with its reason and its end |
internal_permission_assignment | a permission given to an employee directly |
account_role_assignment | a customer’s role in a customer account |
7. Entitlement
Entitlement holds what an account has bought or been given: subscriptions, their terms on a plan, their add-ons, and temporary grants. What an account is entitled to is computed from these, never stored. Figure 14 draws its 4 tables, every column.
A grant adds its quota to the feature for its time, and the grant it extends is a key of its own, unique, so a grant is extended at most once and the chain of extensions can be followed back to the first.
7.1 Entitlement's tables
7.1 Entitlement’s tables
The tables, and what each holds.


Figure 14 — Entitlement: its tables
| table | holds |
subscription | a customer account’s subscription |
subscription_term | a term of a subscription on a plan, its start and its end |
subscription_addon | an add-on on a subscription, while it lasts |
entitlement_grant | a temporary grant of a feature and its quota, and the grant it extends |
8. Policy
Policy holds the policy set in versions: each policy, when it applies, what it requires in CEL, its outcome when unmet, the actions it governs and its prerequisites, and which version is active. Figure 15 draws its 5 tables, every column.
A version is registered with the source revision it was built from, and an activation names the version in force from its moment. A prerequisite names the permission that fulfils it — for the approval a long grant needs, the permission to decide approvals.
8.1 Policy's tables
8.1 Policy’s tables
The tables, and what each holds.


Figure 15 — Policy: its tables
| table | holds |
policy_set | a registered version of the policy set and its source revision |
policy | a policy of a set: when it applies, what it requires, its outcome unmet |
policy_governed_action | the actions a policy governs |
policy_prerequisite | what fulfils a policy’s prerequisite: an approval, a permission |
policy_set_activation | which version of the policy set is active, since when |
9. Support
Support holds the cases: who works each, its subject and contact, the names it recorded, its change requests and their approvals. Figure 16 draws its 5 tables, every column.
A case records the names of the accounts it touched as they were when it saw them, so a later rename does not rewrite its history; it records too how its contact was authenticated, and by whom.
9.1 Support's tables
9.1 Support’s tables
The tables, and what each holds.


Figure 16 — Support: its tables
| table | holds |
support_case | a case: its subject, its contact and their authentication, its close |
case_assignment | who works a case, and when that ended |
case_account_name | the names of accounts as the case recorded them |
change_request | a prepared change: its kind, its diff, its preview, its status |
change_approval | the approval a change waits for, its deadline and its decision |
10. Attribution
Attribution holds the audit chain: its records, the accounts and reasons each names, and the chain’s head. Figure 17 draws its 4 tables, every column.
A record names its participants with their roles and carries reason codes, not words, so it holds no personal data; the head is one row, locked while a record joins, holding the last sequence number and hash.
10.1 Attribution's tables
10.1 Attribution’s tables
The tables, and what each holds.


Figure 17 — Attribution: its tables
| table | holds |
audit_chain_record | a record of the chain: who did what, the outcome, the hashes |
audit_participant | the accounts a record names, with their roles |
audit_reason | the reasons a record carries |
audit_chain_head | the chain’s last sequence number and hash |
11. State machines
Five things in the model have a lifecycle, and the change request’s is the one every change passes through. It is stored awaiting confirmation whatever its preview says; a confirmation the decision point answers Deny is refused with a 403 and leaves it awaiting confirmation; a confirmation applies it at once, or sends it to wait for an approval; it ends applied, refused, expired or canceled — canceled too when it has gone stale, because the account moved since its preview.
The project records lifecycles as statuses and facts as timestamps: a change request’s status says where it stands, and its timestamps say when it was requested, confirmed and applied, each with the account that did it. A change request’s status never goes back: from awaiting confirmation or approval it moves once to a later state, and its four end states are final. A case once closed is not reopened; a question that comes back opens a new case (Figure 18).


Figure 18 — The change request's states
| state | what moves it there |
|---|---|
AwaitingConfirmation | the model’s prepare_grant, its revocation or extension: stored |
AwaitingApproval | a confirmation the decision point answers Conditional |
Applied | a confirmation answered Allow, or an approval re-checked and applied |
Refused | a supervisor’s denial, or a refusal at application — the re-check, or the system identity refused change-request.apply or approval.expire |
Expired | an approval’s deadline, passed with no decision |
Canceled | the representative’s cancel, the case’s close, or a stale account |
11.1 The approval
11.1 The approval
An approval exists only for a change the decision point answered Conditional. It is pending for the configured window — 72 hours from the confirmation — and a supervisor decides it before then, or the deadline does, by the workflow’s timer or the sweep (Figure 19).


Figure 19 — The approval's states
| state | what moves it there |
Pending | a confirmation answered Conditional |
Approved | a supervisor, before the deadline, who neither requested nor confirmed |
Denied | a supervisor, with a reason |
Expired | the deadline, at the workflow’s timer or the sweep |
Canceled | the case’s close while it is pending, or the system refused approval.expire |
11.2 The grant
11.2 The grant
A grant starts when its change applies and is active until it ends or its time passes. Past its end it grants nothing at once, though its row records the expiry only when the sweep comes by. An extension ends the grant and starts a new one linked to it (Figure 20).


Figure 20 — The grant's states
| how it ends | what ends it |
Revoked | a revocation applied |
Extended | an extension applied: a new grant starts, linked to this one |
Expired | the sweep, recording a grant past its end |
SubscriptionEnded | allowed by the schema; no code ends a grant so |
11.3 The case
11.3 The case
A case opens assigned to whoever opened it. Its subject is set once; its contact may change, and setting it clears the contact’s authentication; an authentication counts for 30 minutes. It closes with a reason and a summary (Figure 21).


Figure 21 — The case's states
| how it closes | with |
Resolved | a summary of 1 to 4,000 characters, no email address in it; pending requests canceled |
Abandoned | the same, pending requests canceled; an approval already given still applied |
11.4 The session
11.4 The session
A session is a signed token, not a row: it lives 30 minutes, is renewed only past half its life, and no renewal reaches beyond eight hours from the sign-in. Signing out clears the cookie; the token itself lives until it expires, and a WebSocket is closed when its token does (Figure 22).


Figure 22 — The session's states
| state | what moves it there |
| active | a sign-in, or a renewal past half the token’s life |
| expired | the token’s 30 minutes passing |
| signed out | the cookie cleared |
| at its end | a renewal refused, eight hours from the sign-in reached |
References
- The project’s data model:
tech-docs/data-model/README.mdand a document a subsystem. - The migrations, at
typescript/system/subsystem/*/src/infra/migrations/.