Modules Architecture
The Blobs platform is built as a small set of cooperating modules behind a single HTTP / gRPC entry point. Each module owns a discrete piece of domain logic (blobs, media, users, etc.), exposes its surface through routes registered on a shared chi router, and reaches Postgres + (optional) Redis through its own controller/service/repository chain. Modules can talk to each other only through narrow, explicit interfaces — never via shared state — which is what makes the platform safely extensible.
Goals
- One canonical content tree. Every piece of user-authored content is a blob in a parent/child tree with object-level permissions. Subsystems hang off this tree (media attachments, themes, settings) rather than building parallel data models.
- Hybrid permissions. System roles (
admin/moderator/user) layer over per-object grants (read,write,delete,share) with optional inheritance from the blob’s parent. See Permission System. - Pluggable storage. Binary content (file uploads, snapshots) is decoupled from the blob tree via a
storage.Providerabstraction; users plug in S3, GCS, local FS, Dropbox, Drive, etc. - Multi-tenant by construction. Networks, organizations, and per-user data scoping are first-class — not bolted on.
- No back-channels. Modules call each other only through documented interfaces (e.g.
StorageProviderGetter); nothing reaches into another module’s repository or cache directly.
High-Level Architecture
Authentication is a separate process (apps/auth) that the API server proxies to over gRPC — it shares the same users table but owns the credential columns. See the Auth Module deep-dive for the cross-process contract.
Module Responsibilities
Every module follows the same skeleton — a thin controller, a service that owns business logic + permission checks, and a repository for SQL — but the responsibilities they sit on top of are quite different.
Applications
OAuth-style API client applications and their API keys. Generates keys on demand, stores only their hash, and answers the auth middleware’s X-API-Key validation calls. Lets external clients (CLIs, server-side integrations, mobile builds) authenticate without a user JWT. → Applications deep-dive
Auth
Standalone gRPC + HTTP service (apps/auth) that mints, refreshes, and revokes JWTs. Logout revokes the refresh token and denylists the access token in the shared Redis cache (a SHA-256 fingerprint, kept until the token’s expiry); every server’s auth middleware checks that list, so a logout takes effect across the cluster immediately without a per-request DB scan. → Auth deep-dive
Blobs
The platform’s core. Owns the blob tree (parent/child, soft-delete with cascade restore via delete_batch_id, permission inheritance toggle, real-time WebSocket events) and is the single canonical surface for user content. Bulk operations return per-item results so partial failures surface cleanly. → Blobs deep-dive
Media
Owns the lifecycle of binary attachments — single-shot + chunked uploads, downloads, streaming, deletes — but never holds bytes itself. Streams every byte through the storage.Provider selected by the user’s configured provider. Computes content-hash on ingestion (deduplication groundwork) and listens for blob.deleted events to schedule attachment cleanup. → Media deep-dive
Settings
Per-user key/value preferences with two parallel access patterns — whole-object (UserSettings document) and per-key (atomic reads/writes, batch sets, prefix queries) — both backed by a single JSON column. Cache invalidation is unified so the two surfaces never drift. → Settings deep-dive
Storage
User-owned storage provider configurations (S3 buckets, GCS buckets, local-FS roots, Dropbox, Drive). Encrypts credentials at rest, runs delegated OAuth flows where required, and answers GetProviderForUser() queries from the Media module. Lets users bring their own backend without re-architecting the platform. → Storage deep-dive
Themes
User-authored UI themes — bundles of CSS-variable overrides + component-token tweaks — with a draft-vs-published lifecycle and per-publish versioning. Drafts are private; published snapshots are world-readable and immutable, which is what lets the SDK’s ThemeRegistry resolve them on a logged-out home page. → Themes deep-dive
Users
The canonical user record + the editable profile sub-resource. Splits administrative fields (email, username, system role) from cosmetic profile fields (display name, avatar, bio) so a profile edit can never accidentally change auth-critical fields. Admin/moderator-gated list and search live here. → Users deep-dive
Worked Example — Uploading a File to a Blob
The flow below shows how the Blobs, Media, and Storage modules cooperate when a user attaches a file to a blob. It’s the most common multi-module path in the system.
A few things worth noting from this flow:
- Bytes never touch the Blobs module. It owns the blob row and its tree position; Media and Storage own the bytes. This is what lets us swap storage backends per-user without rewriting blob code.
- Storage is a runtime broker, not a passthrough.
GetProviderForUserdecrypts credentials lazily and returns a typedstorage.Providerinterface. Future cross-cutting concerns (per-provider rate limits, audit logging on byte movement) all land in this single chokepoint. - Cleanup is event-driven. When a blob is later soft-deleted, the Blobs module emits
blob.deletedon the in-processevents.EventBus; the Media module’s subscriber schedules attachment cleanup that respects the delete batch (a restore reattaches the bytes correctly).
Layered Architecture (Inside Each Module)
Every module repeats the same three-layer skeleton:
- Controller — request parsing, status codes, response shape. No business logic.
- Service — permission checks, transaction management, cache reads/writes, event emission. All business logic lives here.
- Repository — raw SQL only. No ORM. Maps rows to typed models.
The split is rigid; rule-of-thumb is that a service method should never produce an HTTP-shaped response and a repository should never make permission decisions.
Cross-Cutting Concerns
- Caching — every module has optional Redis support. Bypass capability, namespaced keys, configurable TTL, automatic invalidation on writes. Cache failures are non-fatal (services degrade to direct repository reads).
- Permissions — checks live in the service layer, never in the controller or repository. The Blobs module exposes a
PermissionServicethat other modules consume rather than reimplementing checks. - Events — modules that mutate state emit on the in-process
events.EventBus(blob.created,blob.deleted,media.uploaded, etc.). Cross-module reactions (Media cleanup on blob delete, search index invalidation, etc.) subscribe rather than being hard-coded into the producing module. - Real-time — the Blobs module’s WebSocket handler at
/blobs/wsrepublishes scoped events to subscribed clients so SDKs can offer live updates without polling.