Monorepo Structure

1. Why a Monorepo

All .proto definitions live in packages/proto/ and are consumed by Go services, the CLI, and TypeScript via codegen — no cross-repo version skew possible (ADR-0003). A change to a node schema, its event, its consumers, the API surface, and the docs lands in one PR. Shared platform libs (observability, config, storage adapters, auth context propagation) live in platform/ once and are referenced by workspace link. The extraction story is a diff inside services/, not a new repository: adding services/<context>/cmd/main.go and a Helm chart is all it takes to split a module out (ADR-0001). Tooling complexity — the real cost of a monorepo — is mitigated by Go workspaces, pnpm + Turborepo for JS, and path-filtered CI that avoids rebuilding unrelated artifacts (ADR-0002).


2. Top-Level Structure

bitvault/
├── go.work                      # Go workspace: services/, platform/, apps/cli/, packages/gen/go/, packages/sdk-go/
├── pnpm-workspace.yaml          # JS workspace: apps/web/, apps/mobile/, packages/sdk-ts/
├── turbo.json                   # Turborepo build pipeline (JS/TS tasks)
├── Taskfile.yml                 # Developer task runner: gen, up:lite, up:full, test, test:e2e, test:chaos
├── buf.work.yaml                # buf workspace root → packages/proto/
├── .golangci.yml                # Linter: import-boundary rules enforce ADR-0001 seams
├── .github/
│   └── workflows/
│       ├── pr.yml               # lint · unit/integration tests (path-filtered) · scan · preview env
│       ├── main.yml             # build · SBOM · sign (cosign/OIDC) · push · GitOps PR (ADR-0032)
│       └── release.yml         # changelog · GitHub Release · promote → staging · publish Helm + Compose
│
├── apps/                        # User-facing applications
│   ├── web/                     # Next.js SSR web app (FR-H3)
│   │   ├── package.json
│   │   └── src/
│   │       ├── app/             #   Next.js App Router pages
│   │       ├── components/      #   React components
│   │       └── lib/             #   API client (uses packages/sdk-ts)
│   ├── mobile/                  # React Native (future — post-P4 / post-/v1-stable) (FR-H4)
│   │   └── package.json
│   └── cli/                     # Go CLI — dogfoods the REST API (FR-H2)
│       ├── go.mod               #   github.com/bitvault/cli
│       ├── main.go
│       └── cmd/
│           ├── auth/
│           ├── files/
│           ├── sync/
│           └── share/
│
├── services/                    # All bounded-context domain code (ADR-0001 / 04)
│   ├── go.mod                   # github.com/bitvault/services
│   │
│   ├── bitvaultd/               # v1 ENTRY POINT: wires all modules into one binary
│   │   └── main.go              #   config-driven wire; zero domain logic
│   │
│   ├── gateway/                 # API Gateway / BFF
│   │   ├── rest/                #   HTTP handlers, OpenAPI wiring
│   │   ├── middleware/          #   authn, rate-limit, tracing, tenant-context
│   │   └── bff/                 #   web/mobile BFF aggregation
│   │
│   ├── identity/                # Identity & Access
│   │   ├── domain/              #   Tenant, User, Role, ApiToken, Session
│   │   ├── app/                 #   authenticate, issue-token, check-permission
│   │   ├── repo/                #   Postgres: tenants, users, memberships, api_tokens, sessions
│   │   └── grpc/                #   Identity gRPC server
│   │
│   ├── files/                   # File & Metadata — source of truth
│   │   ├── domain/              #   Node, Version, Tag; commit invariants (I1–I6)
│   │   ├── app/                 #   commit-upload, move, copy, rename, trash, version
│   │   ├── repo/                #   Postgres: nodes, versions, node_metadata, OUTBOX
│   │   └── grpc/                #   Files gRPC server
│   │
│   ├── storage/                 # Storage abstraction (ADR-0005, 0011, 0016–0019, 0021)
│   │   ├── domain/              #   Blob, MultipartUpload, presign interface
│   │   ├── provider/            #   Adapters: minio/ s3/ r2/ gcs/ azure/
│   │   ├── conformance/         #   Provider conformance test suite
│   │   ├── app/                 #   presign, commit-blob, head-verify, gc
│   │   ├── repo/                #   Postgres: blobs, multipart_uploads
│   │   ├── grpc/                #   Storage gRPC server
│   │   └── worker/              #   GC/finalizer goroutine pool
│   │
│   ├── sync/                    # Synchronization (ADR-0008, 0022–0027)
│   │   ├── domain/              #   Change, DeviceCursor, ConflictRecord
│   │   ├── app/                 #   journal-projector, delta-pull, conflict-resolver
│   │   ├── repo/                #   Postgres: change_journal, device_cursors, conflicts
│   │   └── grpc/                #   Sync gRPC server
│   │
│   ├── sharing/                 # Sharing & Permissions (ADR-0037)
│   │   ├── domain/              #   Share, ShareLink, Permission
│   │   ├── app/                 #   grant, create-link, resolve-access, revoke
│   │   ├── repo/                #   Postgres: shares, share_links, permissions
│   │   └── grpc/                #   Sharing gRPC server
│   │
│   ├── search/                  # Search & Indexing (ADR-0009)
│   │   ├── domain/              #   IndexDocument, SearchQuery (ACL over index)
│   │   ├── app/                 #   query, reindex
│   │   ├── repo/                #   OpenSearch client; Postgres-FTS fallback
│   │   ├── grpc/                #   Search gRPC server
│   │   └── worker/              #   Indexer goroutine pool (NodeChanged consumer)
│   │
│   ├── notification/            # Notifications & Events
│   │   ├── domain/              #   Subscription, WebhookEndpoint, Notification
│   │   ├── app/                 #   fan-out, deliver, retry
│   │   ├── repo/                #   Postgres: subscriptions, webhook_endpoints
│   │   ├── grpc/                #   Notify gRPC server
│   │   └── worker/              #   Delivery goroutine pool (event consumer)
│   │
│   ├── billing/                 # Billing & Metering
│   │   ├── domain/              #   UsageMeter, Quota, Plan
│   │   ├── app/                 #   check-quota (sync gate), record-usage (async)
│   │   ├── repo/                #   Postgres: usage_meters, quotas, plans
│   │   └── grpc/                #   Billing gRPC server
│   │
│   └── admin/                   # Administration & Platform
│       ├── domain/              #   FeatureFlag, AuditEntry
│       ├── app/                 #   audit-sink, flag-eval, tenant-admin
│       ├── repo/                #   Postgres: audit_log (append-only), feature_flags
│       └── grpc/                #   Admin gRPC server
│
├── platform/                    # Cross-cutting infrastructure — zero domain knowledge
│   ├── go.mod                   # github.com/bitvault/platform
│   ├── observability/           # OTel SDK: trace/metric/log providers (ADR-0013)
│   ├── config/                  # 12-factor env config; lite/standard/full profiles (ADR-0012)
│   ├── server/                  # gRPC + HTTP server scaffolding; /healthz /readyz
│   ├── db/                      # Postgres pool; migrations runner; RLS session context
│   ├── bus/                     # Event bus interface + in-proc impl + NATS impl (ADR-0006)
│   ├── outbox/                  # Transactional outbox writer + drainer
│   └── authctx/                 # Tenant/principal request-scoped context propagation
│
├── packages/                    # Cross-language shared packages
│   ├── proto/                   # Protobuf definitions — SINGLE CONTRACT SOURCE (ADR-0003)
│   │   ├── buf.yaml
│   │   ├── buf.gen.yaml         # codegen: Go stubs, TS client, OpenAPI spec
│   │   └── bitvault/
│   │       ├── identity/v1/
│   │       ├── files/v1/
│   │       ├── storage/v1/
│   │       ├── sync/v1/
│   │       ├── sharing/v1/
│   │       ├── search/v1/
│   │       └── events/v1/       # Published Language: domain events (ADR-0006)
│   │
│   ├── gen/                     # Generated code (task gen; committed; never hand-edited)
│   │   ├── go/                  # Go gRPC stubs — module github.com/bitvault/gen
│   │   │   ├── go.mod
│   │   │   └── bitvault/...
│   │   ├── ts/                  # TypeScript gRPC-web / connect stubs
│   │   └── openapi/
│   │       └── bitvault.yaml    # Public OpenAPI spec — authoritative REST contract
│   │
│   ├── sdk-go/                  # Public Go REST SDK (importable by external consumers)
│   │   ├── go.mod               # github.com/bitvault/sdk-go
│   │   └── client/
│   │
│   ├── sdk-ts/                  # TypeScript / JS SDK (@bitvault/sdk)
│   │   ├── package.json
│   │   └── src/
│   │
│   └── apperr/                  # Shared error taxonomy (used by services/ + sdk-go/)
│       └── (Go package inside services/ module)
│
├── deploy/                      # All deployment artifacts (ADR-0012, 0028–0034)
│   ├── docker/                  # Dockerfiles: multi-stage, distroless, non-root (ADR-0032)
│   │   ├── bitvaultd.Dockerfile
│   │   ├── worker.Dockerfile
│   │   └── web.Dockerfile
│   │
│   ├── compose/                 # Docker Compose per tier — self-host on-ramp (ADR-0012)
│   │   ├── docker-compose.lite.yml
│   │   ├── docker-compose.standard.yml
│   │   └── docker-compose.full.yml
│   │
│   ├── helm/                    # Helm charts — K8s/SaaS packaging (ADR-0012)
│   │   ├── charts/
│   │   │   ├── bitvault-common/ # Library chart: Rollout/Deployment, HPA, PDB, NetworkPolicy
│   │   │   ├── bitvaultd/       # App chart (thin, uses library)
│   │   │   ├── worker/          # Worker chart (future: extracted workers)
│   │   │   ├── gateway/         # Gateway chart (future: extracted gateway)
│   │   │   └── web/             # Web chart (Next.js)
│   │   └── values/
│   │       ├── values.yaml      # Safe defaults
│   │       ├── values.lite.yaml
│   │       ├── values.standard.yaml
│   │       └── values.full.yaml
│   │
│   ├── k8s/                     # Raw manifests / Kustomize bases
│   │   └── base/                # Namespace defs, RBAC, PodSecurity, NetworkPolicies
│   │
│   └── terraform/               # OpenTofu IaC (ADR-0031)
│       ├── modules/
│       │   ├── cluster/         # Kubernetes cluster provisioning
│       │   ├── database/        # Managed Postgres (CloudNativePG / cloud)
│       │   ├── object-storage/  # S3/GCS/Azure buckets + lifecycle rules
│       │   └── kms/             # KMS key rings + workload identity bindings
│       └── envs/
│           ├── nonprod/         # dev + staging + previews cluster
│           └── prod/            # isolated prod cluster
│
├── migrations/                  # SQL migrations — forward-only, versioned (ADR-0004)
│   ├── 0001_initial_schema.sql
│   └── ...
│
├── test/                        # Cross-cutting tests (not unit tests — those live in services/)
│   ├── e2e/                     # Black-box against a running stack (any tier)
│   ├── load/                    # k6/vegeta — validate NFR-3 SLO targets
│   └── chaos/                   # Fault injection: dual-write orphan, sync conflict harness, GC
│
└── docs/                        # Architecture, ADRs, service docs, API reference
    ├── architecture/
    ├── services/
    ├── deployment/
    ├── security/
    ├── observability/
    ├── development/
    ├── api/
    ├── roadmap/
    └── adr/

