skip to content

Should role definitions ship as a seeded catalogue in your codebase, or as rows each district administrator can edit, and what does each commit you to?

level: principalimportance: should knowfreq 42%

answer

  1. who authors a role's meaning
  2. strings become a published contract
  3. catalogue change is a deploy
  4. split needs a compatibility window
  5. writes during the backfill

basics

~20 s

A seeded catalogue keeps role meanings few, reviewable and changeable in one deploy. Letting organisations compose their own roles turns your permission strings into a published contract you can no longer rename freely, and makes every vocabulary change a data migration across every organisation's rows.

solid answer

~50 s

The question is who owns the meaning of a role. A **seeded catalogue** — role definitions and their permission rows created by migration, versioned with the code — means every organisation gets the same small set, support can reason about `area_supervisor` without asking whose it is, and changing what a role means is a deploy. **Editable roles** move that authorship to customers: they compose roles from your permission strings, which makes the vocabulary a contract you have published, so renaming or splitting `establishment:reopen` becomes a backfill over every organisation's rows rather than an edit in one place. Most products want a seeded catalogue plus scoped grants for years longer than they think, and the honest trigger for changing is a second organisation asking for a genuinely different job, not the first one asking for a different label. If you do open it, decide up front how a permission split reaches grants written while the backfill is running.

code

sql · 20 lines
sql
-- step 1: the new vocabulary exists, nothing demands it yet
INSERT INTO permission (key, description) VALUES
  ('establishment:reopen_request', 'Propose returning a closed establishment to trading'),
  ('establishment:reopen_approve', 'Approve a reopening proposal');

-- step 3: re-runnable backfill — every role holding the old string gains both new ones
INSERT INTO role_permission (role_id, permission_id)
SELECT rp.role_id, p_new.id
FROM role_permission rp
JOIN permission p_old ON p_old.id = rp.permission_id AND p_old.key = 'establishment:reopen'
JOIN permission p_new ON p_new.key IN ('establishment:reopen_request', 'establishment:reopen_approve')
WHERE NOT EXISTS (
  SELECT 1 FROM role_permission x
  WHERE x.role_id = rp.role_id AND x.permission_id = p_new.id
);

-- step 5 gate: run this until it returns zero before closing the compatibility window
SELECT count(*) FROM role_permission rp
JOIN permission p ON p.id = rp.permission_id
WHERE p.key = 'establishment:reopen';

go deeper

for a junior

Know that role definitions live in tables somewhere and that someone decides what a role means — either your migration does, or a customer's administrator does.

for a middle

Explain the practical difference: with a catalogue, changing a role is one edit; with composed roles, the permission strings are exposed and the meanings differ per organisation.

for a senior

Walk the split of a permission end to end, including the compatibility window and the grants written while the backfill runs, and say how the late breakage shows up.

for a principal

Own the call and its horizon: what opening the vocabulary costs for years, which smaller opening buys most of the benefit, and what the exit looks like if you were wrong.

