Skip to content

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.Provider abstraction; 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

Data Layer

Core Modules

Client Application

Chi Router

Middleware Stack

auth · CORS · rate-limit · logging

Applications Module

Blobs Module

Media Module

Settings Module

Storage Module

Themes Module

Users Module

PostgreSQL

Redis

storage.Provider

S3 · GCS · local · Dropbox · Drive

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.

PostgreSQLstorage.Provider(S3 / local / etc.)Storage ModuleMedia ModuleBlobs ModuleClientPostgreSQLstorage.Provider(S3 / local / etc.)Storage ModuleMedia ModuleBlobs ModuleClientPOST /blobs (create note)insert blob row{ id, ... }POST /media/prepare-upload{ blob_id, size, mime }insert media row(status = preparing){ media_id, upload_url }PUT bytes to upload_urlGetProviderForUser(userID)load user's provider config+ decrypt credentialsstorage.Provider instancePut(key, bytes)okmedia row(status = ready, hash, size){ media_id, url, ... }

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. GetProviderForUser decrypts credentials lazily and returns a typed storage.Provider interface. 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.deleted on the in-process events.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:

delegates

cache check

on miss

SQL

emits

HTTP Request

Controller

Service

Redis

Repository

PostgreSQL

events.EventBus

  • 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 PermissionService that 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/ws republishes scoped events to subscribed clients so SDKs can offer live updates without polling.