Service Boundaries

In v1 the “services” described here are modules inside the bitvaultd monolith. The boundaries are seams designed so that extraction to a standalone service is a deployment change, not a rewrite (ADR-0001).

Golden Rules of Ownership

  1. One owner per piece of data. The module that owns a table is the only one that writes it. Others read via its API or via events — never by reaching into its tables. This rule is what makes extraction possible.
  2. Postgres is the source of truth; derived stores are disposable. Search indexes, notification state, usage counters, and previews are projections. They are rebuildable from events.
  3. Cross-boundary writes go through APIs (sync) or events (async). No shared mutable tables across owners. No distributed transactions (ADR-0006).
  4. The data plane (bytes) never flows through compute. Presigned URLs only (ADR-0011).

Services Overview

Logical service Owns (authoritative data) Internal API (gRPC) Async events v1 form
API Gateway / BFF Nothing (stateless) — (calls others) Module: HTTP server + router in bitvaultd
Identity tenants, users, memberships, roles, api_tokens, sessions Identity.* (authn, authz, token introspection) Emits UserCreated, TenantSuspended Module
File & Metadata nodes, versions, node_metadata, tags, trash Files.* (create/commit/move/list/version/trash) Emits NodeChanged via outbox Module
Storage blobs, multipart_uploads, provider config Storage.* (presign, head, commit-blob, delete, GC) Emits BlobCommitted, BlobOrphaned Module + worker (GC/finalizer)
Sync change_journal, device_cursors, conflicts Sync.* (register device, pull deltas, push) Consumes NodeChanged → journal Module (first extraction candidate)
Sharing shares, share_links, permissions Sharing.* (grant, link, resolve-access) Emits ShareCreated Module
Search & Indexing OpenSearch / Postgres-FTS indexes (derived) Search.* (query) Consumes node/share events → index Module + worker (early extraction candidate)
Notification & Events subscriptions, webhook_endpoints, notifications, delivery state Notify.* (subscribe, deliver) Consumes domain events → fan-out Module + worker
Billing & Metering usage_meters, quotas, plans Billing.* (check-quota, record-usage) Consumes usage events Module
Admin & Platform feature_flags, config, audit_log (append-only) Admin.* Consumes all events → audit Module

Workers are the async halves (GC, indexing, notification delivery). In v1 they run as goroutine pools inside bitvaultd driven by the in-process event bus; they are the first things to become standalone deployments because their scaling profile (bursty, CPU-bound, retry-heavy) differs from request-serving.


The Three Planes

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
Plane Consistency Scales with
Control Strong, synchronous Read replicas + stateless replicas
Data High-throughput, compute-bypass Object store, independent of control
Async / Derivation Eventual Per-worker; failures never block control

See System Overview → The Three Planes for the full table.


Service Detail

API Gateway / BFF

The single external edge. Terminates REST, authenticates every request, enforces per-tenant rate limits, translates REST↔gRPC, and aggregates responses for web and mobile BFF use cases.


Identity

Security kernel. All token issuance, introspection, and tenant-scoped RBAC live here.


File & Metadata

The namespace spine. Source of truth for every file, folder, version, and trash entry.

See Data Model → Key Invariants I1 for the transactional guarantee.


Storage

Isolates the multi-cloud byte lifecycle. Issues presigned URLs, manages multipart uploads, runs the GC/finalizer worker.


Sync

Projects NodeChanged events into a per-tenant monotonic change journal. Serves delta pulls and handles conflict detection.

See System Overview → Sync Flow for the sequence diagram.


Sharing

Access resolution is security-sensitive and crosses Identity + File. Isolating it keeps the authz story coherent and independently testable.


Search & Indexing

Derived, disposable, CPU/IO-bursty. Anti-corruption layer over the search index.


Notification & Events

Fan-out, retries, external delivery with failure semantics that must not contaminate the control plane.


Billing & Metering

Generic, swappable, event-driven usage accounting.


Admin & Platform

Cross-cutting config, feature flags, and the append-only audit sink.


Extraction Forcing Functions

Per ADR-0001, extraction is evidence-driven. A module graduates to a service only when one of these is demonstrated:

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

“Microservices look better” is not a trigger. Disciplined, justified extraction — each with a forcing function and an ADR — is the architecture story.


Anti-patterns

The following are explicitly forbidden by this boundary design:


Back: Data Model · System Overview