System Overview

BitVault v1 ships as a modular monolith: one bitvaultd binary containing all logical services as in-process modules. The internal seams are drawn exactly where future service boundaries would be cut — extraction is a deployment change, not a rewrite (ADR-0001). The diagrams on this page show the logical architecture, which is identical whether deployed as one process or many.

See Service Boundaries for per-module ownership detail and Data Model for the entity schema.


System Context (C4 Level 1)

Who and what interacts with BitVault, and what external systems it depends on.

flowchart TB
    classDef person fill:#dbeafe,stroke:#1e40af,color:#111827;
    classDef system fill:#fde68a,stroke:#b45309,color:#111827;
    classDef ext fill:#e5e7eb,stroke:#6b7280,color:#111827;
    user["End User\n(web / mobile)"]:::person
    dev["Developer / Operator\n(CLI, automation)"]:::person
    admin["Tenant Admin"]:::person
    bv["BitVault Platform\nfile storage · sync · sharing · search"]:::system
    obj["Object Storage\nMinIO / S3 / R2 / GCS / Azure"]:::ext
    idp["External IdP\n(OIDC / SSO)"]:::ext
    smtp["Email / SMTP"]:::ext
    hook["Tenant Webhook Endpoints"]:::ext
    user -->|"REST / HTTPS"| bv
    dev -->|"REST + gRPC (CLI)"| bv
    admin -->|"REST / HTTPS"| bv
    bv -->|"presigned PUT/GET"| obj
    bv -->|"authn"| idp
    bv -->|"notifications"| smtp
    bv -->|"signed events"| hook
    user <-. "direct byte transfer (presigned)" .-> obj

The dashed line is load-bearing: users transfer bytes directly to and from object storage. BitVault issues the presigned URL but the payload never traverses its compute (ADR-0011).


Container View (C4 Level 2)

Internal building blocks and the stores they own. All boxes are modules in bitvaultd in v1.

flowchart TB
    classDef edge fill:#c7d2fe,stroke:#3730a3,color:#111827;
    classDef svc fill:#fde68a,stroke:#b45309,color:#111827;
    classDef worker fill:#fed7aa,stroke:#c2410c,color:#111827;
    classDef store fill:#bbf7d0,stroke:#15803d,color:#111827;
    classDef bus fill:#fbcfe8,stroke:#be185d,color:#111827;

    subgraph clients[Clients]
        web["Next.js Web"]:::edge
        cli["Go CLI"]:::edge
        mob["React Native\n(future)"]:::edge
    end

    gw["API Gateway / BFF\nREST edge · authn · rate-limit · REST↔gRPC"]:::edge

    subgraph control[Control plane services - gRPC]
        id["Identity"]:::svc
        fm["File & Metadata\n(source of truth + outbox)"]:::svc
        st["Storage\n(presign · multipart · commit)"]:::svc
        sh["Sharing"]:::svc
        sy["Sync"]:::svc
        srch["Search (query)"]:::svc
    end

    subgraph workers[Async workers]
        idx["Search Indexer"]:::worker
        ntf["Notifier"]:::worker
        gc["Finalizer / GC"]:::worker
        mtr["Meter"]:::worker
        aud["Audit Sink"]:::worker
    end

    bus{{"NATS JetStream\n(in-proc bus in v1)"}}:::bus

    pg[("PostgreSQL\nmetadata · outbox")]:::store
    rd[("Redis\ncache · sessions · locks · rate-limit")]:::store
    os[("OpenSearch\n(optional; PG-FTS fallback)")]:::store
    obj[("Object Storage\nblobs")]:::store

    web & cli & mob --> gw
    gw --> id & fm & st & sh & sy & srch

    id --> pg
    fm --> pg
    sh --> pg
    sy --> pg
    st --> obj
    id & gw --> rd
    srch --> os

    fm -->|outbox→publish| bus
    st -->|outbox→publish| bus
    sh -->|outbox→publish| bus
    bus --> idx & ntf & gc & mtr & aud & sy
    idx --> os
    gc --> obj
    mtr --> pg
    aud --> pg

    web -. presigned .-> obj
    cli -. presigned .-> obj

Layers

  1. Clients — Next.js web, Go CLI, future React Native mobile, and third-party integrations. All speak REST to the gateway and transfer bytes directly to object storage.
  2. Edge / Gateway — TLS termination, authn, per-tenant rate limiting, REST↔gRPC translation, BFF aggregation. Stateless; the primary horizontal-scale unit.
  3. Control-plane services (gRPC) — Identity, File & Metadata, Storage, Sharing, Sync, Search-query. Strong consistency; each owns its Postgres tables.
  4. Event backbone — transactional outbox → NATS JetStream (in-proc bus in v1). The published language between core and consumers.
  5. Async workers — indexer, notifier, GC/finalizer, meter, audit. Eventually consistent; independently scalable; failures isolated from the control plane.
  6. Data stores — Postgres (truth + outbox), Redis (cache/sessions/locks/rate-limit), OpenSearch (optional derived index), object storage (blobs).

The Three Planes

Plane Consistency Components Scales with
Control Strong, synchronous Gateway, Identity, File & Metadata, Sharing, Storage presign/commit Read replicas + stateless replicas
Data High-throughput, compute-bypass Client ⇄ Object Store via presigned URLs Object store, independently of control
Async / Derivation Eventual Sync projector, Search indexer, Notifier, Meter, Audit, GC Per-worker; failures never block control

