Back to Cairnholm

How Cairnholm is built

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.

System overview

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.

Loading diagram…

Services at a glance

The repos that make up the system, what each one is responsible for, and where it runs.

ServicePurposeRuntimeHosting
homepageNext.js dashboard UINode.jsFirebase App Hosting
earthbound_apiHono REST API + MCP server, owns the DB connectionDenoGCP Cloud Run
earthbound-sharedShared Zod types, published to JSRDenoJSR registry
homepage-notification-serviceScheduled AI nudges/insights + push notificationsNode.jsFirebase Cloud Functions
image_processingHEIC/DNG/JPG → JPEG/WebP conversionPython + DockerGCP Cloud Run (+ local Celery/Redis)

Local development

ServiceCommandNotes
homepagenpm run devlocalhost:3000
earthbound_apideno task devlocalhost:8000, docs at /docs, spec at /openapi
earthbound-sharednpx jsr publishversion bump required to propagate
notification-servicenpx firebase deploy --only functionsCI also runs via GitHub Actions
image_processingdocker-compose up -dFastAPI + Celery + Redis; frontend via npm run dev on :5173

Key workflows

Authentication

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.

Loading diagram…
homepage/app/api/auth/firebase-session/route.tshomepage/lib/api/earthbound.tsearthbound_api/middleware/auth.tsearthbound_api/middleware/rate-limit.ts

Onboarding ("The Ascent")

First-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.

Loading diagram…
homepage/app/(onboarding)/desktop-flow/page.tsxhomepage/app/(onboarding)/mobile-flow/page.tsxhomepage/app/actions/onboarding.ts

Chat & AI assistant ("Ranger")

The 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.

Loading diagram…
earthbound_api/chat/service.tsearthbound_api/chat/router.tsearthbound_api/chat/tools.tsearthbound_api/chat/genai.ts

Notifications & AI nudges

Scheduled 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.

Loading diagram…
homepage-notification-service/manageNotificationTask.tshomepage-notification-service/sendNotification.ts

Photo upload & conversion

Uploads 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.

Loading diagram…
homepage/lib/services/image-processor.tsimage_processing/backend/app.pyimage_processing/backend/converter.py

Deployment & infrastructure

Everything 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.

Loading diagram…
Secrets: A build script pulls roughly a dozen named secrets (Firebase keys, auth secrets, and third-party integration keys) from GCP Secret Manager during the production build. Several are marked optional so a missing integration key doesn't fail the whole build.

API domain reference

54 route domains grouped by the part of the app they serve. Every widget on the dashboard talks to one or more of these.

Daily life & wellbeing

habitsmoodjournalsachievementsnudgesscratch-padfeedback

Fitness & games

workoutsworkout-activitiesworkout-goalspersonal-recordssteamduolingosports

Food & kitchen

mealsdaily-mealsgrocerydrinks

Finance

budgetdebtssavingssubscriptionsfinancial-profilegift-cards

Places & travel

parksrestaurantsvacationslocationscitiesholidayssaved-searches

Planning & productivity

taskstask-templatescalendargoalsweeklyquick-links

Social & relationships

peoplerelationshipeventscreatorsmaterials

Media & culture

mediacurationsrelated-content

Platform & admin

adminadmin-chatchatusersearchtagsactivity-logpublichome

See it in action

Read more about what Cairnholm does day to day, or go check out the live app.