Files
oO/services/api
alvis ad6747c242 feat(profile): /api/profile + eligibility filter + inference framework (ADR-0014 steps 4-6)
Step 4 — /api/profile read-through API:
  GET  /api/profile          → { user, prefs, consents, contexts }
  PATCH /api/profile/prefs/:scope  upsert user_preferences (source='user')
  PATCH /api/profile/consents      grant / revoke consent keys
  PATCH /api/profile/contexts      create / activate / deactivate contexts
  Legacy consentGiven bit folded in as data:core fallback.

Step 5 — registry-driven eligibility filter:
  fetchRegistry() exported from agent-registry.ts.
  profile/eligibility.ts: getEligibleAgentIds(userId) — filters by required
  consents, silenced_in_contexts, and user_preferences[enabled=false].
  fetchOrchestratorTip filters agent_outputs to eligible set before calling
  ml/serving /recommend. Fail-closed: registry unavailable → empty set.

Step 6 — shared context-inference framework (#111) + time-of-day proof (#112):
  ml/agents/inference/: UserHistory, FeedbackEvent, run_inference().
  Framework: cold-start, min_history gating, error fallback, structured logs.
  TimeOfDayAgent v1.1.0: inferred_params=[preferred_hour]; also reads
  quiet_start/quiet_end from agent_prefs. agent_prefs injected by TS caller.
  AgentInput gains agent_prefs field.
  ml/serving: POST /agents/{agent_id}/infer endpoint.
  agent-outputs.ts computeAndStore: loads prefs before compute, calls /infer
  after, persists results (source='inferred'); user overrides never touched.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 11:14:25 +00:00
..

services/api

Express BFF that serves all client-facing routes, manages sessions, runs background signal sync, and proxies admin calls to ml/serving.

Contract

GET  /health                             { ok: true }

POST /api/auth/login                     → redirect to Google OAuth
GET  /api/auth/callback                  OAuth return URL
POST /api/auth/logout
GET  /api/auth/session                   → { user? }
POST /api/auth/token                     { token } → set sid cookie (ADMIN_TOKEN auth)

GET  /api/integrations                   list connected integrations
POST /api/integrations/todoist/connect   start Todoist OAuth
GET  /api/integrations/todoist/callback
DELETE /api/integrations/:provider       disconnect

POST /api/recommend                      → { tip }
POST /api/tip/:id/feedback               { action } → { ok }

GET  /api/user/profile
DELETE /api/user                         account deletion

POST /api/push/subscribe
DELETE /api/push/subscribe

GET  /api/admin/stats                    DAU/WAU, feedback breakdown
GET  /api/admin/users                    user list with pagination
GET  /api/user/:id                       user detail, consents, integrations
GET  /api/admin/events                   recent event stream (ring buffer or NATS JetStream)
GET  /api/admin/events/history           historical event query (time range, filters)
GET  /api/admin/sim/runs                 offline sim run list
POST /api/admin/sim/run                  launch offline sim with policy/judge params
GET  /api/admin/sim/runs/:id/output      tail sim stdout
GET  /api/admin/features/:userId         per-user profile features + freshness
GET  /api/admin/features/:userId/context context features for last score call
POST /api/admin/policies                 list shadow policies + active policy
POST /api/admin/policies/:name/toggle    enable/disable shadow policy
POST /api/admin/users/:id/actions        revoke-integration, reset-bandit, rebuild-profile
GET  /api/admin/health                   system health: api, ml/serving, db, bus, mlflow
GET  /api/admin/docs                     admin documentation index
GET  /api/ml/*                           admin-only proxy to ml/serving

Middleware stack (request order)

  1. cors — origin limited to WEB_BASE_URL
  2. tracingMiddleware — reads or generates W3C traceparent; sets req.traceId + req.traceparent
  3. pinoHttp — structured JSON request/response logs with traceId field; /health suppressed
  4. express.json() / cookieParser
  5. sessionMiddleware — validates sid cookie, attaches req.userId

Observability

Logs are structured JSON via pino. Every line includes traceId (extracted from the incoming W3C traceparent header, or generated fresh). The same traceparent is forwarded on all outbound HTTP calls to ml/serving so traces correlate end-to-end.

Sentry error capture is active when SENTRY_DSN is set.

Background tasks

  • Todoist sync scheduler — runs every TODOIST_SYNC_INTERVAL_MS (default 15 min); starts 10 s after boot to avoid startup surge.
  • Retention purge — deletes tipScores and tipFeedback rows older than 30 days; runs on boot and daily.
  • Profile TTL invalidation — listens to signals.task.synced and signals.tip.feedback on the in-process Bus; invalidates cached user-level profile features so the next /recommend gets fresh values.

Config

Env var Default Description
PORT 3001 Listen port
NODE_ENV development Environment label
DATABASE_PATH ./data/oo.db SQLite file
SESSION_SECRET required Cookie signing secret
GOOGLE_CLIENT_ID/SECRET required OAuth
TODOIST_CLIENT_ID/SECRET required OAuth
API_BASE_URL http://localhost:3001 Self-referential redirect URI
WEB_BASE_URL http://localhost:3000 CORS + post-login redirect
ML_SERVING_URL http://localhost:8000 ml/serving base URL
NATS_URL `` NATS broker; empty = in-process bus only
TODOIST_SYNC_INTERVAL_MS 900000 Background sync cadence
TIP_PROMPT_VERSION `` Prompt variant(s) for /generate
LOG_LEVEL info pino log level
SENTRY_DSN `` Sentry DSN; empty = Sentry disabled
VAPID_* Web push keys
ADMIN_TOKEN `` Static token for service/Playwright admin auth; empty = disabled

Health story

GET /health returns { ok: true }. No dependency checks — upstream deps (ml/serving, NATS) have their own health endpoints checked separately.

Extraction criteria

Extract to its own host when:

  • Auth session management needs a dedicated Redis/PG session store, or
  • Background sync load (Todoist, future connectors) displaces API serving on the shared host, or
  • Team boundary emerges between auth/BFF and recommender orchestration.