:::tip Data Plane Scalability Bytes never traverse BitVault compute. The presigned URL model means the data plane scales with the object store, independently of the control plane. :::


Upload Flow

The commit protocol is the correctness heart of the system. It defeats the dual-write problem: the namespace row and its event are written in one transaction, and bytes are verified before commit.

sequenceDiagram
    autonumber
    participant C as Client
    participant GW as API Gateway
    participant F as File & Metadata
    participant S as Storage
    participant O as Object Store
    participant B as Event Bus
    participant IX as Indexer

    C->>GW: POST /v1/files (init upload: path, size, hash)
    GW->>F: CreateUpload(node draft) [gRPC]
    F->>S: PresignPut(staging key, size-range, ttl)
    S-->>F: presigned URL (+ uploadId if multipart)
    F-->>GW: uploadId + presigned URL(s)
    GW-->>C: 201 {uploadId, url}

    C->>O: PUT bytes (direct, presigned)
    O-->>C: 200 (ETag)

    C->>GW: POST /v1/files/{uploadId}/commit (etag, hash)
    GW->>F: CommitUpload(uploadId, hash)
    F->>S: HeadObject(staging key) — verify size/etag/hash
    S->>O: HEAD staging key
    O-->>S: metadata (size, etag)
    S-->>F: verified ✔ (+ blob refcount++)
    Note over F: BEGIN TX<br/>insert/replace node version<br/>insert outbox(NodeChanged)<br/>COMMIT
    F-->>GW: 200 (node, version)
    GW-->>C: 200 committed

    F->>B: publish NodeChanged (from outbox)
    B->>IX: NodeChanged
    IX->>IX: index name+metadata (eventual)
    Note over C,IX: UI may show "indexing…" until IX catches up

Failure handling:


Download Flow

Authz happens before URL issuance; the URL is scoped to an exact key with a short TTL. Bytes never touch BitVault compute.

sequenceDiagram
    autonumber
    participant C as Client
    participant GW as API Gateway
    participant SH as Sharing
    participant F as File & Metadata
    participant S as Storage
    participant O as Object Store

    C->>GW: GET /v1/files/{id}/content
    GW->>SH: CheckAccess(principal, node, read)
    SH-->>GW: allow
    GW->>F: ResolveVersion(node) → blob hash/key
    F->>S: PresignGet(key, ttl, response-headers)
    S-->>F: presigned GET URL
    F-->>GW: redirect URL
    GW-->>C: 302 (or {url})
    C->>O: GET bytes (direct, presigned)
    O-->>C: 200 bytes

Sync Flow

The sync journal is a per-tenant monotonic sequence of CHANGE rows. Devices hold a cursor; delta pull is WHERE seq > cursor. A stale base version never overwrites — it becomes a conflicted copy. Both histories survive.

sequenceDiagram
    autonumber
    participant D as Device
    participant GW as API Gateway
    participant SY as Sync
    participant F as File & Metadata
    participant S as Storage

    Note over SY: Sync projects NodeChanged events into a<br/>per-tenant monotonic change journal
    D->>GW: GET /v1/sync/changes?cursor=N
    GW->>SY: PullDeltas(cursor=N)
    SY-->>GW: changes[N+1..M] + new cursor M
    GW-->>D: deltas (created/updated/moved/deleted)

    loop for each changed file to fetch
        D->>GW: GET content URL (download flow above)
    end

    Note over D: local edit while offline →
    D->>GW: POST /v1/sync/push (node, baseVersion, hash)
    GW->>SY: Push(node, baseVersion)
    SY->>F: CompareAndCommit(baseVersion)
    alt baseVersion is current
        F-->>SY: committed new version
        SY-->>D: ok (no conflict)
    else baseVersion stale (concurrent change)
        F-->>SY: conflict (current != base)
        SY->>F: CreateConflictedCopy(node, device, ts)
        SY-->>D: conflict resolved as copy (both versions kept)
    end

See ADR-0008, ADR-0022, and ADR-0024 for the full conflict and cursor model.


Event Flow

All producers write to the outbox in the same transaction as the aggregate change. The outbox drainer publishes to NATS JetStream. All consumers are idempotent (dedup on event id), per-aggregate ordered, with DLQ for poison messages. Derived stores are rebuildable from the journal.

flowchart LR
    classDef src fill:#fde68a,stroke:#b45309,color:#111827;
    classDef bus fill:#fbcfe8,stroke:#be185d,color:#111827;
    classDef cons fill:#fed7aa,stroke:#c2410c,color:#111827;
    classDef sink fill:#bbf7d0,stroke:#15803d,color:#111827;

    FM["File & Metadata\n(outbox)"]:::src
    ST["Storage\n(outbox)"]:::src
    SH["Sharing\n(outbox)"]:::src

    J{{"NATS JetStream\nsubjects: node.*, blob.*, share.*"}}:::bus

    SY["Sync projector"]:::cons
    IX["Search indexer"]:::cons
    NT["Notifier"]:::cons
    MT["Meter"]:::cons
    AU["Audit"]:::cons
    GC["GC / finalizer"]:::cons

    OS[("OpenSearch")]:::sink
    PG[("Postgres: journal/meters/audit")]:::sink
    HK[("Webhooks / Email")]:::sink
    OB[("Object Store")]:::sink

    FM & ST & SH -->|"at-least-once"| J
    J --> SY --> PG
    J --> IX --> OS
    J --> NT --> HK
    J --> MT --> PG
    J --> AU --> PG
    J --> GC --> OB

Next: Data Model · Service Boundaries