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
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 byis_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 requiresshare.delete— soft-delete the blob (and, by cascade, its descendants — see Cascade Soft-Delete). The owner / admin / anyone withdeletecan do it.share— grant other users permissions on this blob.shareis itself a permission, so it’s distributable: an owner can give one collaboratorshareand 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:
| Role | What it overrides |
|---|---|
admin | Bypasses every per-blob check — admins can read, write, delete, and share any blob in any organization. Used for incident response, data export, support escalation. |
moderator | Can list, search, and read every blob; cannot write or delete unless they have a per-blob grant. Used for content review, audit work. |
user | The 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.
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 tofalsere-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: truedescendant — 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 storedis_public/anonymous_accessequal 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.Action Result Child created under a public parent (inherit on, the default) Child is public Parent switched to private Every inheriting descendant becomes private Visibility changed directly on an inheriting child That child stops inheriting and keeps the new value Child switched back to inherit It 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: falsewith 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_publicis per-blob and does not inherit, even withinherit_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 != nullreturns404to 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
readonis_publicblobs ever resolves to allow.
Where the Code Lives
- Backend:
apps/api/modules/blobs/permissions.go— the singlePermissionServiceother 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.