Docs·4ff474d·Updated Mar 14, 2026·43 ADRs
All Services

Request Service

Port 3003productioncritical

22

API Endpoints

2

Service Deps

3

Infrastructure

1

DB Schemas

API Endpoints

GET
/requests

Get all help requests with optional filters.

GET
/requests/matched/for-user

Get requests matching user's skills from their communities (skill-based matching algorithm).

GET
/requests/curated (v9.0 + ADR-031 Multi-Tier)

Get curated feed with match scores, trust distance, and multi-tier visibility.

GET
/requests/:id

Get specific request details.

POST
/requests (v9.0 Polymorphic + ADR-022 Visibility)

Create new polymorphic help request (supports 5 types) with visibility scope.

PUT
/requests/:id

Update help request (requester only).

DELETE
/requests/:id

Cancel help request (requester only).

PUT
/requests/:id/privacy

Update privacy settings for a request (Social Karma v2.0).

PATCH
/requests/:id/admin-triage

Override request urgency and/or add a community-scoped admin note (Sprint 25).

GET
/offers

Get all help offers with optional filters.

GET
/offers/:id

Get specific offer details.

POST
/offers

Create new help offer.

PUT
/offers/:id/privacy

Update privacy settings for an offer (Social Karma v2.0).

GET
/matches

Get all matches with optional filters.

GET
/matches/:id

Get specific match details.

POST
/matches

Create a match between request and responder.

PUT
/matches/:id/complete

Two-phase match completion. Each party calls this independently; the match

POST
/matches/:id/feedback

Submit interaction feedback for a completed match.

GET
/matches/:id/feedback

Get feedback for a match.

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_membe

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.

Infrastructure

postgresredisbull-queue

Service Dependencies

Publishes Events

match_completedrequest_createdoffer_created

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 names
  • auth.user_skills - User skills for skill-based matching
  • auth.user_feed_preferences - Feed visibility preferences (ADR-022)
  • auth.social_distances - Trust distance between users (ADR-031)
  • communities.communities - Community names, details, and default_request_scope
  • communities.members - Verify community membership
  • communities.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 community
  • status (string) - Filter by status (default: 'open')
  • type (string) - Filter by category
  • limit (number) - Max results (default: 50)
  • offset (number) - Pagination offset (default: 0)
  • include_admin_notes (boolean) - When true and community_id is also provided, each request in the response includes an admin_note field scoped to that community (sourced from requests.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 for
  • limit (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 where show_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):

  1. Fetch user feed preferences from auth.user_feed_preferences
  2. Fetch user preferences (subscribed request types from auth.user_request_preferences)
  3. Fetch user skills from auth.user_skills
  4. 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)
  5. Batch-fetch trust distances from auth.social_distances and karma from reputation.karma_records
  6. Resolve source tier per request using resolveSourceTier() from @karmyq/shared/matching
  7. Calculate feed scores using community-configurable weights (ADR-031)
  8. Sort by tier priority (community > trust_network > platform), then by feedScore
  9. 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's default_request_scope if 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 union
  • packages/shared/src/schemas/requests/generic.ts - Generic schema
  • packages/shared/src/schemas/requests/ride.ts - Ride schema
  • packages/shared/src/schemas/requests/borrow.ts - Borrow schema
  • packages/shared/src/schemas/requests/service.ts - Service schema
  • packages/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 action
  • urgency (optional) - One of 'low', 'medium', 'high', 'critical'. Updates help_requests.urgency when provided.
  • note (optional) - Free-text admin note. Upserts requests.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 — Neither urgency nor note provided
  • 403 — 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 request
  • offer_id - Filter by offer
  • status - 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_id must 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:

  1. Check requester_visibility_consent and responder_visibility_consent
  2. If both true: Names visible in featured story
  3. If either false: Anonymous story only
  4. Update matches.requester_visible and matches.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 NameQueuePayloadWhen Emitted
request.createdkarmyq-events{ request_id, requester_id, community_id, category }After successful request creation
request.completedkarmyq-events{ request_id, requester_id, community_id }When request status changed to 'completed'
request.cancelledkarmyq-events{ request_id, requester_id, community_id }When request is cancelled
offer.createdkarmyq-events{ offer_id, offerer_id, community_id, category }After successful offer creation
match.createdkarmyq-events{ match_id, request_id, offer_id, responder_id }When request and responder are matched
match.completedkarmyq-events{ match_id, request_id, responder_id, completed_at }When match is marked as completed
interaction_feedback.submittedkarmyq-events{ feedback_id, match_id, from_user_id, to_user_id, ratings }When user submits feedback (Social Karma v2.0)
privacy_settings.updatedkarmyq-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:

