Files
sxchnge/docs/local-development.md
T

7.9 KiB

Local development

Task #42, PR #43, branch dev-env-42.

Build and start

Requires Make, a POSIX shell and a local Docker engine with Compose v2 and BuildKit. No host Python, Go, Flutter, jq or curl is required. Make runs bootstrap tooling in Docker with the local Docker socket and workspace mounted. Remote Docker engines are unsupported.

make build && make up

The build resolves the latest stable releases and pins their digests in ignored .local/dev/images.env. Prereleases are excluded, resolution failures stop the build, and previous pins are retained on failure. Flutter is pinned to the stable release commit. Tooling uses a freshly pulled stable Docker CLI digest. Generated protobuf bindings and Flutter output are built locally/in Docker and never tracked.

Compose is at infra/local/compose.yml; Make supplies the repository root as its project directory. Dockerfiles are in infra/images/. Runtime and startup files are grouped by service under infra/local/. The stack consists of Vault, Mongo, NATS, notifications, BFF and Caddy serving the frontend.

On first startup, bootstrap initializes/unseals persistent Vault, provisions AppRoles and secrets, initializes Mongo replica set rs0, configures Vault's Mongo plugin, and starts authenticated NATS JetStream. Product startup fails explicitly until Resend credentials exist. Infrastructure remains available for provisioning.

Vault layout

Paths below are inside the KV v2 mount kv; the API inserts data/ after kv/.

kv/
├── services/
│   ├── bff/jwt                         # jwt
│   └── notifications/
│       ├── callers/bff                 # token; read by BFF and notifications
│       └── email/resend                # driver, api_key, sender_address, sender_name
├── infrastructure/
│   └── nats/auth                       # token; read by NATS and notifications
└── bootstrap/
    └── mongo                           # local administrator and replica-set bootstrap material

Every secret has an owning service or infrastructure component. Service policies permit only the exact paths they consume. Applications cannot read bootstrap credentials. Mongo application users are dynamic secrets under database/creds/bff and database/creds/notifications, not KV entries. They have readWrite only on their respective sxchnge / sxchnge_notifications databases. Vault uses mongodb-database-plugin and its own vault-manager account, whose password is rotated immediately after initial configuration.

There are no frontend certificates, notification certificates or generic tls entries in KV. Browser certificates belong to Caddy; internal gRPC is plaintext on the private Docker network and retains caller-token authentication.

Resend

Open Vault and edit kv/services/notifications/email/resend:

Field Value
driver resend
api_key Your Resend API key
sender_address Your verified sender address
sender_name Your display name

Local administrator login material is in private .local/dev/admin/init.json. Product containers cannot access it. Alternatively use the hidden terminal prompt:

make email
make up

No placeholder key or delivery fallback is installed. Startup validates presence; it does not send an email or validate domain ownership with Resend. Updating email settings causes Agent to restart notifications. Existing values survive bootstrap.

Browser HTTPS and internal connections

Caddy serves Flutter files and proxies /api/* to BFF over HTTP. Open https://localhost:5126. HTTPS preserves Secure/HttpOnly cookies and same-origin API access. Caddy uses tls internal and manages its local CA, certificates and renewal in the caddy-data volume. The caddy-config volume stores Caddy runtime configuration. Neither volume nor Caddy has Vault access.

After frontend starts, Make exports only the public Caddy CA to .local/dev/ca.pem. Trust it in your browser/OS to avoid certificate errors. This is a new CA when migrating from Vault-issued browser certificates; the old CA will not trust it. Trust installation is not automated. A CLI check can use:

curl --cacert .local/dev/ca.pem https://localhost:5126/api/v1/health

BFF calls notifications over plaintext gRPC with its caller token. Mongo and NATS also stay within the private Compose network; no internal service publishes a host port. Only frontend and Vault bind host loopback ports. TLS remains optional in the reusable notification client/server for deployments that explicitly configure it; local runtime configuration contains no certificate settings.

Caddy behavior follows its internal TLS and automatic HTTPS documentation.

Mongo transactions and credential rotation

Mongo is an authenticated single-node replica set (rs0), supporting multi-document transactions. It has no failover or redundant data copy. A Mongo restart interrupts access even though application credential replacement does not.

The shared database wrapper authenticates with each service's AppRole and obtains an independent token/lease for each pool. At half the shorter token/lease lifetime, it verifies a replacement pool before publishing it. Active operations, cursors and full transactions retain the old pool until they finish, then it closes and its token is revoked. Nested stores and commit retries keep the same pool.

Failed refreshes retain valid credentials and retry with bounded backoff. After expiry, database operations fail closed until automatic recovery. Administrative revocation, infrastructure outages and operations longer than their lease can still interrupt work. Repository consumers use source.Source operation scopes; source.Fixed adapts externally owned static/test connections.

Migration and persistence

Bootstrap migrates the old flat bff, caller, email and nats paths into the hierarchy above. It copies with CAS protection, compares values, and refuses to overwrite conflicting destinations. After consumers use the new paths, it removes old KV metadata and obsolete tls, frontend-tls, notifications-tls entries. Readable old version history and metadata are archived privately in .local/dev/migrations/organized-v1/ before removal. That archive is administrative recovery material; applications do not mount it. Old deleted/destroyed versions are not recreated. The old PKI engine is not used or provisioned by this stack; an existing PKI mount is retained to avoid destroying its non-exportable signing key and history as part of a KV reorganization.

Vault, Mongo, JetStream and Caddy data use project-scoped named volumes. Private bootstrap material lives in .local/dev/. make down preserves both. Vault's local unseal file allows automatic unsealing on restart. This local convenience and loopback HTTP Vault endpoint are for development. Keep bootstrap files paired with their matching volumes; do not discard one side independently.

Verification

make ps
make logs
make down
make test-dev-tooling   # shell analysis, failure paths, migration conflict checks
make test-dev-infra     # policy isolation, real Mongo transactions and lease lifecycle
make test-dev-rotation  # temporarily deploy BFF/frontend and exercise short leases
make test-dev           # requires all six services; verified frontend/API HTTPS

Rotation validation temporarily shortens the BFF role TTL, recreates BFF/frontend, and requires three replacements with unchanged BFF PID and zero unexpected transaction-backed HTTP responses. It restores TTLs and removes services it started. No verification command sends email. Backend validation remains make test and make lint-backend, using the host SDKs documented in module guides.