Request Service
22
API Endpoints
2
Service Deps
3
Infrastructure
1
DB Schemas
API Endpoints
/requestsGet all help requests with optional filters.
/requests/matched/for-userGet requests matching user's skills from their communities (skill-based matching algorithm).
/requests/curated (v9.0 + ADR-031 Multi-Tier)Get curated feed with match scores, trust distance, and multi-tier visibility.
/requests/:idGet specific request details.
/requests (v9.0 Polymorphic + ADR-022 Visibility)Create new polymorphic help request (supports 5 types) with visibility scope.
/requests/:idUpdate help request (requester only).
/requests/:idCancel help request (requester only).
/requests/:id/privacyUpdate privacy settings for a request (Social Karma v2.0).
/requests/:id/admin-triageOverride request urgency and/or add a community-scoped admin note (Sprint 25).
/offersGet all help offers with optional filters.
/offers/:idGet specific offer details.
/offersCreate new help offer.
/offers/:id/privacyUpdate privacy settings for an offer (Social Karma v2.0).
/matchesGet all matches with optional filters.
/matches/:idGet specific match details.
/matchesCreate a match between request and responder.
/matches/:id/completeTwo-phase match completion. Each party calls this independently; the match
/matches/:id/feedbackSubmit interaction feedback for a completed match.
/matches/:id/feedbackGet feedback for a match.
/providersList all provider profiles. Optional query param: service_type.
/providers/myGet the authenticated user's own provider profiles. Auth required.
/providers/:idGet a single provider profile by ID, including ride details if applicable.
/providersCreate a provider profile for the authenticated user.
/providers/:idUpdate a provider profile. Owner only.
/providers/:idDelete a provider profile. Owner only.
/collectivesList all provider collectives. Optional query param: service_type.
/collectives/myGet collectives the authenticated user belongs to (via their provider profiles). Auth required.
/collectives/:idGet a collective with members and communities served.
/collectivesCreate a new provider collective.
/collectives/:idUpdate a collective. Collective admin only.
/collectives/:idDelete a collective. Collective admin only.
/collectives/:id/membersJoin a collective as a member.
/collectives/:id/members/:providerIdRemove a member from a collective. Collective admin only.
/collectives/:id/communitiesLink a collective to a community.
/collectives/:id/communities/:communityIdUnlink a collective from a community. Auth: collective admin OR community admin (Sprint 26).
/collectives/:id/statsReturns aggregate performance stats for a collective: `total_requests_matched`, `fulfillment_rate`, `avg_completion_hours` (null if no completed matches), `communities_served_count`, `available_membe
/providers/:providerId/availabilityToggle a provider's availability status. Body: `{ is_available: boolean }`. Auth: owner only (provider_profiles.user_id must match JWT userId). Returns `{ id, is_available }`. (Sprint 26)
/healthService health check.
Infrastructure
Service Dependencies
Publishes Events
Full Documentation
Request Service - Complete Context Documentation
Last Updated: 2026-02-06 Version: v9.0.0 Port: 3003 Status: Production (Polymorphic Request System + Curated Feed)
Quick Start
# Start this service
docker-compose up request-service
# Start in development mode
cd services/request-service && npm run dev
# Test this service
npm run test:integration -- integration/request-service.test.ts
# View logs
docker logs karmyq-request-service -f
# Health check
curl http://localhost:3003/health
1. Overview
1.1 Purpose
The Request Service manages polymorphic help requests (v9.0), help offers, and matches between requesters and helpers within communities. It implements skill-based matching and curated feed filtering to intelligently suggest relevant requests to users.
1.2 Responsibilities (v9.0)
- Polymorphic Request Management - CRUD for 5 request types (generic, ride, service, event, borrow)
- Curated Feed Filtering - Skill-based + preference-based feed curation with match scores
- Help Offer Management - CRUD operations for help offers
- Type-Specific Matching - Match using specialized algorithms per request type
- Privacy Controls - Social Karma v2.0 privacy and consent management
- Interaction Feedback - Collect exchange quality ratings (not person ratings)
- Event Publishing - Emit domain events for request lifecycle
1.3 NOT Responsible For
- Karma Calculation - Handled by Reputation Service
- User Authentication - Handled by Auth Service
- Messaging - Handled by Messaging Service
- Community Management - Handled by Community Service
2. Architecture
2.1 Technology Stack
- Runtime: Node.js 18
- Framework: Express.js
- Database Schema:
requests - Event Queues:
karmyq-events(Bull/Redis) - External Services: PostgreSQL, Redis
2.2 Key Components (v9.0)
src/
├── index.ts # Express app initialization, route registration
├── routes/
│ ├── requests.ts # Polymorphic request CRUD + curated feed endpoint
│ ├── offers.ts # Help offer CRUD
│ ├── matches.ts # Match creation and status updates
│ └── feedback.ts # Interaction feedback (Social Karma v2.0)
├── services/
│ └── matcher.ts # Type-specific matching algorithms
├── database/
│ └── db.ts # PostgreSQL connection pool
└── events/
└── publisher.ts # Redis event publishing (Bull)
Shared Packages (v9.0):
├── packages/shared/src/schemas/requests/
│ ├── index.ts # Zod discriminated union schema
│ ├── generic.ts # Generic request schema
│ ├── ride.ts # Ride request schema
│ ├── service.ts # Service request schema
│ ├── event.ts # Event request schema
│ └── borrow.ts # Borrow request schema
└── packages/shared/src/matching/
├── index.ts # Matching algorithm exports
├── types.ts # UserProfile, MatchScore interfaces
└── matchers/ # Type-specific matchers
├── generic.ts
├── ride.ts
├── service.ts
├── event.ts
└── borrow.ts
2.3 Database Schema
Tables Owned by This Service
requests.help_requests - Core polymorphic help request table (v9.0)
CREATE TABLE requests.help_requests (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
requester_id UUID NOT NULL REFERENCES auth.users(id),
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
-- v9.0: Polymorphic Request System
request_type request_type_enum NOT NULL DEFAULT 'generic',
payload JSONB, -- Type-specific data
requirements JSONB, -- Type-specific requirements
-- Legacy fields (maintained for backward compatibility)
category VARCHAR(100), -- transportation, moving, childcare, etc.
urgency VARCHAR(50) DEFAULT 'medium', -- low, medium, high, critical
preferred_start_date TIMESTAMP,
preferred_end_date TIMESTAMP,
status VARCHAR(50) DEFAULT 'open', -- open, matched, completed, cancelled
expired BOOLEAN DEFAULT false,
-- ADR-022: Multi-Tier Visibility
visibility_scope visibility_scope_enum NOT NULL DEFAULT 'community',
visibility_max_degrees INTEGER DEFAULT 3 CHECK (visibility_max_degrees BETWEEN 1 AND 6),
-- Social Karma v2.0 Privacy
is_public BOOLEAN DEFAULT false,
requester_visibility_consent BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ADR-022: Visibility scope enum
CREATE TYPE visibility_scope_enum AS ENUM ('community', 'trust_network', 'platform');
-- v9.0: Request Type Enum
CREATE TYPE request_type_enum AS ENUM ('generic', 'ride', 'service', 'event', 'borrow');
-- v9.0: Multi-community support
CREATE TABLE requests.request_communities (
request_id UUID NOT NULL REFERENCES requests.help_requests(id) ON DELETE CASCADE,
community_id UUID NOT NULL REFERENCES communities.communities(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (request_id, community_id)
);
-- Indexes
CREATE INDEX idx_help_requests_community_id ON requests.help_requests(community_id);
CREATE INDEX idx_help_requests_status ON requests.help_requests(status);
CREATE INDEX idx_help_requests_category ON requests.help_requests(category);
requests.request_admin_notes - Community-scoped admin triage notes (Sprint 25)
CREATE TABLE requests.request_admin_notes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
request_id UUID NOT NULL REFERENCES requests.help_requests(id) ON DELETE CASCADE,
community_id UUID NOT NULL REFERENCES communities.communities(id) ON DELETE CASCADE,
note TEXT NOT NULL,
updated_by UUID NOT NULL REFERENCES auth.users(id),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(request_id, community_id)
);
One note per (request, community) pair; upserted via PATCH /requests/:id/admin-triage.
requests.help_offers - Help offers from community members
CREATE TABLE requests.help_offers (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
community_id UUID NOT NULL REFERENCES communities.communities(id),
offerer_id UUID NOT NULL REFERENCES auth.users(id),
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
category VARCHAR(100) NOT NULL,
availability_start TIMESTAMP,
availability_end TIMESTAMP,
status VARCHAR(50) DEFAULT 'active', -- active, matched, expired
-- Social Karma v2.0 Privacy
is_public BOOLEAN DEFAULT false,
offerer_visibility_consent BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_help_offers_community_id ON requests.help_offers(community_id);
requests.matches - Connections between requests and responders
CREATE TABLE requests.matches (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
request_id UUID NOT NULL REFERENCES requests.help_requests(id),
offer_id UUID REFERENCES requests.help_offers(id),
responder_id UUID NOT NULL REFERENCES auth.users(id),
status VARCHAR(50) DEFAULT 'pending', -- pending, accepted, in_progress, completed, cancelled
-- Social Karma v2.0 Privacy
requester_visible BOOLEAN DEFAULT false,
responder_visible BOOLEAN DEFAULT false,
interaction_category VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP,
requester_done_at TIMESTAMP, -- set when requester clicks Done (migration 017)
responder_done_at TIMESTAMP, -- set when responder clicks Done (migration 017)
UNIQUE(request_id, offer_id)
);
CREATE INDEX idx_matches_request_id ON requests.matches(request_id);
CREATE INDEX idx_matches_status ON requests.matches(status);
requests.interaction_feedback - Social Karma v2.0 exchange quality ratings
CREATE TABLE requests.interaction_feedback (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
match_id UUID NOT NULL REFERENCES requests.matches(id) ON DELETE CASCADE,
from_user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
to_user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
-- Interaction quality ratings (1-5)
helpfulness INTEGER CHECK (helpfulness BETWEEN 1 AND 5),
responsiveness INTEGER CHECK (responsiveness BETWEEN 1 AND 5),
clarity INTEGER CHECK (clarity BETWEEN 1 AND 5),
-- Optional comment about the exchange (not the person)
comment TEXT,
-- Visibility consent for featuring in stories
allow_featuring BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(match_id, from_user_id)
);
CREATE INDEX idx_interaction_feedback_match ON requests.interaction_feedback(match_id);
CREATE INDEX idx_interaction_feedback_to_user ON requests.interaction_feedback(to_user_id);
Privacy Design Principles (Social Karma v2.0)
- Privacy First: All requests/offers default to
is_public = false - Two-Way Consent: Both parties must consent for names in featured stories
- Interaction Ratings: Rate the exchange quality, NOT the individual
Tables Read by This Service
auth.users- User details for requester/helper namesauth.user_skills- User skills for skill-based matchingauth.user_feed_preferences- Feed visibility preferences (ADR-022)auth.social_distances- Trust distance between users (ADR-031)communities.communities- Community names, details, anddefault_request_scopecommunities.members- Verify community membershipcommunities.community_configs- Feed scoring weights (ADR-031)reputation.karma_records- Karma scores for feed display (ADR-031)reputation.trust_scores- Trust scores for feed display (ADR-031)
3. API Reference
3.1 Help Requests
GET /requests
Get all help requests with optional filters.
Query Parameters:
community_id(UUID) - Filter by communitystatus(string) - Filter by status (default: 'open')type(string) - Filter by categorylimit(number) - Max results (default: 50)offset(number) - Pagination offset (default: 0)include_admin_notes(boolean) - Whentrueandcommunity_idis also provided, each request in the response includes anadmin_notefield scoped to that community (sourced fromrequests.request_admin_notes). Only returned to active admins/moderators of the community.
Response:
{
"success": true,
"data": [{
"id": "uuid",
"community_id": "uuid",
"community_name": "Seattle Mutual Aid",
"requester_id": "uuid",
"requester_name": "Alice Smith",
"title": "Need help moving furniture",
"description": "Moving couch upstairs, need 2-3 people",
"category": "moving",
"urgency": "medium",
"status": "open",
"created_at": "2025-01-10T12:00:00Z"
}],
"count": 1
}
Implementation: src/routes/requests.ts:8
GET /requests/matched/for-user
Get requests matching user's skills from their communities (skill-based matching algorithm).
Query Parameters:
user_id(UUID, required) - User to match requests forlimit(number) - Max results (default: 10)
Response:
{
"success": true,
"data": [{
"id": "uuid",
"title": "Need help moving furniture",
"category": "moving",
"urgency": "high",
"urgency_priority": 3,
"community_name": "Seattle Mutual Aid",
"requester_name": "Alice Smith",
"created_at": "2025-01-10T12:00:00Z"
}],
"count": 1
}
Algorithm:
- Orders by urgency (high=3, medium=2, low=1) then creation date
- Matches based on category-to-skill mapping (see Section 5.2)
- Excludes user's own requests
- Only includes communities user is a member of
GET /requests/curated (v9.0 + ADR-031 Multi-Tier)
Get curated feed with match scores, trust distance, and multi-tier visibility.
Query Parameters:
minScore(number) - Minimum match score 0-100 (default: 30)limit(number) - Max results (default: 20)community_id(UUID) - Filter by specific community (optional)tier(string) - Filter by visibility tier:community,trust_network,platform,sister_community(optional)includeSisterCommunities(boolean) - Include requests from linked sister communities whereshow_in_sister_feeds=true, scored with trust_carry_factor applied (Sprint 15)
Authentication: Required (JWT token)
Response:
{
"success": true,
"data": {
"requests": [{
"id": "uuid",
"request_type": "service",
"title": "Need plumber for leak repair",
"description": "Kitchen pipe leaking...",
"urgency": "high",
"visibility_scope": "community",
"visibility_max_degrees": 3,
"payload": {
"service_category": "plumbing",
"skill_level_required": "intermediate"
},
"matchScore": 85,
"feedScore": 72,
"sourceTier": "community",
"trustDistance": 2,
"karmaScore": 150,
"matchReasons": [
"You have plumbing skill",
"Skill level matches",
"High urgency bonus"
],
"matchBreakdown": {
"skillScore": 50,
"urgencyBonus": 35
},
"community_name": "Seattle Mutual Aid",
"requester_name": "Alice Smith"
}],
"count": 15,
"tiers": {
"community": 10,
"trust_network": 3,
"platform": 2
},
"filters": {
"minMatchScore": 30,
"totalRequests": 50,
"matchedRequests": 15,
"subscribedTypes": ["generic", "service", "event"]
},
"feedPreferences": {
"feed_show_trust_network": true,
"feed_trust_network_max_degrees": 3,
"feed_show_platform": false,
"feed_platform_categories": ["digital", "questions"]
},
"userProfile": {
"skills": ["plumbing", "carpentry"],
"skillCount": 2
}
}
}
Algorithm (ADR-031):
- Fetch user feed preferences from
auth.user_feed_preferences - Fetch user preferences (subscribed request types from auth.user_request_preferences)
- Fetch user skills from auth.user_skills
- Get open requests across three tiers:
- Community: Requests in user's communities
- Trust Network: Requests with
visibility_scope != 'community'within trust degree limits - Platform: Requests with
visibility_scope = 'platform'(if user opted in)
- Batch-fetch trust distances from
auth.social_distancesand karma fromreputation.karma_records - Resolve source tier per request using
resolveSourceTier()from@karmyq/shared/matching - Calculate feed scores using community-configurable weights (ADR-031)
- Sort by tier priority (community > trust_network > platform), then by feedScore
- Return top N results with transparency (scores, tier, trust distance, karma)
Implementation: src/routes/requests.ts:194
GET /requests/:id
Get specific request details.
Response:
{
"success": true,
"data": {
"id": "uuid",
"title": "Need help moving furniture",
"description": "Moving couch upstairs",
"category": "moving",
"urgency": "medium",
"status": "open",
"requester_id": "uuid",
"requester_name": "Alice Smith",
"requester_email": "alice@example.com",
"community_id": "uuid",
"community_name": "Seattle Mutual Aid",
"created_at": "2025-01-10T12:00:00Z"
}
}
Implementation: src/routes/requests.ts:134
POST /requests (v9.0 Polymorphic + ADR-022 Visibility)
Create new polymorphic help request (supports 5 types) with visibility scope.
Request (Generic):
{
"community_id": "uuid",
"request_type": "generic",
"title": "Need help moving furniture",
"description": "Moving couch upstairs, need 2-3 strong people",
"urgency": "medium",
"payload": {},
"visibility_scope": "community",
"visibility_max_degrees": 3
}
Visibility Fields (ADR-022):
visibility_scope- One of:community(default),trust_network,platform. Falls back to community'sdefault_request_scopeif omitted.visibility_max_degrees- Max trust hops for trust_network scope (1-6, default: 3)
Request (Service - Plumbing):
{
"community_id": "uuid",
"request_type": "service",
"title": "Need plumber for leak repair",
"description": "Kitchen pipe leaking, needs professional help",
"urgency": "high",
"payload": {
"service_category": "plumbing",
"skill_level_required": "intermediate",
"estimated_duration_hours": 2,
"budget_range": {
"min": 50,
"max": 100,
"currency": "USD"
},
"location_type": "on_site",
"certifications_required": ["Licensed Plumber"]
}
}
Request (Ride):
{
"community_id": "uuid",
"request_type": "ride",
"title": "Need ride to airport",
"description": "Flying out tomorrow morning",
"urgency": "medium",
"payload": {
"origin": {
"address": "123 Main St, Seattle, WA",
"lat": 47.6062,
"lng": -122.3321
},
"destination": {
"address": "SEA Airport",
"lat": 47.4502,
"lng": -122.3088
},
"seats_needed": 1,
"departure_time": "2024-06-15T10:00:00Z",
"preferences": {
"pet_friendly": false,
"luggage_space": "medium"
}
}
}
Multi-Community Posting (v9.0):
{
"post_to_all_communities": true,
"request_type": "generic",
"title": "Need general help",
"description": "..."
}
Validation:
- User must be active member of community (or all communities if post_to_all_communities)
- request_type must be one of: generic, ride, service, event, borrow
- payload must conform to type-specific Zod schema (see Section 3.6)
- Required fields:
community_id,requester_id,title,type
Response:
{
"success": true,
"data": {
"id": "uuid",
"title": "Need help moving furniture",
"category": "moving",
"urgency": "high",
"status": "open",
"created_at": "2025-01-10T12:00:00Z"
},
"message": "Request created successfully"
}
Events Published: request.created
Implementation: src/routes/requests.ts:173
PUT /requests/:id
Update help request (requester only).
Request:
{
"user_id": "uuid",
"title": "Updated title",
"description": "Updated description",
"urgency": "low",
"status": "completed"
}
Authorization: Only the original requester can update
Events Published: request.completed (when status changed to 'completed')
Implementation: src/routes/requests.ts:235
DELETE /requests/:id
Cancel help request (requester only).
Request:
{
"user_id": "uuid"
}
Events Published: request.cancelled
Implementation: src/routes/requests.ts:316
PUT /requests/:id/privacy
Update privacy settings for a request (Social Karma v2.0).
Request:
{
"user_id": "uuid",
"is_public": true,
"requester_visibility_consent": true
}
Authorization: Only requester can update their request privacy
Events Published: privacy_settings.updated
Implementation: src/routes/requests.ts (Social Karma v2.0)
3.6 Polymorphic Request Type Schemas (v9.0)
This section documents the payload structure for each request type. All payloads are validated using Zod discriminated unions in packages/shared/src/schemas/requests/.
Generic Request
The default request type for simple help requests.
Payload Schema:
{
request_type: 'generic',
payload: {} // Empty object, no type-specific fields
}
Example:
{
"request_type": "generic",
"title": "Need help moving furniture",
"description": "Moving couch upstairs, need 2-3 people",
"urgency": "medium",
"payload": {}
}
Ride Request
For transportation help (rides, carpools, etc.).
Payload Schema:
{
request_type: 'ride',
payload: {
origin: {
address: string, // Human-readable address
lat: number, // Latitude (-90 to 90)
lng: number // Longitude (-180 to 180)
},
destination: {
address: string,
lat: number,
lng: number
},
seats_needed: number, // 1-10
departure_time: string, // ISO 8601 timestamp
preferences?: { // Optional
pet_friendly?: boolean,
luggage_space?: 'small' | 'medium' | 'large',
wheelchair_accessible?: boolean
}
}
}
Example:
{
"request_type": "ride",
"title": "Need ride to airport",
"description": "Flying out tomorrow morning",
"urgency": "medium",
"payload": {
"origin": {
"address": "123 Main St, Seattle, WA",
"lat": 47.6062,
"lng": -122.3321
},
"destination": {
"address": "SEA Airport",
"lat": 47.4502,
"lng": -122.3088
},
"seats_needed": 1,
"departure_time": "2024-06-15T10:00:00Z",
"preferences": {
"pet_friendly": false,
"luggage_space": "medium"
}
}
}
Borrow Request
For borrowing items from community members.
Payload Schema:
{
request_type: 'borrow',
payload: {
item_category: 'tools' | 'electronics' | 'furniture' | 'vehicles' |
'sports_equipment' | 'books' | 'clothing' | 'kitchen' | 'other',
item_description: string,
duration_days: number, // 1-30 days
return_date?: string, // ISO 8601 date (optional)
condition_min?: 'any' | 'good' | 'excellent', // Optional
images?: string[] // Array of image URLs (optional)
}
}
Example:
{
"request_type": "borrow",
"title": "Need ladder for weekend",
"description": "Painting exterior walls, need 6-8ft ladder",
"urgency": "low",
"payload": {
"item_category": "tools",
"item_description": "Extension ladder, 6-8 feet",
"duration_days": 2,
"return_date": "2024-06-17",
"condition_min": "good"
}
}
Service Request
For professional or skilled services.
Payload Schema:
{
request_type: 'service',
payload: {
service_category: 'plumbing' | 'electrical' | 'carpentry' | 'tutoring' |
'tech_support' | 'cleaning' | 'pet_care' | 'childcare' |
'landscaping' | 'photography' | 'legal' | 'financial' | 'other',
skill_level_required: 'beginner' | 'intermediate' | 'expert',
estimated_duration_hours?: number, // Optional
budget_range?: { // Optional
min: number,
max: number,
currency: string // e.g., 'USD'
},
location_type: 'on_site' | 'remote' | 'flexible',
preferred_schedule?: { // Optional
days: string[], // e.g., ['monday', 'tuesday']
times: string[] // e.g., ['morning', 'afternoon']
},
certifications_required?: string[] // Optional
}
}
Example:
{
"request_type": "service",
"title": "Need plumber for leak repair",
"description": "Kitchen pipe leaking, needs professional help",
"urgency": "high",
"payload": {
"service_category": "plumbing",
"skill_level_required": "intermediate",
"estimated_duration_hours": 2,
"budget_range": {
"min": 50,
"max": 100,
"currency": "USD"
},
"location_type": "on_site",
"certifications_required": ["Licensed Plumber"]
}
}
Event Request
For community events needing volunteers or participants.
Payload Schema:
{
request_type: 'event',
payload: {
event_type: 'volunteer' | 'social' | 'educational' | 'fundraiser' | 'meeting' | 'other',
event_date: string, // ISO 8601 timestamp
event_duration_hours?: number, // Optional
participants_needed: number, // 1-1000
location: {
is_virtual: boolean,
address?: string, // Required if not virtual
lat?: number, // Required if not virtual
lng?: number, // Required if not virtual
virtual_link?: string // Required if virtual
},
roles?: Array<{ // Optional
name: string,
count: number,
description: string
}>,
recurring?: { // Optional
frequency: 'daily' | 'weekly' | 'monthly',
end_date: string
}
}
}
Example (Physical Event):
{
"request_type": "event",
"title": "Community Garden Cleanup",
"description": "Monthly garden maintenance day",
"urgency": "low",
"payload": {
"event_type": "volunteer",
"event_date": "2024-06-20T09:00:00Z",
"event_duration_hours": 3,
"participants_needed": 10,
"location": {
"is_virtual": false,
"address": "456 Park Ave, Seattle, WA",
"lat": 47.6097,
"lng": -122.3331
},
"roles": [
{
"name": "Weeding",
"count": 5,
"description": "Help remove weeds"
},
{
"name": "Planting",
"count": 5,
"description": "Plant new flowers"
}
]
}
}
Example (Virtual Event):
{
"request_type": "event",
"title": "Online Tutoring Session",
"description": "Math help for high school students",
"urgency": "medium",
"payload": {
"event_type": "educational",
"event_date": "2024-06-18T18:00:00Z",
"event_duration_hours": 1,
"participants_needed": 2,
"location": {
"is_virtual": true,
"virtual_link": "https://zoom.us/j/123456789"
}
}
}
Validation Rules (All Types)
All polymorphic requests are validated using Zod discriminated unions. The request_type field acts as the discriminator, and TypeScript/Zod ensures the payload structure matches the selected type.
Validation Files:
packages/shared/src/schemas/requests/index.ts- Discriminated unionpackages/shared/src/schemas/requests/generic.ts- Generic schemapackages/shared/src/schemas/requests/ride.ts- Ride schemapackages/shared/src/schemas/requests/borrow.ts- Borrow schemapackages/shared/src/schemas/requests/service.ts- Service schemapackages/shared/src/schemas/requests/event.ts- Event schema
Type Guards:
import { isRideRequest, isBorrowRequest, isServiceRequest,
isEventRequest, isGenericRequest } from '@karmyq/shared/schemas/requests';
// Runtime type narrowing
if (isRideRequest(request)) {
// TypeScript knows request.payload has origin, destination, etc.
const distance = calculateDistance(
request.payload.origin,
request.payload.destination
);
}
Validation Example:
import { validateRequest } from '@karmyq/shared/schemas/requests';
const result = validateRequest({
request_type: 'service',
title: 'Need plumber',
description: 'Leak repair',
payload: {
service_category: 'plumbing',
skill_level_required: 'intermediate',
location_type: 'on_site'
}
});
if (result.success) {
// result.data is fully typed
console.log(result.data.payload.service_category);
} else {
// result.error contains Zod validation errors
console.error(result.error.errors);
}
PATCH /requests/:id/admin-triage
Override request urgency and/or add a community-scoped admin note (Sprint 25).
Auth: Caller must be an active admin or moderator of the community the request belongs to.
Request:
{
"community_id": "uuid",
"urgency": "high",
"note": "Escalated — requester confirmed no transport available"
}
community_id(required) - The community context for the triage actionurgency(optional) - One of'low','medium','high','critical'. Updateshelp_requests.urgencywhen provided.note(optional) - Free-text admin note. Upsertsrequests.request_admin_notes(one note per request per community).
At least one of urgency or note must be provided (400 if neither is present).
Response:
{ "message": "Triage saved" }
Errors:
400— Neitherurgencynornoteprovided403— Caller is not an admin or moderator of the request's community
Implementation: src/routes/requests.ts (Sprint 25)
3.2 Help Offers
GET /offers
Get all help offers with optional filters.
Query Parameters: Same as GET /requests
Implementation: src/routes/offers.ts:8
GET /offers/:id
Get specific offer details.
Implementation: src/routes/offers.ts:60
POST /offers
Create new help offer.
Request:
{
"community_id": "uuid",
"offerer_id": "uuid",
"title": "I can help with moving",
"description": "Available on weekends, have truck",
"type": "moving"
}
Events Published: offer.created
Implementation: src/routes/offers.ts:99
PUT /offers/:id/privacy
Update privacy settings for an offer (Social Karma v2.0).
Implementation: src/routes/offers.ts (Social Karma v2.0)
3.3 Matches
GET /matches
Get all matches with optional filters.
Query Parameters:
request_id- Filter by requestoffer_id- Filter by offerstatus- Filter by status
Implementation: src/routes/matches.ts:8
GET /matches/:id
Get specific match details.
Implementation: src/routes/matches.ts:69
POST /matches
Create a match between request and responder.
Request:
{
"request_id": "uuid",
"offer_id": "uuid",
"responder_id": "uuid"
}
Note: offer_id is optional (direct response without offer)
Validation:
- Request must exist and be 'open'
- Offer must exist and be 'active' (if provided)
- Responder cannot match their own request
Events Published: match.created
Implementation: src/routes/matches.ts:113
PUT /matches/:id/complete
Two-phase match completion. Each party calls this independently; the match
only becomes completed and karma fires when both parties have confirmed.
Request:
{
"user_id": "uuid"
}
Response:
{
"success": true,
"data": {
"fully_completed": false,
"waiting_for": "helper"
},
"message": "Your completion recorded — waiting for the other party"
}
Authorization: Only requester or responder can complete
Side Effects (first party only): Sets requester_done_at or responder_done_at timestamp
Side Effects (both parties): Sets status = 'completed', updates request status, fires match_completed event
Events Published: match_completed (only when both parties have confirmed)
Implementation: src/routes/matches.ts
Schema changes (migration 017): Added requester_done_at TIMESTAMP and responder_done_at TIMESTAMP to requests.matches
3.4 Interaction Feedback (Social Karma v2.0)
POST /matches/:id/feedback
Submit interaction feedback for a completed match.
Request:
{
"from_user_id": "uuid",
"helpfulness": 5,
"responsiveness": 4,
"clarity": 5,
"comment": "Great communication, very helpful exchange!",
"allow_featuring": true
}
Validation:
- Match must be completed
from_user_idmust be requester or responder- Can only submit feedback once per match
- All ratings must be 1-5
Two-Way Consent Logic:
When both parties submit feedback with allow_featuring = true:
- Check
requester_visibility_consentandresponder_visibility_consent - If both true: Names visible in featured story
- If either false: Anonymous story only
- Update
matches.requester_visibleandmatches.responder_visible
Events Published: interaction_feedback.submitted
Implementation: src/routes/feedback.ts
GET /matches/:id/feedback
Get feedback for a match.
Authorization: Only requester or responder can view
Implementation: src/routes/feedback.ts
3.5 Health Check
GET /providers
List all provider profiles. Optional query param: service_type.
GET /providers/my
Get the authenticated user's own provider profiles. Auth required.
GET /providers/:id
Get a single provider profile by ID, including ride details if applicable.
POST /providers
Create a provider profile for the authenticated user.
PUT /providers/:id
Update a provider profile. Owner only.
DELETE /providers/:id
Delete a provider profile. Owner only.
GET /collectives
List all provider collectives. Optional query param: service_type.
GET /collectives/my
Get collectives the authenticated user belongs to (via their provider profiles). Auth required.
GET /collectives/:id
Get a collective with members and communities served.
POST /collectives
Create a new provider collective.
PUT /collectives/:id
Update a collective. Collective admin only.
DELETE /collectives/:id
Delete a collective. Collective admin only.
POST /collectives/:id/members
Join a collective as a member.
DELETE /collectives/:id/members/:providerId
Remove a member from a collective. Collective admin only.
POST /collectives/:id/communities
Link a collective to a community.
DELETE /collectives/:id/communities/:communityId
Unlink a collective from a community. Auth: collective admin OR community admin (Sprint 26).
GET /collectives/:id/stats
Returns aggregate performance stats for a collective: total_requests_matched, fulfillment_rate, avg_completion_hours (null if no completed matches), communities_served_count, available_member_count. Auth required.
PATCH /providers/:providerId/availability
Toggle a provider's availability status. Body: { is_available: boolean }. Auth: owner only (provider_profiles.user_id must match JWT userId). Returns { id, is_available }. (Sprint 26)
GET /health
Service health check.
Response:
{
"service": "request-service",
"status": "healthy",
"timestamp": "2025-01-10T12:00:00Z"
}
4. Events
4.1 Published Events
| Event Name | Queue | Payload | When Emitted |
|---|---|---|---|
request.created | karmyq-events | { request_id, requester_id, community_id, category } | After successful request creation |
request.completed | karmyq-events | { request_id, requester_id, community_id } | When request status changed to 'completed' |
request.cancelled | karmyq-events | { request_id, requester_id, community_id } | When request is cancelled |
offer.created | karmyq-events | { offer_id, offerer_id, community_id, category } | After successful offer creation |
match.created | karmyq-events | { match_id, request_id, offer_id, responder_id } | When request and responder are matched |
match.completed | karmyq-events | { match_id, request_id, responder_id, completed_at } | When match is marked as completed |
interaction_feedback.submitted | karmyq-events | { feedback_id, match_id, from_user_id, to_user_id, ratings } | When user submits feedback (Social Karma v2.0) |
privacy_settings.updated | karmyq-events | { entity_type, entity_id, is_public, visibility_consent } | When request/offer privacy changes (Social Karma v2.0) |
4.2 Consumed Events
None currently. Request Service does not consume events.
Note: In v9.0 (Everything App), this service will consume:
user.verified- To unlock premium request types (rides, services)
4.3 Event Publishing Pattern
// src/routes/requests.ts
import { publishEvent } from '../events/publisher';
// After successful request creation
await publishEvent('request.created', {
request_id: newRequest.id,
requester_id: req.user.id,
community_id: req.community.id,
category: newRequest.category
});
5. Key Patterns
5.1 Authentication & Authorization Flow
Standard Auth Pattern:
// All routes protected with auth middleware
router.post('/requests',
authenticateToken, // Verify JWT
extractCommunityContext, // Set req.community
requireRole('member'), // Check minimum role
async (req, res) => { ... }
);
Membership Verification:
// Verify user is active community member before allowing post
const memberCheck = await db.query(
`SELECT id FROM communities.members
WHERE community_id = $1 AND user_id = $2 AND status = 'active'`,
[community_id, requester_id]
);
if (memberCheck.rowCount === 0) {
return res.status(403).json({
success: false,
message: 'Only community members can post requests'
});
}
Requester-Only Updates:
// Only original requester can update/cancel their request
const requestCheck = await db.query(
`SELECT requester_id FROM requests.help_requests WHERE id = $1`,
[id]
);
if (requestCheck.rows[0].requester_id !== user_id) {
return res.status(403).json({
success: false,
message: 'Only the requester can update this request'
});
}
5.2 Skill-Based Matching Algorithm
Category-to-Skill Mapping:
| Category | Matched Skills |
|---|---|
| transportation | driving |
| moving | moving, handyman |
| childcare | childcare |
| pet_care | pet_care |
| tech_support | tech_support, coding |
| home_repair | home_repair, handyman, electrical, plumbing, carpentry |
| gardening | gardening |
| cooking | cooking, baking |
| tutoring | tutoring |
| language | languages |
| professional_advice | career_advice |
| cleaning | cleaning, organizing |
Matching Query Pattern:
SELECT
r.id, r.title, r.category, r.urgency,
CASE
WHEN r.urgency = 'high' THEN 3
WHEN r.urgency = 'medium' THEN 2
ELSE 1
END as urgency_priority,
c.name as community_name,
u.name as requester_name,
r.created_at
FROM requests.help_requests r
INNER JOIN communities.communities c ON r.community_id = c.id
INNER JOIN auth.users u ON r.requester_id = u.id
WHERE r.status = 'open'
AND r.requester_id != $1 -- Exclude user's own requests
AND EXISTS (
SELECT 1 FROM communities.members m
WHERE m.user_id = $1
AND m.community_id = r.community_id
AND m.status = 'active' -- Only user's communities
)
AND EXISTS (
SELECT 1 FROM auth.user_skills s
WHERE s.user_id = $1
AND (
(r.category = 'moving' AND s.skill IN ('moving', 'handyman'))
OR (r.category = 'tech_support' AND s.skill IN ('tech_support', 'coding'))
-- ... other category mappings
)
)
ORDER BY urgency_priority DESC, r.created_at ASC
LIMIT $2;
5.3 Database Query Pattern (RLS-Aware)
// All queries respect community_id for multi-tenant isolation
const result = await db.query(
`SELECT * FROM requests.help_requests
WHERE community_id = $1 AND status = $2`,
[req.community.id, 'open']
);
5.4 Event Publishing Pattern
// Publish event after successful database operation
const newRequest = await db.query(
'INSERT INTO requests.help_requests (...) VALUES (...) RETURNING *',
[...]
);
// Fire and forget (don't block response)
await publishEvent('request.created', {
request_id: newRequest.rows[0].id,
requester_id: req.user.id,
community_id: req.community.id
});
return res.status(201).json({
success: true,
data: newRequest.rows[0]
});
6. Dependencies
6.1 Upstream Services (This service calls)
- Community Service (via database) - Verify community membership
- Auth Service (via database) - Get user details and skills
6.2 Downstream Services (This service is called by)
- Gateway - All client requests route through gateway
- Frontend (Web) - For browsing/creating requests and offers
- Frontend (Mobile) - Mobile app access
- Feed Service - Reads open requests for personalized feed
6.3 Event Consumers (Who listens to our events)
- Reputation Service - Listens to
match.completed→ Awards karma - Notification Service - Listens to
request.created→ Notifies community - Feed Service - Listens to
request.created→ Updates feed
6.4 Shared Libraries
@karmyq/shared/middleware-authenticateToken,extractCommunityContext,requireRole@karmyq/shared/utils/logger- Structured logging@karmyq/shared/database- PostgreSQL connection utilities@karmyq/shared/schemas/requests- Zod validation schemas for polymorphic requests (v9.0)@karmyq/shared/matching- Match scoring,resolveSourceTier(),DEFAULT_FEED_PREFERENCES, feed scoring utilities (ADR-031)
7. Testing
7.1 Unit Tests
Run Tests:
cd services/request-service
npm test
Test Structure:
src/__tests__/
├── requests.test.ts # Request CRUD and matching
├── offers.test.ts # Offer CRUD
└── matches.test.ts # Match creation and completion
7.2 Integration Tests
Run Integration Tests:
cd tests
npm run test:integration -- integration/request-service.test.ts
Test Scenarios:
- Request lifecycle (create → match → complete)
- Skill-based matching algorithm
- Privacy controls (Social Karma v2.0)
- Event publishing
7.3 Test Fixtures
Test Personas:
tests/fixtures/quick-seed.sql- 7 test personastests/fixtures/large-dataset.sql- 2000 users, realistic data
Mock Data:
// Example test request
const testRequest = {
community_id: 'test-community-uuid',
requester_id: 'test-user-uuid',
title: 'Test: Need help moving',
description: 'Moving couch upstairs',
type: 'moving',
urgency: 'high'
};
7.4 Key Test Scenarios
Generic Requests (v8.0 - Legacy Tests):
- Create generic request successfully
- Reject request with missing required fields
- Only requester can update their request
- User cannot match their own request
- Skill-based matching returns relevant requests
- Privacy settings update correctly
- Two-way consent logic works correctly
Polymorphic Requests (v9.0 - Production):
- Create generic request (backward compatibility) -
tests/integration/polymorphic-requests-lifecycle.test.ts - Create ride request with valid coordinates -
tests/integration/polymorphic-requests-lifecycle.test.ts - Create service request with skill requirements -
tests/integration/polymorphic-requests-lifecycle.test.ts - Create event request (physical + virtual) -
tests/integration/polymorphic-requests-lifecycle.test.ts - Create borrow request with item details -
tests/integration/polymorphic-requests-lifecycle.test.ts - Reject ride request with invalid coordinates -
tests/unit/validation.test.ts - Validate payload against Zod schema -
tests/unit/validation.test.ts - Emit
request.createdwithrequest_typefield -tests/integration/events.test.ts - Multi-community posting with post_to_all_communities -
tests/integration/polymorphic-requests-lifecycle.test.ts
Curated Feed & Matching (v9.0 - Production):
- Calculate match scores for all 5 request types -
tests/unit/curated-feed.test.ts(69 tests) - Filter by user skills and preferences -
tests/integration/curated-feed-preferences.test.ts - Sort by match score descending -
tests/unit/curated-feed.test.ts - Apply minimum match score threshold -
tests/integration/curated-feed-preferences.test.ts - Return match reasons and breakdown -
tests/unit/curated-feed.test.ts - Respect user request type subscriptions -
tests/integration/curated-feed-preferences.test.ts
User Preferences (v9.0 - Production):
- Subscribe/unsubscribe from request types -
tests/integration/curated-feed-preferences.test.ts - Add/remove user interests -
tests/integration/curated-feed-preferences.test.ts - Persist preferences across sessions -
tests/integration/curated-feed-preferences.test.ts - Filter curated feed by preferences -
tests/integration/curated-feed-preferences.test.ts
UX & Smart Defaults (v9.0 - Production):
- Generic type shown by default -
tests/unit/smart-defaults.test.tsx - Type selector collapsible (progressive disclosure) -
tests/unit/smart-defaults.test.tsx - Create generic request in < 3 clicks -
tests/e2e/12-polymorphic-requests-ux.spec.ts - Display match scores on request cards -
tests/e2e/12-polymorphic-requests-ux.spec.ts - Toggle between all requests and curated feed -
tests/e2e/12-polymorphic-requests-ux.spec.ts
Event Publishing:
-
request.createdevent published on creation -
match.completedevent published on completion - Events include all required payload fields
7.5 Manual Testing with curl
Create Request:
curl -X POST http://localhost:3003/requests \
-H "Content-Type: application/json" \
-d '{
"community_id": "uuid-here",
"requester_id": "uuid-here",
"title": "Need help moving couch",
"description": "Heavy couch, need 2-3 people",
"type": "moving",
"urgency": "high"
}'
Get Matched Requests:
curl "http://localhost:3003/requests/matched/for-user?user_id=uuid-here&limit=5"
Complete Match:
curl -X PUT http://localhost:3003/matches/uuid-here \
-H "Content-Type: application/json" \
-d '{
"status": "completed",
"user_id": "uuid-here"
}'
8. Configuration
8.1 Environment Variables
# Server
PORT=3003
NODE_ENV=development # development | production
# Database
DATABASE_URL=postgresql://karmyq_user:password@localhost:5432/karmyq_db
# Redis (for events)
REDIS_URL=redis://localhost:6379
# Logging
LOG_LEVEL=info # debug | info | warn | error
8.2 Feature Flags
Current (v8.0):
- All features enabled by default
Planned (v9.0 - Everything App):
# Feature flags for new verticals
ENABLE_RIDE_REQUESTS=false
ENABLE_BORROW_REQUESTS=false
ENABLE_SERVICE_REQUESTS=false
ENABLE_EVENT_REQUESTS=false
8.3 Database Connection Pool
// src/database/db.ts
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // Maximum connections
idleTimeoutMillis: 30000, // Close idle connections after 30s
connectionTimeoutMillis: 2000
});
9. Monitoring & Observability
9.1 Key Metrics
Request Metrics:
- Total requests created (counter)
- Requests by category (counter with labels)
- Requests by urgency (counter with labels)
- Open requests count (gauge)
Match Metrics:
- Matches created (counter)
- Matches completed (counter)
- Time to first match (histogram)
API Performance:
- Request latency (histogram)
- Error rate (counter)
- Throughput (requests/second)
9.2 Logging
Structured JSON Logging:
import { logger } from '@karmyq/shared/utils/logger';
logger.info('Request created', {
request_id: newRequest.id,
requester_id: req.user.id,
community_id: req.community.id,
category: newRequest.category
});
logger.error('Failed to create request', {
error: err.message,
stack: err.stack,
requester_id: req.user.id
});
Log Levels:
DEBUG- Detailed query logs, matching algorithm stepsINFO- Request creation, match completionWARN- Invalid input, failed validationsERROR- Database errors, event publishing failures
9.3 Health Checks
Endpoint: GET /health
Health Check Logic:
// src/routes/health.ts
router.get('/health', async (req, res) => {
try {
// Check database connection
await db.query('SELECT 1');
// Check Redis connection
await redis.ping();
res.json({
service: 'request-service',
status: 'healthy',
timestamp: new Date().toISOString(),
checks: {
database: 'connected',
redis: 'connected'
}
});
} catch (error) {
res.status(503).json({
service: 'request-service',
status: 'unhealthy',
error: error.message
});
}
});
Monitoring Alerts:
- Database connection failures
- Redis connection failures
- High error rate (>5% of requests)
- High latency (P95 > 500ms)
10. Troubleshooting
10.1 Common Issues
Issue: Skill-based matching returns no results
Symptoms: GET /requests/matched/for-user returns empty array
Diagnosis:
-
Check user has skills:
SELECT * FROM auth.user_skills WHERE user_id = 'uuid-here'; -
Check user is member of communities:
SELECT * FROM communities.members WHERE user_id = 'uuid-here' AND status = 'active'; -
Check requests exist in those communities:
SELECT * FROM requests.help_requests WHERE community_id IN (...) AND status = 'open'; -
Verify category-to-skill mapping matches user's skills
-
Ensure user is not the requester (excluded from results)
Solution:
- Add skills to user:
INSERT INTO auth.user_skills ... - Ensure user joined communities
- Verify skill mapping in Section 5.2
Issue: Request not appearing in list
Symptoms: Request exists but not in GET /requests response
Diagnosis:
-
Check request status:
SELECT status FROM requests.help_requests WHERE id = 'uuid-here'; -
Verify
community_idfilter if applied -
Check pagination (limit/offset)
-
Verify request hasn't been soft-deleted
Solution:
- Use correct status filter
- Increase limit or adjust offset
- Check all status values:
open,matched,completed,cancelled
Issue: Match creation fails
Symptoms: POST /matches returns 400 or 403
Diagnosis:
-
Verify request exists and is 'open':
SELECT id, status FROM requests.help_requests WHERE id = 'uuid-here'; -
If using offer, verify it's 'active':
SELECT id, status FROM requests.help_offers WHERE id = 'uuid-here'; -
Check responder is not requester
-
Look for duplicate match error (unique constraint)
Solution:
- Ensure request is in 'open' status
- Verify offer_id is valid (or omit for direct match)
- Different user_id for responder
Issue: Events not publishing
Symptoms: No events in Redis queue, downstream services not reacting
Diagnosis:
-
Check Redis connection:
docker exec -it karmyq-redis redis-cli PING -
Verify REDIS_URL environment variable
-
Check event publisher initialization in logs
-
Look for try-catch that swallows errors
Solution:
- Restart Redis:
docker-compose restart redis - Update REDIS_URL in
.env - Check publisher initialization:
src/events/publisher.ts
Issue: Database connection errors
Symptoms: 500 errors, "connection pool exhausted"
Diagnosis:
-
Check DATABASE_URL is correct
-
Verify PostgreSQL is running:
docker ps | grep postgres -
Test connection:
psql $DATABASE_URL -
Check requests schema exists:
\dn
Solution:
- Restart PostgreSQL:
docker-compose restart postgres - Verify connection string format
- Run migrations:
psql $DATABASE_URL < infrastructure/postgres/init.sql
10.2 Performance Issues
Issue: Slow skill-based matching queries
Solution:
- Verify indexes exist:
idx_help_requests_community_id,idx_help_requests_category - Add index on
auth.user_skills(user_id, skill)if missing - Consider materialized view for frequently accessed matches
Issue: High memory usage
Solution:
- Check connection pool size (default: 20)
- Verify connections are being released properly
- Monitor with:
docker stats karmyq-request-service
10.3 Recent Changes (v9.0)
Version 9.0.0 - Polymorphic Request System (2026-02-05)
This major release transforms the request system from single-type generic requests to a polymorphic system supporting 5 specialized request types with intelligent feed curation.
Core Features:
-
Polymorphic Request Types (Days 1-5)
- Added
request_typeenum column:generic,ride,service,event,borrow - Added JSONB
payloadcolumn for type-specific data - Added JSONB
requirementscolumn for matching criteria - Implemented Zod discriminated union validation in
@karmyq/shared/schemas/requests - Created type-specific schemas for all 5 request types
- Added type guards for runtime type narrowing
- Added
-
Smart Defaults & Progressive Disclosure (Day 6)
- Default to
genericrequest type (reduces clicks from 3 to 2) - Collapsible type selector with progressive disclosure UX
- Request type examples and "Most Used" badges
- Target: < 3 clicks to create generic request (achieved: 2 clicks)
- Default to
-
Curated Feed with Match Scores (Day 7)
- New endpoint:
GET /requests/curatedwith match score calculation - Type-specific matching algorithms in
@karmyq/shared/matching - Match scores (0-100%) with transparency (reasons + breakdown)
- Skill-based filtering using user profile
- Result: 85% noise reduction (100 requests → 15 relevant)
- New endpoint:
-
User Preferences System (Day 8)
- Request type subscriptions (subscribe/unsubscribe per type)
- Interest-based filtering (service categories, item categories, event types)
- Preference persistence in
auth.user_request_preferencestable - Interest storage in
auth.user_intereststable - Result: 67% additional reduction (15 requests → 5 highly relevant)
- Combined: 95% total noise reduction
-
Multi-Community Posting (Days 1-5)
- New junction table:
requests.request_communities - Support for
post_to_all_communitiesflag - Requests can appear in multiple communities
- New junction table:
-
Comprehensive Testing (Days 9-12)
- 200+ tests covering all features
- Unit tests (69 tests for curated feed algorithm)
- Regression tests (40+ integration tests for polymorphic lifecycle)
- Integration tests (30+ tests for preferences + curated feed)
- E2E tests (25+ Playwright tests for complete user flows)
Database Changes:
-- Migration 009_polymorphic_requests.sql
ALTER TABLE requests.help_requests
ADD COLUMN request_type request_type_enum NOT NULL DEFAULT 'generic',
ADD COLUMN payload JSONB,
ADD COLUMN requirements JSONB;
CREATE TYPE request_type_enum AS ENUM ('generic', 'ride', 'service', 'event', 'borrow');
-- Migration 010_user_request_preferences.sql
CREATE TABLE auth.user_request_preferences (
user_id UUID NOT NULL,
request_type request_type_enum NOT NULL,
subscribed BOOLEAN DEFAULT true,
PRIMARY KEY (user_id, request_type)
);
CREATE TABLE auth.user_interests (
user_id UUID NOT NULL,
interest_type VARCHAR(50) NOT NULL,
interest_value VARCHAR(100) NOT NULL,
PRIMARY KEY (user_id, interest_type, interest_value)
);
API Changes:
- ✅ POST /requests - Now accepts polymorphic payloads with
request_type+payload - ✅ GET /requests/curated - New endpoint for intelligent feed curation
- ✅ Backward compatible - Generic requests work exactly as before
Frontend Changes:
- ✅ Smart defaults with progressive disclosure UX
- ✅ Curated feed toggle with match score slider
- ✅ Match score badges and reason tooltips
- ✅ Preferences page for type subscriptions and interests
Performance Impact:
- Payload column uses JSONB with GIN indexes for fast queries
- Curated feed endpoint optimized with user preference filtering
- Match score calculation in-memory (no additional DB queries per request)
Migration Path:
- All existing requests automatically assigned
request_type = 'generic' payload = {}for backward compatibility- No breaking changes to existing API contracts
10.4 Known Issues
Current Issues (v9.0.0):
-
Location-Based Matching Not Implemented
- Match scores don't yet consider geographic proximity for ride/service requests
- Planned: PostGIS integration for distance-based scoring
- Workaround: Users manually filter by community (implicit location)
-
Event Recurring Patterns Not Fully Implemented
- Event schema includes
recurringfield but no backend logic to create recurring instances - Planned: Scheduled job to generate recurring event requests
- Workaround: Users manually create multiple event requests
- Event schema includes
-
Image Upload for Borrow Requests Not Implemented
- Borrow schema includes
imagesfield but no upload endpoint - Planned: Image upload service integration
- Workaround: Users include image URLs in description
- Borrow schema includes
-
Budget Range Not Enforced in Matching
- Service requests include budget_range but not used in match score calculation
- Planned: Budget-based filtering in curated feed
- Workaround: Users manually review budget in request details
-
Certification Verification Not Automated
- Service requests can require certifications but no automated verification
- Planned: Integration with credential verification service
- Workaround: Manual verification during match acceptance
Resolved Issues:
-
✅ TypeScript Type Narrowing for Polymorphic Payloads (Resolved Day 6)
- Issue: TypeScript couldn't infer payload structure from request_type
- Solution: Implemented type guards (isRideRequest, isServiceRequest, etc.)
- Files:
packages/shared/src/schemas/requests/index.ts
-
✅ Jest Not Finding Regression Tests (Resolved Days 4-5)
- Issue: New regression test directory not included in jest.config.js
- Solution: Updated testMatch pattern to include
tests/regression/**/*.test.ts - Files:
services/request-service/jest.config.js
-
✅ Event Matcher Accessing Undefined Location (Resolved Day 7)
- Issue: Event location.is_virtual check failed when location undefined
- Solution: Added proper null checking and required location field validation
- Files:
packages/shared/src/matching/matchers/event.ts
-
✅ Workspace Alias Imports in Unit Tests (Resolved Day 6)
- Issue:
@karmyq/sharedimports failed in Jest tests - Solution: Changed to relative path imports from workspace root
- Files: All unit test files
- Issue:
Performance Considerations:
- JSONB payload queries are fast with GIN indexes but not as fast as native columns
- Curated feed endpoint performs N+1 matching calculations (optimized with limit parameter)
- Match score calculation is CPU-bound but fast (~0.1ms per request)
- User preference lookup adds ~5ms to curated feed endpoint
11. Future Enhancements
11.1 v9.0 - Polymorphic Request System ✅ COMPLETED (2026-02-05)
- Polymorphic Data Model - Added
request_type,payload,requirementscolumns - Zod Schema Validation - Validate payloads against type-specific schemas
- Ride Requests - Origin/destination coordinates, seats needed, preferences
- Borrow Requests - Item category, condition, duration, return date
- Service Requests - Professional services with skill levels, budget, certifications
- Event Requests - Community events with RSVP, physical + virtual support
- Curated Feed - Match score algorithm with skill-based filtering
- User Preferences - Request type subscriptions and interest-based filtering
- Smart Defaults - Progressive disclosure UX (< 3 clicks to post)
- Multi-Community Posting - Requests visible across multiple communities
- Comprehensive Testing - 200+ tests (unit, regression, integration, E2E)
11.2 v9.1 - Provider Profiles (ADR-041) ✅ COMPLETED (2026-02-27)
New endpoints (see src/routes/providers.ts):
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /providers | Public | Browse active providers, filter by service_type |
| GET | /providers/:id | Public | Get single provider with ride details + trust score |
| POST | /providers | Required | Create provider profile (+ ride details if service_type=ride) |
| PUT | /providers/:id | Owner | Update profile or ride details |
| DELETE | /providers/:id | Owner | Delete profile (cascades reviews/trust scores) |
New tables (migration 022):
requests.provider_profiles— generic base (Sprint 26:is_available BOOLEAN DEFAULT FALSEadded via migration 20260314)requests.provider_ride_details— ride-specific extensionreputation.provider_reviews— stars + text, tied to match_idreputation.provider_trust_scores— computed cache (ADR-042)
Community config additions:
provider_services_enabled— opt-in per communityprovider_min_personal_trust_score— gate by ADR-037 trustprovider_services_list— allowed service types
11.4 Matching Engine Enhancements
- Auto-matching algorithm (suggest best helpers)
- Location-based matching (PostGIS integration)
- Skill proficiency levels (beginner, intermediate, expert)
- Multi-helper requests (request needs 3 people)
11.3 Request Lifecycle
- Request expiration (auto-cancel old requests)
- Request templates (common request types)
- Recurring requests (weekly/monthly help)
- Request attachments/images
11.4 Quality & Trust
- Helper ratings aggregation
- Verified helper badges
- Request categories requiring verification (e.g., childcare)
12. Related Documentation
12.1 Architecture Documentation
12.2 Database Documentation
- Database Schema - Lines 91-144 (requests schema)
- Polymorphic Migration - v9.0
12.3 Testing Documentation
12.4 Development Guides
12.5 API Documentation
- API Gateway endpoints (TBD - v9.0)
- Swagger/OpenAPI spec (TBD - v9.0)
Appendix A: Request Categories Reference
Complete category-to-skill mapping for skill-based matching:
// src/services/matcher.ts
export const CATEGORY_SKILL_MAP = {
transportation: ['driving'],
moving: ['moving', 'handyman'],
childcare: ['childcare'],
pet_care: ['pet_care'],
tech_support: ['tech_support', 'coding'],
home_repair: ['home_repair', 'handyman', 'electrical', 'plumbing', 'carpentry'],
gardening: ['gardening'],
cooking: ['cooking', 'baking'],
tutoring: ['tutoring'],
language: ['languages'],
professional_advice: ['career_advice'],
cleaning: ['cleaning', 'organizing']
};
Appendix B: Development Tasks Reference
Add New Request Category
- Add to skill mapping in
src/routes/requests.ts - Update CATEGORY_SKILL_MAP in Appendix A
- Update documentation
Add New Request Field
- Create migration:
ALTER TABLE requests.help_requests ADD COLUMN ... - Update POST endpoint to accept field
- Update GET endpoints to return field
- Update tests
Change Urgency Levels
Update urgency priority calculation in skill matching query (see Section 5.2)
End of Request Service Context Documentation
This document is the gold standard for service documentation. All other services should follow this structure.
Admin Schema API (Server-Driven UI - Phase 2)
Last Updated: 2026-02-17
Purpose: Enable non-technical admins to create and manage request type schemas without code deployments.
API Endpoints: All endpoints require admin role authentication (super_admin or admin).
Schema Management
GET /admin/schemas- List all schemas (supports status, type, pagination filtering)GET /admin/schemas/:id- Get specific schema by ID (includes version history)POST /admin/schemas- Create new schema with type, sections, metadataPUT /admin/schemas/:id- Update schema (increments version automatically)POST /admin/schemas/:id/publish- Publish draft schema (validates structure: sections array, non-empty field keys/labels/types, valid summary field refs; returns 400 witherrors[]array on failure)POST /admin/schemas/:id/archive- Archive schema (hide from users)GET /admin/schemas/:id/versions- Get version history for rollbackPOST /admin/schemas/:id/rollback/:version- Rollback to specific versionPOST /admin/schemas/:id/variants- Create A/B test variantPOST /schemas/:type/validate- Validate schema payload (for testing)
Data Models:
- Uses
requests.ui_schemastable for schema storage - Uses
requests.ui_schema_versionsfor version history - JSON Schema validation in
requests.validation_rulesfor custom types
Frontend Integration:
/admin/schemas- Schema list with filtering/admin/schemas/new- Schema creation with form builder/admin/schemas/[id]/edit- Full editor with tabs (Sections/Fields/Preview/Versions)/admin/schemas/[id]/versions- Version timeline with rollback- Admin authentication middleware (super_admin/admin check)
Usage Example:
// Create new schema
const newSchema = {
type: 'dogwalking',
label: 'Dog Walking Request',
icon: '🐕',
color: '#f59e0b',
description: 'Help with walking your dog',
sections: [
{
id: 'section-1',
title: 'Dog Details',
fields: [
{ id: 'field-1', type: 'text', label: 'Dog Breed', required: true },
{ id: 'field-2', type: 'number', label: 'Duration (hours)', required: true }
]
}
]
}
await uiSchemaService.createSchema(newSchema)