- Added REPO_INVENTORY.md with all repos, branches, remotes, and staging info - Added .gitignore - Synced all existing docs from local workspace - Centralized documentation hub for GrowQR team
23 KiB
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-servicefor/users/me,/users/ensure, photo upload, QR - frontend →
resume-builderfor resumes, versions, export, parse - frontend →
social-brandingfor profiles, analysis history, content generation - frontend →
interview-service/roleplay-serviceartifact 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:<agent-name> - service worker consumes it
- service publishes response messages to
responses:<task_id> - orchestrator relays those messages back to frontend
Fallback: HTTP A2A
- orchestrator POSTs to
/<service>/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.tssrc/lib/api/resumes.tssrc/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=<clerk_jwt> - sends:
{ "type": "session_start", "page": "...", "params": {...} } - later sends:
{ "type": "user_action", "action": "...", "params": {...} }
And it listens for structured messages like:
capabilities_updatedagent_routedagent_thinkingagent_dataagent_suggestionagent_activityagent_errorqscore_loadedqscore_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:
- connect to orchestrator
- orchestrator authenticates
- orchestrator resolves user context via user-service
- orchestrator routes page to correct agent
- agent sends structured messages back
- 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
homeand 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.pyorchestrator/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:
- WebSocket opens with
?token=<clerk_jwt> - orchestrator verifies Clerk JWT
- extracts Clerk user id
- creates
SuperAgentSession
Then in SuperAgentSession.run():
Step 1
Send capabilities snapshot
Step 2
Resolve user identity via user-service
Step 3
Process:
session_startuser_actionsession_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.pyorchestrator/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-serviceresume-hub→resume-buildersocial-media→social-brandingjob-matching→matchmaking-servicesettings→user-servicepathways→pathways-service- etc.
Action routing
ACTION_TO_AGENT maps actions like:
ai_analyze→resume-builderrun_analysis→social-brandingupdate_preferences→user-serviceget_feed→matchmaking-servicegenerate_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():
- call
user-serviceactionview_profile - get the real user profile
- extract the app UUID
- 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.pyorchestrator/app/a2a/client.py
Redis mode
- publish task to
tasks:<agent> - subscribe to
responses:<task_id> - stream messages to frontend in real time
HTTP mode
- POST to
/<agent>/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.pypublisher.pysubscriber.pyclient.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_compatibilitylinkedin.headline_qualitymatching.feed_activeinterview.overall_scorepathway.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-builderafter resume uploadsocial-brandingafter LinkedIn fetch/connect
Flow:
- service notifies orchestrator
- orchestrator resolves user context
- dispatches background task to agent
- collects messages
- extracts Q-Score signals
- extracts auto-populate info
- 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.pyapp/api/v1/users.pyapp/api/v1/webhooks.pyapp/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.createduser.updateduser.deletedsession.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:
UserAgentSessionDashboardAgentSession- 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:<agent> 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-servicevia RESTresume-buildervia RESTsocial-brandingvia RESTorchestratorvia WSinterview-servicevia direct WS + RESTroleplay-servicevia direct WS + REST- internal Next
/api/chatroute for helper chat
6.2 Orchestrator
Talks to:
user-servicefor identity resolution + metadata persistence + auto-populate targetresume-builderfor page/actions + skill enrichment + background parse/analyzesocial-brandingfor page/actions + skill enrichment + background analysisdashboard-servicefor home page payloadmatchmaking-servicefor job matching actionspathways-servicefor pathway actionsqscore-servicefor 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
- frontend connects to orchestrator WS
- orchestrator verifies Clerk JWT
- orchestrator asks user-service for profile
- orchestrator enriches user context from resume/social state
- orchestrator fetches initial Q-Score
- frontend sends
session_start(page="home") - orchestrator routes to dashboard-service
- dashboard-service fetches state from user/resume/social/qscore
- dashboard-service builds
dashboard_loaded - orchestrator relays it to frontend
- later Q-Score updates can stream in live
That’s a very nontraditional home-page load.
Flow B — User changes settings
- frontend calls
user-service /users/medirectly - user-service updates preferences
- those preferences become part of future orchestrator-resolved context
- 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
- frontend uploads PDF directly to
resume-builder - resume-builder stores PDF + thumbnail
- resume-builder triggers orchestrator
/api/v1/background-task - orchestrator resolves user context
- orchestrator dispatches
bg_parse_analyze - resume-builder parses PDF, builds structured content, runs AI analysis
- orchestrator extracts:
- auto-populate fields
- qscore signals
- orchestrator PATCHes
user-service /users/auto-populate - user settings/profile become prefilled
That’s a really elegant async flow.
Flow D — User connects LinkedIn
- frontend hits social-branding
- social-branding stores/fetches profile
- social-branding triggers orchestrator background task
- orchestrator dispatches
bg_analyze - social-branding emits
profile_loaded,profile_scored,analysis_complete - 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
- frontend likely uses orchestrator/setup flow to configure session metadata
- interview-service returns
session_id - frontend opens direct WS to interview-service
- audio packets stream directly
- review/artifacts are later fetched from interview-service
- 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:
- draw you a plain-English sequence diagram for the whole app
- map every page in frontend to its backend path/service
- explain only the orchestrator internals deeply
- make you an architecture doc you can share with your team