Skip to main content

Milestone 3 - Dashboard, User Accounts & Registered-User Gating

Status: delivered (v0.3.0) - all tickets shipped and deployed. Beyond the plan, the detail view (#7) grew a live SDLC graph over an SSE event stream, and the web app got paired dev/prod Pages deployments. Theme: Give Bloom a front door. Today Bloom is Telegram-only and gated by a static username allowlist. M3 introduces a real web dashboard where users register / sign in (Google) and watch their projects' progress, and rewires access control so Bloom only answers Telegram accounts that belong to a registered, linked user. This is a new app, so M3 also converts the repo into a monorepo.


Why this milestone​

The PRD's FR-14 access control currently keys on mutable Telegram usernames in a static env allowlist - the code itself flags this as "not a strong identity." M3 replaces that with a durable account record owned by the user themselves via self-serve registration. The dashboard also delivers the read-side of FR-9 (progress tracking) and FR-10 (notifications) as a persistent surface, not just chat messages.

One-liner: A user signs in with Google on the dashboard, links their Telegram account, and only then can Bloom talk to them - and they can watch their projects there.


Scope​

In scope​

  1. Monorepo conversion - restructure the repo so the Python backend and a new web frontend coexist cleanly, without breaking CI/CD or the existing 101 tests.
  2. Accounts + auth - registration and sign-in via Google OAuth (OIDC). Sessions, a users table, a dashboard-facing API.
  3. Telegram <-> account linking - a verified link between a registered user and their Telegram account (numeric chat_id/user_id, not username).
  4. Registered-user gating - Bloom's Telegram webhook resolves the sender against the account registry; unlinked/unregistered senders are ignored silently (preserving the current "never reveal itself to outsiders" behavior).
  5. Dashboard: project monitoring - authenticated users see their projects with milestone / issue / PR status and blocked/escalated work (read model over the existing state + GitHub).

Out of scope (future / deferred)​

  • Non-Google auth methods (email+password, GitHub, magic link) - initial method is Google only, but the auth layer is built provider-agnostic so more can be added later.
  • Real-time push to the dashboard (websockets) - M3 is poll/refresh; live updates later.
  • Team / org accounts, roles, billing.
  • Replacing the engineer-swarm simulator (still its own future track).

Key decisions to lock (asked before building)​

These are the choices I'd confirm with you before issue #1 lands, because they shape the stack:

  1. Frontend stack. Given your standing template (React + Vite + TS + Tailwind + shadcn/ui, deployed to Cloudflare Pages), the dashboard SPA fits that mold and stays consistent with your other projects. Backend stays Python/FastAPI and serves the dashboard API (/api/*) while the SPA is hosted on Pages. Alternative: server-rendered templates in FastAPI (no separate deploy) - simpler infra, less rich UX. Recommendation: React SPA on Pages + FastAPI API, matching your other apps.
  2. Session mechanism. OIDC login -> server-set httpOnly cookie session (backed by the existing Supabase Postgres) vs. JWT. Recommendation: httpOnly cookie session in Postgres - simplest correct default, revocable, no token-in-JS.
  3. Telegram link flow. Dashboard shows a one-time deep link / code; user sends it to the bot (or clicks t.me/<bot>?start=<code>), bot binds that chat_id to the account. This is the standard verified-link pattern and reuses the existing webhook. Recommendation: /start <code> deep-link.
  4. Monorepo layout. apps/api (Python) + apps/web (React) + shared docs/, or keep Python at root and add web/. Recommendation: apps/api + apps/web - explicit, room to grow, minimal import churn if the Python package path is preserved.

I'll default to the recommendations if you don't object, and note the decision in each ticket.


Tickets​

Ordered by dependency. Each is bite-sized and independently reviewable.

#1 - Monorepo conversion (foundation)​

Restructure to apps/api (existing Python app, package path preserved) + reserve apps/web. Update CI, Dockerfile, compose, deploy scripts, and paths so all existing tests stay green and dev CD still deploys the API. No behavior change. AC: repo builds/tests/deploys from the new layout; 101 tests still pass; CI green; README + architecture docs updated to describe the monorepo.

#2 - Accounts data model + migrations​

users, sessions, oauth_identities, and telegram_links tables (Postgres via the existing store; in-memory equivalents for tests). Backend AccountStore seam mirroring the existing StateStore pattern. AC: schema + store protocol + memory/Postgres backends; unit tests; no auth wired yet.

#3 - Google OAuth (OIDC) sign-in + sessions​

Backend /api/auth/google/login + /callback, OIDC code flow, create-or-fetch user, set an httpOnly session cookie; /api/auth/me and /api/auth/logout. AC: end-to-end login against a mocked OIDC provider in tests; secure cookie flags in production; unauthenticated /api/* returns 401.

#4 - Dashboard SPA shell + auth UX​

apps/web: React+Vite+TS+Tailwind+shadcn/ui. "Sign in with Google", authed layout, session handling, empty "Projects" screen. Deploy to Cloudflare Pages (repo stays private). AC: deployed Pages URL; sign-in -> lands on dashboard; sign-out works; calls the API with credentials.

#5 - Telegram account linking​

Dashboard "Link Telegram" issues a one-time code; bot /start <code> (or deep link) binds the sender's numeric chat_id/user_id to the user and marks the link verified. Show link status in the dashboard; allow unlink. AC: a signed-in user can link their Telegram account; the link stores the numeric id; re-link/unlink handled; covered by tests.

#6 - Registered-user gating (replace the static allowlist)​

Telegram webhook resolves the sender's numeric id against verified telegram_links; only linked users pass. Unlinked senders are ignored silently. Keep the env allowlist as an optional dev/local fallback (empty registry + set env still works for local). Update FR-14 in the PRD. AC: unlinked sender -> silently ignored; linked sender -> normal flow; existing webhook de-dupe/durability preserved; PRD FR-14 revised; tests cover allowed/denied paths.

#7 - Project monitoring view​

Authenticated users see their projects (scoped to what they're linked to) with milestone / issue / PR / blocked / escalated status - a read model over existing run state + GitHub. Manual refresh (no live push yet). AC: dashboard lists the user's projects with live-ish status; access-scoped (a user can't see another's projects); covered by API tests.

#8 - Docs, PRD reconcile & version bump​

Update PRD (FR-14 rewrite, new "Dashboard & Accounts" FRs), architecture (new app + auth seam), deployment (two-artifact: API + Pages), CHANGELOG; bump version to 0.3.0. AC: docs reflect delivered M3; version bumped in pyproject/__init__; CHANGELOG entry.


Risks / watch-list​

  • CI/CD breakage during monorepo move (#1) is the highest-risk step - do it first, isolated, behavior-neutral, verify green before anything else stacks on it.
  • Two deploy artifacts now (FastAPI API + Cloudflare Pages SPA). Deploy docs must cover both; CORS/cookie-domain between the Pages origin and the API origin needs care.
  • OAuth secrets (Google client id/secret) join the config surface - repo/env secrets, never committed.
  • Identity migration: existing username allowlist -> numeric-id links. Keep the env allowlist as a dev fallback so we don't lock ourselves out mid-migration.