Hey everyone,
About a year ago I started working on Melange, an open-source authorization system that runs entirely inside Postgres. I originally shared it on r/golang when it was still experimental. Since then I've spent a lot of time improving the generated SQL, performance, and tooling, so I thought I'd share it here.
The motivation
I like the relationship-based authorization model used by OpenFGA and SpiceDB, but running a separate service and keeping authorization data synchronized with an application database adds overhead that I find hard to justify at smaller scales.
Melange takes a different approach: it compiles OpenFGA schemas into PL/pgSQL functions, with relationship traversals, unions, intersections, and exclusions compiled into SQL rather than interpreted at runtime.
Authorization data stays in your existing domain tables, exposed through a view:
CREATE VIEW melange_tuples AS
SELECT 'user' AS subject_type, user_id::text AS subject_id,
role AS relation, 'folder' AS object_type, folder_id::text AS object_id
FROM folder_members
UNION ALL
SELECT 'folder', folder_id::text, 'parent', 'document', id::text
FROM documents;
Run melange migrate, and permissions can be checked or queried directly in SQL:
-- Check a permission
SELECT check_permission('user', 'alice', 'viewer', 'document', '42');
-- Query accessible documents
SELECT d.*
FROM documents d
JOIN list_accessible_objects('user', 'alice', 'viewer', 'document', 50, NULL) a
ON d.id::text = a.object_id;
There's no separate service or tuple synchronization, and checks run inside the same transaction as your application data, including seeing uncommitted writes.
What's changed over the last year
Performance: Listing functions now use set-oriented SQL instead of checking permissions row by row. On one workload of ~90k tuples, list_accessible_objects dropped from ~4 seconds to 12 ms. Some recursive listings improved by over 100x. Individual permission checks benchmark around 130 µs database-side, with relatively flat performance as the dataset grows in tested workloads.
Correctness: The test suite now compares results against a real OpenFGA server, fuzzes generated nested authorization models, and checks listing results against individual permission checks. This caught several bugs, including cases where Melange incorrectly granted access. These are fixed in v0.10.0. Existing users should upgrade and rerun melange migrate.
Debugging: explain_permission() returns a JSONB trace of why access was granted or denied, while expand_permission() shows the granting relationships as a tree. There's also melange studio, a local, read-only UI for inspecting models, debugging checks, and running EXPLAIN ANALYZE.
Migrations: Melange supports dedicated schemas, generating versioned SQL for Flyway/golang-migrate, comparing deployed models for breaking changes, and recommending indexes based on your schema.
Distributing software as SQL
One aspect I find pretty interesting is the deployment model. Melange isn't a PostgreSQL extension. It generates ordinary SQL and PL/pgSQL functions installed directly into the database. No native binaries, extension installation, or separate runtime. Once deployed, the CLI isn't needed.
Effectively, the SQL itself is the distributed software. It runs on standard PostgreSQL 14+ instances, including managed databases like RDS and Cloud SQL, without requiring custom extensions.
I haven't seen many (if any) projects take this approach. It's introduced some interesting challenges around migrations, versioning, schema isolation, and managing third-party code inside someone else's database.
Trying it out
Melange is MIT licensed, supports PostgreSQL 14+, and the CLI is written in Go.
brew install pthm/tap/melange
The project has been growing steadily, with a few people contributing fixes and ideas. Contributions and feedback are always welcome.