- 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
16 KiB
Missions are not chat flows. Missions are event-driven product journeys.
Chat/Grow Agent is only one optional surface that can read mission state and help the user decide what to do next.
The mission dashboard should work even if the user never opens chat.
I read the Rivet docs you linked, the current backend mission/actor code, the old growqr-app/orchestrator, and the service event patterns. The correct production design is a mission event system + dashboard projection, not a chat-first workflow.
Revised product model
User buys a mission
Example:
Interview-to-Offer Accelerator — $10/month
Includes limits like:
- 10 mock interviews/month
- X resume analyses/optimizations
- maybe 1 final readiness report
- optional add-ons later: roleplay, extra interviews, video analysis, etc.
After purchase, the user gets an active mission dashboard.
The dashboard shows:
- current mission progress
- next required action
- completed service actions
- generated artifacts
- interview/resume results
- Q Score/readiness trend
- quota usage
- recommendations
Chat can help, but the dashboard is the primary surface.
Important correction for Interview-to-Offer
If the team says this mission currently uses only:
- Resume
- Interview
Then don’t force Roleplay into the core package.
Roleplay can become:
- an upsell,
- a recommended recovery action,
- a premium variant,
- or a later stage only if interview results show communication weakness.
So the core mission becomes:
Interview-to-Offer Accelerator v1
Included services
- Resume Building
- Mock Interview
- Q Score/readiness analytics
- Grow Agent/planning only as backend intelligence, not necessarily chat
Core stages
- Purchase / mission activated
- Resume uploaded or selected
- Resume analyzed/improved for target interview
- Interview prep plan generated
- Mock interview configured
- Mock interview completed
- Review received
- Final readiness score/report generated
That is much cleaner.
What exists today
From the code:
Backend already has
grow_active_missions- mission actors like
interviewToOfferMissionActor - mission snapshots
- explicit APIs to update stages and artifacts
- PG-first persistence for active missions
- Q Score snapshots/workflow event tables from older workflow infra
But right now, mission progress updates mostly happen through explicit backend calls like:
POST /missions/active/:instanceId/stages/:stageId
POST /missions/active/:instanceId/artifacts
That means the dashboard does not automatically know when a microservice finishes something unless we explicitly update the mission.
Old orchestrator already had a useful pattern
The old growqr-app/orchestrator:
- routes actions to services
- receives
agent_datamessages - extracts Q Score signals from service results
- publishes signals to
qscore.signal_events - handles real-time score updates
But it has too much routing registry logic:
ACTION_TO_AGENT
PAGE_TO_AGENT
COMPOSITE_WORKFLOWS
We should not rebuild that as another large registry.
Services emit useful events/messages already
Examples:
Resume emits A2A-style messages:
resume_createdresume_loadedversion_savedai_analysis_completeai_summary_optimizedai_skills_suggestedexport_ready
Interview emits:
interview_configuredsession.completedover live WS- review endpoint returns
completed,processing,failed,not_eligible review_loadedthrough agentic path
Roleplay emits similar patterns.
Q Score already has a strong event ingestion path:
qscore.signal_events Redis stream
So the missing layer is not “service capability.” It is a GrowQR mission event ingestion/projection layer.
Correct architecture
Principle
Postgres is the source of truth. Actors are live reducers/projectors. Chat is just one consumer.
Event flow
Service action happens
↓
GrowQR backend receives/creates event
↓
Raw event is stored in Postgres event inbox
↓
Event is forwarded to the relevant mission actor
↓
Mission actor updates mission snapshot/progress/artifacts
↓
Dashboard reads updated mission snapshot
↓
Q Score signals are published/updated
↓
Grow/chat may optionally read the same state
New concept: dynamic mission events
We do not need a rigid event registry.
We need one flexible event envelope.
Generic event envelope
type GrowEvent = {
id: string;
userId: string;
orgId?: string;
source: string;
// resume-service, interview-service, roleplay-service, qscore-service, grow-backend
type: string;
// resume.ai_analysis_complete
// interview.session_completed
// interview.review_completed
// mission.purchased
// qscore.updated
category:
| "mission"
| "service"
| "artifact"
| "usage"
| "qscore"
| "entitlement"
| "system";
occurredAt: string;
mission?: {
instanceId?: string;
missionId?: string;
stageId?: string;
};
subject?: {
kind: string;
id: string;
};
// example: { kind: "interview_session", id: sessionId }
correlation?: {
requestId?: string;
sessionId?: string;
resumeId?: string;
externalId?: string;
};
payload: Record<string, unknown>;
};
The important thing: we store unknown events too.
If an event type is new, we don’t reject it. We save it, attach it to the user/mission if possible, and show it in debugging/timeline if useful.
Postgres tables needed
Do not rely on actor state for durable history.
Add something like:
grow_events
Append-only event inbox.
Fields:
iduser_idmission_instance_idsourcetypecategorysubject_kindsubject_idcorrelation_idpayloadoccurred_atreceived_atprocessed_atprocessing_statuserror
mission_service_sessions
Tracks external service objects created for a mission.
Examples:
- interview session ID
- resume ID
- roleplay session ID
- qscore run ID
Fields:
iduser_idmission_instance_idstage_idservice_idexternal_typeexternal_idstatuslast_event_idlast_checked_atcreated_atupdated_at
This table is crucial because the dashboard needs to know:
“This mission’s interview stage corresponds to interview session
abc123.”
mission_artifacts
Could either reuse current snapshot artifacts or make durable table.
Examples:
- resume fit scan
- interview prep plan
- interview review
- readiness report
Actor design
Do not put this all in growActor.
growActor is becoming chat/conversation/user home state. Missions should not depend on it.
Best actor model
1. Mission actor per active mission
Already exists:
interviewToOfferMissionActor[userId, instanceId]
Extend it conceptually to support:
ingestEvent(event)
recomputeProjection()
scheduleExternalCheck()
getDashboardSnapshot()
The actor stores only bounded state:
- current stage
- progress
- latest known service session IDs
- latest scores
- artifact summaries
- next action
- last few events maybe
Raw events stay in Postgres.
2. Optional user analytics actor
A per-user analytics actor can exist later:
userAnalyticsActor[userId]
Its job:
- update user-level Q Score trend
- aggregate events across missions
- maintain dashboard analytics
- detect recommendations
But don’t make it mandatory for v1. The mission actor + PG event store is enough.
3. Do not create one god event actor
Avoid:
globalEventProcessorActor
That becomes a bottleneck.
Better:
event ingest route → Postgres → mission actor for that mission/user
Rivet docs support actor queues and schedules, which are good primitives here. Use queues for ordered event processing and schedules for later status checks. The docs also note actor noSleep is deprecated in current docs, so we should avoid relying on permanent awake actors long-term, even though the project is pinned to rivetkit ^2.2.1.
Event ingestion paths
We need multiple ingestion paths because not every service currently emits backend webhooks.
1. Backend gateway-generated events
Whenever the dashboard calls GrowQR backend gateway:
POST /services/interview/configure
POST /services/resume/resumes/:id/analyze
POST /services/interview/review/:sessionId
The backend should emit internal events like:
interview.configured
resume.analysis_requested
resume.analysis_completed
interview.review_completed
This is the easiest first layer.
2. Service webhook/event endpoint
Add a backend route:
POST /events/ingest
Services can call this when they complete things:
resume.ai_analysis_complete
interview.session_completed
interview.review_completed
roleplay.review_completed
This is ideal long term.
3. Redis stream bridge
Old services already use Redis streams for task responses.
We can add a backend consumer that listens to service response messages and converts them into Grow events.
But I would not make Redis the only source of truth. It should feed Postgres.
4. Polling/checker fallback
This is mandatory.
Some events will be missed. Some services currently only expose state through REST. Some live WebSocket events go straight to the browser.
So when a mission creates an interview session, store:
mission instance → interview session id
Then schedule checks:
check interview session/review in 30s
check again in 2m
check again in 5m
stop when completed/failed/expired
Actors are good for this because Rivet supports scheduled actions.
How the mission updates automatically
Example: Interview-to-Offer.
User buys mission
Backend creates event:
mission.purchased
Mission actor receives it:
- status: active
- stage: resume
- progress: 0–10%
- next action: upload/select resume
Dashboard shows:
Next: Update your resume for this interview.
User analyzes resume
Dashboard calls Resume service through backend.
Backend stores:
resume.analysis_requested
When result returns or poller finds result:
resume.ai_analysis_complete
Mission actor applies dynamic reducer:
- source = resume-service
- event contains resume analysis
- mission stage = resume
- mark resume stage done
- create artifact: Resume Fit Scan
- update progress
- next action: Start mock interview
No chat required.
User starts interview
Backend gateway calls Interview service configure.
Backend stores:
interview.configured
Mission actor:
- links session ID to mission
- marks interview stage ready/in_progress
- next action: Start interview session
User completes interview
Live WS sends session.completed to browser today.
But backend should know through one of:
- service webhook,
- backend polling review endpoint,
- frontend callback to backend,
- Redis bridge if session completion is emitted there.
Backend stores:
interview.session_completed
Mission actor:
- stage status: review pending
- next action: wait for review / view processing
Review becomes available
Backend fetches:
GET /services/interview/review/:sessionId
If completed, store:
interview.review_completed
Mission actor:
- creates artifact: Mock Interview Review
- marks interview stage done
- extracts score/weaknesses
- publishes Q Score signals
- computes/requests final readiness
- updates dashboard
Q Score integration
Q Score should be event-fed, not chat-fed.
Current old orchestrator extracts signals from service messages. We can reuse that idea but move it into backend event processing.
Signal mapper should be dynamic-ish
Instead of a giant event registry, use a few generic extractors by category/source:
resume event → resume signal extractor
interview event → interview signal extractor
roleplay event → roleplay signal extractor
engagement/mission event → workflow signal extractor
This is not a mission registry. It is a small set of signal extractors.
Example interview review signals:
interview.completed
interview.overall_score
interview.language_score
interview.voice_score
interview.body_language_score
interview.filler_words
interview.technical_quality
Resume signals:
resume.uploaded
resume.ats_compatibility
resume.keyword_relevance
resume.grammar_clarity
resume.impact_statements
Mission signals:
mission.interview_to_offer.started
mission.interview_to_offer.resume_completed
mission.interview_to_offer.interview_completed
mission.interview_to_offer.completed
These are then sent to Q Score.
Dashboard behavior
The mission dashboard should be driven by mission state, not chat messages.
Active mission page
For Interview-to-Offer:
Interview-to-Offer Accelerator
Progress: 55%
Plan: $10/month
Usage:
- Interviews: 2 / 10
- Resume analyses: 1 / 3
Current step:
Mock Interview
Next action:
Start your 15-minute behavioral mock interview.
Completed:
✓ Mission purchased
✓ Resume selected
✓ Resume analyzed
✓ Resume fit scan generated
Artifacts:
- Resume Fit Scan
- Interview Prep Plan
- Mock Interview Review, pending
Readiness:
Baseline Q Score: 64
Current Q Score: 71
Delta: +7
Stage cards
Each card has its own CTA:
Resume stage:
- Upload resume
- Select resume
- Analyze resume
- View fit scan
Interview stage:
- Configure mock interview
- Start interview
- View review
Final stage:
- Generate readiness report
- View final Q Score
No chat required.
Chat/Grow Agent’s new role
Chat becomes a reader/writer of the same mission system.
It can:
- explain current mission status
- recommend next action
- call mission/service tools
- summarize artifacts
- answer “what should I do next?”
But if the user never opens chat, the mission still progresses.
So the Grow Agent should not own the mission. It should consume the mission projection.
Subscription/usage tracking
For paid missions, usage should also be event-based.
Events:
entitlement.granted
usage.interview_session_created
usage.interview_session_completed
usage.resume_analysis_requested
usage.resume_analysis_completed
usage.limit_reached
subscription.renewed
subscription.cancelled
The gateway should enforce limits before creating expensive service sessions.
Example:
User clicks Start Interview
↓
Backend checks entitlement
↓
If allowed, create interview session
↓
Insert usage event
↓
Increment monthly usage
Do not rely on frontend hiding buttons.
What I would build first
Phase 1 — Mission event backbone
No new product logic yet. Just make events durable.
grow_eventsmission_service_sessions/events/ingest- backend helper:
recordGrowEvent() - event forwarding to mission actor
- mission actor
ingestEvent()
Phase 2 — Backend gateway emits mission events
Whenever dashboard uses service routes, backend records events.
Examples:
- interview configured
- interview review fetched
- resume analyzed
- resume version saved
Phase 3 — Mission actor becomes projection reducer
Add generic dynamic handling:
- store unknown events
- update timeline
- if event matches current stage service, update stage
- attach artifacts if payload has review/analysis/export
- update next action
Phase 4 — Polling fallback
For linked external sessions:
- poll interview review until completed
- poll roleplay review until completed
- poll resume analysis state if needed
Phase 5 — Q Score bridge
Convert service events into Q Score signals and publish them.
Phase 6 — Dashboard reads projection
Dashboard should not guess. It reads:
GET /missions/active/:instanceId
and renders:
- stages
- events
- artifacts
- next action
- usage
- score trend
Key recommendation
Do not create another large service/action registry.
Use this instead:
- Feature registry remains the product source of truth.
- Mission definitions define stages and included services.
- Events are dynamic and stored raw.
- Small generic reducers interpret categories/sources.
- Unknown events are accepted, stored, and visible.
- Actors project events into live mission state.
- Postgres remains authoritative.
That gives you the cohesive, event-driven mission system you’re describing without recreating the old orchestrator registry.