Files
docs/event flow.md
-Puter e6685203fe feat: initial docs repo with project inventory and all documentation
- 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
2026-06-22 15:04:27 +05:30

16 KiB
Raw Permalink Blame History

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 dont force Roleplay into the core package.

Roleplay can become:

  1. an upsell,
  2. a recommended recovery action,
  3. a premium variant,
  4. 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

  1. Purchase / mission activated
  2. Resume uploaded or selected
  3. Resume analyzed/improved for target interview
  4. Interview prep plan generated
  5. Mock interview configured
  6. Mock interview completed
  7. Review received
  8. 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_data messages
  • 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_created
  • resume_loaded
  • version_saved
  • ai_analysis_complete
  • ai_summary_optimized
  • ai_skills_suggested
  • export_ready

Interview emits:

  • interview_configured
  • session.completed over live WS
  • review endpoint returns completed, processing, failed, not_eligible
  • review_loaded through 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 dont 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:

  • id
  • user_id
  • mission_instance_id
  • source
  • type
  • category
  • subject_kind
  • subject_id
  • correlation_id
  • payload
  • occurred_at
  • received_at
  • processed_at
  • processing_status
  • error

mission_service_sessions

Tracks external service objects created for a mission.

Examples:

  • interview session ID
  • resume ID
  • roleplay session ID
  • qscore run ID

Fields:

  • id
  • user_id
  • mission_instance_id
  • stage_id
  • service_id
  • external_type
  • external_id
  • status
  • last_event_id
  • last_checked_at
  • created_at
  • updated_at

This table is crucial because the dashboard needs to know:

“This missions 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 dont 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: 010%
  • 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:

  1. service webhook,
  2. backend polling review endpoint,
  3. frontend callback to backend,
  4. 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 Agents 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_events
  • mission_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:

  1. Feature registry remains the product source of truth.
  2. Mission definitions define stages and included services.
  3. Events are dynamic and stored raw.
  4. Small generic reducers interpret categories/sources.
  5. Unknown events are accepted, stored, and visible.
  6. Actors project events into live mission state.
  7. Postgres remains authoritative.

That gives you the cohesive, event-driven mission system youre describing without recreating the old orchestrator registry.