Olympus Docs
CookbookIntegrations & billing

Zanzibar-style relationship authz

For document/project/folder permissions

Zanzibar is Google's authorization system; OpenFGA and SpiceDB are open-source spiritual descendants. The model: permissions emerge from relationships.

When to use ReBAC

When permissions are about resource-relationships:

  • "Alice can edit this document because she's a member of the project that owns it."
  • "Bob can view this folder because he was invited by an admin."
  • "Carol can manage billing because she's the org's owner."

vs RBAC:

  • "Alice is an admin (globally)."

ReBAC fits collaboration (Google Drive, Notion, etc.). RBAC fits SaaS roles.

Model

namespace user
namespace doc
  relation owner: user
  relation editor: user | group#member
  relation viewer: user | group#member
  permission read = owner | editor | viewer
  permission write = owner | editor
  permission delete = owner

namespace group
  relation member: user

Relationships:

  • doc:123#owner@user:alice, alice owns doc 123.
  • doc:123#editor@user:bob, bob can edit doc 123.
  • doc:123#viewer@group:project-1#member, group project-1's members can view doc 123.
  • group:project-1#member@user:carol, carol is a member of project-1.

Compute permission:

  • Can carol read doc 123? → carol is in project-1, project-1 viewers can read doc 123 → yes.
  • Can carol write doc 123? → no (project-1 only viewers).

OpenFGA setup

# Run OpenFGA
podman run -d --name openfga \
  -p 8080:8080 \
  -p 8081:8081 \
  openfga/openfga

Define model:

fga store create --name "olympus-app"
fga model write --store-id $STORE_ID --file model.fga

Add tuples (relationships):

import { OpenFgaClient } from "@openfga/sdk";

const fga = new OpenFgaClient({ apiUrl: "http://localhost:8080", storeId: STORE_ID });

await fga.write({
  writes: [
    { user: "user:alice", relation: "owner", object: "doc:123" },
    { user: "user:bob", relation: "editor", object: "doc:123" },
  ],
});

Check permissions:

const { allowed } = await fga.check({
  user: "user:carol",
  relation: "read",
  object: "doc:123",
});

Hooking into Olympus

// Express middleware
app.get("/docs/:id", async (req, res) => {
  const session = await getSession(req);
  const { allowed } = await fga.check({
    user: `user:${session.identity.id}`,
    relation: "read",
    object: `doc:${req.params.id}`,
  });
  if (!allowed) return res.status(403).send("Forbidden");
  // ... fetch and return doc
});

Hierarchical relationships

folder
  parent: folder
  reader: user | folder#reader  # inherit from parent

So if alice has folder:root#reader, she also has read on folder:nested-1 if parent is root.

Massively expressive.

Performance

OpenFGA / SpiceDB scale to millions of relationships, microsecond reads. Far beyond raw Postgres for these queries.

For < 100k relationships: Postgres is fine. For millions: dedicated.

Sharing UI

// Share a doc
<form action={share}>
  <input name="email" placeholder="user@example.com" />
  <select name="role">
    <option value="viewer">Can view</option>
    <option value="editor">Can edit</option>
  </select>
  <button>Share</button>
</form>
async function share(formData: FormData) {
  const email = formData.get("email");
  const role = formData.get("role");
  const identity = await kratos.findIdentityByEmail(email);
  await fga.write({
    writes: [{ user: `user:${identity.id}`, relation: role, object: `doc:${docId}` }],
  });
}

Removing access

await fga.write({
  deletes: [{ user: `user:${userId}`, relation: "editor", object: `doc:${docId}` }],
});

Listing who has access

const tuples = await fga.read({ object: `doc:${docId}` });
// Returns all relationships for this doc

UI: show people who can view/edit this doc.

Listing what you have access to

const accessible = await fga.listObjects({
  user: `user:${userId}`,
  relation: "read",
  type: "doc",
});
// Returns all docs this user can read

For listing pages: query FGA, then fetch metadata for each.

OpenFGA vs SpiceDB

OpenFGASpiceDB
OriginAuth0 / OktaAuthZed
HostingSelf-host + cloudSelf-host + cloud
APIgRPC + RESTgRPC + REST
MaturityYoungerMore mature
OSS licenseApache 2.0Apache 2.0

Both fine. OpenFGA is slightly simpler getting started; SpiceDB has more advanced features.

Storage

FGA stores its own DB. Don't dual-write (FGA + Postgres), diverges.

When a user signs up in Olympus, add a basic relationship if needed:

// Post-registration hook
await fga.write({
  writes: [{ user: `user:${identity.id}`, relation: "self", object: `user:${identity.id}` }],
});

(Some models have a "self" relation for shorthand.)

Audit

FGA has its own audit log of write operations. Pair with your app's audit:

audit({ event: "doc_shared", actor: alice, target: bob, resource: docId, relation: "editor" });

On this page