···641641VALUES ('test-sub', <user_id>, 'test-customer', 'active');
642642```
643643644644+## Transcription Service Integration (Murmur)
645645+646646+The application uses [Murmur](https://github.com/taciturnaxolotl/murmur) as the transcription backend.
647647+648648+**Murmur API endpoints:**
649649+- `POST /transcribe` - Upload audio file and create transcription job
650650+- `GET /transcribe/:job_id` - Get job status and transcript (supports `?format=json|vtt`)
651651+- `GET /transcribe/:job_id/stream` - Stream real-time progress via Server-Sent Events
652652+- `GET /jobs` - List all jobs (newest first)
653653+- `DELETE /transcribe/:job_id` - Delete a job from Murmur's database
654654+655655+**Job synchronization:**
656656+The `TranscriptionService` runs periodic syncs to reconcile state between our database and Murmur:
657657+- Reconnects to active jobs on server restart
658658+- Syncs status updates for processing/transcribing jobs
659659+- Handles completed jobs (fetches VTT, cleans transcript, saves to storage)
660660+- **Cleans up finished jobs** - After successful completion or failure, jobs are deleted from Murmur
661661+- **Cleans up orphaned jobs** - Jobs found in Murmur but not in our database are automatically deleted
662662+663663+**Job cleanup:**
664664+- **Completed jobs**: After fetching transcript and saving to storage, the job is deleted from Murmur
665665+- **Failed jobs**: After recording the error in our database, the job is deleted from Murmur
666666+- **Orphaned jobs**: Jobs in Murmur but not in our database are deleted on discovery
667667+- All deletions use `DELETE /transcribe/:job_id`
668668+- This prevents Murmur's database from accumulating stale jobs (Murmur doesn't have automatic cleanup)
669669+- Logs success/failure of deletion attempts for monitoring
670670+671671+**Job lifecycle:**
672672+1. User uploads audio → creates transcription in our DB with `status='uploading'`
673673+2. Audio uploaded to Murmur → get `whisper_job_id`, update to `status='processing'`
674674+3. Murmur transcribes → stream progress updates, update to `status='transcribing'`
675675+4. Job completes → fetch VTT, clean with LLM, save transcript, update to `status='completed'`, **delete from Murmur**
676676+5. If job fails in Murmur → update to `status='failed'` with error message, **delete from Murmur**
677677+678678+**Configuration:**
679679+Set `WHISPER_SERVICE_URL` in `.env` (default: `http://localhost:8000`)
680680+644681## Future Additions
645682646683As the codebase grows, document:
647684- Database schema and migrations
648685- API endpoint patterns
649686- Authentication/authorization approach
650650-- Transcription service integration details
651687- Deployment process
652688- Environment variables needed
653689
+29-4
src/lib/transcription.ts
···500500 }
501501 }
502502503503+ private async deleteWhisperJob(jobId: string) {
504504+ try {
505505+ const response = await fetch(
506506+ `${this.serviceUrl}/transcribe/${jobId}`,
507507+ {
508508+ method: "DELETE",
509509+ },
510510+ );
511511+ if (response.ok) {
512512+ console.log(`[Cleanup] Deleted job ${jobId} from Murmur`);
513513+ } else {
514514+ console.warn(
515515+ `[Cleanup] Failed to delete job ${jobId}: ${response.status}`,
516516+ );
517517+ }
518518+ } catch (error) {
519519+ console.error(`[Cleanup] Error deleting job ${jobId}:`, error);
520520+ }
521521+ }
522522+503523 private async handleOrphanedWhisperJob(jobId: string) {
504524 // Check if this Murmur job_id exists in our DB (either as id or whisper_job_id)
505525 const jobExists = this.db
···509529 .get(jobId, jobId);
510530511531 if (!jobExists) {
512512- // Not our job - Murmur will keep it until explicitly deleted
532532+ // Not our job - delete it from Murmur
513533 console.warn(
514514- `[Sync] Found orphaned job ${jobId} in Murmur (not in our DB)`,
534534+ `[Sync] Found orphaned job ${jobId} in Murmur (not in our DB) - deleting...`,
515535 );
536536+ await this.deleteWhisperJob(jobId);
516537 }
517538 }
518539···564585 status: "completed",
565586 progress: 100,
566587 });
588588+589589+ // Clean up job from Murmur after successful completion
590590+ await this.deleteWhisperJob(whisperJob.id);
567591 } else if (details.status === "failed") {
568592 const errorMessage = (
569593 details.error_message ?? "Transcription failed"
···579603 progress: 0,
580604 error_message: errorMessage,
581605 });
606606+607607+ // Clean up failed job from Murmur
608608+ await this.deleteWhisperJob(whisperJob.id);
582609 }
583583-584584- // Job persists in Murmur until explicitly deleted - we just sync state
585610 } catch {
586611 console.warn(
587612 `[Sync] Failed to retrieve details for job ${whisperJob.id}`,