CategoryMatched Skills
transportationdriving
movingmoving, handyman
childcarechildcare
pet_carepet_care
tech_supporttech_support, coding
home_repairhome_repair, handyman, electrical, plumbing, carpentry
gardeninggardening
cookingcooking, baking
tutoringtutoring
languagelanguages
professional_advicecareer_advice
cleaningcleaning, 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 personas
  • tests/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.created with request_type field - 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.created event published on creation
  • match.completed event 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 steps
  • INFO - Request creation, match completion
  • WARN - Invalid input, failed validations
  • ERROR - 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:

  1. Check user has skills:

    SELECT * FROM auth.user_skills WHERE user_id = 'uuid-here';
    
  2. Check user is member of communities:

    SELECT * FROM communities.members
    WHERE user_id = 'uuid-here' AND status = 'active';
    
  3. Check requests exist in those communities:

    SELECT * FROM requests.help_requests
    WHERE community_id IN (...) AND status = 'open';
    
  4. Verify category-to-skill mapping matches user's skills

  5. 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:

  1. Check request status:

    SELECT status FROM requests.help_requests WHERE id = 'uuid-here';
    
  2. Verify community_id filter if applied

  3. Check pagination (limit/offset)

  4. 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:

  1. Verify request exists and is 'open':

    SELECT id, status FROM requests.help_requests WHERE id = 'uuid-here';
    
  2. If using offer, verify it's 'active':

    SELECT id, status FROM requests.help_offers WHERE id = 'uuid-here';
    
  3. Check responder is not requester

  4. 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:

  1. Check Redis connection:

    docker exec -it karmyq-redis redis-cli PING
    
  2. Verify REDIS_URL environment variable

  3. Check event publisher initialization in logs

  4. 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:

  1. Check DATABASE_URL is correct

  2. Verify PostgreSQL is running:

    docker ps | grep postgres
    
  3. Test connection:

    psql $DATABASE_URL
    
  4. 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:

  1. Polymorphic Request Types (Days 1-5)

    • Added request_type enum column: generic, ride, service, event, borrow
    • Added JSONB payload column for type-specific data
    • Added JSONB requirements column 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
  2. Smart Defaults & Progressive Disclosure (Day 6)

    • Default to generic request 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)
  3. Curated Feed with Match Scores (Day 7)

    • New endpoint: GET /requests/curated with 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)
  4. 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_preferences table
    • Interest storage in auth.user_interests table
    • Result: 67% additional reduction (15 requests → 5 highly relevant)
    • Combined: 95% total noise reduction
  5. Multi-Community Posting (Days 1-5)

    • New junction table: requests.request_communities
    • Support for post_to_all_communities flag
    • Requests can appear in multiple communities
  6. 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):

  1. 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)
  2. Event Recurring Patterns Not Fully Implemented

    • Event schema includes recurring field but no backend logic to create recurring instances
    • Planned: Scheduled job to generate recurring event requests
    • Workaround: Users manually create multiple event requests
  3. Image Upload for Borrow Requests Not Implemented

    • Borrow schema includes images field but no upload endpoint
    • Planned: Image upload service integration
    • Workaround: Users include image URLs in description
  4. 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
  5. 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:

  1. 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
  2. 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
  3. 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
  4. Workspace Alias Imports in Unit Tests (Resolved Day 6)

    • Issue: @karmyq/shared imports failed in Jest tests
    • Solution: Changed to relative path imports from workspace root
    • Files: All unit test files

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, requirements columns
  • 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):

MethodPathAuthDescription
GET/providersPublicBrowse active providers, filter by service_type
GET/providers/:idPublicGet single provider with ride details + trust score
POST/providersRequiredCreate provider profile (+ ride details if service_type=ride)
PUT/providers/:idOwnerUpdate profile or ride details
DELETE/providers/:idOwnerDelete profile (cascades reviews/trust scores)

New tables (migration 022):

  • requests.provider_profiles — generic base (Sprint 26: is_available BOOLEAN DEFAULT FALSE added via migration 20260314)
  • requests.provider_ride_details — ride-specific extension
  • reputation.provider_reviews — stars + text, tied to match_id
  • reputation.provider_trust_scores — computed cache (ADR-042)

Community config additions:

  • provider_services_enabled — opt-in per community
  • provider_min_personal_trust_score — gate by ADR-037 trust
  • provider_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

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

  1. Add to skill mapping in src/routes/requests.ts
  2. Update CATEGORY_SKILL_MAP in Appendix A
  3. Update documentation

Add New Request Field

  1. Create migration: ALTER TABLE requests.help_requests ADD COLUMN ...
  2. Update POST endpoint to accept field
  3. Update GET endpoints to return field
  4. 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, metadata
    • PUT /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 with errors[] array on failure)
    • POST /admin/schemas/:id/archive - Archive schema (hide from users)
    • GET /admin/schemas/:id/versions - Get version history for rollback
    • POST /admin/schemas/:id/rollback/:version - Rollback to specific version
    • POST /admin/schemas/:id/variants - Create A/B test variant
    • POST /schemas/:type/validate - Validate schema payload (for testing)

Data Models:

  • Uses requests.ui_schemas table for schema storage
  • Uses requests.ui_schema_versions for version history
  • JSON Schema validation in requests.validation_rules for custom types

Frontend Integration:

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)