department_id in app_metadata controlling what they can access, or an organization’s plan in metadata gating a feature. The same pattern works for any other attribute on these objects; see Other attributes.
Auth0 doesn’t emit a dedicated event per field. Instead, user.updated, organization.updated, and connection.updated fire on any change to the object. The event carries the object’s entire new state in data.object, together with the state before the change in data.previous_object. The tuples that result from a change have to be derived from these two sources, and there are two ways to do it: Option 1 and Option 2, compared under Choosing an approach.
This only works when the value maps onto a bounded, enumerable relation: a department, a cost center, a plan tier, something you’d be comfortable writing as sometype:{value} in your model. Free-form or high-cardinality values (an arbitrary text note, a user’s date of birth, anything with effectively unlimited distinct values) don’t translate into FGA tuples usefully.
Use app_metadata, not user_metadata, for anything that grants access. user_metadata is intended for data the user can edit, so a permission derived from it can be changed by the user it applies to.
Model
The same shape applies to organization metadata: swapuser/department for organization/plan, as in the subscription plan example.
Option 1: Diff against previous_object
Compare the field in data.object with the same field in data.previous_object, and act only when they differ. Use ?. so a user with no app_metadata yet doesn’t make the rule fail.
When the field changes from
a to b, both rules fire on the same event. Every write and delete ignores conflicts (see Idempotent writes and deletes), so reprocessing an event is safe.
Option 2: Reconcile with tuple filters
Ignoreprevious_object and reconcile against what FGA holds now. On every update, the mapping engine reads the tuples matching the filter, compares them to the desired state in tuples, and writes the minimum changes. This is the reconcile pattern (read-diff-write), the same one used for the organization plan in the mapping language reference.
action: delete.
Because the second rule matches every user.updated event where the field is absent, it deletes department tuples for all those users, including ones your application wrote. This is the incompatibility noted in the table above.
Choosing an approach
If Auth0 is the only writer of the relation and you’d rather it correct itself, choose Option 2. If your application writes tuples of the same shape, or event volume is a concern, choose Option 1 and plan to reconcile periodically.
Other entities
The examples above use a user’sapp_metadata. Swap the event and the path for other entities:
For Option 1, read the same path from
input.data.previous_object instead of input.data.object.
Other attributes
Nothing here is specific to metadata. Any field ondata.object works the same way, as long as its value maps onto a bounded relation. On a user, for example, a boolean such as blocked or email_verified can drive a relation, and so can an identity-provider field such as the connection a user signed up through.
Point the rules at the field’s path instead of app_metadata.*. For a top-level user field, that is input.data.object.<field> and input.data.previous_object.<field>. Check the field’s exact path in the event schema, since nested fields such as identities are arrays and need an iterator rather than a direct path.
Don’t use an email address, phone number, or other personally identifiable attribute as the value that ends up in a tuple; see Data privacy. Map it to an opaque value, such as a domain ID, first.
Known limitations
- Both options handle one field at a time. If several attribute-driven relations change in the same update, each needs its own rule set. For Option 2, that can be its own
tuple_filtersentry within one rule, scoped to that relation; a single filter can’t reconcile two different relations against two different desired values at once.