syncc — Local Development Guide
Prerequisites
- Node.js 22+
- PostgreSQL 14 (Homebrew):
brew install postgresql@14 postgis - Docker Desktop (La Paz silo only — for its PostGIS container)
- A local
apps/api-server/.envfile withDATABASE_URLpointing tolocalhost:5432 - A local
apps/api-server/.env.lapazfile withDATABASE_URLpointing tolocalhost:5433
Architecture overview
syncc is a multi-silo system. Each city (Cochabamba, La Paz) runs as a fully isolated backend with its own database and JWT secret. In local development you run both silos simultaneously on different ports:
| Silo | API port | DB port | DB engine |
|---|---|---|---|
| Cochabamba | localhost:3000 |
localhost:5432 |
Homebrew PostgreSQL 14 |
| La Paz | localhost:3001 |
localhost:5433 |
Docker PostGIS 15 |
The web-admin routes API calls to the active city via CityService. The passengers-app and mobile-driver resolve the city at runtime via geolocation.
The analytics dashboard (apps/analytics) is a cross-silo app — it connects to all city APIs simultaneously and aggregates usage data, errors, session locations, and landing interactions into a single view. It requires a SUPER_ADMIN login and runs independently of any specific city stack.
Starting the local stack
Every time you open your Mac for development, run these in separate terminals:
Terminal 1 — Cochabamba database (Homebrew)
brew services start postgresql@14
Terminal 2 — La Paz database (Docker PostGIS)
# From the repo root
docker compose -f docker-compose.lapaz.yml --env-file apps/api-server/.env.lapaz up -d
Terminal 3 — Cochabamba API (localhost:3000)
cd apps/api-server
npm run start:dev
# Reads: apps/api-server/.env → DATABASE_URL → localhost:5432
Terminal 4 — La Paz API (localhost:3001)
cd apps/api-server
NODE_ENV=lapaz npx nest start --watch
# Reads: apps/api-server/.env.lapaz → DATABASE_URL → localhost:5433, PORT=3001, GTFS_SOURCE=lapaz
Terminal 5 — Frontend app (your choice)
# Admin dashboard → http://localhost:4200
cd apps/web-admin
npm start -- --configuration=local
# Passengers app → http://localhost:8100
cd apps/passengers-app
npm start -- --configuration=local
# Mobile driver → http://localhost:8100
cd apps/mobile-driver
npm start -- --configuration=local
# Analytics dashboard → http://localhost:4200
cd apps/analytics
npm start # uses environment.local.ts → localhost:3000 (single local silo)
--configuration=localtells Angular to useenvironment.local.ts(pointing tolocalhost:3000). Omitting it falls back to the default configuration, which may point to a dev or prod API URL.
The web-admin has a city switcher in the top bar. When you switch to La Paz, requests go to localhost:3001. When on Cochabamba, they go to localhost:3000.
Environment & API URL logic
The environments
| Environment | API URL | Purpose |
|---|---|---|
| local | http://localhost:3000 (Cochabamba) |
Local development on your Mac |
| dev | Defined in .env.<city>.dev |
Shared cloud dev server (per city) |
| prod | Defined in .env.<city>.prod |
Production (per city) |
The local environment always points to the Cochabamba silo (
localhost:3000). The web-admin's city switcher overrides the active API base at runtime for the La Paz silo (localhost:3001).
How it works
Each frontend app (web-admin, mobile-driver, passengers-app) has a set-env.cjs script that reads an .env file and writes the API URL and city config into the correct Angular environment file before building or serving.
npm start
└─ node set-env.cjs --environment=local
└─ reads: apps/api-server/.env (API_URL=http://localhost:3000)
└─ writes: src/environments/environment.local.ts
└─ ng serve --configuration=local
└─ uses: environment.local.ts → localhost:3000
npm run build:dev
└─ node set-env.cjs --environment=dev
└─ reads: apps/api-server/.env.<city>.dev (API_URL=<dev-api-url>)
└─ writes: src/environments/environment.ts
└─ ng build --configuration=development
npm run build:prod
└─ node set-env.cjs --environment=prod [CITY=lapaz|cbba]
└─ reads: apps/api-server/.env.<city>.prod (API_URL=<silo-api-url>)
└─ writes: src/environments/environment.prod.ts
└─ ng/ionic build --configuration=production
City resolution at runtime
web-admin resolves its city via CityService: hostname match (e.g. lapaz.admin.syncc.me) → localStorage → geolocation → default. The apiBaseUrl() is derived from the active city at runtime, so the admin never needs a per-city build.
passengers-app and mobile-driver are single builds that resolve the city via geolocation on every launch. They fetch syncc.me/cities.json (with a bundled assets/cities.json fallback) and pick the silo whose bounding box contains the user's location.
Angular environment files (never edit manually)
| File | Written by | Used by Angular config |
|---|---|---|
environment.local.ts |
set-env.cjs --environment=local |
--configuration=local |
environment.ts |
set-env.cjs --environment=dev |
--configuration=development |
environment.prod.ts |
set-env.cjs --environment=prod |
--configuration=production |
Do not manually edit these files — they are overwritten every time you run a
config:*orbuild:*script.
Backend .env files (NestJS / api-server)
These files live in apps/api-server/ and are gitignored (they contain secrets). Create them from .env.example.
| File | Used when | DB host | Port |
|---|---|---|---|
.env |
Local Cochabamba silo (npm run start:dev) |
localhost:5432 |
3000 |
.env.lapaz |
Local La Paz silo (NODE_ENV=lapaz npx nest start --watch) |
localhost:5433 |
3001 |
.env.lapaz.dev |
La Paz dev stack via deploy.sh lapaz dev |
db:5432 (Docker internal) |
— |
.env.lapaz.prod |
La Paz prod stack via deploy.sh lapaz prod |
db:5432 (Docker internal) |
— |
.env.cbba.dev |
Cochabamba dev stack via deploy.sh cbba dev |
db:5432 (Docker internal) |
— |
.env.cbba.prod |
Cochabamba prod stack via deploy.sh cbba prod |
db:5432 (Docker internal) |
— |
Each per-city env file must have unique DB_USER, DB_PASSWORD, DB_NAME, and JWT_SECRET.
La Paz-specific variable: .env.lapaz, .env.lapaz.dev, and .env.lapaz.prod must include:
GTFS_SOURCE=lapaz
This tells the API to load La Paz routes from the bundled GeoJSON instead of fetching Cochabamba GTFS data from GitHub.
Dev-specific variable: .env.cbba.dev and .env.lapaz.dev must include:
BUILD_ENV=dev
Without this, the mobile-driver and passengers-app Dockerfiles default to build:prod and compile with production: true, causing them to fetch city config from syncc.me/cities.json (production) instead of dev.syncc.me/cities.json — which makes the apps talk to the production API instead of the dev API.
npm scripts reference
analytics
| Script | What it does | API target |
|---|---|---|
npm start |
Serves with hot reload using environment.local.ts |
localhost:3000 (single silo) |
npm run build:prod |
Runs set-env.cjs, writes environment.prod.ts, builds |
All silos from ANALYTICS_SILOS |
npm startusesenvironment.local.tsdirectly — noset-env.cjsis called. For a production build,ANALYTICS_SILOSmust be set as a JSON string (e.g.'{"cochabamba":"https://api.cbba.syncc.me","lapaz":"https://api.lapaz.syncc.me"}') andANALYTICS_KEYmust match the value in each silo's.env.
All frontend apps (web-admin, mobile-driver, passengers-app)
| Script | What it does | API target |
|---|---|---|
npm start -- --configuration=local |
Writes environment.local.ts, serves with hot reload |
localhost:3000 |
npm run build:local |
Writes environment.local.ts, builds to dist/ |
localhost:3000 |
npm run build:dev |
Writes environment.ts, builds for dev deploy |
See .env.<city>.dev |
npm run build:prod |
Writes environment.prod.ts, builds for production |
See .env.<city>.prod |
api-server
| Script | What it does |
|---|---|
npm run start:dev |
Start Cochabamba silo in watch mode (reads .env, port 3000) |
NODE_ENV=lapaz npx nest start --watch |
Start La Paz silo in watch mode (reads .env.lapaz, port 3001) |
npx prisma db push |
Sync schema to local Cochabamba DB |
DATABASE_URL="..." npx prisma db push |
Sync schema to La Paz DB |
npm run db:seed:local |
Seed Cochabamba local DB |
npm run db:reset |
Reset and re-seed Cochabamba local DB |
GTFS data (per city)
The GtfsService loads route/operator data differently per city:
| City | Source | File |
|---|---|---|
| Cochabamba | GitHub (Trufi GTFS builder) — fetched on startup | N/A (in-memory) |
| La Paz | Bundled GeoJSON — loaded when GTFS_SOURCE=lapaz |
src/passenger/data/lapaz-routes.geojson |
To refresh the Cochabamba stop index (from the Trufi OSM dataset):
cd apps/api-server
node scripts/fetch-gtfs-stops.mjs
Commit the updated cbba-stops.json so all environments pick it up on the next deploy.
Multi-Agent Development System
syncc includes an AI-powered multi-agent system (apps/agents/) that automates the
design → code → test workflow. Agents are powered by the Anthropic Claude API.
How it works
Requirement (plain text)
↓
RoutingAgent — Claude classifies which apps are affected and in what order
↓
DesignAgent — produces architecture docs and data schemas
↓
CodeAgent — implements feature stubs based on the design artifacts
↓
TestAgent — produces test plans and bug reports
↓
artifacts/ logs/ reports/ (all gitignored, generated at runtime)
Setup
cd apps/agents
nvm use # activates Node 20 from .nvmrc
export ANTHROPIC_API_KEY=sk-ant-...
# Install dependencies (use the nvm node's npm directly — system npm may be Node 15)
~/.nvm/versions/node/v20.x.x/bin/node \
~/.nvm/versions/node/v20.x.x/lib/node_modules/npm/bin/npm-cli.js install
Running the example workflow
cd apps/agents
~/.nvm/versions/node/v20.x.x/bin/node examples/syncc-workflow.js
This runs a sample "ride-rating feature" requirement through all agents across the
Passenger App, Driver App, and Backend. Artifacts land in apps/agents/artifacts/.
Updating docs after a code change
The DocsAgent keeps CONTEXT.md files, README.md, and the syncc-docs/ pages in
sync. Run it whenever you ship a feature, add a new app, or change an API contract:
cd apps/agents
nvm use
npm run update-docs
The agent reads existing files first, then writes only what changed — directly to disk.
Review the diff with git diff before committing.
Proposed workflow for new features
- Write the requirement as a plain-text description (what the feature does, which apps it touches, any constraints).
- Run it through the routing agent — it classifies the work and sequences the agents automatically.
- Review design artifacts in
artifacts/<app>/design/— adjust the architecture doc or schema before moving forward. - Review code artifacts in
artifacts/<app>/code/— use generated stubs as a starting point; the agents produce structure, not production-ready code. - Review test artifacts in
artifacts/<app>/test/— import the test plan into the QA portal (/dispatch→ QA Portal) as manual test cases. - Implement using the stubs and design docs as a spec alongside the
apps/<app>/CONTEXT.mdconventions.
Registering your own apps
const RoutingAgent = require("./src/routing-agent");
const router = new RoutingAgent();
router.registerApp("Passenger App", "typescript");
router.registerApp("Driver App", "typescript");
router.registerApp("Backend", "typescript");
router.registerApp("QA Portal", "typescript");
const result = await router.executeRequirement(`
Add push notifications to alert drivers when a new trip is assigned.
The backend sends the notification via FCM.
The driver app receives it and shows a banner.
`);
router.exportAllLogs();
Adding custom tools to an agent
Extend any agent class to add app-specific tools:
const CodeAgent = require("./src/code-agent");
class BackendCodeAgent extends CodeAgent {
getTools() {
return [
...super.getTools(),
{
name: "generate_prisma_migration",
description: "Generate a Prisma migration SQL file",
input_schema: {
type: "object",
properties: {
migration_name: { type: "string" },
sql: { type: "string" },
},
required: ["migration_name", "sql"],
},
handler: async (input) => {
this.saveArtifact("code", `${input.migration_name}.sql`, input.sql);
return { success: true };
},
},
];
}
}
For full agent API documentation see
apps/agents/CONTEXT.md.
E2E Testing with Playwright
syncc uses Playwright for end-to-end testing. Each app has its own test suite that validates complete user flows using real algorithms (polyline encoding, distance calculations, OSRM routing, etc.).
Test Organization
apps/web-admin/
├── e2e/
│ ├── tests/ # Test files
│ │ ├── auth.spec.ts
│ │ ├── dashboard.spec.ts
│ │ ├── line-creation.spec.ts
│ │ └── passengers/ # Passenger flow tests
│ └── helpers/ # Shared test utilities
├── playwright.config.ts # Web admin test config
└── playwright-passengers.config.ts # Passenger tests config
Running Tests
Web Admin Tests (line creation, auth, dashboard):
cd apps/web-admin
npm run test:e2e # Run all web-admin tests headless
npm run test:e2e:ui # Run with Playwright UI mode (interactive)
npm run test:e2e:headed # Run with browser visible
npx playwright test --grep "line-creation" # Run specific test file
Passenger Flow Tests (using separate config):
cd apps/web-admin
npx playwright test --config=playwright-passengers.config.ts
npx playwright test --config=playwright-passengers.config.ts --headed
Run specific test suites:
# Run only auth tests
npx playwright test auth.spec.ts
# Run tests matching a pattern
npx playwright test --grep "should create line"
# Run in debug mode
npx playwright test --debug
Viewing Test Reports
HTML Report (generated automatically after test runs):
cd apps/web-admin
npx playwright show-report # Open HTML report in browser
npx playwright show-report e2e-report # Specific report directory
Generate report from existing results:
npx playwright test --reporter=html
Testing Strategy
✅ Use real algorithms — tests import actual functions from the codebase ✅ Use real APIs — tests call OSRM, backend endpoints, database ✅ No mocks — validate end-to-end flows with real data ✅ Definition of Done — feature is complete when E2E test passes
See AGENTS.md § E2E Testing Strategy for detailed guidelines.
Common Test Commands
| Command | Description |
|---|---|
npm run test:e2e |
Run all tests headless |
npx playwright test --ui |
Interactive UI mode |
npx playwright test --headed |
See browser while testing |
npx playwright test --debug |
Debug mode with inspector |
npx playwright show-report |
View HTML test report |
npx playwright codegen |
Generate test code interactively |
Native builds (iOS & Android)
Capacitor reads the compiled web output from the www/ folder. You must build first, then sync, then open the native IDE.
iOS
# Dev server (real device / TestFlight)
cd apps/mobile-driver # or apps/passengers-app
npm run build:dev && npx cap sync ios && npx cap open ios
# Production build
npm run build:prod && npx cap sync ios && npx cap open ios
# Against local API (Mac and device on same Wi-Fi)
# Find your Mac's local IP:
ipconfig getifaddr en0 # e.g. 192.168.x.x
# Set API_URL=http://<your-local-ip>:3000 in apps/api-server/.env, then:
npm run build:local && npx cap sync ios && npx cap open ios
Android
# Dev server
npm run build:dev && npx cap sync android && npx cap open android
# Production
npm run build:prod && npx cap sync android && npx cap open android
A physical device cannot reach
localhost. Use the dev server URL or your Mac's LAN IP.
Docker — multi-city deployment
Docker is not needed for local development (except the La Paz PostGIS container). Use the full Docker stack only when deploying to the dev or production server.
Each city+environment runs as a fully isolated stack (db + redis + api + web_admin + passengers_app + mobile_driver + syncc_landing). Stacks are isolated via COMPOSE_PROJECT_NAME so containers, volumes, and networks never collide.
deploy.sh — the deployment command
# Usage
./deploy.sh <city> <env> [compose-action]
# city: lapaz | cbba
# env: dev | prod
# action: up (default) | down | build | logs | ps | ...
# Start (or rebuild + start) the full La Paz production stack
./deploy.sh lapaz prod
# Start the La Paz dev stack
./deploy.sh lapaz dev
# Start the Cochabamba production stack
./deploy.sh cbba prod
# Start the Cochabamba dev stack
./deploy.sh cbba dev
# Rebuild images only (no start)
./deploy.sh lapaz prod build
# Stop a stack (keeps data volumes)
./deploy.sh lapaz prod down
# Tail logs for a stack
./deploy.sh lapaz prod logs
Under the hood each call runs:
COMPOSE_PROJECT_NAME=syncc_<city>_<env> \
docker compose --env-file apps/api-server/.env.<city>.<env> up -d --build
Port assignments (all four stacks)
| Service | La Paz Prod | La Paz Dev | Cbba Prod | Cbba Dev |
|---|---|---|---|---|
| API | 3000 | 3002 | 3100 | 3102 |
| Admin | 8888 | 8892 | 8988 | 8992 |
| Mobile Driver | 8889 | 8893 | 8989 | 8993 |
| Passengers App | 8890 | 8894 | 8990 | 8994 |
| Landing Page | 9999 | 10101 | 10099 | 10102 |
| Analytics | 10101 | 10104 | 10100 | 10103 |
| Docs (wiki) | 8086 | 8086 | 8086 | 8086 |
| User Guide (manual) | 8087 | 8087 | 8087 | 8087 |
| Database (host) | 5432 | 5434 | 5532 | 5534 |
| Redis (host) | 6379 | 6381 | 6479 | 6481 |
| Prisma Studio | 5555 | 5557 | 5655 | 5657 |
Nginx on the server proxies subdomains to these ports. The containers are on isolated Docker networks; only the ports above are exposed to the host.
Env files required on the server
Files are gitignored — copy them to apps/api-server/ on the server manually.
Minimum required variables per file:
# Database
DATABASE_URL="postgresql://<user>:<pw>@db:5432/<dbname>?schema=public"
DB_USER=<user>
DB_PASSWORD=<pw>
DB_NAME=<dbname>
# Ports (must not collide between stacks — see table above)
DB_PORT_EXTERNAL=5432
REDIS_PORT_EXTERNAL=6379
API_PORT_EXTERNAL=3000
ADMIN_PORT_EXTERNAL=8888
MOBILE_PORT_EXTERNAL=8889
PASSENGERS_PORT_EXTERNAL=8890
LANDING_PORT_EXTERNAL=9999
PRISMA_STUDIO_PORT_EXTERNAL=5555
# App
NODE_ENV=production
API_URL=https://api.lapaz.syncc.me
JWT_SECRET=<strong-unique-secret-per-city>
OTP_MASTER_KEY=<only-set-in-dev>
TWILIO_FORCE_REAL_SEND=true
# La Paz only
GTFS_SOURCE=lapaz
# Seed script (used on first boot)
ADMIN_EMAIL=admin@syncc.me
ADMIN_PASSWORD=<strong-password>
ADMIN_PHONE=+591<phone>
JWT_SECRETand DB credentials must be unique per city. A shared JWT secret lets a La Paz token authenticate against the Cochabamba silo.
Analytics-specific variables (required in every .env.<city>.<env> file):
ANALYTICS_CONTAINER_NAME=<city>_<env>_analytics # e.g. lapaz_prod_analytics
ANALYTICS_PORT_EXTERNAL=<port> # see port table above
ANALYTICS_KEY=<shared-key> # must match across all silos — see .env.<city>.prod
ANALYTICS_SILOS='{"cochabamba":"https://api.cbba.syncc.me","lapaz":"https://api.lapaz.syncc.me"}'
The analytics dashboard is cross-silo — both the cbba and lapaz stacks deploy it, with one serving as a fallback if the other is unavailable. Both instances connect to all silos and show the same aggregated data. Login requires a SUPER_ADMIN account.
First deploy for a new city
On first boot, the api container automatically runs prisma db push and the seed script. No manual schema migration is needed.
Useful Docker commands
# View running containers across all stacks
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
# View logs for a specific service
COMPOSE_PROJECT_NAME=syncc_lapaz_prod docker compose --env-file apps/api-server/.env.lapaz.prod logs -f api
# Stop stack but keep data
COMPOSE_PROJECT_NAME=syncc_lapaz_prod docker compose --env-file apps/api-server/.env.lapaz.prod down
# Stop stack and wipe DB volume — DESTRUCTIVE
COMPOSE_PROJECT_NAME=syncc_lapaz_prod docker compose --env-file apps/api-server/.env.lapaz.prod down -v
First-time local setup
# 1. Install and start PostgreSQL (Cochabamba silo)
brew install postgresql@14 postgis
brew services start postgresql@14
# 2. Create the Cochabamba local database
psql postgres -c "CREATE DATABASE syncc;"
# psql syncc -c "CREATE EXTENSION IF NOT EXISTS postgis;" # only if PostGIS is available
# 3. Set up the Cochabamba API schema and seed
cd apps/api-server
cp .env.example .env # then edit DATABASE_URL → postgresql://<user>@localhost:5432/syncc
npm install
npx prisma db push
npm run db:seed:local
# 4. Start the La Paz PostGIS container
# Ensure .env.lapaz exists (see template above), then from the repo root:
docker compose -f docker-compose.lapaz.yml --env-file apps/api-server/.env.lapaz up -d
# 5. Push schema to the La Paz DB
cd apps/api-server
DATABASE_URL="postgresql://lapaz:lapaz_dev_pw@localhost:5433/transit_flow?schema=public" \
npx prisma db push
# 6. Install frontend dependencies
cd ../web-admin && npm install
cd ../mobile-driver && npm install
cd ../passengers-app && npm install
cd ../analytics && npm install
---
## Documentation & Manuals (MkDocs)
The platform includes two separate MkDocs documentation sites:
1. **Internal Developer Wiki:** Protected by `SUPER_ADMIN` credentials via the NestJS API server.
2. **User Guide / Manual:** Publicly accessible documentation for Passengers, Drivers, and Administrators.
### Port assignments
- **Internal Wiki:** Port `8086` (Docker) or `8000` (Local)
- **User Guide:** Port `8087` (Docker) or `8087` (Local)
### 1. Synchronizing the GitHub Wiki
The internal developer wiki pulls pages directly from the separate GitHub Wiki repository. To clone or pull the latest pages and automatically rewrite relative internal links for compatibility with MkDocs, run:
```bash
node scripts/sync-wiki.js
2. Running Local Preview Servers
To run the live-reloading preview servers locally using Python 3:
- To run the Internal Dev Wiki (Port 8000):
bash python3 -m mkdocs serve - To run the public User Guide / Manual (Port 8087):
bash python3 -m mkdocs serve -f user-guide/mkdocs.yml --dev-addr 127.0.0.1:8087
3. Deploying via Docker Compose
To start the services via Docker Compose:
# Internal Wiki
docker compose up -d docs
# User Guide
docker compose up -d user-guide
```