Scaling and OpenFGA
How fast the SQL engine is, what we deliberately don't cache, and how OpenFGA could plug in later.
The default authorizer needs nothing but your database. It's fast enough for a homelab and, with batching, for big tenants. When it isn't, the Authorizer interface is the seam where a heavier engine can slot in.
How Fast Is It?
Measured on Postgres 16 with 1M items at depth 20, 100k grants and 500k group-closure rows:
| Case | Latency | Throughput |
|---|---|---|
| One full check, one client | 0.26 ms | ~3,900/s |
| One full check, 8 clients | 0.65 ms | ~12,000/s |
Through the Go implementation (several round trips, over Docker) one Caps call is about 1-1.5 ms, and 64 items in one CapsMany call take about 8 ms. Batching is the real lever: checking a page of results costs one trip, not one per item.
Listing a folder is one check on the folder plus one batched check on the page, because children inherit. You never pay per file.
What We Don't Cache (Yet)
There's no permission cache, which makes revocation instant: remove a grant and the very next request is refused. We decided that's worth more than shaving a millisecond.
Known Gaps
- Download passports are signed tokens valid for 24 hours. Someone who already opened a download keeps fetching chunks after losing access. Shorten the lifetime before relying on instant revocation for downloads.
- Anonymous reads aren't rate limited.
- No trash or soft delete yet, and no user-deletion purge. Grants and drives carry
drive_idso a purge is a flat batch, not a tree walk. - Searching across shared content needs a pre-filter to the roots you can reach; there's no "everything I can see" query by design.
- Moving between drives is rejected for now.
- Group membership and user deletion don't go through the change rules yet. Emptying a group that is a drive's only admin, or deleting its last admin user, can still orphan a drive.
OpenFGA
OpenFGA is a Zanzibar-style engine: you store relationship tuples (user, relation, object) and ask questions with Check, BatchCheck, ListObjects and ListUsers. It can run embedded in the Go server (as Grafana does with Zanzana) or as its own service, over Postgres, MySQL or SQLite.
It isn't built yet. If a customer needs fine-grained features beyond SQL (conditions, ListUsers audits, giant group graphs), it would be an EE engine behind the same Authorizer interface, never a replacement for the SQL one.
Two Ways To Wire It
| Light sync | Full sync | |
|---|---|---|
| Upload or move | No OpenFGA write | One tuple write per item (a move is delete + write) |
| Consistency | Nothing to race | A new file is denied until its tuple lands, so an outbox plus owner-first checks are needed |
ListObjects | Not available, so "shared with me" stays an indexed SQL lookup on a thin grant index | Available, but slow on big hierarchies |
| Best for | Starting out | The very largest tenants that need enumeration |
Duplication stays proportional to grants, not files, in the light mode. And because the Authorizer owns writes too, only one engine is authoritative at a time. Switching is a one-time backfill, never a continuous dual-write.
What An Engine Must Provide
The Authorizer interface says what an engine does. Four things decide whether a new engine, OpenFGA included, behaves like the SQL one:
- The same change rules.
authz.CheckChangeis engine-neutral Go. An engine builds aChange(the grants before and after, plusItemFacts) and calls it, rather than re-implementing "no self-edit" or "never orphan" in its own terms. Skip this and the engines drift. - Item facts from SQL. Whether an item is a drive root, sits in a shared drive or inherits comes from the item tree, which stays in SQL in the light-sync mode. The engine reads it from there.
- A serialization point.
never-orphanis a read-check-write: two admins removing each other at once must not both pass. SQL takes a row lock on the drive. OpenFGA has no transactions or row locks, so an OpenFGA engine still takes that lock on the SQL drive row around its read and write. - The shared scenarios.
authztest.Runtakes aWorldthat builds a tenant, users, groups and drives in the engine's own storage, then runs the scenarios against theAuthorizerinterface alone. The SQL engine runs them insqlauthz/conformance_test.go. A new engine passes them before it ships.
What The Model Might Look Like
A sketch of the light-sync model: our roles become relations, and the tree isn't in OpenFGA at all.
model
schema 1.1
type user
type group
relations
define member: [user, group#member]
type tenant
relations
define member: [user]
type item
relations
define viewer: [user, group#member, tenant#member, user:*]
define editor: [user, group#member, tenant#member]
define can_view: viewer or editor
define can_edit: editor