Skip to content

Blobs Module

The Blobs module is the heart of the platform — every piece of user-authored content (a note, a folder, a published page, a draft, a comment) is stored as a “blob.” Blobs form a recursive parent/child tree, carry per-object permissions, can attach binary Media, and participate in the platform-wide Permission System. This page covers the runtime architecture of the module and how it cooperates with the rest of the system.

Want to inspect the SDK type graph interactively? Use the Type Explorer button in the top-right of the page header — it opens a global, draggable canvas that can spawn any class/interface from the entire SDK, with edges following property → type references.

Class Diagram

uses

uses

caches

queries

Controller

-service: Service

-logger: Logger

+CreateBlob(w, r)

+GetBlob(w, r)

+ListBlobs(w, r)

+UpdateBlob(w, r)

+DeleteBlob(w, r)

+GetBlobChildren(w, r)

+RemoveChildBlob(w, r)

+RestoreBlob(w, r)

+BulkCreateBlobs(w, r)

+BulkDeleteBlobs(w, r)

Service

-repo: Repository

-cache: Cache

-logger: Logger

+SetStorageProviderGetter()

+CreateBlob()

+GetBlob()

+GetBlobWithOptions()

+ListUserBlobs()

Repository

-db: Database

+Query()

+Insert()

+Update()

+Delete()

Cache

Database

Fundamental Purpose

A blob is a tuple of:

  • Identity — a UUID, an owner (owner_id), and an author (author_id; the user who last edited it).
  • Tree position — a nullable parent_id. Children inherit (by default) from their parent.
  • Content — a title, a content body (text/markdown/JSON, format-agnostic at this layer), and a content_type MIME hint.
  • Visibility — is_public flag plus a fine-grained permission set that can be inherit_permissions-ed from the parent.
  • Attachments — zero or more Media records linked by blob_id.
  • Lifecycle — soft-deleted via deleted_at + delete_batch_id (see Soft-delete + restore below).

The module is the canonical surface for CRUD’ing this tuple. It owns the tree-structure invariants, enforces permissions on every operation, broadcasts lifecycle events, and (when blobs are deleted) coordinates cleanup of attached storage objects through the Storage module.

HTTP Surface

Routes are mounted under <APIPrefix>/blobs (default /api/v1/blobs). All routes require a valid JWT or API key.

MethodPathHandlerPurpose
GET/ListBlobsPaginated list, filtered by visibility + caller perms
POST/CreateBlobCreate a blob (validates parent perms when parent_id set)
GET/{id}GetBlobFetch one (with optional ?include=children,files)
PUT/{id}UpdateBlobUpdate mutable fields
DELETE/{id}DeleteBlobSoft-delete (cascades to descendants in one batch)
GET/{id}/childrenGetBlobChildrenList direct children of a blob
DELETE/{id}/children/{childId}RemoveChildBlobDetach a child without deleting it (re-parents to root)
POST/{id}/restoreRestoreBlobRestore a soft-deleted blob and its delete batch
POST/bulk/createBulkCreateBlobsAtomic-batch create (per-item result envelope)
POST/bulk/deleteBulkDeleteBlobsAtomic-batch soft-delete
GET/wswsHandler.HandleConnectionWebSocket for real-time blob events

Special Behaviors

Cascade soft-delete via delete_batch_id

DELETE /blobs/{id} doesn’t physically remove anything. The blob and every descendant get a shared random UUID written into delete_batch_id, plus deleted_at = now(). Listings and reads filter on deleted_at IS NULL by default.

POST /blobs/{id}/restore reverses the operation: it looks up the blob’s delete_batch_id and clears deleted_at on every row that shares it. So a delete + restore round-trip is exact, even across deeply nested trees.

This avoids the classic problem of orphaning children when a parent is removed and lets the UI offer “Undo delete” without database snapshots.

Permission inheritance

Every blob has an inherit_permissions flag (default true). When set, permission checks walk up the parent chain until they find a blob with inherit_permissions = false or hit a root. Setting inherit_permissions = false on a child fork — that subtree starts its own permission scope.

The module’s PermissionService (see apps/api/modules/blobs/permissions.go) is the single entry point for these checks; never bypass it.

Bulk operations with per-item result envelopes

Both BulkCreateBlobs and BulkDeleteBlobs use the shared pkg/bulkop.Result[T] envelope:

type Result[T any] struct {
Successes []T
Failures []FailedItem
}

A bulk request never fails-all. Each item carries its own status, so a 100-blob delete with three permission errors returns 200 OK with 97 successes + 3 typed failures. The SDK exposes this directly via BulkDeleteResponse (see ApiTypes.BulkDeleteResponse).

Real-time events via WebSocket

GET /blobs/ws upgrades to a WebSocket. After authenticating, clients receive typed events for blob lifecycle (blob.created, blob.updated, blob.deleted, blob.permission_granted, etc.) scoped to blobs the caller has read access on. Event shapes are defined in apps/api/modules/blobs/events.go and mirrored by the SDK’s BlobEvents namespace.

The same events are also published on the in-process events.EventBus, which other modules subscribe to (e.g. Media listens for blob.deleted to schedule attached-file cleanup).

Storage-provider integration (avoid the circular import)

The Service holds a StorageProviderGetter interface, not a direct dependency on the Storage module — that breaks what would otherwise be a circular import (Storage uses Blobs; Blobs needs Storage to clean up files on delete). The dependency is wired post-construction by Service.SetStorageProviderGetter from the application bootstrap.

SDK Surface

The TypeScript SDK exposes this module as the BlobsService singleton on blobs.blobs:

Action layer

For UI flows that need undo/redo (the dashboard treeview is one), use the actions/blob namespace instead of calling the service directly. Each action (CreateBlobAction, UpdateBlobAction, DeleteBlobAction, MoveBlobAction, RemoveChildAction, BulkDeleteBlobsAction) implements a forward and an undo, and pipes through the ActionManager so a Ctrl-Z cleanly reverses the last operation against the live API.

Cross-Module Integration

ModuleRelationship
MediaBlobs hold files: Media[]; deleting a blob soft-cascades and the Media module subscribes to blob.deleted to detach/clean.
PermissionsEvery Service method calls PermissionService.Can{Read,Write,Delete,Share} before touching the repository.
StorageStorageProviderGetter indirection lets the Service resolve a user’s active provider without a hard import.
ThemesDecoupled — themes don’t reference blobs directly.
UsersRead-only consumer for BlobAuthor embedding (denormalized author summary on blob responses).

API Reference

Open the interactive API reference and jump to the /blobs tag for live request/response examples and schema browsers.