From c736dd52f27e6e2963ee3a7081a8416c5d835c89 Mon Sep 17 00:00:00 2001 From: Till JS Date: Tue, 31 Mar 2026 18:43:44 +0200 Subject: [PATCH] docs: add Memoro backend Hono/Bun migration plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .claude/plans/memoro-server-hono-migration.md | 499 ++++++++++++++++++ 1 file changed, 499 insertions(+) create mode 100644 .claude/plans/memoro-server-hono-migration.md diff --git a/.claude/plans/memoro-server-hono-migration.md b/.claude/plans/memoro-server-hono-migration.md new file mode 100644 index 000000000..e0a773088 --- /dev/null +++ b/.claude/plans/memoro-server-hono-migration.md @@ -0,0 +1,499 @@ +# 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) + +1. Create `apps/memoro/apps/audio-server/` with Hono/Bun scaffold +2. Port `azure-speech.ts`, `azure-blob.ts`, Supabase storage download +3. Port FFmpeg service (format conversion, duration detection) +4. Port transcription service with 4-tier fallback +5. Port batch transcription service (Azure long-running jobs) +6. Wire routes: `/api/v1/transcribe`, `/api/v1/convert`, `/api/v1/duration`, `/health` +7. Add `X-Service-Key` auth guard for all routes +8. 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 + +1. Create `apps/memoro/apps/server/` with Hono/Bun scaffold +2. Setup `authMiddleware()` from `@manacore/shared-hono` +3. Create Supabase service-role client factory +4. Implement `X-Service-Key` guard for internal routes +5. Implement `X-Internal-API-Key` guard for cleanup routes +6. Port `pricing.constants.ts` and credit calculation helpers +7. Wire health check and credits/pricing (no auth) routes first +8. Deploy empty shell and confirm reachability + +### Phase 3: Memo routes (core) + +1. Port `memo.service.ts` — `createMemoFromUploadedFile()`: + - Create memo in Supabase (service role + user_id filter) + - Call audio-server `/api/v1/transcribe` async + - Return full memo object immediately +2. Port `handleTranscriptionCompleted()` — update memo after transcription callback +3. Port internal callback routes (`/api/v1/internal/transcription-completed` etc.) +4. Port retry routes +5. Port `append-transcription` flow + +### Phase 4: AI routes + +1. Port `ai.ts` — Gemini primary, Azure OpenAI fallback, same logic as `ai.service.ts` +2. Port `headline.service.ts` +3. Port `question.service.ts` (Q&A) +4. Port `memory.service.ts` (prompt-based memories) +5. Wire `/api/v1/memos/combine`, `/api/v1/memos/:id/question` + +### Phase 5: Spaces + Invites + +1. Port `space.service.ts` — CRUD + membership management +2. Port invitation flow (invite, accept, decline, resend, cancel) +3. Wire all `/api/v1/spaces/*` and `/api/v1/invites/*` routes + +### Phase 6: Settings + Credits + Cleanup + +1. Port settings routes — proxy to mana-credits / mana-user services (or Supabase direct) +2. Port credits routes — call mana-credits service instead of mana-core-middleware +3. Port audio cleanup service +4. Wire `/api/v1/cleanup/*` + +### Phase 7: Meetings + +1. Port meetings proxy service (proxy to external meeting bot API) +2. Port webhook handler (`/api/v1/webhooks/meetings`) +3. 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 + +1. Update `docker-compose.macmini.yml` to use new server/audio-server images +2. Update Memoro web app env vars: `PUBLIC_BACKEND_URL` → new server port +3. Run both old and new in parallel briefly for smoke testing +4. Remove old `backend/` and `audio-backend/` NestJS services + +--- + +## Environment Variables (New Services) + +### `server/.env` + +```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` + +```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): +```typescript +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): +```typescript +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()`: +```typescript +// 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`: +```typescript +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)