Full migration plan for both NestJS services (backend + audio-backend) to Hono/Bun, including endpoint inventory, auth pattern change (Supabase RLS → service role), and phased implementation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
17 KiB
Memoro Backend Migration: NestJS → Hono/Bun
Status: Planned (not started) Priority: High — required before Memoro can be reactivated in production Scope: Two NestJS services → two Hono/Bun servers (keep Supabase DB + Gemini/Azure as-is)
Overview
Memoro currently has two NestJS microservices:
| Service | Path | Port | Responsibility |
|---|---|---|---|
backend |
apps/memoro/apps/backend/ |
3001 | Business logic, AI, credits, spaces |
audio-backend |
apps/memoro/apps/audio-backend/ |
1337 | Audio format conversion, Azure transcription |
Migration target: two Hono/Bun servers following the standard manacore pattern.
| New Service | Path | Port | Replaces |
|---|---|---|---|
server |
apps/memoro/apps/server/ |
3015 | backend/ |
audio-server |
apps/memoro/apps/audio-server/ |
3016 | audio-backend/ |
Key Architectural Decisions
1. Auth: mana-auth JWT (not Supabase middleware)
New servers use authMiddleware() from @manacore/shared-hono. This validates
the JWT from mana-auth and injects userId into context.
Consequence: Supabase DB operations must switch from user-scoped JWTs (RLS) to
service-role key + explicit user_id filter. All queries that relied on Supabase
RLS auto-filtering by JWT sub claim need eq('user_id', userId) added explicitly.
2. Auth Proxy: Remove
The /auth/* proxy endpoints (signin, signup, google, apple, refresh, etc.) are
removed. The web app already calls mana-auth directly. Mobile migration is a separate
concern — handle in the mobile migration plan.
3. Supabase: Keep as-is
Database stays on Supabase (no Drizzle migration). Only the auth mechanism changes (service role key + manual user_id filtering instead of per-user JWT).
4. External Services: Unchanged
- Google Gemini API → keep
- Azure Speech Service → keep
- Azure Blob Storage → keep
- Supabase Storage → keep (audio files)
5. Credits: Call mana-credits directly
Replace calls to mana-core-middleware /credits/* with direct calls to the
mana-credits service. Endpoints map 1:1.
6. Cleanup: Replace GCP Cloud Scheduler → mana-notify cron
The audio cleanup cron (currently triggered by GCP Cloud Scheduler) can be triggered by an internal cron endpoint called by our existing infrastructure or by a scheduled remote agent.
Complete Endpoint Inventory
server/ — All Routes
POST /api/v1/memos/process (was /memoro/process-uploaded-audio)
Receives file path after direct-upload to Supabase Storage. Creates memo record,
triggers async transcription via audio-server.
POST /api/v1/memos/:id/append (was /memoro/append-transcription)
Appends additional audio recording to existing memo.
POST /api/v1/memos/:id/retry-transcription (was /memoro/retry-transcription)
Retry failed transcription.
POST /api/v1/memos/:id/retry-headline (was /memoro/retry-headline)
Retry failed headline generation.
POST /api/v1/memos/:id/reprocess (was /memoro/reprocess-memo)
Reprocess memo with different parameters (e.g. different blueprint).
GET /api/v1/memos/find-by-job/:jobId (was /memoro/find-memo-by-job/:jobId)
Lookup memo by Azure batch job ID (used for webhook recovery).
POST /api/v1/memos/combine (was /memoro/combine-memos)
AI-powered memo combination.
POST /api/v1/memos/:id/question (was /memoro/question-memo)
Ask a question about a memo's transcript.
POST /api/v1/batch-metadata (was /memoro/update-batch-metadata)
Update Azure batch job metadata in memo (called by audio-server callback).
GET /api/v1/spaces (was /memoro/spaces)
List user's spaces.
POST /api/v1/spaces (was /memoro/spaces)
Create space.
GET /api/v1/spaces/:id (was /memoro/spaces/:id)
Get space details.
DELETE /api/v1/spaces/:id (was /memoro/spaces/:id)
Delete space.
POST /api/v1/spaces/:id/memos/link (was /memoro/link-memo)
Link memo to space.
POST /api/v1/spaces/:id/memos/unlink (was /memoro/unlink-memo)
Unlink memo from space.
GET /api/v1/spaces/:id/memos (was /memoro/spaces/:id/memos)
Get all memos in a space.
POST /api/v1/spaces/:id/leave (was /memoro/spaces/:id/leave)
Leave a space.
GET /api/v1/spaces/:id/invites (was /memoro/spaces/:id/invites)
List invitations for a space.
POST /api/v1/spaces/:id/invite (was /memoro/spaces/:id/invite)
Invite user to space.
POST /api/v1/spaces/invites/:inviteId/resend
Resend invitation.
DELETE /api/v1/spaces/invites/:inviteId
Cancel invitation.
GET /api/v1/invites/pending (was /memoro/invites/pending)
Get current user's pending invitations.
POST /api/v1/invites/accept (was /memoro/spaces/invites/accept)
Accept space invitation.
POST /api/v1/invites/decline (was /memoro/spaces/invites/decline)
Decline space invitation.
GET /api/v1/credits/pricing (was /memoro/credits/pricing)
Get credit pricing constants. Public (no auth needed).
POST /api/v1/credits/check (was /memoro/credits/check-transcription)
Validate if user has enough credits for an operation.
POST /api/v1/credits/consume (was /memoro/credits/consume-operation)
Consume credits for completed operation.
GET /api/v1/settings (was /settings)
Get all user settings.
GET /api/v1/settings/memoro (was /settings/memoro)
Get Memoro-specific settings.
PATCH /api/v1/settings/memoro (was /settings/memoro)
Update Memoro settings.
PATCH /api/v1/settings/memoro/data-usage
Update data usage acceptance flag.
PATCH /api/v1/settings/memoro/newsletter
Update newsletter opt-in.
PATCH /api/v1/settings/profile (was /settings/profile)
Update user profile.
GET /api/v1/meetings/bots
List user's meeting bots.
POST /api/v1/meetings/bots
Create meeting recording bot.
GET /api/v1/meetings/bots/:id
Get meeting bot.
POST /api/v1/meetings/bots/:id/stop
Stop meeting bot.
GET /api/v1/meetings/recordings
List recordings.
GET /api/v1/meetings/recordings/:id
Get recording.
POST /api/v1/meetings/recordings/:id/to-memo
Convert meeting recording to memo.
POST /api/v1/webhooks/meetings (was /meetings/webhooks/bot-events)
Webhook for meeting bot completion (no auth, validated by signature/key).
Internal (service-to-service, X-Service-Key header):
POST /api/v1/internal/transcription-completed
Callback from audio-server when transcription finishes.
POST /api/v1/internal/append-transcription-completed
Callback from audio-server for append transcription.
POST /api/v1/internal/batch-metadata
Update batch job metadata by memo ID.
Cleanup (X-Internal-API-Key header):
POST /api/v1/cleanup/run (was /cleanup/trigger-from-cron)
Trigger audio cleanup for users with 30-day auto-delete enabled.
POST /api/v1/cleanup/manual (was /cleanup/trigger-manual)
Manual cleanup trigger.
GET /health
Health check.
audio-server/ — All Routes
POST /api/v1/transcribe (was /transcribe-realtime)
Main transcription endpoint. Called by server/. Handles 4-tier fallback:
fast → retry → format-convert + retry → batch.
POST /api/v1/transcribe/append (was append transcription flow)
Transcribe additional audio for existing memo.
GET /api/v1/duration (was audio duration check)
Get duration of audio file at a Supabase Storage path.
POST /api/v1/convert (was FFmpeg conversion flow)
Convert audio format (M4A → WAV PCM 16kHz mono). Used internally and by server.
GET /health
Health check.
New Folder Structure
apps/memoro/apps/server/
├── src/
│ ├── index.ts # Hono app entry, routes wiring
│ ├── lib/
│ │ ├── supabase.ts # Supabase client factory (service role)
│ │ ├── ai.ts # Gemini + Azure OpenAI client (abstracted)
│ │ └── credits.ts # mana-credits HTTP client
│ ├── routes/
│ │ ├── memos.ts # Process, retry, reprocess, combine, Q&A
│ │ ├── spaces.ts # Spaces CRUD + membership
│ │ ├── invites.ts # Invitation management
│ │ ├── credits.ts # Credits check/consume/pricing
│ │ ├── settings.ts # User settings
│ │ ├── meetings.ts # Meeting bots + recordings
│ │ ├── internal.ts # Service-to-service callbacks
│ │ └── cleanup.ts # Audio cleanup cron endpoint
│ ├── services/
│ │ ├── memo.service.ts # Core memo + transcription orchestration
│ │ ├── headline.service.ts
│ │ ├── memory.service.ts
│ │ ├── question.service.ts
│ │ └── space.service.ts
│ └── types.ts # Shared TypeScript interfaces
├── package.json
└── tsconfig.json
apps/memoro/apps/audio-server/
├── src/
│ ├── index.ts
│ ├── lib/
│ │ ├── azure-speech.ts # Azure Speech Service client
│ │ ├── azure-blob.ts # Azure Blob Storage client
│ │ └── supabase.ts # Supabase storage download client
│ ├── routes/
│ │ ├── transcribe.ts # Main + append transcription
│ │ ├── convert.ts # FFmpeg audio conversion
│ │ └── duration.ts # Audio duration check
│ └── services/
│ ├── transcription.service.ts # 4-tier fallback logic
│ ├── batch.service.ts # Azure batch transcription
│ └── ffmpeg.service.ts # Format conversion
├── package.json
└── tsconfig.json
Implementation Phases
Phase 1: audio-server (simpler, no business logic)
- Create
apps/memoro/apps/audio-server/with Hono/Bun scaffold - Port
azure-speech.ts,azure-blob.ts, Supabase storage download - Port FFmpeg service (format conversion, duration detection)
- Port transcription service with 4-tier fallback
- Port batch transcription service (Azure long-running jobs)
- Wire routes:
/api/v1/transcribe,/api/v1/convert,/api/v1/duration,/health - Add
X-Service-Keyauth guard for all routes - Test against live Azure services manually
No auth migration needed here — audio-server is internal-only (called by server, not frontend).
Phase 2: server scaffold + auth
- Create
apps/memoro/apps/server/with Hono/Bun scaffold - Setup
authMiddleware()from@manacore/shared-hono - Create Supabase service-role client factory
- Implement
X-Service-Keyguard for internal routes - Implement
X-Internal-API-Keyguard for cleanup routes - Port
pricing.constants.tsand credit calculation helpers - Wire health check and credits/pricing (no auth) routes first
- Deploy empty shell and confirm reachability
Phase 3: Memo routes (core)
- Port
memo.service.ts—createMemoFromUploadedFile():- Create memo in Supabase (service role + user_id filter)
- Call audio-server
/api/v1/transcribeasync - Return full memo object immediately
- Port
handleTranscriptionCompleted()— update memo after transcription callback - Port internal callback routes (
/api/v1/internal/transcription-completedetc.) - Port retry routes
- Port
append-transcriptionflow
Phase 4: AI routes
- Port
ai.ts— Gemini primary, Azure OpenAI fallback, same logic asai.service.ts - Port
headline.service.ts - Port
question.service.ts(Q&A) - Port
memory.service.ts(prompt-based memories) - Wire
/api/v1/memos/combine,/api/v1/memos/:id/question
Phase 5: Spaces + Invites
- Port
space.service.ts— CRUD + membership management - Port invitation flow (invite, accept, decline, resend, cancel)
- Wire all
/api/v1/spaces/*and/api/v1/invites/*routes
Phase 6: Settings + Credits + Cleanup
- Port settings routes — proxy to mana-credits / mana-user services (or Supabase direct)
- Port credits routes — call mana-credits service instead of mana-core-middleware
- Port audio cleanup service
- Wire
/api/v1/cleanup/*
Phase 7: Meetings
- Port meetings proxy service (proxy to external meeting bot API)
- Port webhook handler (
/api/v1/webhooks/meetings) - Wire all
/api/v1/meetings/*routes
Phase 8: Remove auth proxy
Remove the NestJS auth-proxy module entirely.
Document in mobile migration plan that mobile must call mana-auth directly.
Phase 9: Cutover
- Update
docker-compose.macmini.ymlto use new server/audio-server images - Update Memoro web app env vars:
PUBLIC_BACKEND_URL→ new server port - Run both old and new in parallel briefly for smoke testing
- Remove old
backend/andaudio-backend/NestJS services
Environment Variables (New Services)
server/.env
PORT=3015
NODE_ENV=production
# Auth
MANA_CORE_AUTH_URL=http://localhost:3001
CORS_ORIGINS=http://localhost:5173
# Internal service auth
SERVICE_KEY=your-internal-service-key
INTERNAL_API_KEY=your-internal-api-key
# Supabase (service role — no user JWT needed for DB)
MEMORO_SUPABASE_URL=https://your-memoro-project.supabase.co
MEMORO_SUPABASE_SERVICE_KEY=your-service-role-key
# External services
GEMINI_API_KEY=your-gemini-key
AZURE_OPENAI_ENDPOINT=https://...
AZURE_OPENAI_KEY=your-key
AZURE_OPENAI_DEPLOYMENT=gpt-4
# Internal services
AUDIO_SERVER_URL=http://localhost:3016
MANA_CREDITS_URL=http://localhost:3004
# App config
MEMORO_APP_ID=973da0c1-b479-4dac-a1b0-ed09c72caca8
audio-server/.env
PORT=3016
NODE_ENV=production
# Internal auth
SERVICE_KEY=your-internal-service-key
# Azure Speech
AZURE_SPEECH_KEY=your-key
AZURE_SPEECH_REGION=swedencentral
# Azure Blob (for batch transcription uploads)
AZURE_STORAGE_ACCOUNT_NAME=your-account
AZURE_STORAGE_ACCOUNT_KEY=your-key
# Supabase (read-only: download audio files)
MEMORO_SUPABASE_URL=https://your-memoro-project.supabase.co
MEMORO_SUPABASE_SERVICE_KEY=your-service-role-key
# Callback
MEMORO_SERVER_URL=http://localhost:3015
Critical Implementation Notes
Supabase RLS → Service Role Pattern
Old NestJS pattern (user JWT for RLS):
const supabase = createClient(url, anonKey, {
global: { headers: { Authorization: `Bearer ${userToken}` } }
});
const { data } = await supabase.from('memos').select('*'); // RLS filters by sub
New Hono pattern (service role + explicit filter):
const supabase = createClient(url, serviceKey); // service role, bypasses RLS
const userId = c.get('userId'); // from authMiddleware()
const { data } = await supabase.from('memos').select('*').eq('user_id', userId);
Every single Supabase query that previously relied on RLS needs eq('user_id', userId) added.
Access control for spaces (where user might not be owner) needs explicit join check.
Async Transcription (fire-and-forget)
Hono/Bun equivalent of setImmediate():
// Fire-and-forget in Bun
queueMicrotask(() => processTranscriptionAsync(memoId, ...));
// or
setTimeout(() => processTranscriptionAsync(memoId, ...), 0);
The endpoint returns immediately after memo creation; transcription runs in background
and calls back via /api/v1/internal/transcription-completed.
FFmpeg in Bun
Bun can run FFmpeg via Bun.spawn:
const proc = Bun.spawn(['ffmpeg', '-i', inputPath, '-ar', '16000', '-ac', '1', outputPath]);
await proc.exited;
FFmpeg must be installed in the Docker image (add RUN apt-get install -y ffmpeg).
Azure Batch Job Recovery
Batch jobs store jobId in memo.metadata.processing.transcription.jobId.
A cron endpoint (or background polling) can check stuck jobs and re-trigger
the transcription-completed callback. Implement in Phase 3 alongside main flow.
Files to Reference During Implementation
| File | Purpose |
|---|---|
apps/memoro/apps/backend/src/memoro/memoro.service.ts |
Core transcription + memo logic (~2600 lines) |
apps/memoro/apps/backend/src/ai/ai.service.ts |
Gemini/Azure AI routing |
apps/memoro/apps/backend/src/ai/headline/headline.service.ts |
Headline generation |
apps/memoro/apps/backend/src/credits/pricing.constants.ts |
Credit cost constants |
apps/memoro/apps/backend/src/credits/credit-consumption.service.ts |
Credit validation/consumption |
apps/memoro/apps/audio-backend/src/audio.service.ts |
4-tier transcription fallback |
apps/memoro/apps/backend/src/cleanup/audio-cleanup.service.ts |
30-day audio cleanup |
apps/todo/apps/server/src/index.ts |
Reference Hono/Bun server pattern |
packages/shared-hono/src/ |
authMiddleware, healthRoute, errorHandler |
Open Questions (resolve when starting)
- Port numbers 3015/3016 — confirm no conflicts with other services
- mana-credits URL in dev environment — confirm port
- Meeting bot external API — which provider? Are credentials available?
- Space sync (
/sync/spaces/*) — is this still needed or replaced by local-store sync? - Should audio-server be publicly reachable or internal-only? (Recommendation: internal only)