Files
sxchnge/docs/local-development.md
T

12 KiB

Local development

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

Build and bootstrap

Requires Make, a POSIX shell and a local Docker engine with Compose v2 and BuildKit. Docker builds Go, protobuf bindings and Flutter web; host Go, protoc and Flutter installations are not required for make build && make up. The tooling container includes Docker CLI, curl, jq, Git and OpenSSL. No host Python, jq, curl or Git installation is needed for these Make targets. It mounts the local Docker socket and workspace to orchestrate Compose; a remote Docker daemon is not supported. The first Flutter SDK download can take several minutes. Both ARM64 and AMD64 build from the same source definitions.

make build && make up

make build resolves the current stable upstream releases, pulls infrastructure and builds NATS packaging, notifications, BFF and frontend images. Vault and Mongo use upstream images directly. Resolution excludes prereleases and selects Nginx's stable channel. The exact versions and manifest digests are saved in the ignored .local/dev/images.env; Flutter's stable release is pinned by Git commit. Build fails if upstream metadata cannot be resolved. Run make build again to refresh versions. Review database release upgrade requirements before updating a populated local volume across major versions; the tooling does not perform data migrations.

make up starts persistent Vault, initializes/unseals it, provisions secrets and AppRoles, initializes an authenticated single-member MongoDB replica set, configures Vault's MongoDB database plugin, and starts authenticated NATS JetStream. It then starts notifications, BFF and the frontend when the required email settings exist.

On the first run, make up intentionally exits with an error after infrastructure bootstrap because Resend credentials have not been supplied. No placeholder API key, mock delivery mode or embedded credential is installed. The three infrastructure services remain running so you can configure Vault, then rerun make up.

Configure Resend in Vault

The default email driver is resend. In the Vault UI at http://127.0.0.1:18200, edit the KV v2 secret kv/email (API path kv/data/email):

Field Value
driver resend
api_key Your Resend API key
sender_address Your verified Resend sender email
sender_name SXCHNGE or your desired display name

Local administrative login material is in the private, ignored .local/dev/admin/init.json (root_token). Keep that file private. Product containers never mount it. You can also provision the same secret with a hidden terminal prompt, without placing the API key in shell history:

make email
make up

The startup check rejects an empty key or the bootstrap sender placeholder. It does not validate the key against Resend or prove domain verification; an invalid key fails at delivery. Vault Agent fetches the key and email settings and injects them into the notifications process. There is no .env secret fallback. Changing kv/email restarts the notification process automatically within about 10 seconds. Bootstrapping again preserves your existing email secret.

Open the product

After providing Resend credentials:

Trust .local/dev/ca.pem in your local browser/OS certificate store to use HTTPS without a certificate error. Trust installation is a separate local action, not performed by the bootstrap. For a command-line check without changing trust:

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

HTTPS keeps the existing Secure/HttpOnly refresh cookie contract. Nginx serves Flutter and proxies /api/ to the BFF on the same origin. The bundle selects the existing backend auth adapter with the public client ID sxchnge-local-web. Bootstrap inserts this public browser client if absent; it has no client secret. No frontend widget, service or provider architecture changes are needed.

The rest of the app retains its existing capabilities and limitations: wallet, KYC and settings fixtures are not turned into production integrations by this container setup. Full email signup/login requires a working Resend account.

Secret and connection lifecycle

Consumer Vault access
BFF database/creds/bff, JWT key, notification caller token and CA
Notifications database/creds/notifications, NATS token, caller token, TLS certificate/key and email settings
NATS NATS authentication token only
Bootstrap Infrastructure administration, PKI and initial identities

The database engine is configured at database/config/mongo with mongodb-database-plugin. It uses its own vault-manager Mongo user, whose password is rotated by Vault immediately after configuration. The manager has userAdminAnyDatabase; applications never receive that credential. The independent local Mongo administrator and replica-set key come from Vault's random generator and are retained in kv/bootstrap/mongo for local provisioning.

The BFF role has readWrite only on sxchnge; notifications has readWrite only on sxchnge_notifications. Mongo URIs come from dynamic Vault credentials, not static application users. Leases default to one hour with a 24-hour maximum. The shared database wrapper obtains credentials directly from Vault using the service's AppRole. At half the effective lifetime (the shorter of the database lease and owning token TTL), it authenticates independently and prepares a new Mongo pool. Only a successful primary ping permits publication. New operations use that pool; active operations, cursors and complete transactions, including nested stores and commit retries, retain the old pool until they finish. The old pool then closes and its dedicated token is revoked. Mongo credential replacement never restarts BFF or notifications. Each generation has a separate token so cleanup cannot revoke the replacement or Vault Agent's identity.

