Six repos, one system. A Next.js dashboard talks to a Hono API on Cloud Run, which is the only thing that touches the database. A separate service handles scheduled AI nudges and push delivery, and a small FastAPI service converts photos on the way into storage.
Requests flow client → API → data; nothing else is allowed to reach the database directly. The shared types package isn't a runtime hop — it's a published package that hands the same validation schemas to both the frontend and the API so their contracts stay in sync.
The repos that make up the system, what each one is responsible for, and where it runs.
| Service | Purpose | Runtime | Hosting |
|---|---|---|---|
| homepage | Next.js dashboard UI | Node.js | Firebase App Hosting |
| earthbound_api | Hono REST API + MCP server, owns the DB connection | Deno | GCP Cloud Run |
| earthbound-shared | Shared Zod types, published to JSR | Deno | JSR registry |
| homepage-notification-service | Scheduled AI nudges/insights + push notifications | Node.js | Firebase Cloud Functions |
| image_processing | HEIC/DNG/JPG → JPEG/WebP conversion | Python + Docker | GCP Cloud Run (+ local Celery/Redis) |
| Service | Command | Notes |
|---|---|---|
| homepage | npm run dev | localhost:3000 |
| earthbound_api | deno task dev | localhost:8000, docs at /docs, spec at /openapi |
| earthbound-shared | npx jsr publish | version bump required to propagate |
| notification-service | npx firebase deploy --only functions | CI also runs via GitHub Actions |
| image_processing | docker-compose up -d | FastAPI + Celery + Redis; frontend via npm run dev on :5173 |
Two tokens travel on every API call: a Firebase ID token proves who the user is, and a Google-issued OIDC token satisfies Cloud Run’s IAM gate on earthbound_api itself. Requests are also rate-limited by client IP and pass through baseline security headers.
homepage/app/api/auth/firebase-session/route.tshomepage/lib/api/earthbound.tsearthbound_api/middleware/auth.tsearthbound_api/middleware/rate-limit.tsFirst-run setup is a five-step trail metaphor — Trailhead → Trails → Basecamp → Ranger → Summit — implemented separately for desktop and mobile, but both driving the same server actions so the step sequence stays in sync.
homepage/app/(onboarding)/desktop-flow/page.tsxhomepage/app/(onboarding)/mobile-flow/page.tsxhomepage/app/actions/onboarding.tsThe chat widget never calls Gemini directly. Every turn goes through earthbound_api, which classifies the question, decides which domain tools Gemini is allowed to call, executes those tools against Postgres, and feeds the results back before the model answers.
earthbound_api/chat/service.tsearthbound_api/chat/router.tsearthbound_api/chat/tools.tsearthbound_api/chat/genai.tsScheduled Cloud Functions read a user’s recent data, ask Gemini to turn it into a short piece of copy (a nudge, a wellbeing insight, a weekly chronicle), then hand it to a task manager that respects quiet hours before anything reaches FCM.
homepage-notification-service/manageNotificationTask.tshomepage-notification-service/sendNotification.tsUploads are handled server-side by the Next.js route itself, not client-side. It only calls out to the standalone image_processing service when a file actually needs converting (e.g. HEIC from an iPhone); already-web-friendly formats pass straight through.
homepage/lib/services/image-processor.tsimage_processing/backend/app.pyimage_processing/backend/converter.pyEverything ships from the same GitHub repo to a handful of GCP and Firebase surfaces. Production secrets never live in the repo — they're pulled from Secret Manager at build time.
54 route domains grouped by the part of the app they serve. Every widget on the dashboard talks to one or more of these.
Read more about what Cairnholm does day to day, or go check out the live app.