Chronogrove (metrics.chrisvogt.me)
Chronogrove is the engine behind provider-backed widgets on www.chrisvogt.me: it syncs third-party accounts (Discogs, Steam, Instagram, Spotify, Goodreads, Flickr, and more), stores normalized widget documents, and serves them over a stable JSON API. Firebase is the reference runtime (App Hosting for the operator console and tenant-facing API domains, Cloud Functions for /api, and Firestore); the design stays portable enough to consider other hosts later.
Production hostnames (target): console.chronogrove.com is the main operator UI (sign-in, schema, sync, settings). api.chronogrove.com is the shared public API surface: /u/{username} (status), /widgets/:provider, and GET /api/widgets/:provider with optional ?username= / ?uid= where the host does not map a single tenant. chronogrove.com will eventually be a marketing site (separate from the console). Today you may still use metrics.chrisvogt.me and per-user domains such as api.chrisvogt.me while custom domains are wired in Firebase; see docs/APP_HOSTING.md.
Consumer experiences today include the open-source Gatsby theme Chronogrove. The goal is for the same API to power other site integrations (WordPress and similar) and, over time, shareable Web Components (and other HTML-native building blocks) that call the public routes directly.
This repository holds the backend and operator console (schema browser, status checks, authenticated sync). The themed marketing site and MDX content live in the Gatsby theme and site repos above.
Note
License: This project is distributed under the Apache License 2.0 (previously MIT). Details are in the root CHANGELOG.
- Install prerequisites
- Node.js (version in .nvmrc, currently 24+)
- pnpm (for example:
corepack enable && corepack prepare pnpm@10.32.1 --activate) - Firebase CLI (
pnpm add -g firebase-toolsornpm install -g firebase-tools) firebase login
- Clone and install
git clone git@github.com:chrisvogt/chronogrove.git cd chronogrove pnpm install - Set local env vars
cp functions/.env.template functions/.env.local # edit functions/.env.local (at least CLIENT_API_KEY, CLIENT_AUTH_DOMAIN, CLIENT_PROJECT_ID) - Run local dev (recommended)
pnpm run dev:full
- Open
- App:
http://localhost:5173 - Emulator UI:
http://127.0.0.1:4000
- App:
If /api calls fail in local dev, the Functions emulator is usually not reachable.
- Fetches and serves widget data for: Spotify, Steam, Goodreads, Instagram, Discogs, Flickr, and GitHub.
- Supports scheduled sync jobs plus manual admin-triggered sync.
- Uses Firebase Auth (Google, email/password, phone) with HTTP-only session cookies and JWT fallback.
- Runs locally with Firebase emulators.
- Serves the Next.js operator console on Firebase App Hosting (metrics.chrisvogt.me today;
console.chronogrove.comas the primary Chronogrove UI).
Note:
githubis a readable widget provider, but not part of the scheduled/manual sync queue.
This service backs widgets on www.chrisvogt.me and any client using the same API contract (for example the Gatsby theme). Each diagram is intentionally focused on one path. For queue semantics and job document fields, see docs/SYNC_JOB_QUEUE.md.
Custom domains attach to the same App Hosting backend (apps/console) unless you split marketing later. console.chronogrove.com is the operator experience; api.chronogrove.com (and tenant api.$user) expose JSON/widgets/status without a second stack.
flowchart TB
ah[Firebase App Hosting<br/>Next.js · apps/console]
cf[Cloud Functions · app]
fs[(Firestore)]
subgraph dns[Target hostnames]
con[console.chronogrove.com<br/>Operator UI]
api[api.chronogrove.com<br/>Public API · /u · /widgets · ?params]
mkt[chronogrove.com<br/>Marketing · future]
end
con --> ah
api --> ah
mkt -.->|future| mktNote[Separate site or route]
ah -->|rewrites /api · /widgets| cf
cf --> fs
The app is SSR on Firebase App Hosting. On whatever host the user opens (console.*, metrics.chrisvogt.me, api.chronogrove.com, api.tenant.example, …), the browser calls /api/* and /widgets/* on that same origin; Next.js rewrites both to the app Cloud Function (/api/... and /api/widgets/... respectively; see apps/console/next.config.mjs). That gives short widget URLs on tenant API domains without a second hostname. Third-party sites can still call the Functions URL or your api.* host from their own pages (diagram 1). CORS for credentialed cross-origin /api traffic uses a fixed regex allowlist in functions/app/create-express-app.ts, not tenant hostname settings—see GitHub #289 (under epic #252). Optional public status lives at /u/{username}; hosts in NEXT_PUBLIC_TENANT_API_ROOT_TO_USERNAME can serve it at / (internal rewrite in src/proxy.ts). Details: docs/APP_HOSTING.md.
flowchart TB
browser[Browser]
ah[Firebase App Hosting<br/>Next.js · console or api.* host]
cf[Cloud Functions · app<br/>/api/**]
fs[(Firestore)]
browser -->|pages, assets, SSR| ah
browser -->|same-origin /api/* · /widgets/*| ah
ah -->|rewrite / proxy| cf
cf --> fs
Unauthenticated widget reads from Firestore-backed content. On a shared API host (api.chronogrove.com), GET /api/widgets/:provider may take optional ?uid= or ?username= to choose the data owner; per-tenant domains map host → user via Functions runtime config (WIDGET_USER_ID_BY_HOSTNAME) and can use same-origin /widgets/:provider (rewritten like diagram 0). Cross-origin fetch from arbitrary customer sites may require CORS to allow their origin; today the allowlist is regex-based in code, not user-configurable (#289).
flowchart LR
site[Third-party site<br/>or Gatsby theme]
apiHost[api.* host<br/>same-origin /widgets]
site -->|GET + Origin| fn[Cloud Functions<br/>GET /api/widgets/:provider]
apiHost -->|rewrite| fn
fn --> fs[(Firestore<br/>users/.../widget-content)]
Planner enqueues one job per syncable provider. Worker claims queued jobs and runs provider sync.
flowchart TB
subgraph sched[Cloud Scheduler]
p[runSyncPlanner · default schedule]
w[runSyncWorker · every 15 min]
end
p --> plan[planSyncJobs]
plan --> q[(Firestore · sync_jobs)]
w --> next[runNextSyncJob]
next --> q
next --> job[processSyncJob + provider sync]
job --> apis[Platform APIs]
job --> docs[(Firestore · widget documents)]
The signed-in operator UI (console.chronogrove.com target, metrics.chrisvogt.me today) uses Firebase Auth + session cookie. /api/* reaches Functions via the rewrite in diagram 0. Manual sync runs inline (enqueue → claim → process) instead of waiting for worker cadence.
flowchart TB
admin[Operator console<br/>App Hosting · console.* / metrics.*] --> auth[Firebase Auth]
admin --> sess[POST /api/auth/session]
admin --> sync[GET /api/widgets/sync/:provider]
admin --> stream[GET .../sync/:provider/stream SSE]
sync --> fn[runSyncForProvider]
stream --> fn
fn --> q[(sync_jobs)]
fn --> job[processSyncJob]
job --> out[Platform APIs + widget writes]
| Flow | Description |
|---|---|
| Widget reads | GET /api/widgets/:provider (public, cached). Reads provider widget document from Firestore and returns it. Optional ?uid= / ?username= on shared hosts; hostname map for dedicated API domains. Console same-origin /widgets/... rewrites to /api/widgets/.... |
| Public status | GET /u/{username} on App Hosting (SSR widget health table), e.g. api.chronogrove.com/u/{username}. Mapped tenant API hosts can show the same page at / via src/proxy.ts. |
| Scheduled sync | runSyncPlanner enqueues queue jobs; runSyncWorker periodically claims and executes queued jobs. |
| Manual sync | Authenticated GET /api/widgets/sync/:provider (JSON) or GET /api/widgets/sync/:provider/stream (SSE). Both use the same queue + inline processing path. |
| Auth | Dashboard signs in with Firebase Auth and creates a session cookie through POST /api/auth/session. Protected routes accept session cookie or JWT. |
This repository is a pnpm workspace with:
apps/console/: Next.js operator console (SSR on Firebase App Hosting)functions/: Firebase Cloud Functions backend (public/apiand jobs)
Turborepo runs workspace scripts from the root and caches work.
Use repo root for commands (do not run per-package installs).
| Command | What it does |
|---|---|
pnpm install |
Install dependencies for root and both packages. |
pnpm run dev |
Run Next.js dev server on localhost:5173. Expects Functions emulator to be running for /api calls. |
pnpm run dev:full |
Run Auth, Firestore + Functions emulators and Next dev together (App Hosting emulator omitted so only one next dev uses port 5173). |
pnpm run build |
Run workspace builds via Turborepo (Next .next output + functions TypeScript build). |
pnpm run lint |
Run workspace lint tasks (currently functions ESLint). |
pnpm run test |
Run workspace tests. |
pnpm run test:coverage |
Run tests with coverage. |
pnpm run deploy:all |
Guard env + build + deploy Firestore, Functions, and production App Hosting (chronogrove-console). |
pnpm run deploy:hosting |
Build and deploy only production App Hosting (chronogrove-console). |
pnpm run deploy:functions |
Guard env + deploy only Functions (Firebase predeploy still builds functions). |
Use
pnpm run deploy:all(withrun).pnpm deployis a pnpm command, not this project's deploy flow.
One terminal:
pnpm run dev:fullOr split terminals:
# Terminal 1
firebase emulators:start --only functions,auth
# Terminal 2
pnpm run devOpen http://localhost:5173.
For a runtime closer to production App Hosting (see firebase.json + apps/console/apphosting.yaml):
pnpm run build
firebase emulators:start --only apphosting,auth,functions,firestoreOpen http://metrics.dev-chrisvogt.me:8084 if that host resolves to localhost (see emulators.apphosting in firebase.json).
| Service | URL |
|---|---|
| Emulator UI | http://127.0.0.1:4000 |
| App Hosting (when started) | http://metrics.dev-chrisvogt.me:8084 (or configured host in firebase.json) |
| Functions | http://127.0.0.1:5001 |
| Auth | http://127.0.0.1:9099 |
| Firestore | http://127.0.0.1:8080 |
For local development:
cp functions/.env.template functions/.env.localSet at minimum:
CLIENT_API_KEYCLIENT_AUTH_DOMAINCLIENT_PROJECT_ID
Optional examples:
NODE_ENV=developmentGEMINI_API_KEY(if AI summary features are enabled)
- Never commit
functions/.env.local. - Avoid
functions/.envduring normal development; Firebase can deploy values from that file into Functions.
GET /api/widgets/:providerwhereprovideris one of:discogs,flickr,github,goodreads,instagram,spotify,steam
- Optional query params on shared API hosts:
uid(Firebase uid) orusername(public slug). Per-tenant API domains use hostname → user mapping on the Functions side instead. - Same-origin alias on the operator console (production or dev with rewrites):
GET /widgets/:provider→ Cloud Functions/api/widgets/:provider.
GET /api/widgets/sync/:provider(JSON)GET /api/widgets/sync/:provider/stream(SSE)
Syncable provider values are:
discogs,flickr,goodreads,instagram,spotify,steam
POST /api/auth/sessionPOST /api/auth/logoutGET /api/client-auth-configGET /api/firebase-config(compat alias)
firebase.json registers two App Hosting backends, both with rootDir: apps/console:
| Backend | Typical use |
|---|---|
chronogrove-console |
Production console; alwaysDeployFromSource: true in repo config. |
chronogrove-console-pr |
Optional second backend (e.g. previews/staging); same app tree, separate deploy target. |
Deploy scripts use chronogrove-console by default (pnpm run deploy:hosting). Classic Firebase Hosting (static CDN sites) is not used for this console.
- Production (App Hosting): Next.js rewrites
/api/:path*to the deployedappCloud Functions URL (same-origin in the browser; seeapps/console/next.config.mjs). - Production (App Hosting): Next.js also rewrites
/widgets/:path*to{CLOUD_FUNCTIONS_APP_ORIGIN}/api/widgets/:path*so tenant-facing domains can use short widget URLs. - Local dev: both rewrites target the Functions emulator on
127.0.0.1:5001(beforeFilesso the App Router does not handle/apior/widgetsfirst). - Tenant status home: for hosts in
NEXT_PUBLIC_TENANT_API_ROOT_TO_USERNAME,src/proxy.tsrewrites/internally to/u/{slug}(browser URL stays/). See docs/APP_HOSTING.md. - Environment for rewrites: public origins, tenant display host, and optional tenant hostname map are set in
apps/console/apphosting.yaml(NEXT_PUBLIC_*); see docs/APP_HOSTING.md.
- Provider-neutral bootstrap wires runtime/config/store/auth adapters.
- Current implementation uses Firebase runtime/auth/document adapters.
- Functions source is TypeScript; build output is
functions/lib/.
From repo root:
pnpm run test
pnpm run test:coverageFunctions watch mode:
pnpm --filter chronogrove-functions run test:watchCI runs lint, tests, and build; it does not deploy. App Hosting and Functions are usually released via the Firebase GitHub integration when connected to this repository. You can also deploy from the repo root with the CLI:
pnpm run build
pnpm run deploy:all
pnpm run deploy:hosting
pnpm run deploy:functionsOperator console layout, backends, apphosting.yaml, and how that ties to Cloud Functions are documented in docs/APP_HOSTING.md.
Reference docs under docs/:
| Document | What it covers |
|---|---|
| docs/APP_HOSTING.md | Firebase App Hosting backends, apphosting.yaml, CI vs Firebase GitHub deploy / CLI, Next /api and /widgets rewrites, tenant / → /u/{slug}, public status SSR. |
| docs/SYNC_JOB_QUEUE.md | sync_jobs queue behavior (planner, worker, manual sync, states, summary metrics). |
| docs/SESSION_COOKIES.md | Session cookie model, /api/auth/session, JWT fallback, security properties. |
| docs/MULTI_TENANT_ARCHITECTURE_PLAN.md | Migration plan from single-tenant env config toward user-scoped storage and sync. |
- Fork the repository.
- Create a feature branch (
git checkout -b feature/amazing-feature). - Install and configure local env.
- Run tests (
pnpm run test). - Ensure builds pass (
pnpm run build). - Open a pull request.
Copyright © 2020-2026 Chris Vogt. Licensed under the Apache License 2.0.