Refresh failures keep the current valid pool and retry with bounded backoff. Operations are bounded by credential expiry. If Vault remains unavailable beyond that deadline, new database operations fail closed; refresh continues and access recovers automatically without restarting the service. Administrative revocation, operations exceeding their lease and infrastructure failures cannot be made uninterrupted by pool rotation. Shutdown rejects new scopes and drains existing work. Failed cleanup relies on Vault expiration as a backstop.

Repositories and transaction factories share source.Source rather than retaining concrete Mongo collections. Low-level consumers must acquire a scope, use its returned context and release it after all work completes; never retain its client or context beyond release. source.Fixed adapts externally owned test/static connections. Database credentials are no longer Agent environment templates; Agent continues to fetch non-database secrets and may restart children when those secrets change.

Each agent authenticates using its own AppRole with a renewable periodic token. Local SecretIDs are reusable for container restarts; each container mounts only its own identity directory under .local/dev/agents/. Revoking an AppRole SecretID requires removing its matching local secret-id file and rerunning make up to issue a replacement. Revoking an active token/lease terminates its credentials; The database wrapper retries with a fresh identity; Agent independently retries its non-database secrets when Vault is reachable.

The notification gRPC connection uses a Vault-issued certificate for notifications and the BFF validates the Vault CA and hostname. Frontend HTTPS certificates also come from Vault PKI. Leaf certificates last 30 days; make up refreshes them when less than seven days remain and reloads Nginx. Run it regularly for a development stack kept online for weeks. Agents restart when their TLS KV secret changes. Application secrets are never put into the frontend bundle.

Vault, Mongo and JetStream data use project-scoped named Docker volumes. Local Vault unseal material, administrator login, per-service bootstrap identities and frontend TLS material are under ignored .local/dev/, outside image build contexts. The local Vault automatically unseals using its local key on container restart. These convenience credentials and loopback HTTP Vault access are for this local machine; this is not a production Vault deployment. Do not share .local/dev/. Keep it paired with its matching Docker volumes for recovery.

Mongo and NATS expose no host ports. The sxchnge-local network, volumes and container names are independent of existing Sendico stacks. Only the frontend and Vault bind host loopback ports. No Sendico database, message bus or secrets are consumed.

Manage and verify

make ps
make logs
make down             # preserves named volumes and local bootstrap material
make up               # idempotent provisioning and readiness checks
make test-dev-tooling # offline shell and failure-path checks
make test-dev-infra   # works before Resend provisioning
make test-dev-rotation # real Vault rotations under BFF HTTP traffic
make test-dev         # requires all six services running

The infrastructure check exercises AppRole access restrictions, real Vault MongoDB credential issuance, per-database access, lease renewal and user deletion on revocation. The complete check also verifies service health and frontend/API HTTPS with the generated CA. These checks never send a real email. test-dev-rotation temporarily sets a 12-second BFF lease, deploys the built BFF and frontend, and requires three pool replacements with unchanged process ID and zero unexpected HTTP responses. Its invalid-token requests exercise Mongo-backed transactions without sending email. The check restores the role TTL and removes services it started; it recreates BFF/frontend at the beginning, so run it as a development validation command, not against active work you need to preserve. Race-enabled backend tests additionally hold a transaction and cursor across a replacement and exercise failed candidate validation, outage recovery, rollback, expiry rejection and graceful shutdown.

Backend regression tests use fresh mongo:latest and nats:latest official stable images, pulled before make test-backend. Normal validation remains make test and make lint-backend and requires the host SDKs described in the repository guides.

Deleting Docker volumes removes local data; make down deliberately keeps them. If starting from scratch, stop the stack and explicitly remove this project's three volumes and its matching .local/dev/ bootstrap state together. Never reuse old unseal material with a newly initialized Vault volume, or retain Mongo users while discarding the Vault data that manages their leases.

Sources and checked releases

On 2026-09-12 the resolver selected Vault 2.1.0, MongoDB 8.3.9, NATS 2.14.6, Go 1.27.1, Nginx stable 1.30.4, Ubuntu 26.04 and Flutter stable 3.47.4. These are verification records, not fixed future defaults. Release selection follows the Vault releases, MongoDB stable release notes, NATS releases, Nginx downloads, Go releases and Flutter stable channel. The implementation follows HashiCorp's MongoDB database plugin and Agent process supervisor contracts.