Melange v0.10.0
Melange v0.10.0 compiles nested combinations of and, or and but not exactly. Earlier versions approximated some of them, and several approximations granted access that OpenFGA denies. It also fixes list functions that disagreed with check_permission, makes recursive and intersection listings up to three orders of magnitude faster, rejects schemas that reference undefined relations, and adds support for working with Melange through coding agents.
Security fix, run melange migrate. Relations that combine and with but not, or that use a direct assignment inside an and, could allow users OpenFGA denies. The fix is in the generated SQL, so it takes effect when you migrate.
Breaking: some schemas that used to compile are now rejected, and parser.ConvertProtoModel returns an error. Run melange validate with v0.10.0 before upgrading. See Upgrading.
Upgrading
- Install v0.10.0 and run
melange validate. If it fails, fix the schema as described in Rejected schemas. - Run
melange migrate, ormelange generate migrationif you use versioned migrations, to replace the generated SQL. - Go library users only:
parser.ConvertProtoModelnow returns([]schema.TypeDefinition, error).
The Go and TypeScript runtimes have no API changes. Building the CLI from source now needs Go 1.27; the runtime module still supports Go 1.24.
Check fixes
The parser flattened OpenFGA expressions into Melange’s relation format with helpers that each handled part of the tree. Some nested shapes were compiled approximately, and the approximations widened access:
| Relation | Before v0.10.0 |
|---|---|
((a but not b) but not c) and d | Only the outermost exclusion applied: a user with a, b and d was allowed |
(a but not (b or c)) and d | An exclusion of anything but a plain relation was dropped |
[user] and a from parent | A direct tuple alone granted the relation, without a on the parent |
([user] and a) or c | Same: the direct tuple granted on its own |
The parser now rewrites every relation into a union of intersections (disjunctive normal form) and compiles each intersection exactly, keeping every exclusion. Relations that were already compiled exactly are unaffected by the rewrite.
Rejected schemas
Three kinds of schema now fail validate, migrate and generate instead of compiling to something other than what they say:
- References to undefined types or relations.
viewer: [user] or editr,[usr],[group#admin]wheregrouphas noadmin, oreditor from parentwhere no parent type haseditor. These used to compile to a branch that never matched, so a typo silently denied. OpenFGA rejects them too. The error names every bad reference. - A parent relation excluded inside a compound expression, such as
(viewer but not blocked from parent) and active. Move the sub-expression into its own relation; see OpenFGA Compatibility. - Expressions that expand past 64 terms in normal form.
Models already deployed are still readable: status, diff, doctor and the previous revision in generate migration skip the reference check, so the migration that fixes a typo can still be generated.
List fixes
list_accessible_objects and list_accessible_subjects returned results that disagreed with check_permission in these cases:
- Exclusions in composed and userset lists. Two list paths applied only some kinds of exclusion (not excluded parent relations or intersections), and the simple userset block tested the userset tuple instead of the member it returns.
- Exclusions on usersets from implied relations, in the recursive
list_subjectspath. - Wildcards narrowed by an exclusion. For
b but not r1withb: [user, user:*], users covered by the wildcard who escape the exclusion were not listed. - Relations made of several parent lookups, such as
v from parent or b from parent. Only the first lookup was listed. - Userset filters and cross-type recursion. A userset granted through an intersection part’s exclusion or parent lookup, or a grant two or more folders up a cross-type hierarchy, was missed.
- Top-level paths next to intersection groups were dropped by two list strategies.
Performance
These are historical release measurements, not the October 2026 Cloud SQL results. The cloud run does not revalidate these before/after figures. For current hardware, schemas, measured ranges, and limitations, see the Performance reference.
Several list strategies used to check every subject in the tuples table, one full check each. They now enumerate candidates directly:
- Recursive parents such as
folder.viewer([user, group#member] or viewer from parent or member from org) walk the parent closure instead of checking each subject against every parent link. - Intersections list one part’s subjects as candidates and filter those, instead of the whole subject pool.
- Implied relations covered by another composed relation are listed once, and check functions skip implied and exclusion checks another callee already makes.
Kitchen-sink schema at 100K tuples, median of 5:
list_accessible_subjects | v0.9.6 | v0.10.0 |
|---|---|---|
folder viewer | 1,206.7 ms | 3.7 ms |
folder editor | 93.7 ms | 0.30 ms |
document viewer | 1,483.4 ms | 26.7 ms |
document can_view | 1,715.3 ms | 52.2 ms |
document gated (intersection) | 841.3 ms | 0.31 ms |
document can_edit (intersection) | 799.1 ms | 0.30 ms |
list_accessible_objects for document can_view fell from 44.3 ms to 21.7 ms at 100K.
Coding agents
melange initcan add a short Melange section to yourAGENTS.mdorCLAUDE.md: where the schema lives, that relationships come from the tuples view, the commands to run after editing the model, and guardrails such as only migrating a local database. The wizard asks;-yadds it only with--agents.melange init agentprints the same section from your config, for existing projects:melange init agent >> AGENTS.mdmelange validate --format jsonandmelange doctor --format jsongive scripts and agents structured results.validatenow also detects relation cycles, matching whatmigratechecks.Docs for agents.
/llms.txtindexes the docs with descriptions,/llms-full.txtholds every page in one file, and any page is available as markdown at<page>.md. See Using Coding Agents.
Testing
TestOpenFGA_GeneratedNested generates nested models (expressions up to three levels deep over or, and and but not, with wildcards, usersets and two-level folder recursion) and compares every Check, ListObjects and ListUsers answer with an in-process OpenFGA server. It found most of the fixes above. A pure-Go oracle compares the flattened relations with OpenFGA’s expression semantics over every tuple assignment.
The OpenFGA test oracle moves to v1.22.0, adding the conformance cases up to v1.21. MELANGE_TEST_PG_IMAGE runs the suites against another PostgreSQL image.
Try it out
# Install / upgrade CLI
brew install pthm/tap/melange
# Or pull the container image
docker pull ghcr.io/pthm/melange:v0.10.0
# Check the schema, then replace the generated SQL
melange validate
melange migrate
# Go runtime
go get github.com/pthm/melange/melange@v0.10.0
# TypeScript runtime
npm install @pthm/melangeFeedback
Report security issues through private vulnerability reporting. Open an issue for bugs or feature requests.
