# Unified ByteLyst Portal — Product Requirements Document > **Date:** 2026-03-21 · **Status:** Draft · **Author:** AI-assisted > **Depends on:** SmartAuth OneAuth (Phase 5), `@bytelyst/dashboard-shell`, platform-service > **See also:** [`CROSS_PRODUCT_USER_DASHBOARD.md`](design/CROSS_PRODUCT_USER_DASHBOARD.md), [`DASHBOARD_UI_GAP_ANALYSIS.md`](DASHBOARD_UI_GAP_ANALYSIS.md) --- ## Review Errata (2026-03-21 self-audit) The following bugs/inaccuracies were found during systematic post-generation review and corrected in-place: | # | Bug | Severity | Fix Applied | | :-: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: | ------------------------------------------------------------------------------------------------------- | | F1 | **Product count wrong:** Said "11 products" but ByteLyst Auth has no backend -- only 10 products have backends the portal can query | HIGH | Changed to "10 products with backends" + noted Auth as companion app | | F2 | **ByteLyst Auth listed as product with backend** -- it's a companion app consuming platform-service, no data to aggregate | HIGH | Removed from product count, clarified in problem statement | | F3 | **product.json count wrong:** Said "7 exist, 4 need creation" but only 3 are missing (FlowMonk, ActionTrail, LocalMemGPT) | MEDIUM | Corrected to "7 exist, 3 need creation" with specific names | | F4 | **Architecture diagram spacing:** "ChronoMind4011" and "ActionTrail4018" missing spaces | LOW | Fixed spacing | | F5 | **GlobalPreferences contained portalLayout:** Widget layouts were inside GlobalPreferences AND in a separate Cosmos container -- contradictory | MEDIUM | Removed `portalLayout` from GlobalPreferences; layouts stored in `portal_widget_layouts` container only | | F6 | **Widget endpoint mismatch:** PRD API table said `/widgets/:widgetId`, roadmap said `/widgets/:widgetId/data` | MEDIUM | Unified to `/widgets/:widgetId/data` | | F7 | **Missing widget layout endpoints in API table:** GET/PUT for layout management were in roadmap but not documented in PRD | MEDIUM | Added `GET/PUT /api/portal/widgets/layout` + `GET /api/portal/widgets/definitions` to API table | | F8 | **Feed event source endpoints unverified:** Several assumed endpoints were wrong (ChronoMind `/timers/recent`, JarvisJr `/jarvis-sessions`, NomGap `/fasting-sessions`, PeakPulse `/sessions`) | HIGH | Verified all 10 endpoints against actual backend route files; corrected paths | | F9 | **Missing `@bytelyst/datastore` in BFF tech stack** -- required for Cosmos container access | MEDIUM | Added to BFF tech stack row | | F10 | **No Cosmos containers section** -- widget layouts need explicit container documentation | MEDIUM | Added section 4.6 with `portal_widget_layouts` container | | F11 | **Dependency table: global prefs marked "Phase 2 blocker"** -- actually used in Phase 5 (Settings) | LOW | Changed to "Phase 5 blocker" | | F12 | **Search example used emojis** -- violates "no emojis unless explicitly asked" rule | LOW | Removed emojis from search example | | F13 | **Missing quick-actions in roadmap** -- PRD listed endpoint but no roadmap task built it | LOW | Added task 1.13 to roadmap Phase 1 | | F14 | **Roadmap missing standard BFF scaffold files** -- `lib/errors.ts`, `lib/datastore.ts`, `lib/cosmos-init.ts`, `vitest.config.ts` not listed | MEDIUM | Added 4 tasks (0.11-0.14) to Phase 0 | | F15 | **Roadmap task 7.19 wrong script location** -- `run-local-all-services.sh` is in `learning_voice_ai_agent/`, not common-plat | LOW | Corrected with "(in learning_voice_ai_agent repo)" note | --- ## 1. Product Identity | Key | Value | | -------------- | -------------------------------------------------------------- | | **Product** | ByteLyst Portal | | **Product ID** | `portal` | | **Domain** | portal.bytelyst.com | | **Repo** | `learning_ai_portal` (new) | | **Ecosystem** | ByteLyst (consumes platform-service + all 10 product backends) | | **Port (web)** | 3010 | | **Port (BFF)** | 4020 | --- ## 2. Problem Statement The ByteLyst ecosystem has grown to **10 products with backends** (LysnrAI, MindLyst, ChronoMind, JarvisJr, NomGap, PeakPulse, FlowMonk, NoteLett, ActionTrail, LocalMemGPT) plus ByteLyst Auth (a companion app with no separate backend). Each product has its own web dashboard, login, and settings. Users who use multiple products face: 1. **Separate logins** — even though OneAuth shares identity, users still open different URLs per product 2. **No unified view** — no way to see "what's happening across all my ByteLyst apps" 3. **Fragmented settings** — notification preferences, theme, timezone set per-product 4. **No cross-product search** — can't search across notes, timers, sessions, tasks from one place 5. **No unified billing** — subscriptions managed per-product with no combined view Meanwhile, the **admin dashboard** has grown to 43+ backend modules with 511+ endpoints, but the gap analysis shows **55 features are hidden or underexposed** in the UI. The admin console is becoming unmanageable — it needs to evolve alongside the user-facing portal. --- ## 3. Goals ### 3.1 User-Facing Portal Goals | # | Goal | Success Metric | | --- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | G1 | **Single login, all products** — one URL, one session, access everything | 100% of 10 products accessible from portal | | G2 | **Cross-product activity feed** — timeline of recent actions across all products | Feed renders within 2s for users with 5+ products | | G3 | **Unified search** — search notes, timers, tasks, sessions, memories from one bar | Cross-product search returns results in <1s | | G4 | **Product launcher** — app-switcher grid (like Google's waffle menu) | All 10 products launchable with deep links | | G5 | **Combined billing** — single view of all subscriptions, invoices, usage | Zero billing queries to support about "which product charges what" | | G6 | **Global settings** — theme, timezone, notification prefs apply cross-product | Settings changes propagate to all products within 30s | | G7 | **Cross-product widgets** — composable dashboard with pinnable product widgets | Users can customize their portal home with ≥10 widget types | ### 3.2 Platform Goals | # | Goal | Success Metric | | --- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------- | | P1 | **Reusable shell** — portal built on `@bytelyst/dashboard-shell` | Shell package consumed without forking | | P2 | **BFF pattern** — portal has its own Backend-For-Frontend that aggregates data from product backends | No direct browser→product-backend calls | | P3 | **Progressive rollout** — portal works even if some product backends are down | Graceful degradation for unavailable products | | P4 | **Extensible product cards** — each product registers a "portal card" config | New products auto-appear in portal via product.json | ### 3.3 Non-Goals (Explicit) - **Not replacing product dashboards** — portal is a hub, not a replacement. Deep actions still happen in product UIs. - **Not an admin console** — portal is user-facing. Admin features stay in admin-web. - **Not a mobile app** — web-only for v1. Native portal is a future consideration. - **Not real-time collaboration** — this is a personal dashboard, not a shared workspace. --- ## 4. Architecture ### 4.1 High-Level Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ ByteLyst Portal (web) │ │ Next.js 16 · port 3010 │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ │ Product │ │ Activity │ │ Search │ │ Settings │ │ │ │ Launcher │ │ Feed │ │ Bar │ │ & Billing │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └──────┬───────┘ │ │ │ │ │ │ │ │ └─────────────┴────────────┴───────────────┘ │ │ │ │ │ Portal BFF (Fastify 5 · port 4020) │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ /api/portal/products — product registry │ │ │ │ /api/portal/feed — aggregated activity │ │ │ │ /api/portal/search — cross-product search │ │ │ │ /api/portal/widgets — widget data aggregation │ │ │ │ /api/portal/preferences — global user preferences │ │ │ │ /api/portal/billing — combined billing summary │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ └──────────────────────────┼──────────────────────────────────┘ │ ┌──────────────────┼──────────────────────┐ ▼ ▼ ▼ platform-service product backends product backends (port 4003) (ports 4010-4019) (ports 4010-4019) ┌──────────┐ ┌───────────────┐ ┌───────────────┐ │ auth │ │ LysnrAI 4015 │ │ FlowMonk 4017 │ │ billing │ │ MindLyst 4014 │ │ NoteLett 4016 │ │ flags │ │ ChronoMind 4011│ │ ActionTrail 4018│ │ telemetry│ │ JarvisJr 4012 │ │ LocalMem 4019 │ │ usage │ │ NomGap 4013 │ │ PeakPulse 4010 │ └──────────┘ └───────────────┘ └───────────────┘ ``` ### 4.2 Data Model #### 4.2.1 UserDoc Enhancement (already in place via OneAuth) ```typescript interface UserDoc { id: string; email: string; primaryProductId: string; memberships: ProductMembership[]; // ← from SmartAuth Phase 5 globalPreferences?: GlobalPreferences; // ← NEW for portal } interface ProductMembership { productId: string; role: 'user' | 'admin'; plan: 'free' | 'pro' | 'enterprise'; firstAccessAt: string; subscriptionId?: string; licenseId?: string; } interface GlobalPreferences { theme: 'light' | 'dark' | 'system'; timezone: string; locale: string; notifications: { email: boolean; push: boolean; inApp: boolean; digest: 'none' | 'daily' | 'weekly'; }; // NOTE: Widget layouts are stored separately in Cosmos container // `portal_widget_layouts` (partition: /userId), not here. // This keeps GlobalPreferences lightweight on UserDoc. } ``` #### 4.2.2 Portal Widget Config ```typescript interface WidgetLayout { widgetId: string; // e.g. 'lysnrai:recent-transcripts' productId: string; position: { row: number; col: number; width: number; height: number }; config?: Record; } interface PortalWidgetDefinition { id: string; productId: string; name: string; description: string; icon: string; sizes: ('small' | 'medium' | 'large')[]; dataEndpoint: string; // BFF endpoint that fetches widget data } ``` #### 4.2.3 Product Registry (from existing product.json files) Each product already has a `product.json` with: - `id`, `name`, `displayName`, `domain`, `description` - `theme` (colors) - `features` (capability flags) - `ports` (service + dashboard) The BFF reads all `products/*/product.json` at startup to build the product registry. No new schema needed. ### 4.3 BFF API Endpoints | Method | Path | Description | Source | | ------ | ------------------------------------ | ------------------------------------- | ----------------------------------- | | GET | `/api/portal/products` | List products user has access to | platform-service `/auth/me` | | GET | `/api/portal/feed` | Cross-product activity feed (last 7d) | Fan-out to product backends | | GET | `/api/portal/search?q=...` | Cross-product search | Fan-out to product search endpoints | | GET | `/api/portal/widgets/definitions` | List available widget definitions | Static registry in BFF | | GET | `/api/portal/widgets/layout` | Get user's widget layout | Cosmos `portal_widget_layouts` | | PUT | `/api/portal/widgets/layout` | Save user's widget layout | Cosmos `portal_widget_layouts` | | GET | `/api/portal/widgets/:widgetId/data` | Fetch data for a specific widget | Product backend endpoint | | GET | `/api/portal/preferences` | Get global preferences | platform-service | | PATCH | `/api/portal/preferences` | Update global preferences | platform-service | | GET | `/api/portal/billing/summary` | Combined billing across products | platform-service subscriptions | | GET | `/api/portal/billing/invoices` | All invoices across products | platform-service stripe | | POST | `/api/portal/products/:id/switch` | Switch active product context | JWT re-issue | | GET | `/api/portal/notifications` | Aggregated notifications | platform-service | | GET | `/api/portal/quick-actions` | Available quick actions per product | Product registrations | ### 4.4 JWT Enhancement Current JWT (already includes memberships via OneAuth): ```json { "sub": "user-123", "productId": "lysnrai", "primaryProductId": "lysnrai", "memberships": [ { "productId": "lysnrai", "role": "user" }, { "productId": "mindlyst", "role": "admin" } ] } ``` Portal JWT adds a `scope: "portal"` claim so product backends know the request comes from the portal BFF: ```json { "sub": "user-123", "scope": "portal", "primaryProductId": "lysnrai", "memberships": [...] } ``` ### 4.5 Tech Stack | Layer | Technology | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Web** | Next.js 16 (App Router), React 19, TailwindCSS v4, `@bytelyst/dashboard-shell`, Zustand, Recharts, Lucide icons | | **BFF** | Fastify 5, TypeScript ESM, `@bytelyst/fastify-core`, `@bytelyst/api-client`, `@bytelyst/auth`, `@bytelyst/datastore` | | **Auth** | JWT via `jose`, cookie-based session, `@bytelyst/react-auth` | | **Shared packages** | `@bytelyst/config`, `@bytelyst/errors`, `@bytelyst/logger`, `@bytelyst/design-tokens`, `@bytelyst/telemetry-client`, `@bytelyst/feature-flag-client`, `@bytelyst/kill-switch-client` | | **Database** | Azure Cosmos DB (`productId: "portal"`) for portal-specific data (widget layouts, preferences) | | **Tests** | Vitest (BFF), Playwright (E2E) | ### 4.6 Cosmos Containers Portal-specific data stored in Azure Cosmos DB with `productId: "portal"`: | Container | Partition Key | Purpose | | ----------------------- | ------------- | ------------------------------- | | `portal_widget_layouts` | `/userId` | User widget grid configurations | Global user preferences (`GlobalPreferences`) are stored on the existing `UserDoc` in platform-service's `users` container -- no new container needed for preferences. --- ## 5. Pages & UI ### 5.1 Page Map | Page | Route | Description | | -------------------- | ---------------- | ------------------------------------------------------- | | **Home / Dashboard** | `/` | Customizable widget grid + activity feed | | **Products** | `/products` | Product launcher grid with status badges | | **Activity** | `/activity` | Full cross-product activity timeline with filters | | **Search** | `/search` | Cross-product search results grouped by product | | **Billing** | `/billing` | Combined subscriptions, invoices, usage | | **Settings** | `/settings` | Global preferences (theme, timezone, notifications) | | **Security** | `/security` | MFA, passkeys, devices, sessions (from SmartAuth) | | **Profile** | `/profile` | User profile, connected accounts (OAuth link/unlink) | | **Product Detail** | `/products/[id]` | Deep link into product with embedded iframe or redirect | | **Support** | `/support` | Cross-product support ticket creation and tracking | ### 5.2 Product Launcher (Home) The home page has three zones: 1. **Product Grid** (top) — 3×4 grid of product cards. Each card shows: - Product icon + name - Status badge (active/trial/free) - Last activity timestamp - Quick action button (e.g., "New Timer" for ChronoMind, "Dictate" for LysnrAI) - Click → opens product dashboard in new tab or embedded view 2. **Activity Feed** (left 60%) — chronological feed of recent events: - "You completed a 25-min Pomodoro in ChronoMind" - "New note created in NoteLett: 'Meeting Notes'" - "JarvisJr coaching session: 3 insights extracted" - Each item has product icon, timestamp, and deep link 3. **Widgets** (right 40%) — user-configurable widget stack: - ChronoMind: next timer countdown - NomGap: current fast progress - FlowMonk: today's schedule - MindLyst: brain activity heatmap - LysnrAI: recent transcripts - PeakPulse: weekly stats ### 5.3 Cross-Product Search Global search bar in the top bar (Cmd+K). Results grouped by product: ``` "meeting notes" NoteLett (3 results) - Meeting Notes -- Project Alpha (2h ago) - Weekly Standup Notes (yesterday) - Client Meeting Follow-up (3d ago) LysnrAI (2 results) - Meeting transcript -- 03/21 (today) - Meeting transcript -- 03/18 MindLyst (1 result) - Memory: "Meeting with design team" in Work brain ``` ### 5.4 Widget System Widgets are the portal's core differentiator. Each product registers widget definitions: | Widget ID | Product | Size | Description | | ---------------------------- | ----------- | ------ | ---------------------------------- | | `lysnrai:recent-transcripts` | LysnrAI | Medium | Last 5 transcripts with duration | | `lysnrai:usage-chart` | LysnrAI | Large | Weekly transcription minutes chart | | `chronomind:next-timer` | ChronoMind | Small | Countdown to next timer/alarm | | `chronomind:today-schedule` | ChronoMind | Medium | Today's timers and routines | | `nomgap:fast-progress` | NomGap | Small | Current fast elapsed/remaining | | `nomgap:streak` | NomGap | Small | Fasting streak count | | `mindlyst:brain-activity` | MindLyst | Medium | Brain captures heatmap (7d) | | `mindlyst:recent-captures` | MindLyst | Medium | Last 5 captures with brain tags | | `jarvisjr:coaching-streak` | JarvisJr | Small | Coaching session streak | | `jarvisjr:recent-sessions` | JarvisJr | Medium | Last 3 agent sessions | | `flowmonk:today-schedule` | FlowMonk | Medium | Today's planned flow slots | | `flowmonk:task-summary` | FlowMonk | Small | Tasks: pending/done/overdue | | `notelett:recent-notes` | NoteLett | Medium | Last 5 notes with tags | | `notelett:workspace-summary` | NoteLett | Small | Workspace note counts | | `actiontrail:recent-actions` | ActionTrail | Medium | Last 10 agent actions | | `actiontrail:risk-summary` | ActionTrail | Small | Risk level distribution | | `peakpulse:weekly-stats` | PeakPulse | Medium | Distance, elevation, sessions | | `peakpulse:recent-sessions` | PeakPulse | Medium | Last 3 adventure sessions | | `localmemgpt:recent-chats` | LocalMemGPT | Medium | Last 5 conversations | | `portal:billing-summary` | Portal | Small | Combined monthly cost | | `portal:security-status` | Portal | Small | MFA status, device count | --- ## 6. Cross-Product Activity Feed ### 6.1 Feed Event Schema ```typescript interface FeedEvent { id: string; productId: string; productName: string; productIcon: string; userId: string; type: string; // product-specific event type title: string; // human-readable summary description?: string; deepLink?: string; // URL to the item in the product dashboard metadata?: Record; createdAt: string; } ``` ### 6.2 Event Sources per Product | Product | Event Types | Verified Backend Endpoint | | ----------- | ------------------------------------------------------ | ----------------------------------------- | | LysnrAI | `transcript.created`, `session.completed` | `GET /api/transcripts?limit=10` | | MindLyst | `capture.created`, `triage.completed` | `GET /api/memory-items?limit=10` | | ChronoMind | `timer.completed`, `routine.finished`, `pomodoro.done` | `GET /api/timers?limit=10&sort=updatedAt` | | JarvisJr | `session.completed`, `memory.created` | `GET /api/jarvis/sessions?limit=10` | | NomGap | `fast.started`, `fast.completed`, `milestone.reached` | `GET /api/fasting/sessions?limit=10` | | PeakPulse | `session.recorded`, `goal.achieved`, `badge.unlocked` | `GET /api/peak/sessions?limit=10` | | FlowMonk | `task.completed`, `schedule.generated`, `entry.done` | `GET /api/schedule-entries?limit=10` | | NoteLett | `note.created`, `note.updated`, `task.extracted` | `GET /api/notes?limit=10` | | ActionTrail | `action.ingested`, `alert.fired`, `approval.decided` | `GET /api/actions?limit=10` | | LocalMemGPT | `conversation.created`, `document.uploaded` | `GET /api/conversations` (no limit param) | ### 6.3 Aggregation Strategy The BFF fetches from all product backends in parallel with: - **Timeout:** 3s per product (fail-open — missing products show "temporarily unavailable") - **Caching:** 60s TTL in-memory cache per user - **Pagination:** Feed returns 50 items max, sorted by `createdAt` desc - **Circuit breaker:** After 3 consecutive failures, skip product for 5 minutes --- ## 7. Security & Auth ### 7.1 Authentication Flow 1. User visits `portal.bytelyst.com` 2. If no session, redirect to `/login` (uses platform-service `/auth/login`) 3. On success, BFF issues a portal-scoped JWT with all memberships 4. JWT stored as httpOnly cookie (not localStorage) 5. BFF proxies requests to product backends using service-to-service auth ### 7.2 Authorization - Portal BFF has a **service API key** for each product backend - When fetching product data, BFF sends `x-user-id` + `x-portal-token` headers - Product backends validate the portal service key and scope data to the user - Users can only see data for products they have memberships in ### 7.3 Security Page Features Leverages existing SmartAuth infrastructure: - **MFA management** — TOTP setup/disable, recovery codes - **Passkey management** — register/remove WebAuthn credentials - **Device management** — view trusted devices, revoke sessions - **Login history** — recent login events with IP/location - **Connected accounts** — link/unlink Google, Microsoft, Apple OAuth --- ## 8. Offline & Resilience ### 8.1 Graceful Degradation The portal must work even when some product backends are down: | Scenario | Behavior | | -------------------------- | ------------------------------------------------------------ | | Product backend down | Widget shows "Temporarily unavailable" with retry button | | Platform-service down | Portal shows cached data, auth redirects to maintenance page | | BFF down | Static Next.js pages with "Service connecting..." message | | Slow product backend (>3s) | Widget shows skeleton, loads async when available | ### 8.2 Caching Strategy | Data | Cache TTL | Storage | | ------------------ | ---------- | --------------------- | | Product registry | 1 hour | BFF memory | | User memberships | 5 minutes | BFF memory | | Activity feed | 60 seconds | BFF memory per user | | Widget data | 30 seconds | BFF memory per widget | | Global preferences | 10 minutes | BFF memory per user | | Search results | No cache | Real-time fan-out | --- ## 9. Telemetry & Analytics ### 9.1 Portal-Specific Events | Event | Properties | | --------------------------- | ----------------------------------------------- | | `portal.product_launched` | `productId`, `source` (grid/feed/search/widget) | | `portal.search_executed` | `query`, `resultCount`, `productHits[]` | | `portal.widget_interacted` | `widgetId`, `action` | | `portal.layout_customized` | `widgetCount`, `widgetIds[]` | | `portal.preference_changed` | `key`, `oldValue`, `newValue` | | `portal.feed_scrolled` | `depth`, `productsVisible[]` | ### 9.2 Cross-Product Insights (Admin) The portal BFF can provide admin-web with: - **Cross-product adoption:** How many users use 1, 2, 3+ products - **Product affinity:** Which products are commonly used together - **Portal engagement:** Time spent in portal vs. direct product access - **Widget popularity:** Most/least configured widgets --- ## 10. Open Questions | # | Question | Options | Recommendation | | --- | ---------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------- | | Q1 | Standalone repo or inside common-plat? | Standalone `learning_ai_portal` vs. `dashboards/portal-web/` | **Standalone repo** — it has its own BFF, making it more than a dashboard | | Q2 | Embedded product views or redirect? | iframe embed vs. open in new tab | **New tab** for v1, embedded for v2 | | Q3 | Should BFF own its own Cosmos containers? | Yes (portal-specific) vs. use platform-service only | **Yes** — for widget layouts, portal preferences, feed cache | | Q4 | Widget data: pull or push? | BFF polls products vs. products push events | **Pull** for v1 (simpler), SSE push for v2 | | Q5 | How does portal handle products user hasn't signed up for? | Hide vs. show with "Get Started" CTA | **Show with CTA** — drives cross-product adoption | --- ## 11. Dependencies | Dependency | Status | Blocker? | | ------------------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------- | | OneAuth `memberships[]` on UserDoc | ✅ Implemented | No | | `/auth/me` returns product list | ✅ Implemented | No | | `@bytelyst/dashboard-shell` | ✅ Published | No | | `@bytelyst/react-auth` | ✅ Published | No | | Product backends expose recent-items APIs | ⚠️ Partial — most have list endpoints | Non-blocking (graceful degradation) | | `product.json` files for all 10 products | ✅ 7 exist, 3 need creation (FlowMonk, ActionTrail, LocalMemGPT) | Non-blocking | | Service-to-service auth (portal BFF → product backends) | ❌ Needs implementation | Phase 1 blocker | | Global preferences on UserDoc | ❌ Needs implementation | Phase 5 blocker | --- _This PRD should be reviewed and updated as implementation progresses._