mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:21:10 +02:00
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>
499 lines
17 KiB
Markdown
499 lines
17 KiB
Markdown
# 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)
|