managarten/apps/picture/MIGRATION_PLAN.md
Till-JS c561c4c8d8 feat(picture): migrate from Supabase to NestJS backend API
- Migrate image generation from Supabase Edge Functions to NestJS
- Add profiles and image-likes schemas
- Refactor mobile auth context for backend API
- Update all mobile hooks and services for API integration
- Add Docker configuration for deployment
- Remove Supabase functions and migrations
- Add migration plan documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 14:47:23 +01:00

8.6 KiB

Picture App: Supabase zu Backend API Migration

Ziel

Komplette Entfernung der direkten Supabase-Nutzung in der Mobile App. Alle Datenbankzugriffe sollen über die Backend API erfolgen (wie in der Chat App).


Aktuelle Situation

Backend (bereits implementiert)

Endpoint Status
/api/images/* Vorhanden - CRUD, Archive, Batch-Operationen
/api/generate/* Vorhanden - Bildgenerierung mit Replicate
/api/tags/* Vorhanden - Tag-Management
/api/boards/* Vorhanden - Board-Management
/api/board-items/* Vorhanden - Board-Items
/api/models/* Vorhanden - AI Modelle
/api/explore/* Vorhanden - Öffentliche Galerie
/api/upload/* Vorhanden - Datei-Upload
/api/profiles/* FEHLT - User Profile
/api/likes/* FEHLT - Image Likes

Mobile App (direkte Supabase-Nutzung)

Dateien die supabase.from() oder Supabase-Client nutzen:

  1. app/(tabs)/profile.tsx - Profile-Daten laden/aktualisieren
  2. app/image/[id].tsx - Einzelbild-Details
  3. hooks/useImageFetching.ts - Bilder laden
  4. hooks/useImageLikes.ts - Like-Funktionalität
  5. hooks/useImagePrefetch.ts - Prefetching
  6. hooks/useExploreFetching.ts - Explore-Daten
  7. hooks/useExplorePrefetch.ts - Explore-Prefetching
  8. hooks/useArchiveFetching.ts - Archiv-Daten
  9. store/tagStore.ts - Tag-Daten
  10. store/batchStore.ts - Batch-Generierung (nutzt Edge Functions)
  11. components/RateLimitIndicator.tsx - Rate Limit Check

Migrationsplan

Phase 1: Backend erweitern

1.1 Profile Endpoints hinzufügen

GET    /api/profiles/me           - Eigenes Profil laden
PATCH  /api/profiles/me           - Profil aktualisieren
GET    /api/profiles/stats        - User Stats (Bilder, Favoriten)

Schema erweitern (falls nicht vorhanden):

// profiles table
{
  id: uuid (PK, same as auth user id)
  username: text
  email: text
  avatarUrl: text (optional)
  createdAt: timestamp
  updatedAt: timestamp
}

1.2 Like Endpoints hinzufügen

POST   /api/images/:id/like       - Bild liken
DELETE /api/images/:id/like       - Like entfernen
GET    /api/images/:id/likes      - Like-Status & Anzahl

Schema erweitern (falls nicht vorhanden):

// image_likes table
{
  id: uuid (PK)
  imageId: uuid (FK to images)
  userId: uuid
  createdAt: timestamp
}

1.3 Rate Limit Endpoint

GET    /api/rate-limit            - Aktueller Rate Limit Status

Phase 2: Mobile API Client erstellen

2.1 Zentraler API Client

Datei: services/api/client.ts

import * as SecureStore from 'expo-secure-store';

const APP_TOKEN_KEY = '@manacore/app_token';
const BACKEND_URL = process.env.EXPO_PUBLIC_PICTURE_BACKEND_URL || 'http://localhost:3003';

async function getAuthToken(): Promise<string | null> {
  try {
    return await SecureStore.getItemAsync(APP_TOKEN_KEY);
  } catch {
    return null;
  }
}

export async function apiRequest<T>(
  endpoint: string,
  options: RequestInit = {}
): Promise<{ data: T | null; error: string | null }> {
  const token = await getAuthToken();

  const headers: HeadersInit = {
    'Content-Type': 'application/json',
    ...(options.headers || {}),
  };

  if (token) {
    headers['Authorization'] = `Bearer ${token}`;
  }

  try {
    const response = await fetch(`${BACKEND_URL}/api${endpoint}`, {
      ...options,
      headers,
    });

    if (!response.ok) {
      const error = await response.json().catch(() => ({ message: 'Request failed' }));
      return { data: null, error: error.message || `HTTP ${response.status}` };
    }

    const data = await response.json();
    return { data, error: null };
  } catch (error) {
    return { data: null, error: error instanceof Error ? error.message : 'Network error' };
  }
}

2.2 Domain-spezifische API Module

services/api/profiles.ts

export const profileApi = {
  getMyProfile: () => apiRequest<Profile>('/profiles/me'),
  updateProfile: (data: UpdateProfileDto) => apiRequest<Profile>('/profiles/me', {
    method: 'PATCH',
    body: JSON.stringify(data),
  }),
  getStats: () => apiRequest<UserStats>('/profiles/stats'),
};

services/api/images.ts (erweitern)

export const imageApi = {
  // Bestehende Funktionen...
  getImages: (params) => apiRequest<Image[]>(`/images?${new URLSearchParams(params)}`),
  getImage: (id) => apiRequest<Image>(`/images/${id}`),
  likeImage: (id) => apiRequest(`/images/${id}/like`, { method: 'POST' }),
  unlikeImage: (id) => apiRequest(`/images/${id}/like`, { method: 'DELETE' }),
  // etc.
};

services/api/explore.ts

export const exploreApi = {
  getPublicImages: (params) => apiRequest<Image[]>(`/explore?${new URLSearchParams(params)}`),
  search: (term, params) => apiRequest<Image[]>(`/explore/search?searchTerm=${term}&${new URLSearchParams(params)}`),
};

services/api/tags.ts

export const tagApi = {
  getTags: () => apiRequest<Tag[]>('/tags'),
  createTag: (data) => apiRequest<Tag>('/tags', { method: 'POST', body: JSON.stringify(data) }),
  updateTag: (id, data) => apiRequest<Tag>(`/tags/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
  deleteTag: (id) => apiRequest(`/tags/${id}`, { method: 'DELETE' }),
  getImageTags: (imageId) => apiRequest<Tag[]>(`/tags/image/${imageId}`),
  addTagToImage: (imageId, tagId) => apiRequest(`/tags/image/${imageId}/${tagId}`, { method: 'POST' }),
  removeTagFromImage: (imageId, tagId) => apiRequest(`/tags/image/${imageId}/${tagId}`, { method: 'DELETE' }),
};

Phase 3: Hooks migrieren

3.1 useImageFetching.ts

  • Ersetze supabase.from('images') durch imageApi.getImages()
  • Pagination über API Query-Parameter

3.2 useImageLikes.ts

  • Ersetze supabase.from('image_likes') durch imageApi.likeImage() / unlikeImage()

3.3 useExploreFetching.ts

  • Ersetze supabase.from('images').eq('is_public', true) durch exploreApi.getPublicImages()

3.4 useArchiveFetching.ts

  • Ersetze supabase.from('images').not('archived_at', 'is', null) durch imageApi.getImages({ archived: true })

Phase 4: Stores migrieren

4.1 tagStore.ts

  • Ersetze alle supabase.from('tags') und supabase.from('image_tags') durch tagApi.*

4.2 batchStore.ts

  • Ersetze supabase.functions.invoke() durch Backend API Calls
  • Ersetze supabase.channel() Realtime durch Polling oder WebSocket zum Backend
  • ODER: Batch-Generierung komplett über Backend API abwickeln

Phase 5: Screens migrieren

5.1 profile.tsx

// Vorher:
const { data } = await supabase.from('profiles').select('*').eq('id', user.id).single();

// Nachher:
const { data } = await profileApi.getMyProfile();

5.2 image/[id].tsx

// Vorher:
const { data } = await supabase.from('images').select('*').eq('id', id).single();

// Nachher:
const { data } = await imageApi.getImage(id);

Phase 6: Aufräumen

6.1 Dateien entfernen

  • utils/supabase.ts - Supabase Client
  • Alle Supabase-Typen die nicht mehr gebraucht werden

6.2 Dependencies entfernen

// package.json - Diese entfernen:
"@supabase/supabase-js": "^2.38.4",

6.3 Environment Variables aufräumen

# Nicht mehr benötigt:
EXPO_PUBLIC_SUPABASE_URL
EXPO_PUBLIC_SUPABASE_ANON_KEY

# Weiterhin benötigt:
EXPO_PUBLIC_PICTURE_BACKEND_URL
EXPO_PUBLIC_MANA_CORE_AUTH_URL

Implementierungsreihenfolge

  1. Backend erweitern (Profile, Likes, Rate Limit Endpoints)
  2. API Client erstellen (services/api/client.ts)
  3. Domain APIs erstellen (profiles, images erweitern, explore, tags)
  4. Hooks einzeln migrieren (mit Tests nach jeder Migration)
  5. Stores migrieren (tagStore, batchStore)
  6. Screens migrieren (profile.tsx, image/[id].tsx)
  7. Supabase entfernen (Dependencies, Environment Variables)
  8. Testen (Alle Flows durchgehen)

Geschätzter Aufwand

Phase Aufwand
Phase 1: Backend erweitern ~2-3 Stunden
Phase 2: API Client ~1 Stunde
Phase 3: Hooks migrieren ~2-3 Stunden
Phase 4: Stores migrieren ~1-2 Stunden
Phase 5: Screens migrieren ~1 Stunde
Phase 6: Aufräumen & Testen ~1 Stunde
Gesamt ~8-11 Stunden

Risiken & Hinweise

  1. Realtime-Subscriptions: Supabase Realtime wird durch Polling ersetzt (bereits bei Bildgenerierung umgesetzt)
  2. Batch-Generierung: Der batchStore nutzt Edge Functions - diese müssen ins Backend migriert werden
  3. Storage: Bilder werden weiterhin irgendwo gespeichert - prüfen ob S3/R2 oder weiterhin Supabase Storage
  4. RLS Policies: Backend übernimmt die Autorisierung - alle Queries müssen userId filtern