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)

:::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