Local Development Guide
Run the full PinpointPOS stack locally: Spanner emulator, Firebase Auth
emulator, all 9 local microservices, provider stubs, the API proxy, and website
dev servers. scripts/local-validation.sh is the only supported local stack
lifecycle command surface. Use scripts/test-local.sh for automated test
batteries.
Prerequisites
- Docker
- Bazel 9
- Node.js + pnpm (for website dev servers)
- Firebase CLI (
npm install -g firebase-tools) - PostgreSQL client (
psql) — for super admin seeding via PgAdapter - Python 3 (for UUID generation)
- OpenSSL (for auto-generating dummy credentials)
Quick Start
# Run the full Linux-local validation pass, then leave ports open for manual QA
./scripts/local-validation.sh full
# Or start the local validation stack only
./scripts/local-validation.sh up
./scripts/local-validation.sh smoke all
./scripts/local-validation.sh ports
On first run, the script will automatically:
- Generate dummy GCP service account credentials (for Firebase Admin SDK init)
- Start Spanner emulator + create instance/database
- Start Firebase Auth emulator
- Build and load all OCI images into Docker
- Start all 9 microservices as Docker containers
- Create a super admin Firebase user in the admin tenant
- Insert the super admin into the database with
mgmt_super_adminrole
Default Super Admin
The local validation stack auto-seeds a super admin user:
| Field | Value |
|---|---|
admin@peakpos.co | |
| Password | printed by ./scripts/local-validation.sh ports |
| Tenant | admin-tenant-local |
| IAM Role | mgmt_super_admin |
Use these credentials to sign in to the Support portal at http://localhost:5174.
What's Running
| Service | Port | URL |
|---|---|---|
| Spanner emulator | 9010 | gRPC |
| Spanner emulator REST | 9020 | http://localhost:9020 |
| Firebase Auth emulator | 9099 | http://localhost:9099 |
| Firebase Emulator UI | 4000 | http://localhost:4000 |
| merchant-api | 8081 | http://localhost:8081 |
| management-api | 8082 | http://localhost:8082 |
| tx-bundler | 8083 | http://localhost:8083 |
| terminal-api | 8084 | http://localhost:8084 |
| terminal-onboarding | 8085 | http://localhost:8085 |
| status | 8086 | http://localhost:8086 |
| kitchen-api | 8087 | http://localhost:8087 |
| auth | 8088 | http://localhost:8088 |
| ai-api | 8089 | http://localhost:8089 |
| Customer portal | 5173 | http://localhost:5173 |
| Support portal | 5174 | http://localhost:5174 |
| peakpos.co | 5175 | http://localhost:5175 |
Environment Variables
Most env vars match the Terraform config (infra/tf/gcp/microservices.tf). Spanner paths keep the production-style project name pinpoint-payments.
| Variable | Value | Purpose |
|---|---|---|
SPANNER_EMULATOR_HOST | localhost:9010 | Routes PgAdapter to Spanner emulator |
DATABASE_URL | jdbc:cloudspanner:/projects/pinpoint-payments/instances/test/databases/pinpointpos | Spanner JDBC URL |
GOOGLE_CLOUD_PROJECT | pinpoint-payments | Matches production project ID |
FIREBASE_AUTH_EMULATOR_HOST | localhost:9099 | Routes Firebase Admin SDK to emulator |
APP_CHECK_ENABLED | true | scripts/local-validation.sh up enables App Check with the local HMAC verifier |
FIREBASE_APP_CHECK_VERIFIER_MODE | unset / local-hmac | local-hmac enables deterministic local App Check token verification for QA |
QA_LOCAL_APP_CHECK_SECRET | qa-local-test-only-secret in QA | Local-only HMAC secret used by the QA App Check token harness |
INTERNAL_AUTH_VERIFIER_MODE | unset / local-hmac | local-hmac enables deterministic local service-to-service JWT verification for QA |
QA_LOCAL_INTERNAL_AUTH_SECRET | falls back to QA_LOCAL_APP_CHECK_SECRET | Local-only HMAC secret used for internal service tokens |
INTERNAL_AUTH_ENFORCE_AUDIENCE | false in dev, true in QA | Enables receiver audience checks for local internal service tokens |
IDENTITY_TENANT_ID | user-tenant-local / admin-tenant-local | Firebase multi-tenant ID |
PGADAPTER_PORT | 9432-9438 | Unique PgAdapter port per Spanner-backed service |
GOOGLE_APPLICATION_CREDENTIALS | /tmp/credentials.json | Auto-generated dummy creds in containers (project_id=local for Firebase emulator token verification) |
Management Commands
./scripts/local-validation.sh full # Automated checks, high-fidelity boot, deep smoke, reports, ports
./scripts/local-validation.sh up # Start the full local validation stack
./scripts/local-validation.sh down # Stop the full local validation stack
./scripts/local-validation.sh reset # Stop and clear generated local validation state
./scripts/local-validation.sh seed # Rerun local validation seed
./scripts/local-validation.sh status # Show what's running
./scripts/local-validation.sh logs <svc> # Tail logs for a service/container/component
Local Validation Stack
scripts/local-validation.sh up is the higher-fidelity local setup for PR QA.
It starts the provider stubs, the normal local services, an nginx API proxy on
http://localhost:8000, a terminal mTLS proxy on https://localhost:8443 when
available, and validation-configured portal dev servers. Unlike normal dev, it
sets:
ENVIRONMENT=qaAPP_CHECK_ENABLED=trueFIREBASE_APP_CHECK_VERIFIER_MODE=local-hmacINTERNAL_AUTH_VERIFIER_MODE=local-hmacINTERNAL_AUTH_ENFORCE_AUDIENCE=trueNOTIFICATION_PUBLISHER_MODE=http-captureAI_MODEL_PROVIDER=local-stubPRODUCT_IMAGE_STORE_MODE=http-stubSTATUS_PROVIDER_MODE=local-stub
The validation portals receive a local VITE_LOCAL_APP_CHECK_TOKEN, so browser
requests exercise the App Check header path without calling the real Firebase
App Check provider. The token is minted against the current clock with a
24-hour TTL and regenerated when stale, which keeps local expiry behavior
aligned with the service verifier without breaking long QA sessions. Local
service-to-service calls receive HMAC JWTs with per-service audiences, so
/internal/** routes no longer depend on the dev bypass in QA mode.
Useful validation commands:
./scripts/local-validation.sh doctor
./scripts/local-validation.sh full
./scripts/local-validation.sh up
./scripts/local-validation.sh smoke all
./scripts/local-validation.sh rebuild merchant-api
./scripts/local-validation.sh restart retail
./scripts/local-validation.sh logs merchant-api
./scripts/local-validation.sh ports
./scripts/local-validation.sh down
smoke all signs in through the Firebase Auth emulator, verifies
/auth/merchant/me, checks strict local guardrails, checks feature resolution,
loads the purchasing directory, creates a disposable account purchase-order
draft, proves local adapter captures, and runs focused retail/support browser
smoke against seeded data.
full is the one-command Linux-local validation run. It executes the automated
local checks, excludes iOS/Mac/Xcode-only proof as required Linux gates, boots
the high-fidelity environment with duplicate boot smoke suppressed, runs smoke deep, refreshes external-gate evidence as non-blocking information, and prints
ports at the end. Each run writes:
.dev-logs/full-local-validation/<run-id>/events.jsonl.dev-logs/full-local-validation/<run-id>/summary.json.dev-logs/full-local-validation/<run-id>/summary.md.dev-logs/full-local-validation/<run-id>/logs/*.log
Use ./scripts/local-validation.sh full --plain --json for AI-agent friendly
console output, --dry-run to inspect the step graph, --skip-android when
Android artifacts are intentionally out of scope, and --down-after when the
stack should not remain available for manual QA.
The provider/capture substitutes are now product-selectable in QA mode, not only standalone stubs:
- Notifications publish to
NOTIFICATION_CAPTURE_BASE_URLthrough the shared notification publisher. ai-apicallsQA_AI_PROVIDER_BASE_URLand does not requireOPENAI_API_KEY.- Merchant product image upload/delete paths use
QA_IMAGE_STORE_BASE_URL. - The status service reads
QA_CLOUD_STATUS_BASE_URLthrough its Cloud Run health client seam.
The terminal mTLS proxy uses a generated local CA and simulator client
certificate. In the full QA stack, local-validation/qa.sh also writes that generated
certificate hash to the simulator terminal row and smokes a real terminal sync
endpoint through the proxy, so local QA covers both edge certificate rejection
and terminal-api principal lookup.
./scripts/local-validation/qa.sh test terminal-lifecycle renders dry-run SQL fixtures for
active, expired, revoked, and wrong-terminal certificate states without starting
the stack.
Device & Simulator Reachability
By default the local stack binds host loopback only, so it is reachable from this machine but not from a separate device. Device reachability is opt-in and does not change the default behavior:
# Localhost-only (emulator / simulator on this host): unchanged default
./scripts/local-validation.sh up
# Also publish the API proxy (:8000), terminal mTLS proxy (:8443), and Firebase
# Auth emulator (:9099) on the host LAN so emulators/simulators/physical devices
# can reach the stack, then print resolved base URLs and probe them.
./scripts/local-validation.sh up --expose=lan
# Re-resolve + probe reachability against an already-running stack
./scripts/local-validation.sh expose lan
--expose=lan detects the host LAN IP (override with LOCAL_VALIDATION_LAN_IP)
and rebinds the Firebase Auth emulator to 0.0.0.0 via a generated runtime
config (the checked-in firebase.json default stays localhost-only). The API
proxy and terminal mTLS proxy already publish on all interfaces through their
Docker ports: mappings. --expose=tunnel uses cloudflared/ngrok if one is
installed and otherwise reports that tunnel mode is unavailable.
Resolved base URLs by audience:
| Audience | Host | API proxy | Firebase Auth |
|---|---|---|---|
| Android emulator | 10.0.2.2 | http://10.0.2.2:8000 | 10.0.2.2:9099 |
| iOS simulator | 127.0.0.1 | http://127.0.0.1:8000 | 127.0.0.1:9099 |
| Physical device | host LAN IP | http://<lan-ip>:8000 | <lan-ip>:9099 |
Per-app helper (prints base URLs, the exact env vars / system properties the app consumes, and mints a fresh terminal onboarding token via management-api):
./scripts/local-validation.sh mobile android
./scripts/local-validation.sh mobile ios
./scripts/local-validation.sh mobile peak-mobile-android
./scripts/local-validation.sh mobile peak-mobile-ios
# Append `activate` to also print the build+activate command for that lane
./scripts/local-validation.sh mobile android activate
The Peak POS Android app reads QA_ANDROID_BACKEND_HOST (and the related
QA_ANDROID_PUBLIC_API_BASE_URL / QA_ANDROID_TERMINAL_MTLS_BASE_URL /
QA_ANDROID_ONBOARDING_BASE_URL / QA_ANDROID_FIREBASE_AUTH_EMULATOR_HOST
overrides, or their peakpos.qa.android.* system-property equivalents) when its
environment is switched to "Local QA"; set QA_ANDROID_BACKEND_HOST to the host
LAN IP for physical devices. The iOS / Peak Mobile iOS local-QA XCTest profile
points the local-merchant-proxy SDK transport profile at the merchant API
proxy base URL. Mac/Xcode and physical-device runs remain external gates on a
Linux host.
Authentication
Firebase Auth runs via the emulator. The super admin is auto-seeded.
- Open the Support portal at http://localhost:5174
- Sign in with
admin@peakpos.co/LOCAL_SUPPORT_PASSWORD - Or open the Firebase Emulator UI at http://localhost:4000 to manage users
# Health check (no auth)
curl http://localhost:8081/health
# Authenticated request
TOKEN="..." # From frontend dev tools or emulator
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8081/api/v1/orgs/{orgId}/stores/{storeId}/products?page=0&size=10"
Unified Local Testing
scripts/test-local.sh is the canonical local test runner:
./scripts/test-local.sh doctor
./scripts/test-local.sh quick
./scripts/test-local.sh contracts
./scripts/test-local.sh local
./scripts/test-local.sh no-heavy
./scripts/test-local.sh full
./scripts/test-local.sh external-gates
Use ./scripts/test-local.sh no-heavy when memory is constrained. It does not
source Gateway Maven credentials and does not run Bazel, Docker Compose,
Playwright, Android tooling, or live database checks. It runs shell syntax,
Python QA unit tests/verifiers, static wiring checks, docs/seed/proxy contracts,
workflow contracts, staging/local parity checks, stub scenario dry-runs,
seed-to-workflow coverage, PCI redaction, and git diff --check. The docs
render verifier is skipped in this mode when Docusaurus CLIs are not installed
under docs/*/node_modules.
Full stack startup, Bazel compilation, browser E2E, Android/device proof, and
staging smoke remain high-memory validation until explicitly allowed.
Deferred high-memory validation includes the full service stack, Bazel
compilation, browser E2E, Android/device proof, and staging smoke.
The low-memory workflow checks are also available as focused QA scopes:
./scripts/local-validation/qa.sh test workflow-contracts
./scripts/local-validation/qa.sh test staging-parity
./scripts/local-validation/qa.sh test stub-scenarios
./scripts/local-validation/qa.sh test seed-workflow-coverage
By default these scopes are static or dry-run only. To query already-running local stub/capture endpoints without starting anything, run:
QA_RUN_OPTIONAL_RUNTIME_PROBES=true ./scripts/local-validation/qa.sh test workflow-contracts
QA_RUN_OPTIONAL_RUNTIME_PROBES=true ./scripts/local-validation/qa.sh test stub-scenarios
If Gateway Maven artifacts fail with 401/403, source the credential helper
before rerunning. The helper can use authenticated gcloud access-token state:
source scripts/gateway-maven-credentials.sh
Android emulator execution is externally gated from WSL unless a Windows emulator/ADB bridge is already reachable. Mac/Xcode XCFramework proof is external-only on this host.
Build Verification
# Compile check (fastest)
bazel build //apps/microservices/merchant-api:core
# Build all microservices
bazel build //apps/microservices/...
# Unit tests (no emulator needed)
bazel test //apps/microservices/merchant-api:core_test
bazel test //apps/microservices/...
Architecture Notes
- Docker containers with
--network host: All microservices run as Docker containers using--network hostso they can reach the Spanner and Firebase emulators on localhost. Each service gets a uniqueSERVER_PORT. Only the Spanner-backed services get aPGADAPTER_PORT. - PgAdapter is embedded in the Spanner-using microservices (
merchant-api,auth,management-api,terminal-api,terminal-onboarding,tx-bundler, andkitchen-api). It translates PostgreSQL wire protocol to Spanner gRPC. Each of these containers gets a uniquePGADAPTER_PORTto avoid conflicts on the shared host network. Thestatusandai-apiservices do not use Spanner/PgAdapter. - Schema auto-creation:
SqlDelightDrivers.ensureSchema()creates all tables on first startup using Spanner DDL batch (START BATCH DDL/RUN BATCH). This only works via PgAdapter -> Spanner, not plain PostgreSQL. - IAM seeding: management-api's
IamSeederauto-createsmgmt_super_admin,mgmt_admin, andmgmt_userroles on startup. The local validation seed then assigns the super admin user tomgmt_super_admin. - OCI images are built by
rules_img(no Dockerfiles).bazel run //apps/microservices/<name>:image_tarballloads each image into Docker. - Firebase multi-tenancy: merchant-api, auth, terminal-api, tx-bundler, and kitchen-api validate Firebase tokens against the
user-tenant-localtenant where applicable. management-api validates Firebase tokens against theadmin-tenant-localtenant. terminal-onboarding does not use Firebase Auth. - Validation runtime:
scripts/local-validation.shis the local stack runtime interface.scripts/local-validation/qa.shis an internal helper for focused checks invoked byscripts/test-local.sh. - Test command surface:
scripts/test-local.shis the public automated testing interface. It reuses lower-level helpers for local-only reset, seed verification, proxy/stub checks, Android handoff evidence, and external-gate probes. The old QA ledger system is retired. - Do NOT use
SPRING_PROFILES_ACTIVE=local— it activates a PostgreSQL DataSource that can't run Spanner DDL. Use the default profile withSPANNER_EMULATOR_HOSTset. - Credentials are auto-generated:
scripts/dummy-credentials.jsonis created on first run with a fresh RSA key. It's gitignored. For local auth emulator compatibility, the generated Firebaseproject_idislocal(so backend token verification matches emulator-issuedaud=localtokens).