Model a custom relation
Do this when directreader/writer/manager grants cannot express what you
need — a reusable permission bundle assigned to many objects, a new inheritance
edge, or a relation for a resource kind that needs its own semantics.
Do not do this to add a new kind of governed object. Agents, skills, MCP servers
and clients are all resource:<uuid> and the kind lives in the database, so a new
kind needs no model change at all. And do not do this for runtime restrictions —
tool rules, budgets and approvals are policy rules, not graph relations.
Model changes are shared infrastructure. The same store backs workspace
membership and every resource grant, so a bad rollout breaks all authorization at
once. Make changes additive.
Prerequisites
- The
fgaCLI installed. Every step below depends on it. - Write access to
config/auth/openfga/in the repository. - A dev stack you can restart:
ACCESS_CONTROL_BACKEND=openfgawithACCESS_CONTROL_OPENFGA_AUTO_BOOTSTRAP=trueandACCESS_CONTROL_OPENFGA_AUTO_APPLY_MODEL=true, which is whatdocker-compose.dev.yamlsets. Start it withmake up-dev, notmake up— the latter runsdocker-compose.yaml, which names neither the setting nor OpenFGA, so the backend falls through to itsdisableddefault and none of the verification steps below will work. - Read the AgentArea model — you are editing exactly what that page describes.
Steps
1. Choose the shape
A role bundle is the usual answer, and it needs no model change at all. Create arole object carrying the bits, then bind it to a subject and one object with a
role_assignment. This is how you get “reviewer” or “operator” across many
projects without a relation per bundle:
2. Edit the model
Add toconfig/auth/openfga/model.fga. Keep changes additive: a new type, or a
new relation on an existing type. Changing or removing an existing relation
breaks live tuples that reference it.
User, Agent, Workspace); governance types are lowercase (role,
role_assignment, project, resource). And permission bits are independent
— do not write can_write: writer or can_manage, because the model deliberately
has no roll-up.
3. Add tests before you validate
Every new relation needs at least one positive and one negative assertion inconfig/auth/openfga/model.fga.yaml. Add tuples to the shared tuples block and
a case to tests:
4. Validate and test
validate catches DSL errors; test runs
every assertion in the fixture, including the ones that were already there — a
change that breaks existing inheritance shows up here.
5. Generate the deployable model
The JSON is generated, never hand-edited:config/ copy at
/app/config/auth/openfga/authorization-model.json; the Helm chart packages the
charts/ copy as a ConfigMap mounted at
/etc/agentarea/openfga/authorization-model.json.
6. Roll it out
Restart the API and the worker. Both bootstrap OpenFGA at startup: they find or create the store named byACCESS_CONTROL_OPENFGA_STORE_NAME, compare the model
on disk against the models already in the store, write it if it is new, and use
the returned model id for the rest of the process lifetime.
Verify
The CLI tests pass. This is the gate; run it before anything else:Troubleshooting
The new relation is not recognised after a restart. The bootstrap compares models by normalized content, ignoring ids. If your edited model is byte-identical in structure to one already in the store, it reuses that model id and writes nothing — which is correct. If it genuinely differs and is still not picked up, check thatACCESS_CONTROL_OPENFGA_AUTO_APPLY_MODEL=true and that
ACCESS_CONTROL_OPENFGA_MODEL_PATH points at the file you regenerated. With
auto-apply off, the process uses whatever ACCESS_CONTROL_OPENFGA_AUTHORIZATION_MODEL_ID
names and your file is ignored.
It works in compose and not in Kubernetes. You regenerated
config/auth/openfga/authorization-model.json and forgot the copy under
charts/agentarea/files/openfga/. They are two files and nothing keeps them in
sync automatically.
Agents are denied where users are allowed. Role bits are typed
[User:*, Agent:*] and both must be written per enabled bit. A role object
created with only User:* tuples silently denies every agent assignee — a
fail-closed footgun that only appears when a real agent runs.
A manager grant does not confer read. Working as designed. The bits are
independent; write all three when you mean full access. See
grant access to a resource.
422 Unsupported relation for a resource grant when writing through the API.
The relationship endpoint accepts a fixed set of relation names and maps them onto
reader, writer or manager. A relation you added to the model is not
automatically writable through that endpoint — it needs a code change, or write
the tuple directly against OpenFGA during development.
422 Group (subject_set) grants are managed via project/role. The
relationship endpoint writes direct grants only. Bundled access is exactly what
role and role_assignment are for; write those tuples through OpenFGA or a
migration script.
Authorization broke everywhere after the change. You made a destructive edit —
renamed or removed a relation that live tuples reference. Restore the previous
model.fga, regenerate both JSON copies, and restart. OpenFGA keeps prior model
versions, so re-pinning ACCESS_CONTROL_OPENFGA_AUTHORIZATION_MODEL_ID to the
last known-good id is the fastest rollback while you fix the DSL.
Related
- The AgentArea model — the model you are extending, and the invariants the writer enforces outside the graph.
- Grant access to a resource — the direct grants that cover most needs.
- Authorization models — why roles are objects here rather than a table.