A crowdsourced platform tracking historical food price developments in Singapore. Users submit what they paid at hawker centres and eateries; a trust-weighted confidence algorithm computes consensus prices; everyone can see how prices trend over time.
Status: Active development (localhost only, env-parity config for future deployment).
- Java 25+ (Temurin recommended)
- Node.js 22+
- Python 3.12+ (for pre-commit hooks)
- Docker & Docker Compose
git clone <repo-url> && cd SaLoB
# Copy env files (each service needs one)
for dir in backend/user-service backend/food-service backend/api-gateway backend/rabbitmq backend/minio; do
cp "$dir/.env.example" "$dir/.env"
done
# Install pre-commit hooks
pip3 install --user --break-system-packages pre-commit
pre-commit install --hook-type pre-commit --hook-type pre-pushcd backend
docker compose up -dThis starts: PostgreSQL ×2, PostGIS, Redis ×3, RabbitMQ, MinIO.
Order matters — food-service queries user-service via gRPC during seeding.
# Terminal 1: User Service (seeds users)
cd backend/user-service
SPRING_PROFILES_ACTIVE=dev ./gradlew bootRun
# Terminal 2: Food Service (seeds eateries, foods, entries — waits for user-service gRPC)
cd backend/food-service
SPRING_PROFILES_ACTIVE=dev ./gradlew bootRun
# Terminal 3: API Gateway
cd backend/api-gateway
./gradlew bootRunSeeding takes 30–60 seconds per service on first run.
cd frontend
npm install
npm run devOpens at http://localhost:3000.
SaLoB/
├── backend/
│ ├── api-gateway/ # Spring Cloud Gateway (port 8081)
│ ├── user-service/ # Auth, profiles, WTF scores (port 8082, gRPC 9092)
│ ├── food-service/ # Eateries, foods, entries, votes (port 8083, gRPC 9093)
│ ├── shared-proto/ # Protobuf contracts for gRPC
│ ├── k8s/ # Kubernetes manifests + Kustomize
│ ├── docker-compose.yaml
│ └── run.py # Launcher (opens terminals per service)
├── frontend/
│ ├── src/
│ │ ├── pages/ # Route-level page components
│ │ ├── components/ # Reusable UI components
│ │ └── shared/ # Hooks, types, utils, MSW mocks
│ ├── tests/ # Playwright E2E smoke tests
│ └── public/ # Static assets + MSW service worker
├── docs/
│ ├── PRD.md # Product requirements (domain, WTF, architecture)
│ ├── ADR.md # Architectural decision records
│ ├── ROADMAP.md # Feature backlog with priorities
│ └── TECHNICAL.md # Data flows, caching, implementation details
├── misc/ # Pseudocode, sketches
├── .pre-commit-config.yaml
├── .secrets.baseline
└── AGENTS.md # AI agent workflow instructions
- Fail fast, no silent defaults — Application YAML has no
${VAR:default}fallbacks. Missing env vars crash immediately. - Env parity — All config reads from
.envfiles (gitignored)..env.exampleis the team contract for required variables. - Deterministic builds — Pre-commit hooks are pinned to exact versions (
rev: vX.Y.Z, neverlatest). - Test with MSW — Frontend tests use Mock Service Worker (not live backends). Fast, deterministic, offline-friendly.
- Microservices for learning — Architecture is intentionally over-engineered as a portfolio piece demonstrating Spring Boot, gRPC, RabbitMQ, PostGIS, Redis, and container orchestration.
All hooks are defined in .pre-commit-config.yaml with pinned revisions for deterministic behavior.
| Hook | Stage | What it checks |
|---|---|---|
| trailing-whitespace | commit | Trims trailing whitespace |
| end-of-file-fixer | commit | Ensures files end with newline |
| check-yaml / check-json | commit | Validates syntax |
| check-merge-conflict | commit | Detects unresolved merge markers |
| detect-private-key | commit | Blocks accidental key commits |
| no-commit-to-branch | commit | Prevents direct commits to main |
| check-added-large-files | commit | Blocks files >512KB |
| ruff (lint + format) | commit | Python linting & formatting |
| detect-secrets | commit | Scans for secrets against .secrets.baseline |
| shellcheck | commit | Shell script validation |
| backend-compile | pre-push | Gradle compileJava in all 3 services |
| frontend-typecheck | pre-push | tsc --noEmit |
| frontend-lint | pre-push | ESLint |
To run manually:
pre-commit run --all-files # all hooks
pre-commit run detect-secrets # single hook- Unit tests:
./gradlew testper service - Integration tests: Requires Testcontainers (see AGENTS.md)
- Compile check:
./gradlew compileJavaper service (shared-proto must be published to MavenLocal first)
- Type check:
npx tsc --noEmitfromfrontend/ - E2E smoke tests:
npm run test(Playwright, requires dev server) - MSW: All HTTP requests in tests are intercepted by Mock Service Worker. Handlers in
src/shared/test/mocks/.
Backend seeders have a hard dependency: user-service must seed BEFORE food-service (food-service queries user-service via gRPC for user IDs during seeding).
| File | Git status | Purpose |
|---|---|---|
.env.example |
Tracked | Template with dev defaults (safe) |
.env |
Ignored | Actual secrets per developer (never commit) |
.secrets.baseline |
Tracked | detect-secrets allowlist baseline |
Each backend service reads its .env via spring.config.import: optional:file:.env[.properties].
The following variables in .env.example files are left blank and must be filled before certain features work:
| Variable | Service | Purpose |
|---|---|---|
ONEMAP_API_TOKEN |
food-service | OneMap POI search/autocomplete for Singapore eateries |
GOOGLE_CLIENT_ID |
user-service | Google OAuth login (optional for local dev) |
GOOGLE_CLIENT_SECRET |
user-service | Google OAuth login (optional for local dev) |
OPENAI_API_KEY |
food-service | AI food verification pipeline (future) |
These are intentionally blank in .env.example — the application will run without them, but the corresponding features will be disabled or fall back to defaults.