Adds the original NestJS backends (backend, audio-backend), Expo mobile app, and Astro landing page as-is from the standalone memoro repo. These are not yet migrated to monorepo standards (migration tracked in memory/CLAUDE.md). Also adds eslint.config.mjs ignore for apps/*/apps/audio-backend/** and .prettierignore entries for legacy memoro dirs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
19 KiB
SvelteKit Migration Analysis
Status: Analysis Complete Date: 2025-10-26 Analyst: CODER Agent (Hive Mind Swarm ID: swarm-1761491548336-9t6qop57g)
Executive Summary
This document provides a comprehensive analysis of porting the Memoro React/Expo web application to SvelteKit. The current application is a cross-platform mobile app built with Expo and React Native that also supports web via react-native-web.
Current Application Architecture
Technology Stack
- Framework: Expo 54 + React Native 0.81.4
- Router: Expo Router (file-based routing)
- State Management: Zustand
- UI: React Native components with NativeWind (Tailwind CSS)
- Backend: Supabase (PostgreSQL + Realtime + Auth + Storage)
- Analytics: PostHog + Sentry
- Payments: RevenueCat
- i18n: react-i18next
- Forms: React Native components
- Web Support: react-dom + react-native-web
Route Structure Analysis
Public Routes (Unauthenticated)
/(public)
├── / (index.tsx) - Landing page
├── /login - User login
└── /register - User registration
Protected Routes (Authenticated)
/(protected)
├── /(tabs)
│ ├── / (index.tsx) - Home/Recording page
│ └── /memos - Memo list page
├── /(memo)
│ └── /[id] - Memo detail page (UUID route)
├── /(space)
│ └── /[id] - Space detail page
├── /audio-archive - Audio archive
├── /blueprints - Blueprints list
├── /create-blueprint - Blueprint creator
├── /memories - Memories view
├── /prompts - Prompts management
├── /settings - User settings
├── /statistics - Analytics/statistics
├── /subscription - Subscription management
└── /tags - Tags management
Context Providers (To Port to SvelteKit)
The application uses extensive React Context providers that need to be converted to Svelte stores:
- AuthProvider - User authentication state
- ThemeProvider - Dark/light theme + color variants
- SpaceProvider - Workspace/space management
- CreditProvider - Credit/subscription tracking
- LanguageProvider - i18n language selection
- LocationProvider - Geolocation tracking
- RecordingLanguageProvider - Audio recording language
- ToastProvider - Toast notifications
- AnalyticsProvider - Analytics tracking
- NetworkStatusProvider - Network connectivity
- MemoRealtimeProvider - Supabase realtime subscriptions
- HeaderProvider - Header configuration
- OnboardingProvider - Onboarding flow state
Component Inventory
The application contains approximately:
- 100+ React components (.tsx files)
- 50+ TypeScript utility files
- 20+ feature modules (auth, memos, spaces, tags, etc.)
- Multiple modals and overlays
- Complex forms with validation
- Audio recording/playback functionality
- Real-time data synchronization
Key Features to Port
1. Authentication & Authorization
- Email/password login
- Google Sign-In
- Apple Sign-In
- Password reset flow
- Session management
- Protected routes
2. Memo Management
- Create memos (audio recording - WEB VERSION ONLY)
- View memo list with infinite scroll
- Memo detail view
- Edit memo metadata
- Delete memos
- Search and filter memos
- Tag memos
- Add photos to memos
- Realtime updates
3. Spaces (Workspaces)
- Create/edit spaces
- Share spaces with team
- Space-specific memos
- Space invitations
4. Tags & Organization
- Create/edit tags with colors
- Tag filtering
- Tag analytics
- Tag-based organization
5. Blueprints
- Create blueprint templates
- Apply blueprints to memos
- Blueprint management
6. Statistics & Analytics
- Usage statistics
- Tag analytics
- Weekly charts
- Activity tracking
7. Subscription Management
- Credit system
- Subscription plans
- Payment integration (RevenueCat)
- Credit purchase
- Usage limits
8. Settings
- Profile management
- Language selection
- Theme preferences
- Notification settings
- Email preferences
SvelteKit Migration Strategy
Phase 1: Project Setup & Infrastructure
1.1 Initialize SvelteKit Project
npm create svelte@latest memoro-sveltekit
cd memoro-sveltekit
npm install
Configuration:
- TypeScript: Yes
- ESLint: Yes
- Prettier: Yes
- Playwright: Yes
- Vitest: Yes
1.2 Install Core Dependencies
npm install @supabase/supabase-js @supabase/auth-helpers-sveltekit
npm install tailwindcss postcss autoprefixer
npm install date-fns
npm install svelte-i18n
1.3 Configure Tailwind CSS
- Port existing tailwind.config.js
- Preserve color schemes and theme variants
- Ensure dark mode support
Phase 2: Core Architecture
2.1 Supabase Integration
File: src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY)
2.2 Auth Hooks
File: src/hooks.server.ts
import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit'
export const handle = async ({ event, resolve }) => {
event.locals.supabase = createSupabaseServerClient({
supabaseUrl: process.env.PUBLIC_SUPABASE_URL,
supabaseKey: process.env.PUBLIC_SUPABASE_ANON_KEY,
event
})
event.locals.getSession = async () => {
const { data: { session } } = await event.locals.supabase.auth.getSession()
return session
}
return resolve(event)
}
2.3 Svelte Stores (State Management)
Convert Zustand stores to Svelte stores:
// src/lib/stores/auth.ts
import { writable, derived } from 'svelte/store'
import type { User, Session } from '@supabase/supabase-js'
export const session = writable<Session | null>(null)
export const user = writable<User | null>(null)
export const isAuthenticated = derived(session, $session => !!$session)
export const loading = writable(true)
// src/lib/stores/theme.ts
import { writable } from 'svelte/store'
export const isDark = writable(false)
export const themeVariant = writable('default')
// src/lib/stores/memos.ts
import { writable } from 'svelte/store'
import type { Memo } from '$lib/types'
export const memos = writable<Memo[]>([])
export const selectedMemo = writable<Memo | null>(null)
Phase 3: Route Structure
3.1 SvelteKit Route Mapping
Public Routes:
src/routes/
├── +layout.svelte (root layout)
├── +layout.ts (root layout load)
├── +page.svelte (landing page)
├── login/
│ └── +page.svelte
└── register/
└── +page.svelte
Protected Routes:
src/routes/(protected)/
├── +layout.svelte (protected layout with auth guard)
├── +layout.server.ts (server-side auth check)
├── +page.svelte (home/recording page)
├── memos/
│ └── +page.svelte
├── [id]/
│ ├── +page.svelte (memo detail)
│ └── +page.ts (load memo data)
├── spaces/
│ └── [id]/
│ ├── +page.svelte
│ └── +page.ts
├── audio-archive/
│ └── +page.svelte
├── blueprints/
│ ├── +page.svelte
│ └── create/
│ └── +page.svelte
├── memories/
│ └── +page.svelte
├── prompts/
│ └── +page.svelte
├── settings/
│ ├── +page.svelte
│ └── +page.server.ts (handle form actions)
├── statistics/
│ └── +page.svelte
├── subscription/
│ └── +page.svelte
└── tags/
└── +page.svelte
3.2 Auth Guards
File: src/routes/(protected)/+layout.server.ts
import { redirect } from '@sveltejs/kit'
import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async ({ locals }) => {
const session = await locals.getSession()
if (!session) {
throw redirect(303, '/login')
}
return {
session,
user: session.user
}
}
Phase 4: Component Migration
4.1 Component Conversion Pattern
React Component (Before):
import React from 'react'
import { View, Text, TouchableOpacity } from 'react-native'
interface ButtonProps {
onPress: () => void
title: string
disabled?: boolean
}
export const Button: React.FC<ButtonProps> = ({ onPress, title, disabled }) => {
return (
<TouchableOpacity onPress={onPress} disabled={disabled}>
<View className="bg-blue-500 px-4 py-2 rounded">
<Text className="text-white">{title}</Text>
</View>
</TouchableOpacity>
)
}
Svelte Component (After):
<script lang="ts">
export let onClick: () => void
export let title: string
export let disabled = false
</script>
<button
on:click={onClick}
{disabled}
class="bg-blue-500 px-4 py-2 rounded text-white"
>
{title}
</button>
4.2 Component Organization
src/lib/components/
├── atoms/
│ ├── Button.svelte
│ ├── Input.svelte
│ ├── Alert.svelte
│ └── LoadingOverlay.svelte
├── molecules/
│ ├── TagList.svelte
│ ├── SearchOverlay.svelte
│ └── MemoPreview.svelte
├── organisms/
│ ├── Header.svelte
│ ├── MemoList.svelte
│ └── TranscriptDisplay.svelte
└── templates/
└── PageLayout.svelte
Phase 5: Data Fetching
5.1 Load Functions
Server-side data loading:
// src/routes/(protected)/memos/+page.ts
import type { PageLoad } from './$types'
export const load: PageLoad = async ({ parent, url }) => {
const { supabase } = await parent()
const page = Number(url.searchParams.get('page') || '1')
const limit = 20
const offset = (page - 1) * limit
const { data: memos, error } = await supabase
.from('memos')
.select('*')
.order('created_at', { ascending: false })
.range(offset, offset + limit - 1)
if (error) throw error
return {
memos: memos || [],
page
}
}
5.2 Form Actions
Server-side form handling:
// src/routes/(protected)/settings/+page.server.ts
import { fail } from '@sveltejs/kit'
import type { Actions } from './$types'
export const actions: Actions = {
updateProfile: async ({ request, locals }) => {
const session = await locals.getSession()
if (!session) return fail(401, { message: 'Unauthorized' })
const formData = await request.formData()
const name = formData.get('name')
const { error } = await locals.supabase
.from('profiles')
.update({ name })
.eq('id', session.user.id)
if (error) return fail(500, { message: error.message })
return { success: true }
}
}
Phase 6: Realtime Features
6.1 Realtime Subscriptions
// src/lib/realtime/memos.ts
import { get } from 'svelte/store'
import { memos } from '$lib/stores/memos'
import { supabase } from '$lib/supabase'
export function subscribeToMemos(userId: string) {
const subscription = supabase
.channel('memos')
.on('postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'memos', filter: `user_id=eq.${userId}` },
(payload) => {
memos.update(list => [payload.new, ...list])
}
)
.on('postgres_changes',
{ event: 'UPDATE', schema: 'public', table: 'memos', filter: `user_id=eq.${userId}` },
(payload) => {
memos.update(list =>
list.map(memo => memo.id === payload.new.id ? payload.new : memo)
)
}
)
.on('postgres_changes',
{ event: 'DELETE', schema: 'public', table: 'memos', filter: `user_id=eq.${userId}` },
(payload) => {
memos.update(list => list.filter(memo => memo.id !== payload.old.id))
}
)
.subscribe()
return () => {
subscription.unsubscribe()
}
}
Phase 7: Web-Specific Considerations
7.1 Audio Recording (Web Only)
- Use Web Audio API instead of Expo Audio
- Implement MediaRecorder for audio capture
- Handle browser permissions
- Fallback for unsupported browsers
// src/lib/audio/recorder.ts
export class AudioRecorder {
private mediaRecorder: MediaRecorder | null = null
private audioChunks: Blob[] = []
async start(): Promise<void> {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
this.mediaRecorder = new MediaRecorder(stream)
this.mediaRecorder.ondataavailable = (event) => {
this.audioChunks.push(event.data)
}
this.mediaRecorder.start()
}
async stop(): Promise<Blob> {
return new Promise((resolve) => {
if (!this.mediaRecorder) throw new Error('No recorder')
this.mediaRecorder.onstop = () => {
const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' })
this.audioChunks = []
resolve(audioBlob)
}
this.mediaRecorder.stop()
this.mediaRecorder.stream.getTracks().forEach(track => track.stop())
})
}
}
7.2 File Uploads
- Use File API for image/document uploads
- Handle drag-and-drop
- Progress indicators
- Client-side validation
7.3 Removed Features (Mobile-Only)
- Push notifications (use email notifications instead)
- Haptic feedback
- Native device features (camera, location on mobile)
- App store review prompts
- Native gesture handlers
Phase 8: Styling & Theming
8.1 Tailwind Configuration
File: tailwind.config.js
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
darkMode: 'class',
theme: {
extend: {
colors: {
// Port existing color scheme
primary: { /* ... */ },
secondary: { /* ... */ },
dark: { /* ... */ },
light: { /* ... */ }
}
}
},
plugins: []
}
8.2 Dark Mode Implementation
<!-- src/lib/components/ThemeToggle.svelte -->
<script lang="ts">
import { isDark } from '$lib/stores/theme'
import { browser } from '$app/environment'
function toggleTheme() {
$isDark = !$isDark
if (browser) {
document.documentElement.classList.toggle('dark', $isDark)
localStorage.setItem('theme', $isDark ? 'dark' : 'light')
}
}
</script>
<button on:click={toggleTheme}>
{$isDark ? '☀️' : '🌙'}
</button>
Phase 9: Testing Strategy
9.1 Unit Tests (Vitest)
// src/lib/stores/auth.test.ts
import { describe, it, expect } from 'vitest'
import { get } from 'svelte/store'
import { session, isAuthenticated } from './auth'
describe('auth store', () => {
it('should initialize as not authenticated', () => {
expect(get(isAuthenticated)).toBe(false)
})
})
9.2 Integration Tests (Playwright)
// tests/auth.spec.ts
import { test, expect } from '@playwright/test'
test('user can login', async ({ page }) => {
await page.goto('/login')
await page.fill('[name="email"]', 'test@example.com')
await page.fill('[name="password"]', 'password123')
await page.click('button[type="submit"]')
await expect(page).toHaveURL('/')
})
Phase 10: Deployment
10.1 Adapter Configuration
File: svelte.config.js
import adapter from '@sveltejs/adapter-auto'
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
export default {
preprocess: vitePreprocess(),
kit: {
adapter: adapter()
}
}
10.2 Environment Variables
File: .env
PUBLIC_SUPABASE_URL=your-supabase-url
PUBLIC_SUPABASE_ANON_KEY=your-anon-key
PRIVATE_SUPABASE_SERVICE_KEY=your-service-key
Migration Challenges & Solutions
Challenge 1: React Native Components
Problem: No direct equivalent for React Native components Solution: Use native HTML elements with Tailwind CSS for styling
Challenge 2: Context Providers
Problem: SvelteKit doesn't use context providers Solution: Convert to Svelte stores with proper initialization
Challenge 3: Expo-Specific Features
Problem: Many Expo modules (audio, camera, etc.) don't work on web Solution: Implement web-native alternatives or gracefully degrade
Challenge 4: Realtime State Management
Problem: Complex realtime synchronization Solution: Use Supabase Realtime with Svelte reactive stores
Challenge 5: Authentication Flow
Problem: Different auth patterns in SvelteKit Solution: Use SvelteKit hooks and server-side load functions
Challenge 6: Route Protection
Problem: Different routing mechanisms Solution: Use SvelteKit layout load functions for auth guards
Estimated Migration Effort
Time Estimates (Person-Days)
- Project Setup: 2 days
- Core Architecture: 3 days
- Route Structure: 2 days
- Component Migration: 15 days (100+ components)
- Data Fetching: 5 days
- Realtime Features: 5 days
- Web-Specific Features: 5 days
- Styling & Theming: 3 days
- Testing: 5 days
- Deployment & Docs: 2 days
Total: ~47 person-days (~9-10 weeks for single developer)
Success Criteria
✅ All routes accessible and functional ✅ Authentication working (login, register, password reset) ✅ Memo CRUD operations working ✅ Realtime updates functional ✅ Tags, spaces, blueprints working ✅ Settings and profile management working ✅ Subscription/credit system working ✅ Audio recording working (web version) ✅ Dark mode and theming working ✅ i18n working ✅ All tests passing ✅ Performance metrics acceptable
Next Steps
- Decision Point: Confirm stakeholder approval for migration
- Team Assignment: Assign developers to migration phases
- Environment Setup: Create SvelteKit project
- Parallel Development: Maintain React app while building SvelteKit version
- Feature Parity Testing: Ensure all features work correctly
- Migration Plan: Plan user migration from old to new app
- Launch: Deploy SvelteKit version
Appendix: File Structure Comparison
Current Expo/React Structure
memoro_app/
├── app/
│ ├── (public)/
│ ├── (protected)/
│ └── _layout.tsx
├── components/
├── features/
├── hooks/
├── store/
├── utils/
└── package.json
Proposed SvelteKit Structure
memoro-sveltekit/
├── src/
│ ├── lib/
│ │ ├── components/
│ │ ├── stores/
│ │ ├── utils/
│ │ ├── types/
│ │ └── supabase.ts
│ ├── routes/
│ │ ├── (protected)/
│ │ ├── login/
│ │ ├── register/
│ │ └── +layout.svelte
│ └── app.html
├── static/
├── tests/
├── svelte.config.js
├── vite.config.ts
└── package.json
Conclusion
The migration from React/Expo to SvelteKit is technically feasible and will result in a more performant, maintainable web application. The main challenges involve converting React Native components to web equivalents and adapting to SvelteKit's different architectural patterns. With proper planning and execution, the migration can preserve all core functionality while improving the developer experience and application performance.
Document Version: 1.0 Last Updated: 2025-10-26 Status: Ready for Review