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:
- Control plane — synchronous, strongly consistent (Gateway, Identity, File & Metadata, Sharing, Storage).
- Data plane — bytes transferred directly between client and object store via presigned URLs; never traverse compute.
- Async / derivation plane — eventually consistent workers driven by the event backbone (Sync projector, Indexer, Notifier, GC, Meter, Audit).
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
- Search reading
nodesdirectly from Postgres — must consume events. - Two modules writing the same table — one owner only.
- Synchronous chains across 3+ modules in a single request path.
- Distributed transactions / 2PC across modules — use outbox + sagas.
- Bytes flowing through Gateway or File compute — presigned URLs only.
- A module importing another module’s internal packages instead of its API.