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:
services/<context>/grpc/already contains a runnable gRPC server. Addservices/<context>/cmd/main.gothat constructs only that module’s dependencies and callsplatform/server.Run(...).- Add
deploy/helm/charts/<context>/— a thin chart that wrapsbitvault-common. - 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. - 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.
:::