3. Ownership & Responsibilities

Directory Owner Primary responsibility Governed by
apps/web Frontend Next.js web application FR-H3
apps/mobile Mobile (future) React Native app FR-H4 (post-P4)
apps/cli Backend/DevX Go CLI; dogfoods REST API FR-H2
services/ Backend (by context) All bounded-context domain code ADR-0001, 0004–0009
platform/ Platform/Infra Cross-cutting infra libs; zero domain knowledge ADR-0006, 0013, 0014
packages/proto Architecture Protobuf contract source of truth ADR-0003
packages/gen CI/tooling (generated) Never hand-edited; output of task gen ADR-0003
packages/sdk-go Backend/DevX Public Go SDK for external consumers ADR-0015
packages/sdk-ts Frontend/DevX TypeScript SDK for web and external consumers ADR-0015
deploy/docker Platform Container image design ADR-0032
deploy/compose Platform Self-host packaging ADR-0012
deploy/helm Platform K8s/SaaS packaging ADR-0012, 0029
deploy/terraform Platform/SRE Infrastructure provisioning ADR-0031
migrations/ Backend (files owner) Schema evolution; forward-only ADR-0004
test/e2e QA/Backend End-to-end black-box tests NFR-3, 5
test/load Platform/SRE SLO validation load tests NFR-3, 4
test/chaos Platform/SRE Invariant fault injection NFR-2, 5 / I1–I6
docs/ All (ADRs: Architecture) Architecture decisions + contributor docs ADR corpus

