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.