ML serving: - LinUCB contextual bandit (disjoint, d=5 features: hour_sin/cos, is_overdue, task_age, priority) - /score endpoint replaces stub random; /reward endpoint for online learning - Per-user model state persisted to disk as JSON (survives restarts) - venv at ml/serving/.venv; start with pnpm dev from ml/serving Recommender: - Todoist fetch now extracts features (is_overdue, task_age_days, priority) - RemotePolicy calls ml/serving with 3s timeout; falls back to RandomPolicy - Reward sent to /reward on feedback (done=+1, snooze=0, dismiss=-1) Web Push: - VAPID keys in config; push_subscriptions table in DB - POST/DELETE /api/push/subscribe; GET /api/push/vapid-public-key - Service worker (public/sw.js): push → showNotification, notificationclick → focus/open - "notify me" button on tip page; registers SW + subscribes on permission grant Event bus: - services/api/src/events/bus.ts: typed EventEmitter wrapper - Subjects: signals.tip.served, signals.tip.feedback, signals.task.synced - Same publish/subscribe API NATS JetStream will implement — swap is mechanical Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
services/
Backend modules. Each owns a contract and ships its own README.md. In Phase 0 these are internal packages inside a single Node process (ADR-0003); they extract to their own processes as pressure justifies.
| Dir | Role | Phase-0 shape | Extracts when |
|---|---|---|---|
gateway/ |
BFF for clients; auth check; fan-out | in-proc router | never (stays as the edge) |
auth/ |
Google OAuth (Apple in M1), sessions, JWT | Auth.js behind OIDC shape | mobile native ships (M3) |
profile/ |
user profile, preferences, consents | in-proc module | team ownership diverges |
integrations/ |
connectors + encrypted token vault | in-proc module | credential blast-radius isolation |
recommender/ |
POST /recommend — policy-driven tip selection |
in-proc; calls ml/serving from M1 |
scaling hotspot |
events/ |
event bus + signal log | in-proc emitter (Phase 0); NATS (M1) | always a library + broker, not a service |
notifier/ |
push/email delivery + quiet hours | in-proc; web push in M1 | SLA divergence or mobile push scale |
Contracts that cross module lines (HTTP or events) come from packages/shared-types/. In-module imports across modules are forbidden by import lint.