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
- 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.
- Accounts + auth - registration and sign-in via Google OAuth (OIDC). Sessions,
a
userstable, a dashboard-facing API. - Telegram <-> account linking - a verified link between a registered user and their
Telegram account (numeric
chat_id/user_id, not username). - 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).
- 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:
- 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. - 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.
- 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 thatchat_idto the account. This is the standard verified-link pattern and reuses the existing webhook. Recommendation:/start <code>deep-link. - Monorepo layout.
apps/api(Python) +apps/web(React) + shareddocs/, or keep Python at root and addweb/. 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.