Local Development & Deployment
Deployment targets
| Target | How |
|---|---|
| Local development | Docker Compose, driven through ./deploy.sh at the repo root |
| Staging | Kubernetes on a cloud provider — manifests not yet written |
| Production | Kubernetes with HA — not yet built |
Quick start: ./deploy.sh
The compose file lives at deployment/docker-compose.yml; all Gradle modules are under
:backends:*. Everything runs through ./deploy.sh at the repo root — do not call docker compose directly.
# First time only: map *.localhost hostnames to 127.0.0.1
./deploy.sh setup
# Start everything (postgres, redis, all backends, all frontends, nginx, docs site)
./deploy.sh start
# Start only backends or only frontends
./deploy.sh start backends
./deploy.sh start frontends
# Rebuild + restart everything, or one app
./deploy.sh restart core-engine
# Show running containers + all service URLs
./deploy.sh status
# Stream logs (all services, or one)
./deploy.sh logs
./deploy.sh logs api-gateway
# Stop everything, or one target
./deploy.sh stop
# Full reset: containers, images, volumes, build cache + outputs
./deploy.sh purge./deploy.sh status prints the live URL map. In local Docker Compose mode, services are reached
through nginx virtual hosts, not raw container ports:
http://authority.localhost Attest Authority (Issuer Portal)
http://verify.localhost Verifier Portal
http://credentials.localhost Holder Wallet
http://console.localhost Admin Console
http://custodian.localhost Custodian Portal
http://docs.localhost Documentation site
http://localhost:9200 API Gateway
http://localhost:9280 Core Engine (+ /swagger-ui.html)
http://localhost:9281 Credential Issuance
http://localhost:9282 Verification Engine
http://localhost:9283 Issuer Registry
http://localhost:9284 Admin Console
http://localhost:9285 Program Catalog
http://localhost:9286 Learner Records
http://localhost:9287 Custodian RegistryThese 92xx host ports are Docker Compose’s external mapping only — inside the compose network
(and when running a service directly via bootRun, outside Docker), every service listens on its
CLAUDE.md-documented port (API Gateway 8000, Core Engine 8080, Credential Issuance 8081, etc.).
Don’t mix the two: localhost:8080 works for a directly-bootRun Core Engine, but under
./deploy.sh you’d reach the same service at localhost:9280.
Individual service startup (without Docker)
# Data tier only via Docker
./deploy.sh start backends # or target only the data tier — see docker-compose.yml profiles
# Then run services individually
./gradlew :backends:api-gateway:bootRun
./gradlew :backends:core-engine:bootRun
./gradlew :backends:credential-issuance:bootRun
./gradlew :backends:verification-engine:bootRun
./gradlew :backends:issuer-registry:bootRun
./gradlew :backends:admin-console:bootRun
./gradlew :backends:program-catalog:bootRun
./gradlew :backends:learner-records:bootRun
./gradlew :backends:custodian-registry:bootRunFrontend dev servers
Each frontend is React 19 + TypeScript + Vite with hot reload, run independently (npm install && npm run dev inside its own directory) rather than through ./deploy.sh when actively developing
on it:
| Frontend | Port | Notes |
|---|---|---|
frontends/issuer-portal | 5173 | |
frontends/verifier-portal | 5174 | |
frontends/admin-console | 5175 | |
frontends/credential-wallet | 5176 | Holder-facing PWA |
frontends/landing | 5177 | Public marketing site — no API Gateway proxy |
frontends/custodian-portal | 5178 | Bulk document intake/review/attestation — see Custodian Onboarding |
frontends/docs | 5179 | This documentation site — reads documentation/ via a symlink, no API Gateway proxy |
All frontends except docs proxy API calls to API Gateway (http://localhost:8000) via each
app’s own vite.config.ts.
Environment configuration
application.yml per service, overridable via environment variables. In
deployment/docker-compose.yml, DB_URL points through pgbouncer (port 6432) while
FLYWAY_DB_URL connects straight to Postgres (port 5432) for migrations:
DB_URL=jdbc:postgresql://pgbouncer:6432/attest_pro?prepareThreshold=0
FLYWAY_DB_URL=jdbc:postgresql://postgres:5432/attest_pro
SPRING_DATA_REDIS_HOST=redis
SPRING_DATA_REDIS_PORT=6379
GRPC_SERVER_PORT=50051Container images
# Build all images
./deploy.sh build
# Build one service's image (build context is the repo root)
docker build -t attestpro/credential-issuance:latest \
-f backends/credential-issuance/Dockerfile .Kubernetes — not yet built
No Kubernetes manifests exist in this repository — local/staging today is Docker Compose only via
./deploy.sh. The snippets below are illustrative of the target shape, not runnable today.
kubectl create namespace attest-pro
kubectl apply -f k8s/config.yaml -n attest-pro
kubectl apply -f k8s/ -n attest-pro
kubectl get pods -n attest-proapiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: credential-issuance-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: credential-issuance
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: 70 }Monitoring & observability
Each service exposes /actuator/prometheus:
curl http://localhost:8080/actuator/prometheus
curl http://localhost:8080/actuator/health/liveness
curl http://localhost:8080/actuator/health/readinessEnforced Prometheus/Grafana alerting is not yet in place — an explicit launch-blocking item in CLAUDE.md’s Roadmap Context, not a solved problem.
Database migrations
Flyway runs automatically on service startup. Each service owns its own migration directory:
V1__schema.sql (+ V2__seed_data.sql where seed data exists) — see
Database Schema Ownership. Project convention is in-place edits
to V1/V2 only, never V3+ files.
Editing V1__schema.sql in place changes its checksum, so any environment with migrations already
applied needs its database reset (docker-compose down -v or equivalent, then re-up) before
Flyway will apply the edited V1 cleanly — expected whenever V1 changes.
./gradlew flywayInfo
./gradlew flywayRepairBackup & recovery
pg_dump attest_pro > backup.sql
psql attest_pro < backup.sqlA verified backup/restore drill has not been performed — an explicit launch-blocking item. Redis is configured for RDB snapshots and AOF for durability.
Security
- All external endpoints use HTTPS/TLS 1.3 in the target production design; local dev via
./deploy.shruns plain HTTP through nginx virtual hosts. - Internal gRPC mTLS is implemented, not just planned — every gRPC connection between Core Engine and its downstream services requires mutual TLS with no plaintext fallback. See Internal gRPC mTLS.
Scaling strategies
| Service | Notes |
|---|---|
| Verification Engine | Stateless — scales to any number of replicas |
| API Gateway | Scales horizontally |
| Core Engine | Run 2+ for HA |
| Credential Issuance | Stateless, scales horizontally |
| Issuer Registry / Custodian Registry | Redis cache (Issuer Registry only), scale with cache strategy |
| PostgreSQL | Primary bottleneck — consider read replication; local dev already runs pgbouncer in front |
Performance tuning
JAVA_OPTS="-Xms2g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 --enable-preview"- Connection pooling: HikariCP + pgbouncer in front of Postgres.
- Query caching: Redis for frequently accessed issuers/DIDs/catalog entries.
- Indexes on
issuer_id,credential_id,status,created_at. - Redis eviction: LRU, 1-hour TTL for most data; 10-minute TTL for verification results; 24-hour TTL for DID document cache.
Troubleshooting
Prefer ./deploy.sh logs <app> and ./gradlew :backends:<service>:... throughout — there is no
raw docker-compose invocation in this project’s actual workflow.
# Service won't start
./deploy.sh logs api-gateway
lsof -i :9200 # see the 92xx host-port map above
./deploy.sh restart gateway
# High latency
grpcurl -plaintext localhost:50051 list
redis-cli INFO stats
# Out of memory (directly-bootRun service)
JAVA_OPTS="-Xms4g -Xmx8g" ./gradlew :backends:api-gateway:bootRunRollback
# Docker Compose
./deploy.sh stop
docker tag attestpro/api-gateway:latest attestpro/api-gateway:rollback
./deploy.sh start # redeploy with previous image tag
# Kubernetes (once manifests exist)
kubectl rollout history deployment/api-gateway -n attest-pro
kubectl rollout undo deployment/api-gateway -n attest-pro
kubectl rollout undo deployment/api-gateway -n attest-pro --to-revision=5Production checklist
See CLAUDE.md’s “Roadmap Context” for the authoritative, current launch-blocking list. Still open as of this writing: staging environment with a promotion gate, enforced Prometheus/Grafana alerting, end-to-end tracing, a third-party security audit, legal/compliance sign-off, a verified backup/restore drill, load testing, an incident-response runbook, and holder claim-ownership integrity (see Holder Claim Integrity).
- All services passing tests
- Security scan (OWASP, CVE checks)
- Load testing completed
- Database backups configured and a restore drill verified
- Monitoring/alerting enforced (not just instrumented)
- Incident response plan in place
- TLS certificates installed and valid (production)
- Rate limiting configured — see API & gRPC Reference
- Database connection pooling optimized (pgbouncer already in place locally)
- Redis persistence enabled
- Graceful shutdown configured (10–30 second timeout)
See also CI/CD Pipeline for the automated build/deploy path.