Skip to content

Permission System

The Blobs platform combines two access-control mechanisms in one model: system-level roles (who you are) and object-level grants (what you’ve been given on this specific blob). Either alone is enough to authorize most operations; the model only gets interesting when they interact — through inheritance, public flags, and overrides.

The check is fast (constant-time for direct grants, O(depth) for inherited ones) and lives in the service layer so it’s enforced uniformly regardless of whether a request comes via REST, gRPC, or the WebSocket subscriber.

Authorization Model

has

owns

granted

on

inherits from

Permission Check (per request)

Yes

No

Yes

No

Yes

No

Yes

No

Yes

No

Check Permission

System role:

admin?

✓ Allow

Is owner /

author?

is_public

and read?

Direct grant

on this blob?

inherit_permissions

and parent exists?

✗ Deny

User

System Role

admin / moderator / user

Blob Resource

Object Grant

read · write · delete · share

Parent Blob

The check short-circuits as soon as something says yes — admin always wins, ownership trumps grants, public-readable bypasses the explicit-grant check for read operations, and inheritance walks up the parent chain only when none of the direct paths matched.

Permission Types

The four object-level grants are the leaves of the model — every authorization decision ultimately reduces to one of them.

  • read — view blob content + metadata. Required to GET, list, or subscribe to events for a blob. Bypassed by is_public = true.
  • write — modify the blob’s content, title, type, or visibility flags. Does not include the right to change the permission set itself; that requires share.
  • delete — soft-delete the blob (and, by cascade, its descendants — see Cascade Soft-Delete). The owner / admin / anyone with delete can do it.
  • share — grant other users permissions on this blob. share is itself a permission, so it’s distributable: an owner can give one collaborator share and let them onboard the rest of the team without re-touching the blob themselves.

Grants are additive; there’s no concept of an explicit deny. Removal is by revocation, not by writing a “denied” row.

System Roles

System roles override the per-blob model in two narrow places:

RoleWhat it overrides
adminBypasses every per-blob check — admins can read, write, delete, and share any blob in any organization. Used for incident response, data export, support escalation.
moderatorCan list, search, and read every blob; cannot write or delete unless they have a per-blob grant. Used for content review, audit work.
userThe default. Only permissions on blobs they own or have been granted.

The admin override is intentionally absolute — the platform’s security boundary stops outside the platform (at the auth server’s JWT signing) rather than inside the permission check. Trust the sysadmin or revoke their account; there’s no in-platform “admin but not really” tier.

Inheritance

Every blob has an inherit_permissions flag, defaulting to true. When set, the child has no permission rows of its own — it transparently uses its parent’s. The most common pattern is a folder-style hierarchy where a single share at the top reaches every descendant.

Root Blob

Owner: Alice

Grants: Bob: read+write

Child Blob 1

inherit_permissions: true

Child Blob 2

inherit_permissions: false

Grants: Carol: read

Grandchild

inherit_permissions: true

Grandchild 2

inherit_permissions: true

Bob has read+write

(via Root)

Bob is denied

(Child2 doesn't inherit)

Carol has read

Bob has read+write

(walks Child1 → Root)

Carol has read

(walks GrandChild2 → Child2)

A few non-obvious behaviors worth knowing:

  • Inheritance is replacement, not merge. When inherit_permissions: true, the child’s own grants list is ignored and the parent’s set is used wholesale. Flipping back to false re-activates whatever per-blob grants the child had stored.

  • Inheritance walks all the way up. A grandchild that inherits from a child that inherits from a root effectively has the root’s grants. There’s no depth limit; the lookup is short and cached per-request.

  • The root’s owner is the canonical authority for the subtree. Sharing on the root reaches every inherit_permissions: true descendant — even ones added later. That’s the property that makes “share my notebook” a single-click operation.

  • Visibility inherits too. An inheriting blob is Public or Private exactly as its nearest non-inheriting ancestor is, and so is “viewable without signing in” (anonymous_access). The database keeps the child’s stored is_public / anonymous_access equal to the parent’s (a trigger copies them on create, move and switch-to-inherit, and carries a change on the parent down through every inheriting descendant), so lists, search, public links and file access all see the effective value.

    ActionResult
    Child created under a public parent (inherit on, the default)Child is public
    Parent switched to privateEvery inheriting descendant becomes private
    Visibility changed directly on an inheriting childThat child stops inheriting and keeps the new value
    Child switched back to inheritIt takes its parent’s visibility again
  • Independent subtrees coexist with inheriting ones. A team workspace can have a public discussion area (inheriting from the team root) alongside a private brainstorm folder (inherit_permissions: false with a tighter grant set) without either affecting the other.

Patterns

The model is small enough to keep in your head, but a few patterns come up often:

Shared workspace

Set inherit_permissions: true on every blob in the workspace. Grant team members read+write on the workspace root once. New blobs added under the root automatically inherit. Removing a member is a single revocation on the root.

Private brainstorm

Set inherit_permissions: false on a blob inside a shared workspace. The blob now has its own permission scope — only people you explicitly grant can see it, even though it lives in a shared tree. Useful for draft work, performance reviews, anything you want hidden from the rest of the team.

Public reference doc

Set is_public: true on a blob. Any user (and, depending on the network’s auth config, anonymous visitors) can read it without an explicit grant. Writes still require ownership or a write grant — public is a read override only.

Distributable sharing

Grant a trusted collaborator share permission. They can onboard others onto the blob without you needing to do it. Combined with inherit_permissions: true on a workspace root, this lets a team lead manage membership without bothering the owner.

Soft revocation

Removing a grant is immediate but the user’s existing JWT-derived sessions stay valid until their next request — at which point they’ll get the new (denied) result. There’s no need to invalidate tokens for permission changes; the check happens server-side on every request.

Edge Cases

  • Public + inheritance. is_public is per-blob and does not inherit, even with inherit_permissions: true. A public root does not implicitly publicize its descendants. This is deliberate — accidental “public-by-cascade” would be a security footgun.
  • Owner change. Ownership is an attribute of the blob row, not a grant. Transferring ownership is a single field update; it doesn’t disturb the existing grants list.
  • Share on yourself. A user always has full permissions on blobs they own; sharing the same set with themselves is a no-op and the platform doesn’t store self-grants.
  • Soft-deleted blob. A blob with deleted_at != null returns 404 to non-admins regardless of grants. The grant rows survive — restore brings the blob and its permissions back atomically.
  • Anonymous reads. When the network is configured for anonymous-readable public content, the permission check still runs but the “user” is the special anonymous principal. Only read on is_public blobs ever resolves to allow.

Where the Code Lives

  • Backend: apps/api/modules/blobs/permissions.go — the single PermissionService other modules consume.
  • SDK: PermissionsService — wraps grant/revoke/list endpoints; permission checks happen server-side, so the client only needs to handle authorization errors uniformly.
  • Wire types: Permission, BlobPermissions — the canonical request/response shapes for the grant CRUD surface.