Skip to main content

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:

  1. Generate dummy GCP service account credentials (for Firebase Admin SDK init)
  2. Start Spanner emulator + create instance/database
  3. Start Firebase Auth emulator
  4. Build and load all OCI images into Docker
  5. Start all 9 microservices as Docker containers
  6. Create a super admin Firebase user in the admin tenant
  7. Insert the super admin into the database with mgmt_super_admin role

Default Super Admin

The local validation stack auto-seeds a super admin user:

FieldValue
Emailadmin@peakpos.co
Passwordprinted by ./scripts/local-validation.sh ports
Tenantadmin-tenant-local
IAM Rolemgmt_super_admin

Use these credentials to sign in to the Support portal at http://localhost:5174.

What's Running

ServicePortURL
Spanner emulator9010gRPC
Spanner emulator REST9020http://localhost:9020
Firebase Auth emulator9099http://localhost:9099
Firebase Emulator UI4000http://localhost:4000
merchant-api8081http://localhost:8081
management-api8082http://localhost:8082
tx-bundler8083http://localhost:8083
terminal-api8084http://localhost:8084
terminal-onboarding8085http://localhost:8085
status8086http://localhost:8086
kitchen-api8087http://localhost:8087
auth8088http://localhost:8088
ai-api8089http://localhost:8089
Customer portal5173http://localhost:5173
Support portal5174http://localhost:5174
peakpos.co5175http://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.

VariableValuePurpose
SPANNER_EMULATOR_HOSTlocalhost:9010Routes PgAdapter to Spanner emulator
DATABASE_URLjdbc:cloudspanner:/projects/pinpoint-payments/instances/test/databases/pinpointposSpanner JDBC URL
GOOGLE_CLOUD_PROJECTpinpoint-paymentsMatches production project ID
FIREBASE_AUTH_EMULATOR_HOSTlocalhost:9099Routes Firebase Admin SDK to emulator
APP_CHECK_ENABLEDtruescripts/local-validation.sh up enables App Check with the local HMAC verifier
FIREBASE_APP_CHECK_VERIFIER_MODEunset / local-hmaclocal-hmac enables deterministic local App Check token verification for QA
QA_LOCAL_APP_CHECK_SECRETqa-local-test-only-secret in QALocal-only HMAC secret used by the QA App Check token harness
INTERNAL_AUTH_VERIFIER_MODEunset / local-hmaclocal-hmac enables deterministic local service-to-service JWT verification for QA
QA_LOCAL_INTERNAL_AUTH_SECRETfalls back to QA_LOCAL_APP_CHECK_SECRETLocal-only HMAC secret used for internal service tokens
INTERNAL_AUTH_ENFORCE_AUDIENCEfalse in dev, true in QAEnables receiver audience checks for local internal service tokens
IDENTITY_TENANT_IDuser-tenant-local / admin-tenant-localFirebase multi-tenant ID
PGADAPTER_PORT9432-9438Unique PgAdapter port per Spanner-backed service
GOOGLE_APPLICATION_CREDENTIALS/tmp/credentials.jsonAuto-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=qa
  • APP_CHECK_ENABLED=true
  • FIREBASE_APP_CHECK_VERIFIER_MODE=local-hmac
  • INTERNAL_AUTH_VERIFIER_MODE=local-hmac
  • INTERNAL_AUTH_ENFORCE_AUDIENCE=true
  • NOTIFICATION_PUBLISHER_MODE=http-capture
  • AI_MODEL_PROVIDER=local-stub
  • PRODUCT_IMAGE_STORE_MODE=http-stub
  • STATUS_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_URL through the shared notification publisher.
  • ai-api calls QA_AI_PROVIDER_BASE_URL and does not require OPENAI_API_KEY.
  • Merchant product image upload/delete paths use QA_IMAGE_STORE_BASE_URL.
  • The status service reads QA_CLOUD_STATUS_BASE_URL through 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:

AudienceHostAPI proxyFirebase Auth
Android emulator10.0.2.2http://10.0.2.2:800010.0.2.2:9099
iOS simulator127.0.0.1http://127.0.0.1:8000127.0.0.1:9099
Physical devicehost LAN IPhttp://<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.

  1. Open the Support portal at http://localhost:5174
  2. Sign in with admin@peakpos.co / LOCAL_SUPPORT_PASSWORD
  3. 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 host so they can reach the Spanner and Firebase emulators on localhost. Each service gets a unique SERVER_PORT. Only the Spanner-backed services get a PGADAPTER_PORT.
  • PgAdapter is embedded in the Spanner-using microservices (merchant-api, auth, management-api, terminal-api, terminal-onboarding, tx-bundler, and kitchen-api). It translates PostgreSQL wire protocol to Spanner gRPC. Each of these containers gets a unique PGADAPTER_PORT to avoid conflicts on the shared host network. The status and ai-api services 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 IamSeeder auto-creates mgmt_super_admin, mgmt_admin, and mgmt_user roles on startup. The local validation seed then assigns the super admin user to mgmt_super_admin.
  • OCI images are built by rules_img (no Dockerfiles). bazel run //apps/microservices/<name>:image_tarball loads each image into Docker.
  • Firebase multi-tenancy: merchant-api, auth, terminal-api, tx-bundler, and kitchen-api validate Firebase tokens against the user-tenant-local tenant where applicable. management-api validates Firebase tokens against the admin-tenant-local tenant. terminal-onboarding does not use Firebase Auth.
  • Validation runtime: scripts/local-validation.sh is the local stack runtime interface. scripts/local-validation/qa.sh is an internal helper for focused checks invoked by scripts/test-local.sh.
  • Test command surface: scripts/test-local.sh is 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 with SPANNER_EMULATOR_HOST set.
  • Credentials are auto-generated: scripts/dummy-credentials.json is created on first run with a fresh RSA key. It's gitignored. For local auth emulator compatibility, the generated Firebase project_id is local (so backend token verification matches emulator-issued aud=local tokens).