4. Dependency Graph

4a. Go Module Dependency Graph

flowchart LR
  classDef app fill:#dbeafe,stroke:#1e40af,color:#111827;
  classDef svc fill:#fde68a,stroke:#b45309,color:#111827;
  classDef plat fill:#bbf7d0,stroke:#15803d,color:#111827;
  classDef pkg fill:#fbcfe8,stroke:#be185d,color:#111827;

  cli["apps/cli\ngithub.com/bitvault/cli"]:::app
  web["apps/web\n(Next.js / pnpm)"]:::app
  svc["services/\ngithub.com/bitvault/services"]:::svc
  plat["platform/\ngithub.com/bitvault/platform"]:::plat
  gen["packages/gen/go\ngithub.com/bitvault/gen"]:::pkg
  sdkgo["packages/sdk-go\ngithub.com/bitvault/sdk-go"]:::pkg
  sdkts["packages/sdk-ts\n@bitvault/sdk"]:::pkg
  gents["packages/gen/ts"]:::pkg
  proto["packages/proto\n(buf source)"]:::pkg
  openapi["packages/gen/openapi\nbitvault.yaml"]:::pkg

  proto -->|"task gen"| gen
  proto -->|"task gen"| gents
  proto -->|"task gen"| openapi
  gen --> svc
  gen --> cli
  gen --> sdkgo
  plat --> svc
  plat --> cli
  sdkgo --> cli
  gents --> sdkts
  sdkts --> web