## What the choice really is Both options use the same tables. The difference is **who authors the rows in `role` and `role_permission`**, and therefore who you must coordinate with when the vocabulary changes. - **Seeded catalogue** — definitions are created by migration and versioned with the code. Every organisation sees the same roles. The permission vocabulary is internal. - **Customer-composed roles** — each organisation creates roles and picks permissions for them. The vocabulary becomes an external surface: the strings appear in somebody else's configuration screen. The second is not a superset of the first. It is a different product with a different support model and a much slower change cycle. ## What each commits you to | | Seeded catalogue | Customer-composed roles | |---|---|---| | Changing what a role means | One migration, one deploy | Impossible centrally — meanings now differ per organisation | | Renaming or splitting a permission | Edit seeds and the handlers that demand it | Backfill every organisation's rows, with a compatibility window | | Support answering "why was this refused?" | Read the catalogue | Read that organisation's own composition first | | Onboarding a new organisation | Nothing to configure | A configuration task before anyone can work | | Testing | A finite matrix of roles by endpoint | Compositions you have never seen | | The pressure it relieves | None, until a job genuinely differs | A customer whose job titles differ from yours | The hidden cost is in the first two rows and it compounds: a seeded catalogue lets you *fix* an authorization design mistake, because you can redefine a role for everyone at once. Once meanings are per-organisation, a mistake is only ever fixable by migration plus communication, and a permission string you regret is permanent in practice. ## The migration you are signing up for Suppose the reopening rules change and `establishment:reopen` must split into `establishment:reopen_request` and `establishment:reopen_approve`. With a seeded catalogue this is contained; with composed roles it is a forward data migration over live rows. Either way the sequence is the same, and the interesting part is the middle: 1. **Insert** the two new permissions. Nothing demands them yet. 2. **Open a compatibility window** in the check: a handler that needs the new capability accepts either the new string or the old one it was split from. The window is the whole reason this is survivable while writes continue. 3. **Backfill**: for every role carrying the old string, add the new ones. Write the backfill so re-running it is harmless — insert where not exists, keyed on the pair — because it will be re-run. 4. **Handle writes made during the backfill.** New roles are being composed while the job runs, and they may still be built from the old string. Either freeze the administration surface for the duration, or make step 3 re-runnable and run it again until a pass changes nothing. If it is not idempotent, neither option is available to you. 5. **Move the handlers** to demand only the new strings, then remove the old permission rows and close the window. Step 4 is the one that is skipped, and the symptom is specific: a role composed during the migration keeps working until the window closes, then breaks for one organisation weeks later, which is precisely the kind of failure that is never traced back to its cause. ## Deciding Reach for the seeded catalogue while these hold: - The jobs are genuinely the same across organisations, and the differences are **labels** rather than capability sets. - The scoped grant already absorbs the variation — the same role held in different units covers most of what customers actually ask for. - You still expect to change what a role means; early products always do. Open it up when a second organisation needs a capability set that no catalogue role expresses and that you would not want to give the others. Even then, prefer the smallest opening: let organisations **rename** catalogue roles for display, or compose only from a curated subset, before handing over the full vocabulary. A display name is not a contract; a permission string is. ## Federated identity does not decide this for you When accounts arrive from an external directory, the inbound attributes — `roles`, `groups`, `entitlements` — look like they settle the question. They do not. RFC 7643 Section 4.1.2 states plainly that no explicit authorization model is defined by those attributes: they are a shape, a list of strings a directory happens to carry. The application still decides what any of them means, which means a catalogue exists either way; the only question is whether it is yours or your customer's. Routing an identity to the right connection and mapping inbound groups onto roles are their own design problems, but neither removes this decision. ## The question to keep asking Can the model express next year's rule without inventing a role per customer? If the answer is yes because of the scoped grant, stay with the catalogue. If it is no, open the vocabulary deliberately and price the migration story first — because the day you publish those strings is the last day renaming one is cheap.

  • A customer wants to call your area_supervisor role district lead. Is that a reason to open up role composition?
    No — that is a display-name problem. Add a per-organisation label on the role and keep one definition. The trigger for opening composition is a capability set no catalogue role expresses, not vocabulary preference; conceding on the label costs almost nothing and conceding on the definition is permanent.
  • If you keep the catalogue, how do you handle the one organisation that genuinely needs a different capability set?
    Add a catalogue role for it and grant it only to them. The role table describes jobs the product supports, and a job one customer has is still a job. That stays reviewable and testable; it is per-organisation authorship that is expensive, not an extra row.
  • How do you know the split migration is actually finished?
    When no role or grant references the old string and no handler demands it, verified by query rather than by belief. Until both are true the compatibility window must stay open; closing it early is exactly what breaks the organisation whose role was composed mid-backfill.
  • What does the exit look like if you opened composition and regret it?
    Cluster the compositions you find, map each cluster onto a catalogue role, run both readable at once, then move organisations across one at a time. It is a migration measured in quarters because every customer's configuration is a stakeholder, which is why the decision deserves the scrutiny up front.

saying these in an interview costs you the question

  • Opens role composition to customers because one asked for a different label.
  • Assumes customer-composed roles are a superset of a seeded catalogue.
  • Renames a permission string in place and expects stored grants to follow.
  • Backfills without a window where the old and the new string are both accepted.
  • Writes a backfill that cannot safely be re-run, then must freeze the administration surface anyway.
  • Believes inbound directory attributes define an authorization model by themselves.