myTrajectoryTechnical Reference
This page covers the technical detail behind myTrajectory as implemented in the current codebase: architecture layers, the full tech stack, data model, Cloud Functions, security patterns, the design system, and testing approach. The platform is currently in live testing with two charity partners, with the recruiter-facing layer in active discussion with recruitment professionals. For the product thinking and design rationale, start with the narrative.
System Overview
The application is built as a layered architecture with clear separation between client, server, AI, and data concerns.
Tech Stack
Frontend & framework
| Technology | Purpose |
|---|---|
| Next.js 16 (App Router) | Full-stack React framework with server components and API routes |
| React 19 | Component model with hooks and context |
| TypeScript 5 (strict) | Type safety across client and server |
| Tailwind CSS 3.4 | Utility-first styling with custom design tokens |
| Radix UI | Unstyled accessible primitives (20+ components) |
| shadcn/ui | 56 reusable UI component files in src/components/ui, built on Radix patterns |
| React Query v5 | Server state management with caching and background refetch |
| Framer Motion | Animation library for transitions and micro-interactions |
| Recharts | Data visualisation (sparklines, activity charts) |
| React Hook Form + Zod | Form management with schema validation |
Backend & infrastructure
| Technology | Purpose |
|---|---|
| Firebase Auth | Authentication with custom claims and role-based access |
| Firestore | Real-time NoSQL database with security rules |
| Cloud Functions | Firestore triggers and scheduled jobs for async processing |
| Cloud Storage | CV and user file storage |
| Cloud Build | CI/CD pipeline with typecheck, lint, and test gates |
| Resend | Transactional email delivery with RFC 8058 unsubscribe |
AI providers
| Provider | Role |
|---|---|
| Google Gemini | Default routed provider, fast and thinking tiers |
| OpenAI | Supported via routing |
| Anthropic Claude | Supported via routing |
Quality & monitoring
| Technology | Purpose |
|---|---|
| Vitest | Unit and integration testing (NOT Jest) |
| Playwright | Critical-path end-to-end browser testing |
| Sentry | Error tracking across client, server, and edge |
| ESLint + Husky | Linting and pre-commit hooks |
Feature Landscape
myTrajectory spans several feature domains, with shared data and service layers connecting them.
4.1 Core workspace
CV Management
Upload PDFs or paste text. Cloud Functions automatically extract content, run multi-pass AI analysis, and populate the Experience Record. Supports version control with delta analysis, CV tailoring for specific roles, and CV-to-JD coverage matching.
- Automatic text extraction via Cloud Functions
- 3-pass AI feedback system (summary, strengths, improvements)
- Experience Record initialisation from extracted content
- CV ranking for role fit assessment
Job Application Tracking
Full lifecycle management from discovery to offer. Kanban-style status progression with rich metadata for each application.
- Status pipeline: Saved → Assess → Apply → Submitted → Interviewing → Offer → Closed
- Application materials: cover letters, custom answers, take-home tasks
- Interview round tracking with type, date, location, interviewer details
- Communication log: phone, email, meeting, LinkedIn messages
- AI-powered JD extraction (8–20 structured requirements with importance tiers)
- CV-to-JD coverage analysis: strong, partial, or gap per requirement
STAR Answer Library
Structured interview story capture with AI-powered review at both the section and whole-answer level. Two distinct input modes serve different user preferences.
- Per-section coaching: situation, task, action, result reviewed individually
- Full answer grading: clarity, conciseness, impact, relevance
- Star ratings (1–5): specificity, impact, relevance
- Auto-suggested competency tags and follow-up questions
- Progressive-unlock accordion UI for step-by-step review
- Experience node linkage, tying stories to specific CV roles
- Custom competency categories with per-category answer counts
- Print-optimised export view with configurable font size and page breaks
Story Companion (Voice Agent)
The Story Companion is a conversational AI workflow for structured STAR story capture. Rather than filling in four text boxes, the user speaks naturally while the relay streams audio and transcript events, asks follow-up questions, and builds story candidates during the session.
- Live voice relay: WebSocket-based real-time transcript streaming during the conversation, with connection state management and automatic reconnection
- Active story construction: spoken narrative is turned into candidate STAR stories, with follow-up prompts and post-session review hooks
- Transcript windowing: long conversations are managed with a rolling context window that maintains the most relevant context while staying within token limits, preventing context overflow without losing the thread
- Session persistence: transcripts are saved to Firestore
copilotSessions; locally cached story drafts expire after 2 hours; the live relay itself warns at 8 minutes and closes at 9 minutes by default - Post-session feedback: optional ratings and notes captured after each companion session to improve the experience
The result: a user can talk through an interview story in their own words and leave with structured story material, linked responses, and optional AI review, without starting from a blank text area.
Priorities & Focus Sessions
Candidates define their target roles and use Pomodoro-style focus sessions to stay on track. Priorities feed into AI suggestions and activity insight signals.
- Priority list with AI-guided reflection
- Role-fit assessment against applications
- Configurable Pomodoro timer with intention tracking
- Post-session reflection (feeling, completion status)
- Activity logging feeds the intelligence engine
4.2 AI-powered features
AI Flow Layer
The src/ai/flows directory currently contains 17 tested flow modules. Most use templated prompts, Zod-validated outputs, and provider routing; adjacent AI-backed features such as JD coverage, CV ranking, priority reflection, and role-fit assessment are implemented in route/service layers rather than this folder.
Profile Builder
A tailored CV/profile builder that uses the Experience Record and AI to assemble role-specific content with drag-and-drop section management, template selection, and application context linking.
4.3 Relationship management
Networking CRM & Next Action Engine
A relationship management system for professional contacts. Each contact is classified by type (former colleague, mentor, recruiter, alumni, warm introducer) and warmth level (new connection → acquaintance → warm → close). An intelligent Next Action Engine suggests who to reconnect with, based on:
- Reminder due dates set by the user
- Long gaps in contact, with thresholds that vary by warmth level (a close contact warrants faster follow-up than an acquaintance)
- Never-contacted contacts that may be worth reaching out to
- Warmth-appropriate intent suggestions (reconnect vs. ask for perspective vs. request introduction)
4.4 Advisor & collaboration
Advisor Dashboard
Career advisors see a unified view of their clients with status indicators, activity sparklines, and consent-gated access to detailed data.
Messaging & Pinned Recommendations
Thread-based 1-on-1 messaging between advisor and client. Advisors can also pin resources, events, and recommendations directly to a client’s hub, surfacing curated content alongside the client’s own workspace. This extends the collaborative model beyond passive data viewing into active guidance.
4.5 Public profiles & discovery
Public Candidate Profiles (Interrogable)
Candidates can share their profile publicly via a unique slug. The public profile includes an AI-generated orientation summary synthesised from the candidate’s full data. Visitors (including recruiters) can interact with the profile through:
- Interrogable AI chat: ask questions about the candidate’s experience in natural language, with responses grounded in the Experience Record
- Mock role assessments: generate a JD-aligned fit assessment (core, adjacent, and stretch roles) against the candidate’s profile
- Public JD assessment: paste a job description and receive a structured coverage analysis against the candidate
- Conversation turn limits and enable/disable controls give the candidate full control over public visibility
Candidate Search (Vector/Semantic)
Admin candidate search uses Firestore vector search with cosine distance on candidate embeddings. Admins can query candidates by job description text (100–15,000 characters), which is embedded and matched against stored candidate vectors. This enables semantic discovery: finding candidates whose experience is conceptually relevant, not just keyword-matched.
4.6 Onboarding & guidance
Onboarding Intake & Workspace Hub
New users are guided through a multi-step intake flow capturing their job search stage, career challenges, role intent, and targeted roles. This feeds the onboarding checklist, a progress tracker that highlights which features to explore next. The workspace hub surfaces contextual cards based on user role, assignment status, and feature state: advisors see client cards, candidates see activity and resources, and pinned recommendations appear for users with assigned advisors.
Teaching Moments
The platform follows a UX philosophy of “teach only at moments of action, no tutorials, no guides.” One-time dismissible messages appear at key moments (first reminder set, first intent viewed, first gap suggestion) stored in localStorage with no server round-trip. The user learns by doing, not by reading.
4.7 Platform & integration
Chrome Extension
One-click job capture from job boards with offline-first architecture. Applications are queued in chrome.storage.local and synced to the API with retry, exponential backoff, and failure classification. The extension also keeps local history and dedupe fingerprints.
Email Integration
OAuth2 flow for Outlook/Gmail. Emails are automatically classified (confirmation, interview, rejection, offer, recruiter contact) with entity extraction and confidence-scored application matching.
Admin Tooling
A comprehensive admin layer for system health and operations:
- AI usage dashboard: token costs by feature, provider, and organisation
- Credit management: balance view, top-ups, enrolment, and usage monitoring
- System logs: unified view merging activities and system logs with filtering, pagination, and export
- Design system audit: component inventory, token registry, and usage statistics
- Test inventory: auto-populated listing of Vitest suites and case counts
- Script runner: JSON-based configuration scripts for system setup tasks
- Diagnostics: Firebase Storage connectivity validation
- GenAI routing config: system-wide and per-organisation provider overrides with model validation
AI Architecture
The AI subsystem is one of the most complex parts of the application. Rather than calling a single provider directly, the tested AI flow modules and several AI-backed routes use a configurable provider layer with intent-based model selection, cost tracking, and structured output validation.
Multi-provider gateway
Intent-based routing
Each AI flow declares an intent that determines which model class to use. Intents are extensible and the system is not limited to a fixed set. Examples include:
| Intent | Use case | Typical provider |
|---|---|---|
| fast | Quick responses, lower cost | Gemini Flash, GPT-4o mini |
| thinking | Deep analysis, complex reasoning | Gemini Pro, Claude, GPT-4o |
Routing configuration is stored in Firestore, enabling dynamic provider changes without deployment. Organisations can override system-wide routing with their own provider keys.
Structured output validation
JSON-structured AI outputs in the flow layer are validated against Zod schemas before they are used. This keeps the typed flow modules defensive when model output is non-deterministic.
// Example: STAR Answer Review output schema
const AIReviewSchema = z.object({
overallGrade: z.enum(['Excellent', 'Good', 'Fair', 'Draft', 'Error']),
specificity: z.number().min(1).max(5),
impact: z.number().min(1).max(5),
relevance: z.number().min(1).max(5),
feedback: z.string(),
sectionFeedback: z.record(SectionReviewSchema),
followUpQuestions: z.array(z.string()).max(3),
competencyTags: z.array(z.string()).max(5),
});
Token cost tracking
Every AI call records token usage. The platform calculates the real provider cost and applies a multiplier to cover infrastructure alongside AI spend. The resulting amount is deducted from the user’s credit balance in pence, with a ledger entry for every transaction.
Prompt system
Prompts live in src/ai/prompts/ as templates with variable substitution ({{{varName}}}). System instructions define tone rules, output structure, and guardrails. Some prompts embed example output blocks or strict JSON structure examples to keep responses consistent.
Real-time voice relay
The Story Companion uses a WebSocket-based live relay for real-time transcript streaming during voice conversations. This presents specific engineering challenges:
- Connection state management: the relay handles connect, disconnect, and reconnect states with automatic recovery
- Transcript windowing: long conversations accumulate context that exceeds model token limits. A rolling window utility builds reconnection context from the most relevant recent messages while truncating older content, maintaining conversational coherence without overflow
- Activity payloads: the relay streams structured activity data (not just raw text), enabling the AI to distinguish between user speech, system events, and its own prior responses
Candidate embeddings & vector search
Candidate summaries are embedded into vector representations stored in Firestore’s candidate_embeddings collection. Cloud Functions regenerate embeddings after Experience Record updates driven by CVs, STAR answers, and application materials. The admin candidate search uses Firestore’s findNearest() with cosine distance to find semantically similar candidates from job description text.
Credit & cost model
The platform has real running costs. Rather than a monthly subscription, users pay for credits, a model chosen because it is fairer: people pay in proportion to what they use, and charities can fund their own clients directly rather than managing seat licences.
The cost calculation is straightforward: every AI call records tokens in and out, the actual provider cost is calculated, and a platform multiplier is applied to cover infrastructure. Credits are tracked in pence (e.g. £5.00 = 500p) and deducted per use, with a full ledger of every transaction.
A small monthly minimum (~£2) is deducted from inactive accounts so unused balances don’t sit indefinitely. Accounts are never turned off. Without credit, AI-powered features become unavailable and the experience is effectively read-only.
Usage tracking is live; enforcement is the next commercial milestone.
Data Model
Firestore schema
Data is organised into root collections with user-scoped subcollections:
| Collection path | Purpose |
|---|---|
users/{uid} | User profile, preferences, credit balance and status |
users/{uid}/cvs/{cvId} | CV documents with processing status, linkage, and AI feedback |
users/{uid}/applications/{applicationId} | Job applications with lifecycle data, materials, and interviews |
users/{uid}/starResponses/{responseId} | STAR answers with transcript and review data |
users/{uid}/experience/record | Experience Record root document |
users/{uid}/experience/record/nodes/{nodeId} | Experience nodes, with nested items and variations |
users/{uid}/activities/{activityId} | Activity timeline events |
users/{uid}/pomodoro/{sessionId} | Focus sessions with intentions and reflections |
users/{uid}/contacts/{contactId} | Networking CRM contacts |
users/{uid}/networking/{communicationId} | Logged networking communications and follow-up context |
users/{uid}/priorities/user-priorities-v3 | Current priorities document |
users/{uid}/builderProfiles/{profileId} | Builder profiles and assembled sections |
users/{uid}/copilotSessions/{sessionId} | Story Companion session state and transcript persistence |
candidate_embeddings/{uid} | Vector embeddings for semantic candidate search |
organisations/{orgId} | Organisation settings and theme |
organisations/{orgId}/connections/{uid} | Advisor-client relationship, consent mirror, and summary fields |
system-settings/{docId} | System routing, credit model, and related configuration documents |
Experience Record hierarchy
Three-level structure: Node (role/education/skill) → Item (achievement/responsibility) → Variation (phrasing + source + context). Variations preserve how the same achievement has been described across CVs, STAR answers, cover letters, and voice sessions. See the narrative for the product rationale.
Sources feed in automatically via Cloud Functions:
Key domain types
The type system spans 45+ type files. Key domains include:
| Domain | Key types |
|---|---|
| STAR | STARAnswer, AIReview, STARSectionReview, Question, CompetencyCategory |
| Applications | JobApplication, ApplicationMaterial, InterviewRound, Communication, JdExtractionResult |
| CV & Experience | CV, ExperienceRecord, ExperienceNode, ExperienceItem, ExperienceVariation |
| Consent | ConsentScope, ConsentSummary, ConsentAuditEntry, AdvisorAssignment |
| Activity | Activity, ActivitySummary, RankedSignal, DismissedSignal |
| Networking | Contact, ContactType, ContactWarmth, NextAction |
| Credits | SupportPackState, SupportPackLedgerEntry, SupportPackStatus |
Advisor & Collaboration
The advisor system transforms myTrajectory from a solo tool into a collaborative platform. Advisors see a unified dashboard of their client roster with consent-gated access to detailed data.
Client roster
Each client card shows:
- Status indicators: active today, active this week, quiet, needs attention, consent pending/revoked
- Activity sparkline: 28-day visualisation of daily activity counts
- Quick metrics: current streak, recent application count
- Last activity: timestamp with sortable filtering
Consent model
Advisors can only view client data that the client has explicitly consented to share. Three granular scopes control access (see Trust & Collaboration for the design rationale):
| Scope | Grants access to |
|---|---|
viewActivityTimeline | Activity feed: CV uploads, STAR completions, focus sessions, application updates |
viewGoals | Priorities and target roles |
viewApplications | Job applications, materials, and interview schedule |
Consent enforcement
Consent is enforced at the API route level, not the client. Every advisor-facing API route calls requireConsent(caller, targetUserId, scope) before returning data. A 5-minute in-memory cache (hasConsent()) balances freshness against Firestore read costs.
All consent changes are audit-logged with actor, action, and before/after state.
Pinned content & recommendations
Advisors can recommend specific resources, events, and content directly to individual clients. Pinned items appear on the client’s workspace hub alongside their own data, with status tracking (pending/done). This extends the advisor role from passive observer to active guide.
Connection summaries
To avoid expensive cross-user queries, Cloud Functions maintain denormalised ConnectionSummary documents that cache activity metrics, streak data, and sparkline arrays. Fields include lastActivityAt, activityCount7d, activityCountPrev7d, recentApplicationCount, currentStreak, and a weeklySparkline[] array. These update on every activity write via Firestore triggers, with a scheduled reconciliation job to correct any drift.
Activity Intelligence
The Activity Intelligence engine is a signal detection system that analyses recent behaviour across multiple rolling windows and surfaces actionable insights. It detects 13 distinct signals across momentum, gaps, and misalignment.
How it works
- Gather:
gatherSignalDatafetches activity history, applications, STAR answers, focus sessions, and dismissed signals from Firestore. - Detect:
detectActiveSignalsis a pure function that evaluates all 13 signal conditions against the gathered data. No Firestore calls, fully testable. - Rank: signals are prioritised: problem signals outrank positive signals; re-engagement fires alone.
- Display: ranked signals are shown to the user with contextual copy (different messaging when an advisor is present).
- Dismiss: users can dismiss signals; dismissed signals have a 14-day cooldown before they can fire again.
Signal inventory
| Signal ID | Meaning |
|---|---|
general-re-engagement-after-long-gap | User has returned after a long inactive period |
interview-prep-gap-before-known | An interview is approaching but prep activity is missing |
interview-reached-sparse-stories | Interview activity exists but the STAR library is still thin |
drop-off-after-strong-start | Early engagement was strong but recent activity has fallen away |
first-meaningful-milestone | The user has reached an early meaningful milestone |
consistent-week | Activity is spread consistently across the current week |
meaningful-momentum | Recent activity is up against the previous week |
stale-applications | Applications are sitting without recent progress |
priority-misalignment | Applications do not line up with stated priorities |
applications-no-priority-assessment | Applications exist without enough priority assessment context |
scattered-applications | Application effort is spread thinly across different directions |
tunnel-vision | Search behaviour is overly narrow relative to recent activity |
high-intensity | Recent focus-session intensity is unusually high |
Engineering note: The signal detection function (detectActiveSignals) is deliberately pure: it takes data in and returns signals out. All Firestore interaction happens in the separate gatherSignalData function. This separation makes the detection logic fully unit-testable; the current service test file covers 13 signal conditions plus ranking, dismissal, snooze, and helper behaviour across 68 cases.
Security Patterns
Security is enforced at every layer, not assumed at any.
Authentication & authorisation
- Firebase JWT verification on every API route via
requireVerifiedUser - Custom claims synced via Cloud Functions on profile changes
- Role-based gates: standard, advisor, admin via
requireRole - Organisation boundary checks via
requireOrgAccessandrequireOrgMembership - Consent verification with 5-minute cache via
requireConsent - Conversation access checks via
requireConversationAccess
API route pattern
Many sensitive API routes follow a common defensive pattern built from authentication, validation, rate limiting, idempotency, and service-layer execution:
export async function POST(request: NextRequest) {
// 1. Authenticate and authorise
const user = await requireVerifiedUser(request);
// 2. Validate request body against Zod schema
const body = await validateBody(request, CreateApplicationSchema);
// 3. Enforce rate limits
await rateLimitOrThrow(user.uid, 'create-application');
// 4. Check idempotency (prevent duplicate operations)
await checkIdempotency(request, user.uid);
// 5. Execute business logic via service layer
const result = await applicationService.create(user.uid, body);
// 6. Return typed response
return NextResponse.json(result, { status: 201 });
}
Firestore rules
Security rules enforce access at the database level as a second line of defence:
- Visibility model: public, authenticated, organisation, private
- Helper functions:
isOwner(),isAdvisor(),isAdmin(),canAdvisorReadActivity() - Scope-checking functions for advisor access to client data
- Content creator checks and invited-user lists
Rate limiting
Token-bucket algorithm with Firestore-backed state and in-memory fallback. Per-user, per-feature granularity. Graceful degradation if Firestore is unavailable.
Data encryption
Sensitive fields (provider API keys, OAuth tokens) are encrypted at rest using AES-256-GCM via DATA_ENCRYPTION_KEY_CURRENT, with decryption fallback through DATA_ENCRYPTION_KEY_OLD_* during key rotation.
OAuth hardening
The email integration OAuth flow (Google and Microsoft) uses cryptographic state tokens to prevent CSRF attacks. Each authorisation request generates a 256-bit random state token stored in Firestore with a 10-minute TTL. On callback, the token is validated, checked for expiry, and immediately deleted to prevent replay. Callbacks that arrive without an email address from the provider are rejected outright.
Content Security Policy
A strict Content Security Policy controls which scripts, styles, connections, and frames the browser is allowed to load. The policy defaults to self for all resource types and explicitly allowlists only the external origins the platform depends on (Firebase, Google APIs, Sentry, and analytics). Plugins are disabled entirely, forms cannot submit to external sites, and the application cannot be iframed by third parties. Standard hardening headers (HSTS with preload, X-Content-Type-Options, X-Frame-Options, Referrer-Policy) are set alongside the CSP.
Idempotency
Request deduplication prevents double-deduction of credits and duplicate record creation. Crucial for unreliable network conditions and retry scenarios.
Feature flags
Environment-variable-based feature toggles control rollout of new capabilities (live voice relay, AI example finder, AI STAR review). A context provider and useFeatureFlag hook give components clean access to flag state. This enables gradual rollout and safe experimentation without deployment risk.
Request ID tracking
Every client request generates a unique ID via createClientRequestId() that is propagated through the server via getRequestIdFromRequest(). This correlates logs, errors, and metrics across client and server boundaries, making it possible to trace a single user action through the entire stack in Sentry and system logs.
Cloud Functions
Cloud Functions handle asynchronous processing that should not block user interactions. They fall into two categories: Firestore triggers (react to data changes) and scheduled jobs (run on a cadence).
Firestore triggers
| Function | Trigger | Purpose |
|---|---|---|
processCvOnUpload | CV document written / processing requested | Extract text, run 3-pass AI analysis, store feedback |
processExperienceRecordOnCvExtract | CV extraction complete | Auto-populate Experience Record from CV content |
processExperienceRecordOnStarAnswer | STAR answer saved | Add story achievements as experience variations |
processExperienceRecordOnApplicationMaterial | Material uploaded | Extract experience claims from application materials |
cleanupExperienceRecordOnCvDelete | CV deleted | Remove CV-linked Experience Record content |
syncCustomClaimsOnProfileChange | User profile updated | Propagate role and organisation changes to Firebase Auth tokens |
syncConnectionSummaryOnActivityWrite | Activity document written | Update cached advisor dashboard summaries |
syncConnectionPermissionsOnConsentWrite | Consent changed | Mirror consent status and permissions into organisation connection docs |
syncConnectionProfileSnapshotOnUserWrite | User profile updated | Sync denormalised profile fields into connection records |
Scheduled jobs
| Job | Cadence | Purpose |
|---|---|---|
cleanupExpiredSystemLogs | Every 24 hours | Retention management for system logs |
syncUserEmails | Every 30 minutes | Fetch and classify new emails from connected accounts |
sendEventReminders | Every 30 minutes | Send reminders for upcoming events |
chargeMonthlyBaseline | Monthly | Apply minimum monthly charge for accounts below the usage floor |
updateSupportPackBurnRates | Daily at 03:00 | 30-day rolling burn rate calculation |
reconcileConnectionSummaries | Daily at 03:00 | Correct out-of-sync advisor dashboard data |
Domain event bus
Firestore triggers handle data-level reactions, but some actions need coordination across multiple concerns at once: logging an activity, creating an in-app notification, and sending an email should all happen when an advisor is assigned, but none of them belongs inside the trigger itself. A domain event bus handles this by decoupling the action from its consequences.
When a meaningful action occurs, the originating code publishes a single domain event. Registered subscribers (currently an event store, an activity logger, a notification writer, and an email sender) each decide independently whether to act on it. Subscribers declare themselves as either critical (failure rolls back the publish) or best-effort (failure is logged but does not block). The email subscriber, for example, runs as best-effort: a failed email should never prevent an advisor assignment from completing.
Because the same HTTP request can be retried, the bus supports subscriber-level idempotency. Each event generates a deterministic document ID from its type, entity, and request ID. Before a subscriber executes, the bus checks whether that subscriber has already processed the event. This prevents duplicate emails and duplicate activity entries without requiring callers to manage deduplication themselves.
Transactional email
Outbound email is handled by Resend, triggered via the email subscriber on the domain event bus. Events such as advisor assignment, consent changes, resource recommendations, event invitations, and new messages each produce a corresponding email to the affected party. Multi-recipient events are sent in parallel using Promise.allSettled() so that one failed delivery does not block the rest.
Users control what they receive through a two-level preference model: a global kill switch and per-type opt-outs stored on the user profile. New accounts start with all notifications enabled. The preference check is fail-open: if the Firestore lookup fails, the email is sent rather than silently dropped, on the basis that a duplicate is less harmful than a missing notification.
Every email includes List-Unsubscribe and List-Unsubscribe-Post headers conforming to RFC 8058, enabling one-click unsubscribe in supporting mail clients. Delivery outcomes are written to system_logs for admin visibility.
Design System
Component architecture
The UI is built on a three-layer component architecture:
Component inventory
The shared UI layer currently contains 56 component files in src/components/ui, while generated design-system stats track 34 audited shared/domain components used across 278 files.
Design tokens
A feature page token registry (feature-page-token-registry.ts) tracks all design tokens used across feature pages, with auto-generated statistics to prevent token sprawl. Tokens cover colour, spacing, typography, and dark mode overrides.
Responsive & accessible
- Mobile-first responsive design using Tailwind’s responsive utilities plus page-specific media queries
- Dark mode support via CSS class strategy
- Keyboard-friendly interactions across shared primitives and major workflows
- ARIA attributes are used across shared interactive components
- Reduced motion support via
prefers-reduced-motion
Testing & Quality
Test runner
Vitest (not Jest) is the main test runner. Tests live in tests/ mirroring the src/ structure, with Playwright covering critical-path browser journeys under tests/e2e.
Test strategy
| Layer | Approach | Examples |
|---|---|---|
| Services | Unit tests with mocked Firestore | Activity insight detection (68 cases), application CRUD, consent logic |
| AI flows | Prompt validation, schema conformance | STAR review output structure, JD extraction format |
| Server utilities | Auth guards, rate limiting, validation | Consent enforcement, role checks, Zod schema validation |
| Components | Component behaviour tests | Form submission, state transitions |
| E2E | Playwright for key user journeys | CV upload, application creation, STAR recording |
Testing patterns
- Pure function extraction: complex logic (signal detection, scoring) is extracted into pure functions for deterministic testing
- Schema validation tests: Zod schemas are tested against edge cases (empty data, malformed input, boundary values)
- Mock Firestore: test doubles for Firestore operations enable fast, isolated tests
- 281
*.test.tsfiles, 2,485 tests across unit, integration, API-route, extension, and Playwright critical-path coverage