07 — Repository Structure
Covers task 9. A monorepo (ADR-0002) laid out so the v1 modular monolith and the future extracted services share one tree, one set of protobuf contracts, and atomic cross-cutting changes. The layout encodes the bounded-context boundaries (04) so that “extract a service” is a move, not a rewrite.
No implementation code is produced here — this is the proposed shape.
1. Why a monorepo
- One contract source. Protobuf in
proto/is consumed by Go services, the CLI, and (via codegen) the web client. No cross-repo version skew (R8). - Atomic changes. A change to a node’s schema + its event + its consumers + its API + its docs lands in one PR.
- Shared platform libs (observability, config, storage adapters, auth) live once and are imported everywhere.
- The extraction story is visible — moving a module to its own binary is a diff
in
cmd/, not a new repository.
Trade-off (recorded in ADR-0002): monorepo tooling/CI complexity; mitigated with Go workspaces + a JS monorepo tool (pnpm/Turborepo) and path-scoped CI.
2. Top-level layout
bitvault/
├── go.work # Go workspace tying modules together
├── Taskfile.yml # task runner (build/test/lint/gen/up)
├── README.md
├── LICENSE
│
├── proto/ # SINGLE SOURCE OF TRUTH for contracts (ADR-0003)
│ ├── bitvault/
│ │ ├── identity/v1/*.proto
│ │ ├── files/v1/*.proto
│ │ ├── storage/v1/*.proto
│ │ ├── sync/v1/*.proto
│ │ ├── sharing/v1/*.proto
│ │ ├── search/v1/*.proto
│ │ └── events/v1/*.proto # the Published Language (domain events)
│ └── buf.yaml / buf.gen.yaml # lint + multi-language codegen
│
├── gen/ # generated code (checked in or built): go, ts, openapi
│ ├── go/...
│ ├── ts/...
│ └── openapi/bitvault.yaml
│
├── cmd/ # BINARIES — this is where mono↔micro is decided
│ ├── bitvaultd/ # v1: ONE binary running all modules (ADR-0001)
│ │ └── main.go # wires modules by config; runs gateway+control+workers
│ ├── bitvault-cli/ # the Go CLI (dogfoods REST)
│ └── (future) gateway/ identity/ files/ storage/ sync/ search/ worker/
│ └── main.go # each just wires ONE internal module → standalone
│
├── internal/ # all Go domain code (not importable outside repo)
│ ├── platform/ # cross-cutting, no domain knowledge
│ │ ├── observability/ # OTel setup, logging, metrics (ADR-0013)
│ │ ├── config/ # 12-factor config + profiles
│ │ ├── server/ # gRPC + HTTP server scaffolding, health
│ │ ├── db/ # Postgres pool, migrations runner, RLS context
│ │ ├── bus/ # event bus iface: in-proc impl + NATS impl (ADR-0006)
│ │ ├── outbox/ # transactional outbox writer + drainer
│ │ └── authctx/ # tenant/principal context propagation
│ │
│ ├── storage/ # Storage CONTEXT (provider abstraction, ADR-0005)
│ │ ├── domain/ # blob, multipart, refcount, presign iface
│ │ ├── provider/ # adapters: minio/ s3/ r2/ gcs/ azure/
│ │ ├── conformance/ # provider conformance test suite
│ │ ├── app/ # use-cases (presign, commit-blob, gc)
│ │ └── grpc/ # Storage gRPC server
│ │
│ ├── files/ # File & Metadata CONTEXT (source of truth)
│ │ ├── domain/ # node, version, tag aggregates + invariants
│ │ ├── app/ # commit protocol, move/copy, trash
│ │ ├── repo/ # Postgres repositories
│ │ └── grpc/
│ │
│ ├── sync/ # Synchronization CONTEXT
│ │ ├── domain/ # change, cursor, conflict
│ │ ├── app/ # journal projector, delta, conflict resolver
│ │ ├── repo/
│ │ └── grpc/
│ │
│ ├── identity/ # Identity & Access CONTEXT
│ ├── sharing/ # Sharing & Permissions CONTEXT
│ ├── search/ # Search & Indexing CONTEXT (query + indexer)
│ ├── notification/ # Notification & Events CONTEXT
│ ├── billing/ # Billing & Metering CONTEXT
│ ├── admin/ # Administration & Platform CONTEXT (incl. audit)
│ │
│ └── gateway/ # API Gateway / BFF (REST edge, REST↔gRPC)
│ ├── rest/ # handlers, OpenAPI wiring
│ ├── middleware/ # authn, rate-limit, tracing
│ └── bff/ # web/mobile aggregation
│
├── pkg/ # PUBLIC Go libs (importable by external consumers)
│ ├── client/ # generated/hand Go SDK for the REST API
│ └── apperr/ # shared error taxonomy
│
├── apps/ # non-Go clients (JS monorepo: pnpm/turbo)
│ ├── web/ # Next.js app
│ │ └── (uses gen/ts client)
│ └── mobile/ # React Native (FUTURE — placeholder)
│
├── deploy/ # everything ops (ADR-0012)
│ ├── docker/ # Dockerfiles (multi-stage, distroless/nonroot)
│ ├── compose/ # docker-compose.{lite,standard,full}.yml (self-host)
│ ├── helm/bitvault/ # Helm chart with values.{lite,standard,full}.yaml
│ ├── k8s/ # raw manifests / kustomize bases
│ └── terraform/ # optional infra modules (managed PG/object store)
│
├── migrations/ # SQL migrations (forward-only, versioned)
│
├── test/ # cross-cutting tests
│ ├── e2e/ # black-box against a running stack
│ ├── load/ # k6/vegeta scenarios (validate NFR SLOs)
│ └── chaos/ # fault injection (dual-write, conflict harness)
│
├── docs/ # THIS architecture set (see 10-documentation-structure)
│ ├── architecture/
│ ├── adr/
│ └── ...
│
└── .github/ or .gitlab-ci/ # path-scoped CI pipelines
3. The layout is the architecture
| Principle | How the tree enforces it |
|---|---|
| Modular monolith first (ADR-0001) | One cmd/bitvaultd wires every internal/<context> module; no other binary needed to run the product |
| Extractable to services | Each context already exposes a grpc/ server + app/ use-cases; extraction = add cmd/<context>/main.go that wires only that module |
| Bounded contexts are physical (04) | One top-level internal/<context>/ per context; domain/app/repo/grpc inside each |
| One owner per data (05) | Only internal/files/repo touches node tables; others call files/grpc or consume events |
| Contracts don’t drift (ADR-0003) | All .proto in proto/; Go + TS + OpenAPI generated into gen/ |
| Platform is reusable | internal/platform/* has zero domain imports; everything imports it, never the reverse |
| Ops is first-class | deploy/ profiles map 1:1 to the dependency tiers (lite/standard/full) |
| NFRs are testable | test/load and test/chaos exist to validate the SLOs and invariants in 03 |
4. Dependency direction (enforced, e.g. via lint rules)
cmd/* ──▶ internal/<context>/grpc ──▶ internal/<context>/app ──▶ internal/<context>/domain
│
└──▶ internal/platform/* (never the reverse)
internal/<context-A> ──X──▶ internal/<context-B> (forbidden: cross-context import)
internal/<context-A> ──▶ gen/go/<context-B> (allowed: call B's gRPC API)
internal/<context-A> ──▶ proto events (allowed: publish/consume events)
- Domain layers import nothing but the standard library + their own types.
- No context imports another context’s internals — only its generated gRPC client or its events. This is the rule that keeps extraction a no-op for callers (they already go through the API).
platform/is a leaf — imported by all, importing none of the domain.
A CI lint (e.g. an import-boundary linter) should fail the build on violations, so the monolith cannot quietly decay into a tangle that resists extraction.
5. Build & run ergonomics (proposed)
task gen— regenerate Go/TS/OpenAPI fromproto/.task up:lite— Compose with Postgres + MinIO +bitvaultd(self-host smoke).task up:full— Compose with all deps for full-feature local dev.task test/task test:e2e/task test:chaos.helm install bitvault deploy/helm/bitvault -f values.full.yaml— SaaS path.
Same source tree, same image; the deployment profile, not the code, decides mono-vs-micro and which dependencies are present.