Services

BitVault’s v1 ships as a modular monolith: all service logic runs inside a single bitvaultd binary. Every module is designed with a clean API boundary (gRPC interface + explicit data ownership) so that extraction into a standalone service is an operational change, not an architectural rewrite. Extraction happens only when a concrete forcing function is demonstrated — see ADR-0001.

The three planes that cut across all services:

flowchart LR
    subgraph CP["Control plane (strong consistency)"]
        GW[API Gateway]
        ID[Identity]
        FM[File & Metadata]
        SH[Sharing]
        ST[Storage: presign/commit]
    end
    subgraph DP["Data plane (bypasses compute)"]
        OBJ[(Object Store)]
    end
    subgraph AP["Async / derivation plane (eventual)"]
        SY[Sync projector]
        SE[Search indexer]
        NO[Notifier]
        BI[Meter]
        AU[Audit]
        GC[GC / finalizer]
    end
    Client -->|REST| GW
    GW --> ID & FM & SH & ST
    Client -. presigned PUT/GET .-> OBJ
    FM -->|outbox| BUS{{NATS / in-proc bus}}
    ST -->|outbox| BUS
    BUS --> SY & SE & NO & BI & AU & GC
    GC --> OBJ

Service catalog

Service v1 form Primary data owned Detail
API Gateway / BFF Module: HTTP server + router in bitvaultd nothing (stateless)
Identity & Access Module tenants, users, memberships, roles, api_tokens, sessions
File & Metadata Module nodes, versions, node_metadata, tags, trash
Storage Module + GC worker blobs, multipart_uploads, provider config
Sync Module (first extraction candidate) change_journal, device_cursors, conflicts
Sharing Module shares, share_links, permissions
Search & Indexing Module + indexer worker (early extraction candidate) OpenSearch / Postgres-FTS indexes (derived)
Notifications & Events Module + delivery worker subscriptions, webhook_endpoints, notifications, delivery state
Billing & Metering Module usage_meters, quotas, plans
Admin & Platform Module feature_flags, config, audit_log

Extraction forcing functions

Per ADR-0001, a module graduates to a standalone service only when one of these is demonstrated:

Trigger Likely first service
Async workload starves request latency (GC CPU, indexing bursts) Storage worker, Search indexer
Component needs an independent scaling profile (many long-lived sync connections) Sync
Component needs a different datastore lifecycle or can be disabled entirely Search
Independent deploy cadence / blast-radius isolation needed Noisiest module
A team takes ownership That team’s bounded context

Anti-patterns this design forbids