Skip to content

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/.env file with DATABASE_URL pointing to localhost:5432
  • A local apps/api-server/.env.lapaz file with DATABASE_URL pointing to localhost: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=local tells Angular to use environment.local.ts (pointing to localhost: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:* or build:* 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 start uses environment.local.ts directly — no set-env.cjs is called. For a production build, ANALYTICS_SILOS must be set as a JSON string (e.g. '{"cochabamba":"https://api.cbba.syncc.me","lapaz":"https://api.lapaz.syncc.me"}') and ANALYTICS_KEY must 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

  1. Write the requirement as a plain-text description (what the feature does, which apps it touches, any constraints).
  2. Run it through the routing agent — it classifies the work and sequences the agents automatically.
  3. Review design artifacts in artifacts/<app>/design/ — adjust the architecture doc or schema before moving forward.
  4. Review code artifacts in artifacts/<app>/code/ — use generated stubs as a starting point; the agents produce structure, not production-ready code.
  5. Review test artifacts in artifacts/<app>/test/ — import the test plan into the QA portal (/dispatch → QA Portal) as manual test cases.
  6. Implement using the stubs and design docs as a spec alongside the apps/<app>/CONTEXT.md conventions.

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_SECRET and 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

```