4b. Cross-Context Import Rules Inside services/

Allowed:
  services/<context-A>/app/    →  platform/
  services/<context-A>/app/    →  packages/gen/go/<context-B>   (call B's gRPC API)
  services/<context-A>/app/    →  platform/bus                  (publish/consume events)
  services/<context-A>/domain/ →  (stdlib only)

Forbidden (CI lint fails build):
  services/<context-A>/  →X→  services/<context-B>/domain/
  services/<context-A>/  →X→  services/<context-B>/app/
  services/<context-A>/  →X→  services/<context-B>/repo/
  platform/              →X→  services/<any>

:::danger Cross-context internal imports are the rot that makes extraction impossible. The golangci depguard / importas rules enforce these boundaries in CI. A green build with a forbidden import is a CI misconfiguration, not a policy approval. :::

4c. Build Artifact Dependency Graph

flowchart TD
  classDef src fill:#e5e7eb,stroke:#6b7280,color:#111827;
  classDef build fill:#fde68a,stroke:#b45309,color:#111827;
  classDef image fill:#bbf7d0,stroke:#15803d,color:#111827;
  classDef helm fill:#fbcfe8,stroke:#be185d,color:#111827;

  proto["packages/proto/\n(.proto files)"]:::src
  gen["packages/gen/\n(generated stubs)"]:::build
  svcSrc["services/\n(Go source)"]:::src
  platSrc["platform/\n(Go source)"]:::src
  webSrc["apps/web/\n(Next.js)"]:::src
  cliSrc["apps/cli/\n(Go source)"]:::src

  bvdBin["bitvaultd binary"]:::build
  clibin["bitvault CLI binary"]:::build
  webBuild["Next.js build"]:::build

  bvdImg["bitvaultd image\n(distroless)"]:::image
  webImg["web image\n(distroless/nodejs)"]:::image
  cliDist["CLI tarball\n(signed static binary)"]:::image

  helmChart["Helm chart\n(OCI registry)"]:::helm
  composePkg["Compose bundle\n(self-host)"]:::helm

  proto --> gen
  gen --> svcSrc
  gen --> cliSrc
  platSrc --> svcSrc
  svcSrc --> bvdBin
  platSrc --> cliSrc
  cliSrc --> clibin
  webSrc --> webBuild

  bvdBin --> bvdImg
  webBuild --> webImg
  clibin --> cliDist

  bvdImg --> helmChart
  webImg --> helmChart
  bvdImg --> composePkg
  webImg --> composePkg

5. Build Boundaries

:::note Path-filtered CI means a change to apps/web/ never triggers a Go image rebuild. A change to packages/proto/ triggers everything downstream. :::

Boundary Unit Build tool CI trigger Cache key Test scope
Proto codegen packages/proto/ buf changes to *.proto proto hash buf lint + breaking
Platform lib platform/ go build changes to platform/ go.sum hash go test ./platform/...
Services services/ go build changes to services/ go.sum + module hash go test ./services/...; integration tests per context
bitvaultd binary services/bitvaultd/ go build services/ or platform/ change combined hash e2e:lite smoke
web app apps/web/ pnpm/turbo changes to apps/web/ or packages/sdk-ts/ pnpm store vitest unit + playwright e2e
CLI binary apps/cli/ go build changes to apps/cli/ go.sum hash go test + e2e smoke
Docker images deploy/docker/ BuildKit binary changes registry layer cache trivy scan + smoke
Helm charts deploy/helm/ helm lint changes to deploy/helm/ none helm lint + schema + helm-unittest + OPA policy
Terraform deploy/terraform/ tofu plan changes to deploy/terraform/ .terraform.lock.hcl tofu validate + tflint
E2E suite test/e2e/ task test:e2e scheduled + merge to main none black-box against running stack
Load suite test/load/ k6/vegeta scheduled (nightly) none NFR-3 SLO assertions
Chaos suite test/chaos/ task test:chaos scheduled + PR gate for invariant changes none I1 (orphan GC), I3 (isolation), P2 conflict harness

6. The Monolith-to-Services Migration Path

The directory structure directly encodes the extraction story from ADR-0001. No future refactor is required to split a module out — the seams already exist.

Extraction recipe for any bounded context:

  1. services/<context>/grpc/ already contains a runnable gRPC server. Add services/<context>/cmd/main.go that constructs only that module’s dependencies and calls platform/server.Run(...).
  2. Add deploy/helm/charts/<context>/ — a thin chart that wraps bitvault-common.
  3. In bitvaultd/main.go, gate that module’s wire-up behind a config flag. In extracted deployments the flag is off; in the monolith it remains on.
  4. Nothing else changes. Callers already go through the generated gRPC client from packages/gen/go/<context>/v1/. The call is in-process in v1 and a real TCP hop post-extraction — the caller is unaware.

First extraction candidates — workers:

storage/worker/ (GC/finalizer), search/worker/ (indexer), and notification/worker/ (delivery) are the first candidates. Their scaling profile — bursty, CPU-bound, retry-heavy — differs from request-serving contexts. They already run as goroutine pools; adding cmd/main.go makes them standalone processes.

Second extraction candidate — sync:

sync/ has a distinct consistency model (monotonic cursor per device, three-tree reconciliation), long-lived SSE/WebSocket connections, and the clearest portfolio narrative as a standalone service. It is the natural P4 milestone.

Import contracts survive extraction unchanged:

Because inter-context calls already use the gRPC generated client, and events already flow through platform/bus, extraction is a deployment topology change — not a code contract change.

flowchart LR
  subgraph v1["v1: services/bitvaultd/main.go wires everything"]
    b1[gateway] --- b2[identity] --- b3[files] --- b4[storage] --- b5[sync] --- b6[workers]
  end
  subgraph p4["P4: services/<context>/cmd/main.go per extracted service"]
    p1["bitvaultd\ngateway+identity+files+storage+sharing+billing+admin"]
    p2["bitvault-sync\nstandalone"]
    p3["bitvault-worker\nindexer+notifier+GC"]
  end
  v1 -->|"forcing function demonstrated\nnew cmd/ entry point\nnew Helm chart"| p4

:::tip The bitvault-common Helm library chart exists from day one so that adding a new chart for an extracted service is trivial. Do not defer library chart creation until extraction — it creates a migration cliff. :::


7. Task Runner Reference

All commands are defined in Taskfile.yml at the repo root. Prefer task over calling tools directly — it ensures correct env, correct working directory, and correct dependency ordering.

Command What it does Tier
task gen Regenerate packages/gen/ from packages/proto/ via buf all
task up:lite Start Compose lite tier: Postgres + MinIO + bitvaultd lite
task up:standard Start Compose standard: + Redis + NATS standard
task up:full Start Compose full: + OpenSearch + workers full
task down Stop and remove all Compose stacks
task test Go unit + integration tests (services/ + platform/) lite
task test:e2e Black-box E2E against a running stack lite or full
task test:load k6/vegeta SLO load suite full
task test:chaos Fault injection (orphan GC, sync conflict, isolation) full
task lint golangci-lint + helm lint + buf lint all
task migrate Run pending SQL migrations all
task build Build all Go binaries + Next.js all
task image:build Build Docker images locally (no push) all

:::warning task gen commits generated code to packages/gen/. Never hand-edit files under packages/gen/ — they will be overwritten on the next task gen run. If you need to change the generated shape, change the .proto definition in packages/proto/ and regenerate. :::