Yeah — this repo is doing something pretty unusual, and honestly pretty smart. ## The shortest possible explanation This is **not** a normal “frontend ↔ API” app. It’s more like: - the **UI** is a Next.js app - the **orchestrator** is a **super-agent router / session broker** - the **user-service** is the **identity + profile source of truth** - the domain services are treated like **agents** - **Redis Streams** is the internal task bus - **Q-Score** is a cross-cutting scoring engine fed by events extracted from agent outputs - some flows are **agentized**, some are still **direct REST**, and live audio flows are **direct WebSocket to specialty services** So the system has **multiple communication lanes**, not one. --- # 1. The mental model: 4 communication lanes ## Lane A — Direct frontend → service REST Used for normal CRUD. Examples: - frontend → `user-service` for `/users/me`, `/users/ensure`, photo upload, QR - frontend → `resume-builder` for resumes, versions, export, parse - frontend → `social-branding` for profiles, analysis history, content generation - frontend → `interview-service` / `roleplay-service` artifact APIs This is the boring, standard lane. --- ## Lane B — Frontend → orchestrator WebSocket → domain agents Used for **page sessions**, **agent actions**, **cross-service flows**, **progressive updates**. Examples: - home dashboard - resume hub/editor actions - social media page - job matching - pathways - some interview/roleplay setup/feedback flows This is the **signature architecture** of the app. --- ## Lane C — Domain agent ↔ orchestrator via Redis Streams or A2A HTTP This is **internal service-to-service orchestration**. The orchestrator can invoke an agent in two ways: ### Default: Redis Streams - orchestrator writes task to `tasks:` - service worker consumes it - service publishes response messages to `responses:` - orchestrator relays those messages back to frontend ### Fallback: HTTP A2A - orchestrator POSTs to `//a2a/tasks` - service runs the same session logic and returns collected messages So they built a dual transport layer: - **event-driven / stream-based** - with **HTTP fallback** That’s very distinctive. --- ## Lane D — Direct live media WebSockets Used for **real-time interview / roleplay audio sessions**. Examples: - frontend `useInterviewSession()` connects directly to interview-service - frontend `useRoleplaySession()` connects directly to roleplay-service So live voice does **not** go through the orchestrator data path. That’s another important part of the uniqueness: **not everything is forced through one gateway.** --- # 2. UI app: how the frontend is structured Main app: `frontend/` Core stack: - Next.js 16 - React 19 - Clerk auth - App Router - Tailwind - custom hooks for API + agent sessions --- ## 2.1 Frontend has two personalities The UI is doing **both**: ### A. Standard SaaS frontend Using API clients like: - `src/lib/api/users.ts` - `src/lib/api/resumes.ts` - `src/lib/api/socialBranding.ts` These attach Clerk Bearer tokens and call REST endpoints directly. ### B. Agent-driven frontend Using: - `src/hooks/useAgentSession.ts` This hook is the key. It: - gets Clerk token - opens WebSocket to orchestrator: `ws://.../ws/agent?token=` - sends: ```json { "type": "session_start", "page": "...", "params": {...} } ``` - later sends: ```json { "type": "user_action", "action": "...", "params": {...} } ``` And it listens for structured messages like: - `capabilities_updated` - `agent_routed` - `agent_thinking` - `agent_data` - `agent_suggestion` - `agent_activity` - `agent_error` - `qscore_loaded` - `qscore_updated` - copilot streaming chunks So the frontend is basically talking to a **message protocol**, not just fetching JSON. --- ## 2.2 Important frontend providers ### `QScoreProvider` At dashboard layout level: - `frontend/src/app/(dashboard)/layout.tsx` This persists Q-Score across dashboard navigation. ### `UserProvider` In `DashboardLayout.tsx` This does: - `usersApi.ensureUser()` - loads the GrowQR user record - exposes `user`, `isPro`, `refreshUser` So Clerk is auth, but **user-service is the app identity profile**. --- ## 2.3 What page loads feel like When a page uses `useAgentSession(...)`, the frontend is not directly loading page data from the domain service. Instead: 1. connect to orchestrator 2. orchestrator authenticates 3. orchestrator resolves user context via user-service 4. orchestrator routes page to correct agent 5. agent sends structured messages back 6. UI renders from `latestData[action]` Example: - Home page uses: `useAgentSession(WS_URL, "home")` - It waits for `dashboard_loaded` So the page isn’t “call GET /dashboard”. It’s more like: > “Start an agent session for page `home` and stream me the result.” That’s very different from normal SaaS frontend architecture. --- ## 2.4 Frontend communication split by feature ### Home dashboard - goes through orchestrator - page = `"home"` - routed to `dashboard-service` ### Resume pages - direct REST for CRUD/export/parse - orchestrator WS for page session + AI/copilot/page-level orchestration ### Social branding - direct REST for profiles/analysis/content - orchestrator WS for page session and agent-like flows ### Settings / profile - direct REST to `user-service` - user data also available through orchestrator-resolved context ### Job matching - orchestrator-driven agent page ### Pathways - orchestrator-driven agent page ### Interview / roleplay - setup/feedback pages use orchestrator-style session hooks - actual live conversation uses **direct WS** to the interview/roleplay service ### Helper chat There’s also: - `frontend/src/app/api/chat/route.ts` That one calls OpenAI directly from Next server route, outside the orchestrator pattern. So even inside frontend, you’ve got: - direct REST - orchestrated WS - direct live WS - direct Next server AI route --- # 3. Orchestrator: what it really is Main folder: - `orchestrator/` Key file: - `orchestrator/app/main.py` - `orchestrator/app/agent/session.py` ## The orchestrator is basically: ### a WebSocket gateway + auth layer + service discovery layer + routing layer + context enrichment layer + signal extraction layer + Q-Score event publisher + background workflow coordinator That’s why it feels unique. --- ## 3.1 What happens when frontend connects In `orchestrator/app/agent/websocket.py`: 1. WebSocket opens with `?token=` 2. orchestrator verifies Clerk JWT 3. extracts Clerk user id 4. creates `SuperAgentSession` Then in `SuperAgentSession.run()`: ### Step 1 Send capabilities snapshot ### Step 2 Resolve user identity via `user-service` ### Step 3 Process: - `session_start` - `user_action` - `session_end` So every agent session begins with: - who is this user? - what agents are available? - what’s the current user context? --- ## 3.2 Service discovery is dynamic Files: - `orchestrator/app/discovery/registry.py` - `orchestrator/app/discovery/health.py` The orchestrator periodically calls: - `/.well-known/agent-card.json` - fallback `/.well-known/agent.json` from each configured agent URL. It stores: - agent name - description - skills - health state So the orchestrator is not hardcoded to just “known APIs”. It has a **registry of discovered agent capabilities**. That’s a real agent-style pattern. --- ## 3.3 Routing is page-based and action-based In `orchestrator/app/agent/session.py`: ### Page routing `PAGE_TO_AGENT` maps: - `home` → `dashboard-service` - `resume-hub` → `resume-builder` - `social-media` → `social-branding` - `job-matching` → `matchmaking-service` - `settings` → `user-service` - `pathways` → `pathways-service` - etc. ### Action routing `ACTION_TO_AGENT` maps actions like: - `ai_analyze` → `resume-builder` - `run_analysis` → `social-branding` - `update_preferences` → `user-service` - `get_feed` → `matchmaking-service` - `generate_pathway` → `pathways-service` So the orchestrator understands: - “which page belongs to which agent” - “which user action belongs to which agent” That makes the frontend simpler: it just emits actions, not service URLs. --- ## 3.4 The orchestrator always resolves user context first This is one of the most important design choices. In `_resolve_user_identity()`: 1. call `user-service` action `view_profile` 2. get the real user profile 3. extract the app UUID 4. enrich with skills from: - `resume-builder /api/state/{clerk_id}` - `social-branding /api/state/{clerk_id}` So the orchestrator creates a **portable user context** that can be sent to any domain agent. That means agents don’t each need to re-fetch identity basics every time. This is a huge reason the architecture feels “agentic” instead of “API-ish”. --- ## 3.5 Dual transport: Redis first, HTTP fallback Files: - `orchestrator/app/a2a/redis_transport.py` - `orchestrator/app/a2a/client.py` ### Redis mode - publish task to `tasks:` - subscribe to `responses:` - stream messages to frontend in real time ### HTTP mode - POST to `//a2a/tasks` - get collected messages back So the orchestrator can treat agents as: - synchronous HTTP responders - or event-driven streaming workers That’s sophisticated. --- ## 3.6 Q-Score is deeply embedded into orchestration This is another unique thing. The orchestrator doesn’t just route actions. It also watches agent outputs and turns them into **scoring signals**. Files: - `orchestrator/app/qscore/signal_mapper.py` - `publisher.py` - `subscriber.py` - `client.py` ### On session init It: - fetches latest Q-Score over HTTP from qscore-service - registers callback for live score updates via Redis stream - publishes engagement + onboarding signals ### After domain-agent responses It scans returned messages: - resume analysis - LinkedIn analysis - job matching actions - interview review - roleplay review - pathway progress Then extracts signals like: - `resume.ats_compatibility` - `linkedin.headline_quality` - `matching.feed_active` - `interview.overall_score` - `pathway.completion_pct` Then publishes them to: - `qscore.signal_events` So the orchestrator is not just a router. It is also a **semantic event interpreter**. That’s rare. --- ## 3.7 Background workflows are coordinated here Endpoint: - `POST /api/v1/background-task` Used by: - `resume-builder` after resume upload - `social-branding` after LinkedIn fetch/connect Flow: 1. service notifies orchestrator 2. orchestrator resolves user context 3. dispatches background task to agent 4. collects messages 5. extracts Q-Score signals 6. extracts auto-populate info 7. PATCHes `user-service /users/auto-populate` So the orchestrator also acts like a **workflow engine**. --- # 4. User-service: why it’s more important than it first looks Main folder: - `user-service/` Key files: - `app/main.py` - `app/api/v1/users.py` - `app/api/v1/webhooks.py` - `app/agent/session.py` This service is not just “users table CRUD”. It is the **identity gateway** of the whole platform. --- ## 4.1 It is the source of truth for app-level user state It owns: - GrowQR user record - Clerk id mapping - email / first_name / last_name - avatar_url - plan - preferences - metadata - QR code data - profile photo - onboarding state (stored in preferences) This is the canonical application profile. --- ## 4.2 It bridges Clerk identity to internal GrowQR identity Frontend auth is Clerk. But Q-Score and some internal flows need a stable internal user UUID. That comes from `user-service`. So the mapping is basically: - Clerk user id → auth identity - GrowQR user UUID → internal cross-service identity That’s why orchestrator hits user-service first. --- ## 4.3 It supports three access styles ### 1. Direct REST from frontend Examples: - `/api/v1/users/me` - `/api/v1/users/ensure` - `/api/v1/users/me/photo` - `/api/v1/users/me/qr-code` ### 2. Agent-style access Via: - `/ws/agent` - `/a2a/tasks` So user-service itself behaves like an agent. ### 3. Internal state endpoint - `/api/state/{clerk_id}` Used by dashboard-service and orchestrator. So user-service exposes both: - user-facing API - internal aggregation API - agent protocol surface --- ## 4.4 It auto-creates users on first sign-in In `UserProvider`, frontend calls: - `ensureUser()` Server side `/users/ensure`: - if user exists → return it - if not → fetch from Clerk API if possible - create GrowQR user row - generate QR code So app account creation is **lazy and synchronized** off Clerk login. --- ## 4.5 It stores onboarding state The frontend hook: - `useOnboarding()` writes onboarding into: - `user.preferences.onboarding` This is clever because onboarding becomes part of the shared user context, not some isolated frontend state. That means orchestrator and Q-Score can use it. --- ## 4.6 It handles Clerk webhooks In `app/api/v1/webhooks.py`: Supported: - `user.created` - `user.updated` - `user.deleted` - `session.created` Effects: - create/update/soft-delete user - update last sign-in - trigger downstream cleanup on delete So user-service is the **sync point between external auth identity and internal domain data**. --- ## 4.7 It also owns QR/public identity features It handles: - uploaded profile photo - branded QR generation - public profile endpoint - QR regeneration That’s not just admin/user metadata. It also powers the outward-facing GrowQR identity layer. --- ## 4.8 Auto-populate is a hidden important feature Orchestrator can call: - `PATCH /api/v1/users/auto-populate` It fills empty user/profile/settings fields based on: - parsed resume data - LinkedIn profile data But only if those fields are empty. That creates a very smooth onboarding loop: - upload resume / connect LinkedIn - AI/background processing runs - settings fill themselves This is one of the better-designed pieces. --- # 5. Why this architecture feels unique Because the services aren’t just APIs. They are treated like **page/domain agents** with a common invocation model. ## The pattern is: Each service has a central session object, like: - `UserAgentSession` - `DashboardAgentSession` - similar in resume/social/etc. That same session logic can be invoked through: ### A. native WebSocket actual user-agent page session ### B. HTTP A2A via `/a2a/tasks` ### C. Redis Stream worker consumes `tasks:` and publishes responses That means **one domain behavior implementation** is reused across multiple transports. That’s actually a strong architectural idea. --- # 6. How each service communicates with others Here’s the clean map. ## 6.1 Frontend Talks to: - `user-service` via REST - `resume-builder` via REST - `social-branding` via REST - `orchestrator` via WS - `interview-service` via direct WS + REST - `roleplay-service` via direct WS + REST - internal Next `/api/chat` route for helper chat --- ## 6.2 Orchestrator Talks to: - `user-service` for identity resolution + metadata persistence + auto-populate target - `resume-builder` for page/actions + skill enrichment + background parse/analyze - `social-branding` for page/actions + skill enrichment + background analysis - `dashboard-service` for home page payload - `matchmaking-service` for job matching actions - `pathways-service` for pathway actions - `qscore-service` for initial read + signal publication + update subscription Transport used: - Redis Streams preferred - HTTP A2A fallback - HTTP internal state calls for enrichment --- ## 6.3 Dashboard-service Talks to: - `user-service /api/state/{clerk_id}` - `resume-builder /api/state/{clerk_id}` - `social-branding /api/state/{clerk_id}` - `qscore-service /v1/qscore/{uuid}` It acts like an **aggregator service**: - collect distributed state - merge - evaluate card rules - compute lifecycle - return `dashboard_loaded` So dashboard-service is basically a **read model / composition engine**. --- ## 6.4 Resume-builder Talks to: - orchestrator for background-task kickoff after upload - likely user auth verification through Clerk token on direct REST - exposes state to orchestrator/dashboard - emits analysis data consumed by orchestrator → qscore It is both: - CRUD/document service - AI analysis agent --- ## 6.5 Social-branding Talks to: - orchestrator for background-task kickoff after LinkedIn profile connect/fetch - exposes state to orchestrator/dashboard - emits analysis data that becomes qscore signals It is both: - profile ingestion service - AI profile-analysis agent --- ## 6.6 User-service Talks to: - Clerk webhooks - resume/social cleanup endpoints on delete - orchestrator - dashboard-service - frontend It is the central identity/state hub. --- ## 6.7 Qscore-service Talks to: - orchestrator via HTTP read + Redis streams - receives signals on `qscore.signal_events` - computes score - publishes updates on `qscore.score_updates` So Qscore is event-fed, not directly computed inline on every user request. That’s a strong decoupling. --- ## 6.8 Matchmaking-service Observed pattern: - external sibling service - has Redis stream worker + A2A endpoints - orchestrator routes job-matching actions to it - likely handles scraping, ranking, worker queues So it fits the **agentized service** model. --- ## 6.9 Pathways-service Observed pattern: - external sibling service - Redis worker + A2A endpoints - has `/api/state/{clerk_id}` - orchestrator routes pathway actions to it Also fits the agentized pattern. --- ## 6.10 Interview-service / Roleplay-service These are more hybrid. ### Definitely observed: - direct REST configure/review endpoints - direct live WebSocket session endpoints - frontend hooks connect directly for live audio ### Architectural meaning: These are **specialized real-time media services**. They don’t cleanly fit the normal agent/session transport path the way user/resume/social/dashboard/pathways/matchmaking do. So the system is not “pure agent architecture”. It is **agent architecture + specialized realtime sidecars**. That hybrid is part of what makes it interesting. --- # 7. End-to-end flows ## Flow A — User opens Home 1. frontend connects to orchestrator WS 2. orchestrator verifies Clerk JWT 3. orchestrator asks user-service for profile 4. orchestrator enriches user context from resume/social state 5. orchestrator fetches initial Q-Score 6. frontend sends `session_start(page="home")` 7. orchestrator routes to dashboard-service 8. dashboard-service fetches state from user/resume/social/qscore 9. dashboard-service builds `dashboard_loaded` 10. orchestrator relays it to frontend 11. later Q-Score updates can stream in live That’s a very nontraditional home-page load. --- ## Flow B — User changes settings 1. frontend calls `user-service /users/me` directly 2. user-service updates preferences 3. those preferences become part of future orchestrator-resolved context 4. Q-Score / pathways / matching can later use them So settings are not isolated—they feed the broader system. --- ## Flow C — User uploads resume during onboarding 1. frontend uploads PDF directly to `resume-builder` 2. resume-builder stores PDF + thumbnail 3. resume-builder triggers orchestrator `/api/v1/background-task` 4. orchestrator resolves user context 5. orchestrator dispatches `bg_parse_analyze` 6. resume-builder parses PDF, builds structured content, runs AI analysis 7. orchestrator extracts: - auto-populate fields - qscore signals 8. orchestrator PATCHes `user-service /users/auto-populate` 9. user settings/profile become prefilled That’s a really elegant async flow. --- ## Flow D — User connects LinkedIn 1. frontend hits social-branding 2. social-branding stores/fetches profile 3. social-branding triggers orchestrator background task 4. orchestrator dispatches `bg_analyze` 5. social-branding emits `profile_loaded`, `profile_scored`, `analysis_complete` 6. orchestrator: - extracts auto-populate fields - publishes qscore signals - updates user-service Again: domain output becomes shared platform intelligence. --- ## Flow E — User starts interview live session 1. frontend likely uses orchestrator/setup flow to configure session metadata 2. interview-service returns `session_id` 3. frontend opens direct WS to interview-service 4. audio packets stream directly 5. review/artifacts are later fetched from interview-service 6. orchestrator may later use review/history outputs for Q-Score So live media bypasses the general agent path for latency reasons. --- # 8. What makes this architecture strong ## Strengths ### 1. Clear separation of concerns - UI - identity - orchestration - domain intelligence - scoring - realtime media ### 2. Reusable agent/session abstraction Same business session logic can work over: - frontend WS - HTTP A2A - Redis worker ### 3. Great for incremental AI adoption AI doesn’t have to be one giant “assistant”. Each domain agent is specialized. ### 4. Cross-service intelligence Resume + social + onboarding + engagement all feed Q-Score. ### 5. Good async workflow design Background tasks avoid blocking user flows. ### 6. Nice read-model approach on dashboard Dashboard-service composes distributed state into one home payload. --- # 9. Tradeoffs / complexity ## This architecture is powerful, but expensive mentally. ### 1. Multiple protocols You have: - REST - orchestrator WS - Redis streams - A2A HTTP - direct media WS That’s a lot to hold in your head. ### 2. Mixed patterns Some services are fully agentized. Some are direct APIs. Some are hybrid realtime. ### 3. Debugging is multi-hop A page issue might involve: - Clerk token - orchestrator auth - user-service resolution - agent routing - Redis stream worker - qscore side effects ### 4. Service discovery + routing adds indirection Great power, but more moving parts. ### 5. Some services live outside repo root Operationally a bit awkward. --- # 10. The cleanest way to describe this app If I had to describe GrowQR to an engineer in one paragraph: > GrowQR is a hybrid microservice platform where the frontend uses both direct REST and a central orchestrator WebSocket. The orchestrator authenticates users once, resolves a shared user context through user-service, dynamically discovers domain agents, routes page sessions and actions to those agents over Redis Streams or HTTP A2A, extracts semantic signals from their outputs, and feeds those signals into a separate Q-Score engine. Meanwhile, traditional CRUD and low-latency media workflows still use direct service APIs and direct WebSockets. So the system combines SaaS API architecture, agent orchestration, event-driven messaging, and realtime media pipelines in one platform. That’s why it feels unique. --- # 11. The single most important insight The **user-service + orchestrator combo** is the heart. - **user-service** = “who is this user in GrowQR?” - **orchestrator** = “given this user and this page/action, which domain intelligence should run now?” Everything else plugs into that. --- If you want, next I can do one of these: 1. **draw you a plain-English sequence diagram** for the whole app 2. **map every page in frontend to its backend path/service** 3. **explain only the orchestrator internals deeply** 4. **make you an architecture doc you can share with your team**