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
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, acontentbody (text/markdown/JSON, format-agnostic at this layer), and acontent_typeMIME hint. - Visibility —
is_publicflag plus a fine-grained permission set that can beinherit_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.
| Method | Path | Handler | Purpose |
|---|---|---|---|
GET | / | ListBlobs | Paginated list, filtered by visibility + caller perms |
POST | / | CreateBlob | Create a blob (validates parent perms when parent_id set) |
GET | /{id} | GetBlob | Fetch one (with optional ?include=children,files) |
PUT | /{id} | UpdateBlob | Update mutable fields |
DELETE | /{id} | DeleteBlob | Soft-delete (cascades to descendants in one batch) |
GET | /{id}/children | GetBlobChildren | List direct children of a blob |
DELETE | /{id}/children/{childId} | RemoveChildBlob | Detach a child without deleting it (re-parents to root) |
POST | /{id}/restore | RestoreBlob | Restore a soft-deleted blob and its delete batch |
POST | /bulk/create | BulkCreateBlobs | Atomic-batch create (per-item result envelope) |
POST | /bulk/delete | BulkDeleteBlobs | Atomic-batch soft-delete |
GET | /ws | wsHandler.HandleConnection | WebSocket 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:
BlobsService— full method reference (create,getById,list,update,delete,restore,bulkCreate,bulkDelete,getChildren,removeChild).Blob— the canonical wire shape, augmented with an optionalchildren?: Blob[]for client-side hydration.CreateBlobRequest/UpdateBlobRequest— generated from the OpenAPI spec.ApiTypes.BulkDeleteResponse— bulk-op envelope.
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
| Module | Relationship |
|---|---|
| Media | Blobs hold files: Media[]; deleting a blob soft-cascades and the Media module subscribes to blob.deleted to detach/clean. |
| Permissions | Every Service method calls PermissionService.Can{Read,Write,Delete,Share} before touching the repository. |
| Storage | StorageProviderGetter indirection lets the Service resolve a user’s active provider without a hard import. |
| Themes | Decoupled — themes don’t reference blobs directly. |
| Users | Read-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.