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

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)

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)

Same source tree, same image; the deployment profile, not the code, decides mono-vs-micro and which dependencies are present.