Public, no-auth city poster generator built with a Bun + Turborepo monorepo:
apps/web: Next.js (App Router) + Tailwind +shadcn/ui-style components + TanStack Queryapps/api: Go API + Redis queue + MinIO/S3 artifact storage + pure-Go renderer.docker/local: local Docker compose stack.docker/production: production deploy compose + env template
- Frontend: Next.js 16, React 19, Tailwind,
react-hook-form,zod,framer-motion - Backend: Go (
chi,go-redis, AWS SDK S3), Redis queue worker, MinIO/S3, Cloudflare Turnstile - Rendering: OpenStreetMap vector fetch (Overpass + Nominatim), layered map poster renderer
- Tooling: Bun workspaces, Turborepo, Biome, TypeScript
nvm install --lts
nvm use --lts
node -vExpected version: v24.14.0.
- Install JS deps:
bun install- Copy environment template:
cp .env.example .env- Start full stack:
docker compose -f .docker/local/compose.yaml -f .docker/local/compose.dev.yaml up -d --build- Open:
- Web:
http://localhost:3000 - API:
http://localhost:8000 - MinIO Console:
http://localhost:9001
Use Docker for backend infra/services and run frontend on host for HMR.
Go API and worker also run with container HMR (via air) in this mode:
bun run dev:backend
bun run dev:webOne-command variant:
bun run dev:localUseful backend commands:
bun run dev:backend:logs
bun run dev:backend:downProduction-like backend (compiled binaries, no HMR):
docker compose -f .docker/local/compose.yaml up -d redis minio api workerbun run dev # docker backend + web HMR
bun run dev:web
bun run dev:api # go API (requires local Go toolchain)
bun run dev:worker # go worker (requires local Go toolchain)
bun run lint # biome (web) + go vet (api)
bun run check-types # tsc/next + go test
bun run format # biome formatDeployment files:
.docker/production/compose.yaml.docker/production/.env.example.docker/production/images/api.Dockerfile.docker/production/images/web.Dockerfile
Workflows:
.github/workflows/production-deploy.yml- Trigger: push tag
v*(for examplev1.0.0) or manualworkflow_dispatch - Builds and pushes:
ghcr.io/<owner>/city-map-api:productionghcr.io/<owner>/city-map-web:production
- Deploys on production VPS via SSH
- Trigger: push tag
.github/workflows/release.yml- Trigger: push tag
v* - Creates GitHub Release automatically with generated notes
- Trigger: push tag
Required GitHub repository secrets:
PRODUCTION_HOSTPRODUCTION_USERPRODUCTION_SSH_KEYPRODUCTION_PORT(optional, defaults to22)PRODUCTION_PATH(example:/opt/city-map-poster-generator)GHCR_USERNAME(for VPS pulls)GHCR_TOKEN(PAT withread:packagesfor VPS pulls)
Recommended GitHub repository variables (build-time web config):
PRODUCTION_NEXT_PUBLIC_API_BASE_URLPRODUCTION_NEXT_PUBLIC_TURNSTILE_SITE_KEYPRODUCTION_NEXT_PUBLIC_SITE_URL
First-time VPS bootstrap:
mkdir -p /opt/city-map-poster-generator/.docker/productionOn first deploy run, the production workflow copies .env.example to .env on the VPS and stops once so you can fill real values before re-running.
GET /healthGET /v2/themesGET /v2/locationsGET /v2/fontsPOST /v2/previewPOST /v2/jobsGET /v2/jobs/{jobId}GET /v2/jobs/{jobId}/download
- Required: city, country
- Optional: latitude/longitude overrides, distance, dimensions, font family
- Units and dimensions:
- Default size:
30 x 40 cm - Centimeters mode:
10 cmmin,200 cmmax - Inches mode:
5 inmin,80 inmax
- Default size:
- Distance range:
1000 mto18000 m - Themes: all bundled built-in themes
- Export formats:
png,svg,pdf allThemes: generate every theme + ZIP output- Preview caching + artifact storage with presigned downloads
- Live Preview renderer mode:
local-wasmby default, automatic fallback toserver-fallbackwhen local rendering is unavailable or times out - Google Fonts searchable picker for
fontFamily - Custom Google-font rendering for PNG/PDF when
GOOGLE_FONTS_API_KEYis configured
Static theme gallery assets remain in:
apps/web/public/theme-previews/<theme-id>.svg
- Turnstile verification for generation endpoint (
/v2/jobs) whenCAPTCHA_REQUIRED=true - IP rate limits (defaults):
- Location search:
60 / 10 min - Font search:
120 / 10 min - Preview and render snapshot:
20 / 10 min - Jobs and exports:
3 / 10 min - Concurrent jobs:
2
- Location search:
- Development-only bypass:
- In non-production (
APP_ENV != production), the headerDev settingstoggle can reveal a full-width development card above map controls and live preview. - That card exposes toggles for disabling API rate limits and CAPTCHA checks for local testing.
- Production ignores dev bypass headers.
- In non-production (
- Geocoding uses public Nominatim by default. For production throughput, use a dedicated provider.
- Generated artifacts are short-lived (24h retention target).
