Skip to content

Architecture Overview

The Blobs platform is a multi-service system for managing hierarchical content with first-class permissions, multi-tenancy, and pluggable storage backends. The reading order below is the fastest way to get a working mental model — the Modules Architecture page is enough for a 10-minute survey; the deep-dives are there when you need to build against (or contribute to) a specific module.

What the platform does

  • Stores hierarchical content — every note, file, page, comment, draft is a blob in a parent/child tree.
  • Authorizes access with a hybrid model: system roles plus per-object grants, with optional inheritance from the parent.
  • Decouples bytes from records — file uploads stream through a pluggable storage.Provider (S3, GCS, local, Dropbox, Drive) so users can bring their own backend without re-architecting anything.
  • Runs across multiple networks — a single client can talk to multiple Blobs instances in parallel, each with its own auth context.
  • Ships typed SDKs — TypeScript and Go libraries generated from the same OpenAPI + gRPC contracts, so wire shapes can’t drift between server and client.

Reading Order

  1. Modules Architecture — start here. Names every module, what each owns, and walks one cross-module flow (file upload to a blob) end-to-end.
  2. Data Flow — request → router → middleware → controller → service → cache → repository → response, plus the auth handshake.
  3. Permission System — the model that gates every read/write on the platform: system roles, object-level grants, inheritance, public flags, edge cases.

Module Deep Dives

Each deep-dive covers the module’s purpose, HTTP surface, special behaviors (e.g. cascade soft-delete, OAuth flow isolation, single-row JSON storage), the SDK surface, and how it integrates with neighboring modules.

Recurring Patterns

A few patterns repeat across every module — once you’ve seen one, the rest read quickly.

Layered architecture inside each module

Controller ─► Service ─► Repository ─► PostgreSQL
│
└─► Redis cache (optional)
└─► events.EventBus (in-process pub/sub)
  • Controller does HTTP plumbing only — no business logic, no permission checks.
  • Service owns business logic, permission checks, transaction boundaries, cache reads/writes, and event emission.
  • Repository is raw SQL only. No ORM. Returns typed models.

The split is rigid because it’s load-bearing: the action layer in the SDK and the WebSocket subscriber both call into services directly without going through controllers, and the rule that “all permission checks live in the service” is what makes that safe.

Cache strategy

Every module has optional Redis. Cache failures are non-fatal — services degrade to direct repository reads. Writes invalidate cache eagerly so reads can never see stale data after a successful write.

Cross-module communication

Modules never reach into each other’s repositories or caches. Three sanctioned channels:

  • Direct service calls through small, documented interfaces (e.g. Blobs depends on a StorageProviderGetter interface that Storage satisfies).
  • In-process events via events.EventBus — the Media module subscribes to blob.deleted to schedule attachment cleanup, etc.
  • WebSocket fanout — the Blobs module’s WebSocket handler republishes scoped events to subscribed clients.

Soft delete + cascade restore

Blobs are never hard-deleted by default. DELETE writes a deleted_at timestamp + a shared delete_batch_id to the row and every descendant; restore reverses the operation atomically by clearing deleted_at on every row in the batch. Listings filter out deleted rows; the batch id makes “Undo delete” exact even across deep trees.

Versioned contracts

The OpenAPI spec at contracts/rest/api-spec.yaml and the gRPC protos at contracts/grpc/ are the single source of truth for wire shapes. Both Go and TypeScript SDKs generate from them, and a CI drift gate fails any PR where the generated artifacts don’t match the spec.