Melange v0.7.0

Melange v0.7.0

February 7, 2026·pthm
pthm

Melange v0.7.0 introduces the Bulk Check API for batching multiple permission checks into a single SQL call, alongside multi-driver testing, module layout improvements, and a bug fix for intersection parsing.

No breaking changes from v0.6.4. Upgrade and run melange migrate to install the new check_permission_bulk function.

Highlights

Bulk Permission Check API

The headline feature of v0.7.0 is check_permission_bulk (#11) — a new SQL function that evaluates multiple permission checks in a single database call. This is designed for the common pattern of checking many permissions at once, such as filtering a list of resources for display or rendering UI with permission-aware controls.

The problem: An application rendering a dashboard with 50 documents previously needed 50 individual check_permission calls. Even with connection pooling, this means 50 round-trips to the database.

The solution: check_permission_bulk accepts parallel arrays of subjects, relations, and objects, evaluates them all in one call, and returns a row per check indicating allowed or denied.

SELECT * FROM check_permission_bulk(
    ARRAY['user',   'user'],        -- subject types
    ARRAY['alice',  'alice'],       -- subject IDs
    ARRAY['viewer', 'editor'],      -- relations
    ARRAY['document', 'document'],  -- object types
    ARRAY['doc-1',  'doc-2']        -- object IDs
);
-- Returns:
--  idx | allowed
-- -----+---------
--    0 |       1
--    1 |       0

The generated SQL is smart about this — it groups branches by object type with IF guards so that a batch checking only document permissions never touches folder branches, and simple direct-assignment relations are inlined as EXISTS subqueries to avoid function call overhead entirely.

Go API

The Go runtime provides a fluent BulkCheckBuilder:

results, err := checker.NewBulkCheck(ctx).
    Add(user, "viewer", doc1).
    Add(user, "editor", doc2).
    AddWithID("custom-id", user, "owner", doc3).
    Execute()

// Aggregate checks
if results.All() { /* all granted */ }
if results.Any() { /* at least one granted */ }

// Iterate results
for _, r := range results.Allowed() {
    fmt.Printf("%s can %s %s\n", r.Subject(), r.Relation(), r.Object())
}

// Fail-fast: returns error if any check is denied
err = results.AllOrError() // returns BulkCheckDeniedError

TypeScript API

Feature parity with Go:

const results = await checker
  .newBulkCheck()
  .add(user, "viewer", doc1)
  .add(user, "editor", doc2)
  .addWithId("custom-id", user, "owner", doc3)
  .execute();

if (results.all()) {
  /* all granted */
}
if (results.any()) {
  /* at least one granted */
}

results.allOrError(); // throws BulkCheckDeniedError if any denied

Both clients include:

  • Automatic deduplication — identical checks are collapsed into a single SQL row, results fanned out to all original indices
  • Cache integration — reads from the Checker’s cache before SQL, populates it after
  • Decision overrides — works with DecisionAllow/DecisionDeny for testing without a database
  • Batch size guardMaxBulkCheckSize (10,000) prevents accidental resource exhaustion

Thanks to @sonalys for requesting this feature.

Bug Fix: Intersection with Tuple-to-Userset Unions

Fixed a bug (#13) where intersection rules containing unions of tuple-to-userset patterns were silently dropped during parsing. For example:

define viewer: writer and (member from group or owner from group)

The union (member from group or owner from group) was ignored, causing the generated SQL to only check writer — a permission bypass. The parser now correctly extracts both simple relations and tuple-to-userset patterns from unions and distributes them into intersection groups using the distributive law.

Thanks to @jake-wickstrom for reporting this.

Multi-Driver Integration Tests

The Checker interfaces (Querier in Go, Queryable in TypeScript) are designed to work with any PostgreSQL driver, but previously only one driver per language was tested end-to-end. v0.7.0 adds table-driven tests that run the same assertion suite against each supported driver:

  • Go: pgx/stdlib and lib/pq, plus transaction isolation
  • TypeScript: pg (node-postgres) and postgres.js via postgresAdapter()

This ensures that driver-specific edge cases (array encoding, type coercion, connection handling) are caught before release.

Module Layout and go install Fix

go install github.com/pthm/melange/cmd/melange@latest was broken due to a replace directive in go.mod and stale cmd/melange/ tags cached in the Go module proxy. v0.7.0 fixes this by:

  • Removing the replace directive (redundant since go.work handles local development)
  • Renaming internal/ to lib/ so cmd/melange can be a proper sub-module that imports root packages across module boundaries
  • Restoring cmd/melange as a sub-module with retracted stale versions
  • Restructuring the release process into two phases to ensure the Go proxy indexes correctly

go install @latest now works reliably.

CI and Testing Improvements

  • Race detection added to the OpenFGA test workflow
  • TypeScript unit tests now run independently of the database in CI
  • Linting and formatting extended to cover all four Go modules (root, cmd/melange, melange, docs)
  • CI parallelism fix — test packages now run sequentially on the shared CI database to prevent deadlocks

Documentation Updates

  • Checking Permissions guide rewritten to use the @pthm/melange TypeScript client throughout (previously showed raw SQL)
  • SQL API reference updated with full check_permission_bulk documentation
  • Bulk check guide with Go, TypeScript, and SQL examples

What’s Changed Since v0.6.4

  • Bulk permission checkcheck_permission_bulk SQL function, Go BulkCheckBuilder, TypeScript BulkCheckBuilder
  • Bulk dispatcher optimization — branches grouped by object type with IF guards, simple relations inlined as EXISTS
  • Intersection + TTU union bug fix — silently dropped tuple-to-userset unions in intersections (#13)
  • Multi-driver tests — Go (pgx, lib/pq) and TypeScript (pg, postgres.js) integration tests
  • Module restructuringinternal/lib/, cmd/melange sub-module restored, replace directive removed
  • go install @latest fixed — works reliably with the Go module proxy
  • CI improvements — race detection, TypeScript unit tests in CI, all-module linting, parallelism fix
  • Documentation — TypeScript client examples, bulk check guide, SQL API reference
  • Dependency update — OpenFGA v1.11.2 → v1.11.3

Migration Notes

From v0.6.4

No breaking changes. Upgrade and run migrations to install the new bulk dispatcher:

melange migrate

The new check_permission_bulk function is installed alongside existing functions. Your current check_permission, list_accessible_objects, and list_accessible_subjects calls continue to work unchanged.

Go module path: If you were using go install, the module path is unchanged but now resolves correctly via the Go proxy.

go install github.com/pthm/melange/cmd/melange@latest

Try It Out

# Install / upgrade CLI
brew install pthm/melange/melange

# Apply migrations (installs check_permission_bulk)
melange migrate

# Go runtime
go get github.com/pthm/melange/melange@v0.7.0

# TypeScript runtime
npm install @pthm/melange

Feedback

We welcome feedback on the bulk check API and general experience. Please open an issue with questions, bug reports, or feature requests.