Repository Structure
The monorepo layout (ADR-0002) encodes the bounded-context boundaries so that “extract a context to its own service” is a move, not a rewrite. All protobuf contracts, generated code, domain modules, ops profiles, and tests live in one tree under one set of CI checks.
Top-Level Layout
bitvault/
├── go.work # Go workspace
├── Taskfile.yml # task runner (build/test/lint/gen/up)
├── proto/ # contract source of truth (ADR-0003)
│ └── bitvault/{identity,files,storage,sync,sharing,search,events}/v1/
├── gen/ # generated: go/, ts/, openapi/bitvault.yaml
├── cmd/
│ ├── bitvaultd/ # v1 single binary (all modules)
│ ├── bitvault-cli/ # Go CLI
│ └── (future) gateway/ files/ sync/ worker/
├── internal/
│ ├── platform/ # cross-cutting: observability, config, db, bus, outbox, authctx
│ ├── storage/ # Storage context
│ ├── files/ # File & Metadata context
│ ├── sync/ # Sync context
│ ├── identity/ # Identity context
│ ├── sharing/ # Sharing context
│ ├── search/ # Search context
│ ├── notification/ # Notification context
│ ├── billing/ # Billing context
│ ├── admin/ # Admin context
│ └── gateway/ # API Gateway / BFF
├── pkg/ # public Go libs: client/, apperr/
├── apps/
│ ├── web/ # Next.js app
│ └── mobile/ # React Native (future)
├── deploy/
│ ├── docker/ # Dockerfiles
│ ├── compose/ # docker-compose.{lite,standard,full}.yml
│ ├── helm/bitvault/ # Helm chart
│ └── terraform/ # optional infra modules
├── migrations/ # forward-only SQL migrations
└── test/
├── e2e/ # black-box integration tests
├── load/ # k6/vegeta SLO validation
└── chaos/ # fault injection (dual-write, conflict harness)
Dependency Direction
Imports flow one way. The rule is enforced by a CI import-boundary linter; a violation fails the build.
cmd/*
└──▶ internal/<context>/grpc
└──▶ internal/<context>/app
└──▶ internal/<context>/domain
internal/<context>/app ──▶ internal/platform/* (allowed: platform is a leaf)
internal/<context-A> ──X──▶ internal/<context-B> (FORBIDDEN: cross-context)
internal/<context-A> ──▶ gen/go/<context-B> (allowed: via generated gRPC client)
- Domain layers import only the standard library and their own types.
platform/is imported by all contexts; it imports nothing from domain.- Cross-context calls go through generated gRPC clients or NATS events — never direct package imports.
:::warning Cross-context imports
The CI import-boundary linter fails the build on any cross-context internal
import (e.g. internal/files importing internal/sync). Contexts communicate
only through generated gRPC clients (gen/go/...) or published events.
This rule is what makes service extraction a deployment change, not a code change.
:::
Bounded Context Layout
Every internal/<context>/ follows the same four-layer structure:
internal/<context>/
├── domain/ # aggregates, value objects, invariants, no framework deps
├── app/ # use-cases; orchestrates domain + repo + events
├── repo/ # Postgres repositories; only this layer touches SQL
└── grpc/ # gRPC server; thin adapter between proto and app layer
The grpc/ layer is the seam. When a context is extracted to its own binary,
cmd/<context>/main.go wires only that context’s grpc/ server — callers already
call via the generated gRPC client and are unaffected.
Contract Source of Truth
proto/ is the single source of truth for all APIs and events (ADR-0003).
| Generated artifact | Location | Used by |
|---|---|---|
| Go gRPC stubs | gen/go/bitvault/<context>/v1/ |
Backend services |
| TypeScript client | gen/ts/ |
Next.js web app |
| OpenAPI 3.1 spec | gen/openapi/bitvault.yaml |
External consumers, docs |
buf enforces lint rules and breaking-change detection in CI. A field removal
or renumber in a .proto file fails the pipeline before it can ship.
Why One Repo
| Concern | Enforcement in the tree |
|---|---|
| One proto contract | All .proto in proto/; no cross-repo version skew |
| Atomic cross-cutting changes | Schema + event + consumer + API + docs in one PR |
| Visible extraction story | Moving a module to its own binary is a diff in cmd/, not a new repository |
| Shared platform code | internal/platform/* imported everywhere; one version |
| Ops profiles collocated | deploy/compose/*.yml and deploy/helm/ alongside the code they deploy |