Skip to content
bitscrafter

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.

Data: its sections around it, in reading order

  1. Model at a glance — 50 tables, each with its keys
  2. Customer identity — the people and organizations served
  3. Internal identity — employees and system identities
  4. Account — every account, its version, its holds
  5. Product — features, plans and add-ons
  6. Access — roles and permissions
  7. Entitlement — subscriptions, add-ons, grants
  8. Policy — the policy set, in versions
  9. Support — cases, change requests, approvals
  10. Attribution — the audit chain
  11. 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.

Every table of the model, grouped by subsystem, each with its primary and foreign keys

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.

The application's own table
tableholds
outbox_messagea 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.

Customer identity's tables, every column, with their keys

Figure 9 — Customer identity: its tables

tableholds
customerevery customer, an organization or an individual, and its status
organizationan organization’s legal name and primary domain
individuala person’s name and normalized email
organization_membershipa 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.

Internal identity's tables, every column, with their keys

Figure 10 — Internal identity: its tables

tableholds
departmentthe provider’s departments, by key
job_functionthe provider’s job functions, by key
internal_identityevery internal identity, an employee or a system, and its status
employeean employee’s department, job function, level and name
system_identitya system identity’s name and description; grant expiry and the approval process are its rows
employee_credentialan 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.

Account's tables, every column, with their keys

Figure 11 — Account: its tables

tableholds
accountevery account, customer or internal, its status and its version
internal_accountan internal account and the identity it belongs to
account_holda hold that suspends an account, placed and released
customer_accounta 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.

Product's tables, every column, with their keys

Figure 12 — Product: its tables

tableholds
productthe products, by key
featurea product’s features, and a metered unit where one counts
feature_operationthe operations a feature allows
plan_ladderan ordered ladder of plans
plana plan, its ladder and its rank on it
addonthe add-ons, by key
plan_feature_bundlethe features a plan includes, and their quotas
addon_feature_bundlethe features an add-on includes, and their quotas
addon_plan_eligibilitythe 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.

Access's tables, every column, with their keys

Figure 13 — Access: its tables

tableholds
resource_operationthe operations on internal resources
permissiona permission: an operation on a resource or on a feature
rolea role, for employees or for customers
role_permissionthe permissions a role gives
birthright_role_assignmentthe roles a department, function and level give at once
internal_role_assignmenta role given to an employee, with its reason and its end
internal_permission_assignmenta permission given to an employee directly
account_role_assignmenta 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.

Entitlement's tables, every column, with their keys

Figure 14 — Entitlement: its tables

tableholds
subscriptiona customer account’s subscription
subscription_terma term of a subscription on a plan, its start and its end
subscription_addonan add-on on a subscription, while it lasts
entitlement_granta 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.

Policy's tables, every column, with their keys

Figure 15 — Policy: its tables

tableholds
policy_seta registered version of the policy set and its source revision
policya policy of a set: when it applies, what it requires, its outcome unmet
policy_governed_actionthe actions a policy governs
policy_prerequisitewhat fulfils a policy’s prerequisite: an approval, a permission
policy_set_activationwhich 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.

Support's tables, every column, with their keys

Figure 16 — Support: its tables

tableholds
support_casea case: its subject, its contact and their authentication, its close
case_assignmentwho works a case, and when that ended
case_account_namethe names of accounts as the case recorded them
change_requesta prepared change: its kind, its diff, its preview, its status
change_approvalthe 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.

Attribution's tables, every column, with their keys

Figure 17 — Attribution: its tables

tableholds
audit_chain_recorda record of the chain: who did what, the outcome, the hashes
audit_participantthe accounts a record names, with their roles
audit_reasonthe reasons a record carries
audit_chain_headthe 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).

The change request's states: from a change request prepared — a grant's creation, revocation or extension — stored awaiting confirmation, applied at once or awaiting approval, then applied, refused, expired or canceled

Figure 18 — The change request's states

The change request's states, and what moves it there
statewhat moves it there
AwaitingConfirmationthe model’s prepare_grant, its revocation or extension: stored
AwaitingApprovala confirmation the decision point answers Conditional
Applieda confirmation answered Allow, or an approval re-checked and applied
Refuseda supervisor’s denial, or a refusal at application — the re-check, or the system identity refused change-request.apply or approval.expire
Expiredan approval’s deadline, passed with no decision
Canceledthe 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).

The approval's states: pending, then approved, denied, expired or canceled

Figure 19 — The approval's states

statewhat moves it there
Pendinga confirmation answered Conditional
Approveda supervisor, before the deadline, who neither requested nor confirmed
Denieda supervisor, with a reason
Expiredthe deadline, at the workflow’s timer or the sweep
Canceledthe 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).

The grant's states: from a grant or an extension applied, active, then revoked, extended, or lapsed past its end — granting nothing — and recorded expired by the sweep

Figure 20 — The grant's states

how it endswhat ends it
Revokeda revocation applied
Extendedan extension applied: a new grant starts, linked to this one
Expiredthe sweep, recording a grant past its end
SubscriptionEndedallowed 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).

The case's states: open, its subject and contact set, the contact authenticated, then closed

Figure 21 — The case's states

how it closeswith
Resolveda summary of 1 to 4,000 characters, no email address in it; pending requests canceled
Abandonedthe 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).

The session's states: active and renewed past half its life, then expired, signed out, or refused at eight hours

Figure 22 — The session's states

statewhat moves it there
activea sign-in, or a renewal past half the token’s life
expiredthe token’s 30 minutes passing
signed outthe cookie cleared
at its enda renewal refused, eight hours from the sign-in reached

References

  • The project’s data model: tech-docs/data-model/README.md and a document a subsystem.
  • The migrations, at typescript/system/subsystem/*/src/infra/migrations/.