Skip to content
Melange v0.10.0

Melange v0.10.0

October 20, 2026·pthm
pthm

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

  1. Install v0.10.0 and run melange validate. If it fails, fix the schema as described in Rejected schemas.
  2. Run melange migrate, or melange generate migration if you use versioned migrations, to replace the generated SQL.
  3. Go library users only: parser.ConvertProtoModel now 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:

RelationBefore v0.10.0
((a but not b) but not c) and dOnly the outermost exclusion applied: a user with a, b and d was allowed
(a but not (b or c)) and dAn exclusion of anything but a plain relation was dropped
[user] and a from parentA direct tuple alone granted the relation, without a on the parent
([user] and a) or cSame: 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] where group has no admin, or editor from parent where no parent type has editor. 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_subjects path.
  • Wildcards narrowed by an exclusion. For b but not r1 with b: [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_subjectsv0.9.6v0.10.0
folder viewer1,206.7 ms3.7 ms
folder editor93.7 ms0.30 ms
document viewer1,483.4 ms26.7 ms
document can_view1,715.3 ms52.2 ms
document gated (intersection)841.3 ms0.31 ms
document can_edit (intersection)799.1 ms0.30 ms

list_accessible_objects for document can_view fell from 44.3 ms to 21.7 ms at 100K.

Coding agents

  • melange init can add a short Melange section to your AGENTS.md or CLAUDE.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; -y adds it only with --agents.

  • melange init agent prints the same section from your config, for existing projects:

    melange init agent >> AGENTS.md
  • melange validate --format json and melange doctor --format json give scripts and agents structured results. validate now also detects relation cycles, matching what migrate checks.

  • Docs for agents. /llms.txt indexes the docs with descriptions, /llms-full.txt holds 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/melange

Feedback

Report security issues through private vulnerability reporting. Open an issue for bugs or feature requests.