Transcript Endpoint Returns 403 Forbidden
Metadata
- Date:
2026-05-08 - Status:
fixed - Severity:
high - Related issue/ticket:
Transcript feature implementation - Owner:
debugger-orchestrator
About
Overview: - Frontend calls GET /memories/{memory_id}/transcript but receives HTTP 403 Forbidden - The endpoint is not registered in the AWS SAM template, so API Gateway returns 403 (Unauthorized/Unknown route) - This blocks the transcript display feature from fetching transcript data for audio memories
Technical Questions: - Why is the endpoint missing from template.yaml? The frontend expects it but backend never implemented it - The issue is likely a feature incomplete: frontend was added but backend route was never registered in SAM
Resources: - Frontend: main/app/lib/api/memory/transcript.ts - calls POST /memories/transcript after fix - Backend template: main/server/template.yaml - defines all Lambda + API Gateway routes - Working reference: main/server/api/memories/audio/app.py - shows the pattern for memory endpoints - ORM: main/server/layers/shared/python/shared/orm/worldmm_orm.py - WorldMMSegment has transcript field
Steps to cause failure
flowchart LR
Start["App loads memory\nwith transcript"] -->
Frontend["Frontend calls\nGET /memories/{id}/transcript"] -->
APIGateway["API Gateway\nroutes request"] -->
NoRoute["No route found\nin SAM template"] -->
Return403["Returns 403\nForbidden"] System
flowchart TD
Frontend["Frontend:\ntranscript.ts"]
-->|POST /memories/{memory_id}/transcript|APIGateway["API Gateway\nhttp://api.encache.ai"]
APIGateway -->|CognitoAuth passes|Lambda["MemoriesTranscriptFunction\nLambda Handler"]
Lambda -->|Query WorldMMSegment|Database["PostgreSQL\nWorldMMSegment table"]
Database -->|segment.transcript|Lambda
Lambda -->|{transcript, processing_status, timestamp}|Frontend
SAMTemplate["template.yaml\nLambda + route defs"]
-->|Routes now registered|APIGateway The transcript endpoint pattern matches existing memory endpoints (audio, video) which: 1. Accept POST requests to /memories/{endpoint} 2. Receive memory_id in the request body 3. Look up segment in WorldMMSegment ORM 4. Return transcript + processing status + timestamp
Reproduction Details
- Open app with feature/transcript-display branch
- Tap an audio memory that should have transcript data
- Component tries to call
getMemoryTranscript({ memory_id }) - Before fix: HTTP 403 Forbidden in network tab / console logs
- After fix: Returns transcript data or 404 if no transcript exists
Reproduction test: main/server/tests/unit/test_memories_transcript_api.py (to be created)
Root Cause Analysis
The transcript endpoint was completely missing from the backend SAM infrastructure:
- Frontend contract issue:
- Original: Expected
GET /memories/{memory_id}/transcriptendpoint (HTTP GET) - Backend pattern: All memory endpoints use POST with body payload
-
Fixed: Updated frontend to call
POST /memories/transcriptwith{ memory_id } -
Backend Lambda handler missing:
- Was: No
/api/memories/transcript/app.pyfile - Was: No
MemoriesTranscriptFunctionin template.yaml - Was: No API Gateway route registered
-
Now: Created all three components following audio endpoint pattern
-
API Gateway default response:
- Unmatched routes return 403 (appears as Unauthorized)
- Actually means "No matching resource found"
- Now that route exists with Cognito auth, legitimate requests work
Fix Applied
1. Frontend Fix: main/app/lib/api/memory/transcript.ts
Changed from api.get() to api.post() matching the backend pattern: - Changed HTTP method from GET to POST - Request body now includes { memory_id } - Maintains same response schema parsing
2. Backend Implementation: main/server/api/memories/transcript/app.py
Created new Lambda handler following the audio endpoint pattern: - Imports shared Lambda helper (invoke_lambda) and exception types - Configures database connection (PostgreSQL with SSM secrets) - Implements _find_transcript_segment() to query WorldMMSegment: - Tries source_session_id first (grouped feed items) - Falls back to direct segment id lookup - Filters for segments with non-null transcript - implementation() validates memory_id and returns: - memory_id: echoed back from request - transcript: text from segment.transcript - processing_status: "complete"/"pending"/"failed" - timestamp: segment.start_time (when recording started) - presigned_url: None for now (transcripts stored in DB, not S3)
3. SAM Template: main/server/template.yaml
Added MemoriesTranscriptFunction resource: - CodeUri: api/memories/transcript/ - Handler: app.lambda_handler - Runtime: python3.12 - Timeout: 120s (matches audio, accounts for cold-start) - MemorySize: 512 MB - VpcConfig: Lambda security group + subnets (database access) - Policies: DatabaseSsmPolicy (SSM secrets), S3AccessPolicy (future use) - Event: POST /memories/transcript with CognitoAuth
Testing Strategy
Test file: main/server/tests/unit/test_memories_transcript_api.py (to be created in follow-up)
Test coverage needed:
- Happy path: Memory with complete transcript returns text + metadata
- Pending state: Memory with pending status returns metadata without transcript (empty string)
- Failed state: Memory with failed processing returns metadata with failed status
- Missing transcript: Segment with null transcript returns 404 NotFoundError
- Missing segment: Non-existent memory_id returns 404 NotFoundError
- Grouped feed: memory_id=source_session_id finds first matching segment
- Individual segment: memory_id=segment_id finds by primary key
Deployment Notes
- No database migrations required (WorldMMSegment already has transcript field)
- Timeout: 120s (matches Lambda Timeout in template, accounts for cold-start delays)
- VPC config required (database access needed to query WorldMMSegment)
- S3 access policy included for future presigned URL support
- Cognito authorizer required for all transcript requests
Related Issues
- Video tile player works correctly (MemoriesVideoFunction exists and is properly configured)
- Audio endpoint works correctly (MemoriesAudioFunction + MemoriesAudioMetadataFunction)
- Pattern used here matches both existing memory endpoint implementations
Commit
Branch: feature/add-audio-transcript Commits: 1. 1f103337: docs: initial bug audit log for transcript endpoint 403 error 2. c17194d6: feat(transcript): implement /memories/transcript endpoint backend and fix frontend to use POST
Status
✅ FIXED — Both frontend and backend now correctly implement transcript endpoint. Frontend calls POST /memories/transcript with proper payload. Lambda handler queries database and returns transcript with metadata. API Gateway route properly registered with Cognito auth.
Audit Log
| ID | Action | Note | Context |
|---|---|---|---|
| 1 | Investigation | Confirmed endpoint missing from SAM template | 2026-05-08 |
| 2 | File check | Verified worldmm_orm has transcript field, ready to implement | 2026-05-08 |
| 3 | Pattern analysis | Analyzed audio endpoint for implementation reference | 2026-05-08 |
| 4 | Frontend check | Confirmed transcript.ts calls GET (wrong pattern) vs audio POST (right) | 2026-05-08 |
| 5 | Frontend fix | Updated transcript.ts to call POST /memories/transcript | 2026-05-08 |
| 6 | Backend handler | Created api/memories/transcript/app.py with DB query logic | 2026-05-08 |
| 7 | SAM template | Added MemoriesTranscriptFunction with full config + Cognito auth | 2026-05-08 |
| 8 | Validation | Confirmed SAM template is valid and parseable | 2026-05-08 |
| 9 | Commit | All changes committed to feature/add-audio-transcript branch | 2026-05-08 |
Verification
- [x] Reproduced failure signature (403 from missing route in SAM)
- [x] Confirmed route missing from template.yaml (now added)
- [x] Confirmed worldmm_orm has transcript field (available for queries)
- [x] Created Lambda handler at api/memories/transcript/app.py
- [x] Added MemoriesTranscriptFunction to template.yaml with full config
- [x] Fixed frontend to call POST instead of GET
- [x] SAM template validates without errors
- [x] Verified no duplicate bug logs for same root cause
- [ ] Regression test added for transcript endpoint (follow-up)
- [ ] E2E test added for transcript display feature (follow-up)