managarten/apps/memoro/docs/SVELTEKIT_MIGRATION_ANALYSIS.md
Till JS d8a2b37126 chore(memoro): import legacy backend, mobile, and landing apps
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>
2026-03-31 17:30:00 +02:00

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:

  1. AuthProvider - User authentication state
  2. ThemeProvider - Dark/light theme + color variants
  3. SpaceProvider - Workspace/space management
  4. CreditProvider - Credit/subscription tracking
  5. LanguageProvider - i18n language selection
  6. LocationProvider - Geolocation tracking
  7. RecordingLanguageProvider - Audio recording language
  8. ToastProvider - Toast notifications
  9. AnalyticsProvider - Analytics tracking
  10. NetworkStatusProvider - Network connectivity
  11. MemoRealtimeProvider - Supabase realtime subscriptions
  12. HeaderProvider - Header configuration
  13. 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)

  1. Project Setup: 2 days
  2. Core Architecture: 3 days
  3. Route Structure: 2 days
  4. Component Migration: 15 days (100+ components)
  5. Data Fetching: 5 days
  6. Realtime Features: 5 days
  7. Web-Specific Features: 5 days
  8. Styling & Theming: 3 days
  9. Testing: 5 days
  10. 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

  1. Decision Point: Confirm stakeholder approval for migration
  2. Team Assignment: Assign developers to migration phases
  3. Environment Setup: Create SvelteKit project
  4. Parallel Development: Maintain React app while building SvelteKit version
  5. Feature Parity Testing: Ensure all features work correctly
  6. Migration Plan: Plan user migration from old to new app
  7. 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