mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 20:19:39 +02:00
3350 lines
109 KiB
Markdown
3350 lines
109 KiB
Markdown
# Backend Architecture Analysis & Migration Plan
|
||
### Picture AI Image Generation App
|
||
|
||
**Document Version:** 1.0
|
||
**Date:** 2025-10-09
|
||
**Author:** Architecture Review Team
|
||
|
||
---
|
||
|
||
## Executive Summary
|
||
|
||
Picture is an AI image generation application built as a monorepo with web (SvelteKit) and mobile (React Native/Expo) clients. The current backend is **100% Supabase**, leveraging PostgreSQL, Authentication, Storage, and a single Edge Function for image generation.
|
||
|
||
**Current State:** The architecture is functional but has significant scalability and architectural concerns, particularly around the monolithic Edge Function (667 lines) that handles long-running AI image generation tasks.
|
||
|
||
**Key Findings:**
|
||
- Single Edge Function doing too much (API calls, polling, file handling, database updates)
|
||
- No queue system for managing concurrent generations
|
||
- Limited error handling and retry mechanisms
|
||
- Potential cold start issues with Edge Functions
|
||
- Tight coupling to Replicate API within the Edge Function
|
||
|
||
**Recommendations:**
|
||
- **Short-term (0-3 months):** Option E - Refactor Supabase architecture with proper separation of concerns
|
||
- **Long-term (6-12 months):** Option F - Hybrid architecture with dedicated backend for compute-heavy tasks
|
||
- **Cost-conscious alternative:** Option A - Keep Supabase, add lightweight Node.js backend
|
||
|
||
---
|
||
|
||
## Table of Contents
|
||
|
||
1. [Current Architecture Deep Dive](#1-current-architecture-deep-dive)
|
||
2. [Alternative Backend Architectures](#2-alternative-backend-architectures)
|
||
3. [Detailed Comparison Matrix](#3-detailed-comparison-matrix)
|
||
4. [Specific Concerns for Picture App](#4-specific-concerns-for-picture-app)
|
||
5. [Migration Strategies](#5-migration-strategies)
|
||
6. [Final Recommendations](#6-final-recommendations)
|
||
7. [Appendices](#appendices)
|
||
|
||
---
|
||
|
||
## 1. Current Architecture Deep Dive
|
||
|
||
### 1.1 Architecture Overview
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ CLIENT LAYER │
|
||
│ ┌─────────────────────┐ ┌──────────────────────┐ │
|
||
│ │ Web App │ │ Mobile App │ │
|
||
│ │ (SvelteKit) │ │ (Expo/RN) │ │
|
||
│ │ - Tailwind CSS │ │ - NativeWind │ │
|
||
│ │ - Svelte 5 │ │ - React Navigation │ │
|
||
│ └──────────┬──────────┘ └──────────┬───────────┘ │
|
||
└─────────────┼────────────────────────────────┼──────────────────┘
|
||
│ │
|
||
└────────────────┬───────────────┘
|
||
│
|
||
┌────────────────▼─────────────────┐
|
||
│ @picture/shared Package │
|
||
│ - Supabase Client Factory │
|
||
│ - Database Types (auto-gen) │
|
||
│ - Shared Utilities │
|
||
└────────────────┬─────────────────┘
|
||
│
|
||
┌────────────────▼─────────────────────────────────┐
|
||
│ SUPABASE BACKEND │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────┐ │
|
||
│ │ PostgreSQL Database │ │
|
||
│ │ - 12 Tables (profiles, images, etc) │ │
|
||
│ │ - Row Level Security (RLS) enabled │ │
|
||
│ │ - 2 Views (batch_progress, etc) │ │
|
||
│ │ - 6 Functions (rate limiting, etc) │ │
|
||
│ └─────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────┐ │
|
||
│ │ Supabase Auth │ │
|
||
│ │ - Email/Password Authentication │ │
|
||
│ │ - JWT Token Management │ │
|
||
│ │ - Session Persistence │ │
|
||
│ └─────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────┐ │
|
||
│ │ Supabase Storage │ │
|
||
│ │ - Bucket: "generated-images" │ │
|
||
│ │ - Max file size: 50MB │ │
|
||
│ │ - Image transformation enabled │ │
|
||
│ └─────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────┐ │
|
||
│ │ Edge Function: generate-image │ │
|
||
│ │ - 667 lines of TypeScript │ │
|
||
│ │ - Handles 15+ AI models │ │
|
||
│ │ - Polls Replicate API (up to 10 min) │ │
|
||
│ │ - Downloads & uploads images │ │
|
||
│ │ - Updates database records │ │
|
||
│ └──────────────────┬──────────────────────┘ │
|
||
└─────────────────────┼────────────────────────────┘
|
||
│
|
||
┌───────────────▼────────────────┐
|
||
│ Replicate API │
|
||
│ - FLUX, SDXL, Ideogram, etc │
|
||
│ - Async prediction model │
|
||
│ - Pay-per-generation │
|
||
└────────────────────────────────┘
|
||
```
|
||
|
||
### 1.2 Database Schema
|
||
|
||
The application uses **12 tables**, **2 views**, and **6 database functions**:
|
||
|
||
#### Core Tables:
|
||
1. **profiles** (2 rows) - User profiles linked to auth.users
|
||
2. **image_generations** (68 rows) - Generation job tracking
|
||
3. **images** (29 rows) - Completed image records
|
||
4. **models** (14 rows) - AI model configurations
|
||
5. **batch_generations** - Batch job management
|
||
6. **generation_performance** - Performance metrics
|
||
7. **generation_errors** - Error tracking and retry logic
|
||
8. **user_rate_limits** - Rate limiting per user
|
||
9. **tags** - Image tagging system
|
||
10. **image_tags** - Many-to-many relationship
|
||
11. **image_likes** - User favorites
|
||
12. **prompt_templates** - Reusable prompts
|
||
|
||
#### Views:
|
||
- **batch_progress** - Aggregated batch status
|
||
- **multi_generation_groups** - Multi-generation tracking
|
||
|
||
#### Database Functions:
|
||
- `check_rate_limit(p_user_id, p_count)` - Rate limit enforcement
|
||
- `create_multi_generation(...)` - Batch generation setup
|
||
- `get_error_statistics(...)` - Error analytics
|
||
- `get_user_limits(p_user_id)` - Retrieve user quotas
|
||
- `process_error_recovery()` - Automatic error recovery
|
||
- `recover_stale_generations()` - Clean up stuck jobs
|
||
- `schedule_retry(...)` - Retry failed generations
|
||
|
||
### 1.3 Edge Function: generate-image
|
||
|
||
**Location:** `/apps/mobile/supabase/functions/generate-image/index.ts`
|
||
**Size:** 667 lines
|
||
**Purpose:** Complete image generation workflow
|
||
|
||
#### Responsibilities (Too Many!):
|
||
|
||
1. **Authentication & Authorization**
|
||
- Verify JWT token
|
||
- Get user context
|
||
- Create admin client for RLS bypass
|
||
|
||
2. **Model Configuration**
|
||
- Parse 15+ different model parameter formats
|
||
- Handle aspect ratio conversions
|
||
- Map model-specific parameters
|
||
|
||
3. **Image-to-Image Processing**
|
||
- Download source images
|
||
- Convert to base64
|
||
- Validate image formats
|
||
|
||
4. **Replicate API Integration**
|
||
- Call Replicate predictions API
|
||
- Poll for completion (up to 120 attempts × 2s = 4 minutes max)
|
||
- Handle different model response formats
|
||
|
||
5. **File Management**
|
||
- Download generated images from Replicate
|
||
- Process different image formats (webp, png, jpeg, svg)
|
||
- Upload to Supabase Storage
|
||
|
||
6. **Database Operations**
|
||
- Create image records
|
||
- Update generation status
|
||
- Track performance metrics
|
||
- Log errors
|
||
|
||
7. **Error Handling**
|
||
- Catch and log errors
|
||
- Update failed generation records
|
||
- Return error responses
|
||
|
||
#### Supported Models:
|
||
- FLUX (Schnell, Dev, 1.1 Pro, Krea Dev)
|
||
- SDXL (Regular, Lightning, Refiner)
|
||
- Ideogram V3 Turbo
|
||
- Imagen 4 Fast
|
||
- Recraft V3 (Regular + SVG)
|
||
- Stable Diffusion 3.5
|
||
- SeeDream 3 & 4
|
||
- Qwen Image
|
||
|
||
### 1.4 Client-Side Architecture
|
||
|
||
#### Web App (SvelteKit)
|
||
- **Framework:** SvelteKit + Svelte 5
|
||
- **Styling:** Tailwind CSS v4
|
||
- **API Layer:** `/apps/web/src/lib/api/`
|
||
- `generate.ts` - Image generation
|
||
- `images.ts` - Image CRUD operations
|
||
- `models.ts` - Model management
|
||
- `tags.ts` - Tagging system
|
||
- `upload.ts` - Image upload (NEW)
|
||
|
||
#### Mobile App (Expo/React Native)
|
||
- **Framework:** Expo SDK 54 + React Native 0.81
|
||
- **Navigation:** Expo Router (file-based)
|
||
- **Styling:** NativeWind (Tailwind for RN)
|
||
- **State:** Zustand
|
||
|
||
#### Shared Package (@picture/shared)
|
||
```typescript
|
||
// packages/shared/src/api/supabase.ts
|
||
export function createSupabaseClient(url: string, key: string) {
|
||
return createClient<Database>(url, key)
|
||
}
|
||
|
||
// Auto-generated types from database schema
|
||
export type Database = { /* 970 lines of types */ }
|
||
```
|
||
|
||
### 1.5 Current Pain Points
|
||
|
||
#### 1.5.1 Monolithic Edge Function
|
||
- **Single Responsibility Violation:** Does everything from auth to file uploads
|
||
- **Hard to Test:** 667 lines, tightly coupled
|
||
- **Hard to Debug:** All logs in one function
|
||
- **No Separation:** Business logic + infrastructure code mixed
|
||
|
||
#### 1.5.2 Long-Running Process Issues
|
||
- **Synchronous Polling:** Blocks for 2-120 seconds per generation
|
||
- **No Queue System:** Can't prioritize or manage concurrent jobs
|
||
- **Cold Starts:** Edge Functions may have cold start delays
|
||
- **Timeout Risk:** 10-minute max for Edge Functions
|
||
|
||
#### 1.5.3 Scalability Concerns
|
||
- **No Horizontal Scaling:** Single function handles all requests
|
||
- **No Load Balancing:** Can't distribute work
|
||
- **Rate Limiting:** Basic, not sophisticated
|
||
- **No Backpressure:** Can't handle traffic spikes
|
||
|
||
#### 1.5.4 Error Handling
|
||
- **Limited Retry Logic:** Basic retry in database functions
|
||
- **No Circuit Breaker:** Doesn't prevent cascading failures
|
||
- **Manual Recovery:** Requires `recover_stale_generations()`
|
||
|
||
#### 1.5.5 Real-Time Updates
|
||
- **Polling Required:** Clients must poll for generation status
|
||
- **No WebSockets:** Can't push completion notifications
|
||
- **No Server-Sent Events:** No real-time progress updates
|
||
|
||
#### 1.5.6 Cost Optimization
|
||
- **No Caching:** Repeated Replicate API calls
|
||
- **No Batch Optimization:** Each generation is independent
|
||
- **Resource Waste:** Edge Function runs even when just polling
|
||
|
||
### 1.6 Current Strengths
|
||
|
||
1. **Simple Architecture:** Easy to understand, single backend
|
||
2. **Rapid Development:** Supabase provides everything out-of-box
|
||
3. **Type Safety:** Auto-generated TypeScript types from schema
|
||
4. **RLS Security:** Row-level security enforced at database level
|
||
5. **Shared Client:** Single Supabase client across web + mobile
|
||
6. **No DevOps:** Managed infrastructure, automatic scaling
|
||
7. **Cost Effective (Currently):** Free tier + pay-per-use for small scale
|
||
|
||
---
|
||
|
||
## 2. Alternative Backend Architectures
|
||
|
||
### Option A: Keep Supabase + Add Custom Backend
|
||
|
||
**Philosophy:** Minimal change, add a lightweight backend for compute tasks.
|
||
|
||
#### Architecture:
|
||
|
||
```
|
||
┌─────────────┐ ┌─────────────┐
|
||
│ Web App │────▶│ Supabase │
|
||
│ Mobile App │ │ - Database │
|
||
└─────────────┘ │ - Auth │
|
||
│ │ - Storage │
|
||
│ └─────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────┐
|
||
│ Custom Backend │
|
||
│ (Node.js + Express/Fastify) │
|
||
│ - Image generation endpoint │
|
||
│ - Replicate API integration │
|
||
│ - Job queue (BullMQ) │
|
||
│ - File processing │
|
||
└─────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────┐
|
||
│ Replicate │
|
||
│ API │
|
||
└─────────────┘
|
||
```
|
||
|
||
#### Implementation Details:
|
||
|
||
**Backend Stack:**
|
||
- **Runtime:** Node.js 20+ or Bun
|
||
- **Framework:** Fastify (for performance) or Express (for simplicity)
|
||
- **Queue:** BullMQ + Redis for job management
|
||
- **Deployment:** Railway, Render, Fly.io, or AWS Fargate
|
||
|
||
**Code Structure:**
|
||
```
|
||
custom-backend/
|
||
├── src/
|
||
│ ├── routes/
|
||
│ │ └── generate.ts # POST /api/generate
|
||
│ ├── services/
|
||
│ │ ├── replicate.service.ts # Replicate API wrapper
|
||
│ │ ├── storage.service.ts # Supabase Storage client
|
||
│ │ └── queue.service.ts # BullMQ job management
|
||
│ ├── workers/
|
||
│ │ └── generation.worker.ts # Background job processor
|
||
│ ├── middleware/
|
||
│ │ └── auth.ts # Verify Supabase JWT
|
||
│ └── server.ts
|
||
├── Dockerfile
|
||
└── package.json
|
||
```
|
||
|
||
**Generation Flow:**
|
||
1. Client calls custom backend: `POST /api/generate`
|
||
2. Backend verifies Supabase JWT token
|
||
3. Create generation record in Supabase DB
|
||
4. Add job to BullMQ queue
|
||
5. Return immediately with `generation_id`
|
||
6. Worker processes job asynchronously
|
||
7. Update Supabase DB when complete
|
||
8. Client polls Supabase DB for status (or use webhooks)
|
||
|
||
**Example Code:**
|
||
```typescript
|
||
// routes/generate.ts
|
||
export async function generateImage(req: FastifyRequest, reply: FastifyReply) {
|
||
// Verify Supabase JWT
|
||
const user = await verifySupabaseToken(req.headers.authorization);
|
||
|
||
// Create generation record
|
||
const { data: generation } = await supabase
|
||
.from('image_generations')
|
||
.insert({ user_id: user.id, prompt, status: 'queued' })
|
||
.select()
|
||
.single();
|
||
|
||
// Add to queue
|
||
await generationQueue.add('generate', {
|
||
generation_id: generation.id,
|
||
prompt,
|
||
model_id,
|
||
user_id: user.id
|
||
});
|
||
|
||
return { generation_id: generation.id };
|
||
}
|
||
|
||
// workers/generation.worker.ts
|
||
generationQueue.process('generate', async (job) => {
|
||
const { generation_id, prompt, model_id } = job.data;
|
||
|
||
try {
|
||
// Update status to processing
|
||
await supabase
|
||
.from('image_generations')
|
||
.update({ status: 'processing' })
|
||
.eq('id', generation_id);
|
||
|
||
// Call Replicate API
|
||
const prediction = await replicate.predictions.create({
|
||
version: modelVersion,
|
||
input: { prompt }
|
||
});
|
||
|
||
// Poll for completion
|
||
const result = await replicate.wait(prediction);
|
||
|
||
// Download image
|
||
const imageBuffer = await fetch(result.output[0]).then(r => r.arrayBuffer());
|
||
|
||
// Upload to Supabase Storage
|
||
const { data } = await supabase.storage
|
||
.from('generated-images')
|
||
.upload(`${user_id}/${generation_id}.webp`, imageBuffer);
|
||
|
||
// Create image record
|
||
await supabase.from('images').insert({
|
||
generation_id,
|
||
user_id,
|
||
filename: `${generation_id}.webp`,
|
||
storage_path: data.path,
|
||
public_url: data.publicUrl
|
||
});
|
||
|
||
// Update generation status
|
||
await supabase
|
||
.from('image_generations')
|
||
.update({ status: 'completed', completed_at: new Date() })
|
||
.eq('id', generation_id);
|
||
|
||
} catch (error) {
|
||
await supabase
|
||
.from('image_generations')
|
||
.update({ status: 'failed', error_message: error.message })
|
||
.eq('id', generation_id);
|
||
}
|
||
});
|
||
```
|
||
|
||
#### Pros:
|
||
- Minimal changes to existing architecture
|
||
- Keep all Supabase benefits (DB, Auth, Storage, RLS)
|
||
- Better separation of concerns
|
||
- Proper job queue for async processing
|
||
- Easy to add retry logic, rate limiting
|
||
- Can scale backend independently
|
||
- Lower cost than full migration
|
||
|
||
#### Cons:
|
||
- Need to manage another service (backend + Redis)
|
||
- Slightly more complex deployment
|
||
- Still somewhat coupled to Supabase
|
||
- Need to handle JWT verification manually
|
||
|
||
#### Cost Estimate (Monthly):
|
||
- Supabase: $25 (Pro plan) or stay on free tier
|
||
- Backend: $7-20 (Railway/Render/Fly.io)
|
||
- Redis: $0 (Upstash free tier) or $10 (Redis Cloud)
|
||
- **Total: $7-55/month** (vs. $0-25 currently)
|
||
|
||
---
|
||
|
||
### Option B: Migrate to AWS Amplify
|
||
|
||
**Philosophy:** Full AWS ecosystem, enterprise-grade infrastructure.
|
||
|
||
#### Architecture:
|
||
|
||
```
|
||
┌─────────────┐ ┌──────────────────────────────────────┐
|
||
│ Web App │────▶│ AWS Amplify │
|
||
│ Mobile App │ │ │
|
||
└─────────────┘ │ ┌────────────────────────────────┐ │
|
||
│ │ AWS AppSync (GraphQL) │ │
|
||
│ │ - Auto-generated resolvers │ │
|
||
│ │ - Real-time subscriptions │ │
|
||
│ └────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────┐ │
|
||
│ │ Amazon Cognito │ │
|
||
│ │ - User pools │ │
|
||
│ │ - Identity pools │ │
|
||
│ └────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────┐ │
|
||
│ │ AWS Lambda Functions │ │
|
||
│ │ - generateImage │ │
|
||
│ │ - processGeneration │ │
|
||
│ └────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────┐ │
|
||
│ │ Amazon DynamoDB │ │
|
||
│ │ - Serverless NoSQL │ │
|
||
│ │ - Auto-scaling │ │
|
||
│ └────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────┐ │
|
||
│ │ Amazon S3 │ │
|
||
│ │ - Image storage │ │
|
||
│ │ - CloudFront CDN │ │
|
||
│ └────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────┐ │
|
||
│ │ Amazon SQS/EventBridge │ │
|
||
│ │ - Job queue │ │
|
||
│ │ - Event-driven architecture │ │
|
||
│ └────────────────────────────────┘ │
|
||
└──────────────────────────────────────┘
|
||
```
|
||
|
||
#### Implementation Details:
|
||
|
||
**Amplify CLI Setup:**
|
||
```bash
|
||
amplify init
|
||
amplify add auth # Cognito User Pool
|
||
amplify add api # AppSync GraphQL API
|
||
amplify add storage # S3 bucket
|
||
amplify add function # Lambda functions
|
||
amplify push
|
||
```
|
||
|
||
**GraphQL Schema:**
|
||
```graphql
|
||
type User @model @auth(rules: [{ allow: owner }]) {
|
||
id: ID!
|
||
username: String
|
||
email: AWSEmail!
|
||
images: [Image] @hasMany
|
||
generations: [Generation] @hasMany
|
||
}
|
||
|
||
type Model @model @auth(rules: [{ allow: public, operations: [read] }]) {
|
||
id: ID!
|
||
name: String!
|
||
displayName: String!
|
||
replicateId: String!
|
||
defaultSteps: Int
|
||
defaultGuidance: Float
|
||
}
|
||
|
||
type Generation @model @auth(rules: [{ allow: owner }]) {
|
||
id: ID!
|
||
user: User @belongsTo
|
||
model: Model @belongsTo
|
||
prompt: String!
|
||
status: GenerationStatus!
|
||
error: String
|
||
createdAt: AWSDateTime!
|
||
completedAt: AWSDateTime
|
||
image: Image @hasOne
|
||
}
|
||
|
||
type Image @model @auth(rules: [{ allow: owner }]) {
|
||
id: ID!
|
||
user: User @belongsTo
|
||
generation: Generation @belongsTo
|
||
filename: String!
|
||
s3Key: String!
|
||
publicUrl: AWSURL!
|
||
width: Int
|
||
height: Int
|
||
format: String
|
||
createdAt: AWSDateTime!
|
||
}
|
||
|
||
enum GenerationStatus {
|
||
QUEUED
|
||
PROCESSING
|
||
COMPLETED
|
||
FAILED
|
||
}
|
||
```
|
||
|
||
**Lambda Function (TypeScript):**
|
||
```typescript
|
||
// amplify/backend/function/processGeneration/src/index.ts
|
||
import { SQSHandler } from 'aws-lambda';
|
||
import Replicate from 'replicate';
|
||
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
||
import { DynamoDBDocumentClient, UpdateCommand } from '@aws-sdk/lib-dynamodb';
|
||
|
||
const replicate = new Replicate({ auth: process.env.REPLICATE_API_TOKEN });
|
||
const s3 = new S3Client({});
|
||
const docClient = DynamoDBDocumentClient.from(new DynamoDBClient({}));
|
||
|
||
export const handler: SQSHandler = async (event) => {
|
||
for (const record of event.Records) {
|
||
const { generationId, prompt, modelId, userId } = JSON.parse(record.body);
|
||
|
||
try {
|
||
// Update status to processing
|
||
await updateGenerationStatus(generationId, 'PROCESSING');
|
||
|
||
// Call Replicate
|
||
const output = await replicate.run(modelId, { input: { prompt } });
|
||
|
||
// Download image
|
||
const response = await fetch(output[0]);
|
||
const buffer = await response.arrayBuffer();
|
||
|
||
// Upload to S3
|
||
const s3Key = `${userId}/${generationId}.webp`;
|
||
await s3.send(new PutObjectCommand({
|
||
Bucket: process.env.S3_BUCKET,
|
||
Key: s3Key,
|
||
Body: Buffer.from(buffer),
|
||
ContentType: 'image/webp'
|
||
}));
|
||
|
||
// Create image record via AppSync mutation
|
||
await createImageRecord(generationId, s3Key);
|
||
|
||
// Update generation status
|
||
await updateGenerationStatus(generationId, 'COMPLETED');
|
||
|
||
} catch (error) {
|
||
await updateGenerationStatus(generationId, 'FAILED', error.message);
|
||
}
|
||
}
|
||
};
|
||
```
|
||
|
||
**Real-time Subscriptions (Client):**
|
||
```typescript
|
||
// Subscribe to generation updates
|
||
const subscription = API.graphql(
|
||
graphqlOperation(`
|
||
subscription OnGenerationUpdate($userId: ID!) {
|
||
onUpdateGeneration(filter: { userId: { eq: $userId } }) {
|
||
id
|
||
status
|
||
error
|
||
image {
|
||
publicUrl
|
||
}
|
||
}
|
||
}
|
||
`, { userId })
|
||
).subscribe({
|
||
next: ({ value }) => {
|
||
const generation = value.data.onUpdateGeneration;
|
||
if (generation.status === 'COMPLETED') {
|
||
showNotification('Image ready!', generation.image.publicUrl);
|
||
}
|
||
}
|
||
});
|
||
```
|
||
|
||
#### Pros:
|
||
- Enterprise-grade infrastructure
|
||
- Real-time subscriptions built-in (AppSync)
|
||
- GraphQL API auto-generated from schema
|
||
- Excellent mobile SDK (AWS Amplify)
|
||
- Fine-grained security with Cognito
|
||
- Scales automatically (DynamoDB, Lambda)
|
||
- AWS ecosystem benefits (CloudWatch, X-Ray, etc.)
|
||
- Strong offline support with DataStore
|
||
|
||
#### Cons:
|
||
- Steep learning curve (AWS complexity)
|
||
- Vendor lock-in (heavily AWS-specific)
|
||
- DynamoDB has different query patterns than SQL
|
||
- More expensive at scale
|
||
- Amplify CLI can be finicky
|
||
- Migration effort is HIGH (complete rewrite)
|
||
- Need to learn AWS services
|
||
|
||
#### Cost Estimate (Monthly):
|
||
- AWS Amplify Hosting: $15-50
|
||
- AppSync: $4 per million queries + $2 per million minutes
|
||
- Lambda: $0.20 per million requests (likely $5-20)
|
||
- DynamoDB: $1.25 per million writes + $0.25 per million reads ($10-50)
|
||
- S3 + CloudFront: $5-20
|
||
- Cognito: Free for <50k MAU
|
||
- **Total: $40-160/month** for moderate traffic
|
||
|
||
---
|
||
|
||
### Option C: Firebase
|
||
|
||
**Philosophy:** Google's BaaS (Backend as a Service), mobile-first approach.
|
||
|
||
#### Architecture:
|
||
|
||
```
|
||
┌─────────────┐ ┌──────────────────────────────────────┐
|
||
│ Web App │────▶│ Firebase │
|
||
│ Mobile App │ │ │
|
||
└─────────────┘ │ ┌────────────────────────────────┐ │
|
||
│ │ Firebase Authentication │ │
|
||
│ │ - Email/Password, OAuth │ │
|
||
│ │ - Custom claims │ │
|
||
│ └────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────┐ │
|
||
│ │ Cloud Firestore │ │
|
||
│ │ - NoSQL document database │ │
|
||
│ │ - Real-time sync │ │
|
||
│ │ - Offline support │ │
|
||
│ └────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────┐ │
|
||
│ │ Cloud Functions (Node.js) │ │
|
||
│ │ - generateImage (HTTP) │ │
|
||
│ │ - processGeneration (Queue) │ │
|
||
│ │ - onImageCreated (Trigger) │ │
|
||
│ └────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────┐ │
|
||
│ │ Firebase Storage (GCS) │ │
|
||
│ │ - Image uploads │ │
|
||
│ │ - CDN-backed │ │
|
||
│ └────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────┐ │
|
||
│ │ Cloud Tasks │ │
|
||
│ │ - Job queue for async work │ │
|
||
│ └────────────────────────────────┘ │
|
||
└──────────────────────────────────────┘
|
||
```
|
||
|
||
#### Implementation Details:
|
||
|
||
**Firestore Data Model:**
|
||
```typescript
|
||
// collections/users/{userId}
|
||
interface User {
|
||
username: string;
|
||
email: string;
|
||
createdAt: Timestamp;
|
||
}
|
||
|
||
// collections/models/{modelId}
|
||
interface Model {
|
||
name: string;
|
||
displayName: string;
|
||
replicateId: string;
|
||
defaultSteps: number;
|
||
defaultGuidance: number;
|
||
isActive: boolean;
|
||
}
|
||
|
||
// collections/generations/{generationId}
|
||
interface Generation {
|
||
userId: string;
|
||
modelId: string;
|
||
prompt: string;
|
||
status: 'queued' | 'processing' | 'completed' | 'failed';
|
||
error?: string;
|
||
createdAt: Timestamp;
|
||
completedAt?: Timestamp;
|
||
}
|
||
|
||
// collections/images/{imageId}
|
||
interface Image {
|
||
userId: string;
|
||
generationId: string;
|
||
filename: string;
|
||
storagePath: string;
|
||
publicUrl: string;
|
||
width: number;
|
||
height: number;
|
||
createdAt: Timestamp;
|
||
}
|
||
```
|
||
|
||
**Security Rules (firestore.rules):**
|
||
```javascript
|
||
rules_version = '2';
|
||
service cloud.firestore {
|
||
match /databases/{database}/documents {
|
||
// Users can only read/write their own data
|
||
match /users/{userId} {
|
||
allow read, write: if request.auth.uid == userId;
|
||
}
|
||
|
||
// Anyone can read models
|
||
match /models/{modelId} {
|
||
allow read: if true;
|
||
allow write: if false; // Admin only via Cloud Functions
|
||
}
|
||
|
||
// Users can only access their own generations
|
||
match /generations/{generationId} {
|
||
allow read: if request.auth.uid == resource.data.userId;
|
||
allow create: if request.auth.uid == request.resource.data.userId;
|
||
allow update: if false; // Updated by Cloud Functions only
|
||
}
|
||
|
||
// Users can only access their own images
|
||
match /images/{imageId} {
|
||
allow read: if request.auth.uid == resource.data.userId;
|
||
allow write: if false; // Created by Cloud Functions only
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**Cloud Function (TypeScript):**
|
||
```typescript
|
||
// functions/src/index.ts
|
||
import * as functions from 'firebase-functions';
|
||
import * as admin from 'firebase-admin';
|
||
import Replicate from 'replicate';
|
||
|
||
admin.initializeApp();
|
||
const db = admin.firestore();
|
||
const storage = admin.storage();
|
||
|
||
// HTTP-triggered function
|
||
export const generateImage = functions.https.onCall(async (data, context) => {
|
||
// Verify authentication
|
||
if (!context.auth) {
|
||
throw new functions.https.HttpsError('unauthenticated', 'Must be logged in');
|
||
}
|
||
|
||
const { prompt, modelId } = data;
|
||
const userId = context.auth.uid;
|
||
|
||
// Create generation record
|
||
const generationRef = await db.collection('generations').add({
|
||
userId,
|
||
modelId,
|
||
prompt,
|
||
status: 'queued',
|
||
createdAt: admin.firestore.FieldValue.serverTimestamp()
|
||
});
|
||
|
||
// Queue processing task
|
||
await enqueueProcessGeneration(generationRef.id, { userId, prompt, modelId });
|
||
|
||
return { generationId: generationRef.id };
|
||
});
|
||
|
||
// Queue-triggered function
|
||
export const processGeneration = functions.tasks.taskQueue().onDispatch(async (data) => {
|
||
const { generationId, userId, prompt, modelId } = data;
|
||
|
||
try {
|
||
// Update status
|
||
await db.collection('generations').doc(generationId).update({
|
||
status: 'processing'
|
||
});
|
||
|
||
// Get model config
|
||
const modelDoc = await db.collection('models').doc(modelId).get();
|
||
const model = modelDoc.data();
|
||
|
||
// Call Replicate
|
||
const replicate = new Replicate({ auth: process.env.REPLICATE_API_TOKEN });
|
||
const output = await replicate.run(model.replicateId, {
|
||
input: { prompt }
|
||
}) as string[];
|
||
|
||
// Download image
|
||
const response = await fetch(output[0]);
|
||
const buffer = await response.arrayBuffer();
|
||
|
||
// Upload to Firebase Storage
|
||
const filename = `${generationId}.webp`;
|
||
const filePath = `users/${userId}/images/${filename}`;
|
||
const file = storage.bucket().file(filePath);
|
||
|
||
await file.save(Buffer.from(buffer), {
|
||
contentType: 'image/webp',
|
||
metadata: {
|
||
firebaseStorageDownloadTokens: generationId
|
||
}
|
||
});
|
||
|
||
const publicUrl = await file.getSignedUrl({
|
||
action: 'read',
|
||
expires: '03-01-2500'
|
||
});
|
||
|
||
// Create image record
|
||
await db.collection('images').add({
|
||
userId,
|
||
generationId,
|
||
filename,
|
||
storagePath: filePath,
|
||
publicUrl: publicUrl[0],
|
||
width: 1024,
|
||
height: 1024,
|
||
createdAt: admin.firestore.FieldValue.serverTimestamp()
|
||
});
|
||
|
||
// Update generation
|
||
await db.collection('generations').doc(generationId).update({
|
||
status: 'completed',
|
||
completedAt: admin.firestore.FieldValue.serverTimestamp()
|
||
});
|
||
|
||
} catch (error) {
|
||
await db.collection('generations').doc(generationId).update({
|
||
status: 'failed',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
// Real-time listener (Firestore trigger)
|
||
export const onGenerationComplete = functions.firestore
|
||
.document('generations/{generationId}')
|
||
.onUpdate(async (change, context) => {
|
||
const before = change.before.data();
|
||
const after = change.after.data();
|
||
|
||
// Send push notification when completed
|
||
if (before.status !== 'completed' && after.status === 'completed') {
|
||
// Send FCM notification to user
|
||
await admin.messaging().send({
|
||
token: after.fcmToken,
|
||
notification: {
|
||
title: 'Image Ready!',
|
||
body: 'Your generated image is ready to view'
|
||
}
|
||
});
|
||
}
|
||
});
|
||
```
|
||
|
||
**Client-Side Real-time Sync:**
|
||
```typescript
|
||
// React Native with Firebase SDK
|
||
import firestore from '@react-native-firebase/firestore';
|
||
|
||
// Subscribe to generation updates
|
||
const unsubscribe = firestore()
|
||
.collection('generations')
|
||
.doc(generationId)
|
||
.onSnapshot((doc) => {
|
||
const generation = doc.data();
|
||
if (generation.status === 'completed') {
|
||
// Fetch the image
|
||
firestore()
|
||
.collection('images')
|
||
.where('generationId', '==', generationId)
|
||
.get()
|
||
.then((snapshot) => {
|
||
const image = snapshot.docs[0].data();
|
||
showImage(image.publicUrl);
|
||
});
|
||
}
|
||
});
|
||
```
|
||
|
||
#### Pros:
|
||
- Excellent mobile SDK (best-in-class for React Native)
|
||
- Real-time sync out-of-the-box
|
||
- Offline support built-in
|
||
- Simple, intuitive API
|
||
- Generous free tier (Spark plan)
|
||
- Google Cloud integration (if needed)
|
||
- Firebase Extensions marketplace
|
||
- Strong authentication system
|
||
- Push notifications included (FCM)
|
||
|
||
#### Cons:
|
||
- NoSQL only (no relational queries)
|
||
- Query limitations (can be restrictive)
|
||
- Vendor lock-in (Google-specific)
|
||
- Cloud Functions can be slow (cold starts)
|
||
- Limited TypeScript support historically
|
||
- Need to learn NoSQL data modeling
|
||
- Pricing can get expensive with reads/writes
|
||
- Complete migration required
|
||
|
||
#### Cost Estimate (Monthly):
|
||
- Firebase Authentication: Free (<50k MAU)
|
||
- Firestore: $0.06 per 100k reads + $0.18 per 100k writes (~$10-30)
|
||
- Cloud Functions: $0.40 per million invocations (~$5-15)
|
||
- Firebase Storage: $0.026/GB stored + $0.12/GB downloaded (~$5-20)
|
||
- Cloud Tasks: $0.40 per million tasks (~$1-5)
|
||
- **Total: $21-70/month** for moderate traffic
|
||
|
||
---
|
||
|
||
### Option D: Custom Backend (Full Control)
|
||
|
||
**Philosophy:** Build exactly what you need, full flexibility.
|
||
|
||
#### Architecture:
|
||
|
||
```
|
||
┌─────────────┐ ┌──────────────────────────────────────┐
|
||
│ Web App │────▶│ Custom API Backend │
|
||
│ Mobile App │ │ (NestJS / tRPC / Hono) │
|
||
└─────────────┘ │ │
|
||
│ ┌────────────────────────────────┐ │
|
||
│ │ API Layer │ │
|
||
│ │ - REST or tRPC │ │
|
||
│ │ - GraphQL (optional) │ │
|
||
│ │ - WebSocket for real-time │ │
|
||
│ └────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────┐ │
|
||
│ │ Business Logic Layer │ │
|
||
│ │ - Services │ │
|
||
│ │ - Domain models │ │
|
||
│ └────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────┐ │
|
||
│ │ Queue System │ │
|
||
│ │ - BullMQ + Redis │ │
|
||
│ │ - Job processors │ │
|
||
│ └────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────┐ │
|
||
│ │ Authentication │ │
|
||
│ │ - Clerk / Auth.js / Custom JWT │ │
|
||
│ └────────────────────────────────┘ │
|
||
└──────────────────────────────────────┘
|
||
│ │
|
||
┌─────────────┴──────┐ │
|
||
▼ ▼ ▼
|
||
┌─────────────────┐ ┌──────────────────┐
|
||
│ PostgreSQL │ │ Object Storage │
|
||
│ - AWS RDS │ │ - S3 / R2 │
|
||
│ - PlanetScale │ │ - CloudFlare │
|
||
│ - Neon │ └──────────────────┘
|
||
└─────────────────┘
|
||
```
|
||
|
||
#### Technology Stack Options:
|
||
|
||
**Option D1: NestJS (Enterprise-grade)**
|
||
```
|
||
Backend: NestJS (TypeScript, decorators, dependency injection)
|
||
Database: PostgreSQL with Prisma ORM
|
||
Auth: Passport.js with JWT
|
||
Queue: BullMQ + Redis
|
||
Storage: AWS S3 or Cloudflare R2
|
||
Real-time: Socket.io or WebSockets
|
||
Deployment: Docker on AWS ECS/Fargate or Kubernetes
|
||
```
|
||
|
||
**Option D2: tRPC (Type-safe, end-to-end)**
|
||
```
|
||
Backend: tRPC with Express/Fastify
|
||
Database: PostgreSQL with Drizzle ORM or Prisma
|
||
Auth: NextAuth.js (Auth.js) or Clerk
|
||
Queue: BullMQ + Redis
|
||
Storage: Cloudflare R2 (S3-compatible)
|
||
Real-time: tRPC subscriptions
|
||
Deployment: Vercel for API, Railway for workers
|
||
```
|
||
|
||
**Option D3: Hono (Ultra-fast, edge-first)**
|
||
```
|
||
Backend: Hono (runs on Cloudflare Workers)
|
||
Database: Neon (serverless Postgres) or Turso (SQLite)
|
||
Auth: Clerk or custom JWT
|
||
Queue: Cloudflare Queues or Inngest
|
||
Storage: Cloudflare R2
|
||
Real-time: Cloudflare Durable Objects
|
||
Deployment: Cloudflare Workers
|
||
```
|
||
|
||
#### Implementation Example (NestJS):
|
||
|
||
**Project Structure:**
|
||
```
|
||
custom-backend/
|
||
├── src/
|
||
│ ├── auth/
|
||
│ │ ├── auth.module.ts
|
||
│ │ ├── auth.service.ts
|
||
│ │ ├── auth.controller.ts
|
||
│ │ ├── jwt.strategy.ts
|
||
│ │ └── guards/
|
||
│ ├── users/
|
||
│ │ ├── users.module.ts
|
||
│ │ ├── users.service.ts
|
||
│ │ ├── users.controller.ts
|
||
│ │ └── entities/user.entity.ts
|
||
│ ├── models/
|
||
│ │ ├── models.module.ts
|
||
│ │ ├── models.service.ts
|
||
│ │ └── models.controller.ts
|
||
│ ├── generations/
|
||
│ │ ├── generations.module.ts
|
||
│ │ ├── generations.service.ts
|
||
│ │ ├── generations.controller.ts
|
||
│ │ ├── generations.gateway.ts (WebSocket)
|
||
│ │ └── processors/generation.processor.ts
|
||
│ ├── images/
|
||
│ │ ├── images.module.ts
|
||
│ │ ├── images.service.ts
|
||
│ │ └── images.controller.ts
|
||
│ ├── storage/
|
||
│ │ ├── storage.module.ts
|
||
│ │ └── storage.service.ts (S3 client)
|
||
│ ├── replicate/
|
||
│ │ ├── replicate.module.ts
|
||
│ │ └── replicate.service.ts
|
||
│ ├── queue/
|
||
│ │ ├── queue.module.ts
|
||
│ │ └── queue.service.ts
|
||
│ ├── database/
|
||
│ │ └── prisma.service.ts
|
||
│ ├── app.module.ts
|
||
│ └── main.ts
|
||
├── prisma/
|
||
│ └── schema.prisma
|
||
├── Dockerfile
|
||
└── docker-compose.yml
|
||
```
|
||
|
||
**Prisma Schema:**
|
||
```prisma
|
||
// prisma/schema.prisma
|
||
generator client {
|
||
provider = "prisma-client-js"
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
url = env("DATABASE_URL")
|
||
}
|
||
|
||
model User {
|
||
id String @id @default(uuid())
|
||
email String @unique
|
||
username String? @unique
|
||
passwordHash String?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
generations Generation[]
|
||
images Image[]
|
||
|
||
@@map("users")
|
||
}
|
||
|
||
model Model {
|
||
id String @id @default(uuid())
|
||
name String @unique
|
||
displayName String
|
||
replicateId String
|
||
version String?
|
||
description String?
|
||
defaultSteps Int @default(30)
|
||
defaultGuidance Decimal @default(7.5)
|
||
defaultWidth Int @default(1024)
|
||
defaultHeight Int @default(1024)
|
||
supportsNegPrompt Boolean @default(true)
|
||
supportsSeed Boolean @default(true)
|
||
supportsImg2Img Boolean @default(false)
|
||
isActive Boolean @default(true)
|
||
sortOrder Int @default(0)
|
||
|
||
generations Generation[]
|
||
|
||
@@map("models")
|
||
}
|
||
|
||
model Generation {
|
||
id String @id @default(uuid())
|
||
userId String
|
||
modelId String
|
||
prompt String
|
||
negativePrompt String?
|
||
width Int @default(1024)
|
||
height Int @default(1024)
|
||
steps Int @default(30)
|
||
guidance Decimal @default(7.5)
|
||
seed Int?
|
||
status GenerationStatus @default(QUEUED)
|
||
errorMessage String?
|
||
replicatePredId String?
|
||
generationTime Int?
|
||
createdAt DateTime @default(now())
|
||
completedAt DateTime?
|
||
|
||
user User @relation(fields: [userId], references: [id])
|
||
model Model @relation(fields: [modelId], references: [id])
|
||
image Image?
|
||
|
||
@@index([userId])
|
||
@@index([status])
|
||
@@map("generations")
|
||
}
|
||
|
||
model Image {
|
||
id String @id @default(uuid())
|
||
generationId String @unique
|
||
userId String
|
||
filename String
|
||
storagePath String
|
||
publicUrl String
|
||
fileSize Int
|
||
width Int
|
||
height Int
|
||
format String @default("webp")
|
||
isPublic Boolean @default(false)
|
||
isFavorite Boolean @default(false)
|
||
createdAt DateTime @default(now())
|
||
archivedAt DateTime?
|
||
|
||
generation Generation @relation(fields: [generationId], references: [id])
|
||
user User @relation(fields: [userId], references: [id])
|
||
|
||
@@index([userId])
|
||
@@map("images")
|
||
}
|
||
|
||
enum GenerationStatus {
|
||
QUEUED
|
||
PROCESSING
|
||
COMPLETED
|
||
FAILED
|
||
}
|
||
```
|
||
|
||
**Generation Service:**
|
||
```typescript
|
||
// src/generations/generations.service.ts
|
||
import { Injectable } from '@nestjs/common';
|
||
import { InjectQueue } from '@nestjs/bull';
|
||
import { Queue } from 'bull';
|
||
import { PrismaService } from '../database/prisma.service';
|
||
|
||
@Injectable()
|
||
export class GenerationsService {
|
||
constructor(
|
||
private prisma: PrismaService,
|
||
@InjectQueue('generation') private generationQueue: Queue,
|
||
) {}
|
||
|
||
async createGeneration(userId: string, dto: CreateGenerationDto) {
|
||
// Create generation record
|
||
const generation = await this.prisma.generation.create({
|
||
data: {
|
||
userId,
|
||
modelId: dto.modelId,
|
||
prompt: dto.prompt,
|
||
negativePrompt: dto.negativePrompt,
|
||
width: dto.width,
|
||
height: dto.height,
|
||
steps: dto.steps,
|
||
guidance: dto.guidance,
|
||
status: 'QUEUED',
|
||
},
|
||
});
|
||
|
||
// Add to queue
|
||
await this.generationQueue.add('process', {
|
||
generationId: generation.id,
|
||
userId,
|
||
modelId: dto.modelId,
|
||
prompt: dto.prompt,
|
||
}, {
|
||
attempts: 3,
|
||
backoff: {
|
||
type: 'exponential',
|
||
delay: 2000,
|
||
},
|
||
});
|
||
|
||
return generation;
|
||
}
|
||
|
||
async getGenerationStatus(generationId: string, userId: string) {
|
||
return this.prisma.generation.findFirst({
|
||
where: { id: generationId, userId },
|
||
include: { image: true, model: true },
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
**Queue Processor:**
|
||
```typescript
|
||
// src/generations/processors/generation.processor.ts
|
||
import { Process, Processor } from '@nestjs/bull';
|
||
import { Job } from 'bull';
|
||
import { ReplicateService } from '../../replicate/replicate.service';
|
||
import { StorageService } from '../../storage/storage.service';
|
||
import { PrismaService } from '../../database/prisma.service';
|
||
import { GenerationsGateway } from '../generations.gateway';
|
||
|
||
@Processor('generation')
|
||
export class GenerationProcessor {
|
||
constructor(
|
||
private replicate: ReplicateService,
|
||
private storage: StorageService,
|
||
private prisma: PrismaService,
|
||
private gateway: GenerationsGateway,
|
||
) {}
|
||
|
||
@Process('process')
|
||
async processGeneration(job: Job) {
|
||
const { generationId, userId, modelId, prompt } = job.data;
|
||
|
||
try {
|
||
// Update status
|
||
await this.prisma.generation.update({
|
||
where: { id: generationId },
|
||
data: { status: 'PROCESSING' },
|
||
});
|
||
|
||
// Notify via WebSocket
|
||
this.gateway.sendStatusUpdate(userId, generationId, 'PROCESSING');
|
||
|
||
// Get model config
|
||
const model = await this.prisma.model.findUnique({
|
||
where: { id: modelId },
|
||
});
|
||
|
||
// Call Replicate
|
||
const startTime = Date.now();
|
||
const output = await this.replicate.generate({
|
||
modelId: model.replicateId,
|
||
version: model.version,
|
||
input: { prompt },
|
||
});
|
||
|
||
const generationTime = Math.floor((Date.now() - startTime) / 1000);
|
||
|
||
// Download image
|
||
const imageBuffer = await this.replicate.downloadImage(output[0]);
|
||
|
||
// Upload to S3/R2
|
||
const filename = `${generationId}.webp`;
|
||
const storagePath = `${userId}/${filename}`;
|
||
const { publicUrl } = await this.storage.uploadImage(
|
||
storagePath,
|
||
imageBuffer,
|
||
);
|
||
|
||
// Create image record
|
||
const image = await this.prisma.image.create({
|
||
data: {
|
||
generationId,
|
||
userId,
|
||
filename,
|
||
storagePath,
|
||
publicUrl,
|
||
fileSize: imageBuffer.length,
|
||
width: 1024,
|
||
height: 1024,
|
||
format: 'webp',
|
||
},
|
||
});
|
||
|
||
// Update generation
|
||
await this.prisma.generation.update({
|
||
where: { id: generationId },
|
||
data: {
|
||
status: 'COMPLETED',
|
||
completedAt: new Date(),
|
||
generationTime,
|
||
},
|
||
});
|
||
|
||
// Notify completion via WebSocket
|
||
this.gateway.sendGenerationComplete(userId, generationId, image);
|
||
|
||
} catch (error) {
|
||
await this.prisma.generation.update({
|
||
where: { id: generationId },
|
||
data: {
|
||
status: 'FAILED',
|
||
errorMessage: error.message,
|
||
},
|
||
});
|
||
|
||
this.gateway.sendStatusUpdate(userId, generationId, 'FAILED', error.message);
|
||
throw error; // Let Bull retry
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**WebSocket Gateway (Real-time):**
|
||
```typescript
|
||
// src/generations/generations.gateway.ts
|
||
import {
|
||
WebSocketGateway,
|
||
WebSocketServer,
|
||
SubscribeMessage,
|
||
OnGatewayConnection,
|
||
} from '@nestjs/websockets';
|
||
import { Server, Socket } from 'socket.io';
|
||
|
||
@WebSocketGateway({ cors: true })
|
||
export class GenerationsGateway implements OnGatewayConnection {
|
||
@WebSocketServer()
|
||
server: Server;
|
||
|
||
handleConnection(client: Socket) {
|
||
// Authenticate and join user-specific room
|
||
const userId = client.handshake.auth.userId;
|
||
client.join(`user:${userId}`);
|
||
}
|
||
|
||
sendStatusUpdate(
|
||
userId: string,
|
||
generationId: string,
|
||
status: string,
|
||
error?: string,
|
||
) {
|
||
this.server.to(`user:${userId}`).emit('generation:status', {
|
||
generationId,
|
||
status,
|
||
error,
|
||
});
|
||
}
|
||
|
||
sendGenerationComplete(userId: string, generationId: string, image: any) {
|
||
this.server.to(`user:${userId}`).emit('generation:complete', {
|
||
generationId,
|
||
image,
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
**Client Integration:**
|
||
```typescript
|
||
// Web/Mobile client
|
||
import io from 'socket.io-client';
|
||
|
||
const socket = io('https://api.picture.com', {
|
||
auth: { userId: currentUser.id, token: authToken }
|
||
});
|
||
|
||
// Listen for generation updates
|
||
socket.on('generation:status', (data) => {
|
||
console.log(`Generation ${data.generationId}: ${data.status}`);
|
||
});
|
||
|
||
socket.on('generation:complete', (data) => {
|
||
showNotification('Image ready!');
|
||
displayImage(data.image.publicUrl);
|
||
});
|
||
|
||
// Trigger generation
|
||
const response = await fetch('https://api.picture.com/api/generations', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ prompt, modelId })
|
||
});
|
||
```
|
||
|
||
#### Pros:
|
||
- **Total Control:** Build exactly what you need
|
||
- **No Vendor Lock-in:** Use any service, switch providers easily
|
||
- **Best Performance:** Optimize for your specific use case
|
||
- **Flexible Tech Stack:** Choose your preferred tools
|
||
- **Cost Optimization:** Pay only for what you use
|
||
- **Advanced Features:** Implement any feature without limitations
|
||
- **SQL Database:** Full relational query power
|
||
- **Real-time:** WebSockets for instant updates
|
||
|
||
#### Cons:
|
||
- **Most Development Time:** Build everything from scratch
|
||
- **DevOps Overhead:** Need to manage infrastructure
|
||
- **Higher Initial Cost:** Time + resources to build
|
||
- **Maintenance Burden:** Ongoing updates, security patches
|
||
- **Scaling Complexity:** Need to design for scale
|
||
- **Team Expertise Required:** Need skilled developers
|
||
|
||
#### Cost Estimate (Monthly):
|
||
**Option D1 (AWS):**
|
||
- RDS PostgreSQL: $15-50 (db.t3.micro to db.t3.small)
|
||
- ECS Fargate: $30-100 (2 tasks, 0.5 vCPU, 1GB RAM)
|
||
- ElastiCache Redis: $15-30 (cache.t3.micro)
|
||
- S3: $5-15
|
||
- ALB: $20
|
||
- **Total: $85-215/month**
|
||
|
||
**Option D2 (Mixed):**
|
||
- Neon Postgres: $19 (Pro plan)
|
||
- Railway (API + Workers): $20-50
|
||
- Upstash Redis: $0-10 (free tier or paid)
|
||
- Cloudflare R2: $0-5 (10GB free)
|
||
- **Total: $39-84/month**
|
||
|
||
**Option D3 (Cloudflare):**
|
||
- Workers: $5 (Paid plan, unlimited requests)
|
||
- Neon/Turso DB: $0-19
|
||
- R2 Storage: $0-5
|
||
- Queues: Included
|
||
- **Total: $5-29/month** (Most cost-effective!)
|
||
|
||
---
|
||
|
||
### Option E: Supabase with Better Architecture
|
||
|
||
**Philosophy:** Keep Supabase but refactor properly - best of both worlds.
|
||
|
||
#### Architecture:
|
||
|
||
```
|
||
┌─────────────┐ ┌────────────────────────────────────────────────┐
|
||
│ Web App │────▶│ SUPABASE │
|
||
│ Mobile App │ │ │
|
||
└─────────────┘ │ ┌──────────────────────────────────────────┐ │
|
||
│ │ PostgreSQL + PostgREST │ │
|
||
│ │ - Same database schema │ │
|
||
│ │ - RLS enabled │ │
|
||
│ │ - Add pg_cron for scheduled tasks │ │
|
||
│ └──────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────┐ │
|
||
│ │ Supabase Auth (no changes) │ │
|
||
│ └──────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────┐ │
|
||
│ │ Supabase Storage (no changes) │ │
|
||
│ └──────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────┐ │
|
||
│ │ Refactored Edge Functions │ │
|
||
│ │ │ │
|
||
│ │ 1. queue-generation (HTTP) │ │
|
||
│ │ - Validate input │ │
|
||
│ │ - Create DB record │ │
|
||
│ │ - Enqueue job │ │
|
||
│ │ - Return immediately │ │
|
||
│ │ │ │
|
||
│ │ 2. prepare-model-input (Internal) │ │
|
||
│ │ - Model-specific parameter mapping │ │
|
||
│ │ - Aspect ratio calculations │ │
|
||
│ │ - Image-to-image preprocessing │ │
|
||
│ │ │ │
|
||
│ │ 3. call-replicate (Internal) │ │
|
||
│ │ - Call Replicate API │ │
|
||
│ │ - Return prediction ID │ │
|
||
│ │ │ │
|
||
│ │ 4. poll-replicate (Scheduled) │ │
|
||
│ │ - Check status of pending jobs │ │
|
||
│ │ - Update DB when complete │ │
|
||
│ │ │ │
|
||
│ │ 5. download-process-image (Internal) │ │
|
||
│ │ - Download from Replicate │ │
|
||
│ │ - Process image │ │
|
||
│ │ - Upload to Storage │ │
|
||
│ │ │ │
|
||
│ │ 6. finalize-generation (Internal) │ │
|
||
│ │ - Create image record │ │
|
||
│ │ - Update generation status │ │
|
||
│ │ - Send notifications │ │
|
||
│ └──────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────┘
|
||
│
|
||
┌─────────────────▼──────────────────┐
|
||
│ External Queue Service │
|
||
│ (Inngest / Trigger.dev / Defer) │
|
||
│ - Job orchestration │
|
||
│ - Retry logic │
|
||
│ - Scheduling │
|
||
└────────────────────────────────────┘
|
||
```
|
||
|
||
#### Implementation Strategy:
|
||
|
||
**1. Add Job Queue System (Inngest - Recommended)**
|
||
|
||
Why Inngest?
|
||
- Works perfectly with serverless
|
||
- No infrastructure to manage
|
||
- Built-in retry, scheduling, monitoring
|
||
- Great DX with TypeScript
|
||
- Free tier: 50k function runs/month
|
||
|
||
```typescript
|
||
// supabase/functions/shared/inngest.ts
|
||
import { Inngest } from 'inngest';
|
||
|
||
export const inngest = new Inngest({
|
||
id: 'picture-app',
|
||
eventKey: Deno.env.get('INNGEST_EVENT_KEY')
|
||
});
|
||
|
||
// Define functions
|
||
export const processGeneration = inngest.createFunction(
|
||
{
|
||
id: 'process-generation',
|
||
retries: 3,
|
||
timeout: '10m'
|
||
},
|
||
{ event: 'generation/queued' },
|
||
async ({ event, step }) => {
|
||
const { generationId, userId, modelId, prompt } = event.data;
|
||
|
||
// Step 1: Prepare model input
|
||
const input = await step.run('prepare-input', async () => {
|
||
return await prepareModelInput(modelId, prompt);
|
||
});
|
||
|
||
// Step 2: Call Replicate
|
||
const prediction = await step.run('call-replicate', async () => {
|
||
return await callReplicate(modelId, input);
|
||
});
|
||
|
||
// Step 3: Poll for completion
|
||
const result = await step.run('poll-completion', async () => {
|
||
return await pollReplicate(prediction.id);
|
||
});
|
||
|
||
// Step 4: Download and process image
|
||
const { imageBuffer, format } = await step.run('download-image', async () => {
|
||
return await downloadImage(result.output[0]);
|
||
});
|
||
|
||
// Step 5: Upload to storage
|
||
const { publicUrl } = await step.run('upload-storage', async () => {
|
||
return await uploadToSupabase(userId, generationId, imageBuffer, format);
|
||
});
|
||
|
||
// Step 6: Finalize in database
|
||
await step.run('finalize-generation', async () => {
|
||
return await finalizeGeneration(generationId, publicUrl);
|
||
});
|
||
|
||
return { success: true, publicUrl };
|
||
}
|
||
);
|
||
```
|
||
|
||
**2. Refactored Edge Functions**
|
||
|
||
```typescript
|
||
// supabase/functions/queue-generation/index.ts
|
||
import { createClient } from '@supabase/supabase-js';
|
||
import { inngest } from '../shared/inngest.ts';
|
||
|
||
Deno.serve(async (req) => {
|
||
// Auth verification
|
||
const authHeader = req.headers.get('Authorization');
|
||
const supabase = createClient(
|
||
Deno.env.get('SUPABASE_URL')!,
|
||
Deno.env.get('SUPABASE_ANON_KEY')!,
|
||
{ global: { headers: { Authorization: authHeader! } } }
|
||
);
|
||
|
||
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
||
if (authError || !user) {
|
||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||
status: 401
|
||
});
|
||
}
|
||
|
||
// Parse request
|
||
const { prompt, modelId, width, height } = await req.json();
|
||
|
||
// Create generation record
|
||
const { data: generation, error } = await supabase
|
||
.from('image_generations')
|
||
.insert({
|
||
user_id: user.id,
|
||
model_id: modelId,
|
||
prompt,
|
||
width,
|
||
height,
|
||
status: 'queued'
|
||
})
|
||
.select()
|
||
.single();
|
||
|
||
if (error) {
|
||
return new Response(JSON.stringify({ error: error.message }), {
|
||
status: 400
|
||
});
|
||
}
|
||
|
||
// Send event to Inngest
|
||
await inngest.send({
|
||
name: 'generation/queued',
|
||
data: {
|
||
generationId: generation.id,
|
||
userId: user.id,
|
||
modelId,
|
||
prompt,
|
||
width,
|
||
height
|
||
}
|
||
});
|
||
|
||
return new Response(
|
||
JSON.stringify({
|
||
generationId: generation.id,
|
||
status: 'queued'
|
||
}),
|
||
{ status: 200 }
|
||
);
|
||
});
|
||
```
|
||
|
||
```typescript
|
||
// supabase/functions/shared/replicate.ts
|
||
export async function prepareModelInput(modelId: string, prompt: string, params: any) {
|
||
// Extract model-specific logic from monolith
|
||
if (modelId.includes('flux-schnell')) {
|
||
return {
|
||
prompt,
|
||
aspect_ratio: calculateAspectRatio(params.width, params.height),
|
||
num_inference_steps: params.steps || 4,
|
||
output_format: 'webp'
|
||
};
|
||
}
|
||
// ... other models
|
||
}
|
||
|
||
export async function callReplicate(modelId: string, input: any) {
|
||
const REPLICATE_API_TOKEN = Deno.env.get('REPLICATE_API_TOKEN');
|
||
|
||
const response = await fetch('https://api.replicate.com/v1/predictions', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Token ${REPLICATE_API_TOKEN}`,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ version: modelId, input })
|
||
});
|
||
|
||
return await response.json();
|
||
}
|
||
|
||
export async function pollReplicate(predictionId: string): Promise<any> {
|
||
const REPLICATE_API_TOKEN = Deno.env.get('REPLICATE_API_TOKEN');
|
||
|
||
let attempts = 0;
|
||
while (attempts < 120) {
|
||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||
|
||
const response = await fetch(
|
||
`https://api.replicate.com/v1/predictions/${predictionId}`,
|
||
{
|
||
headers: { 'Authorization': `Token ${REPLICATE_API_TOKEN}` }
|
||
}
|
||
);
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.status === 'succeeded') {
|
||
return result;
|
||
} else if (result.status === 'failed') {
|
||
throw new Error(result.error || 'Generation failed');
|
||
}
|
||
|
||
attempts++;
|
||
}
|
||
|
||
throw new Error('Generation timeout');
|
||
}
|
||
```
|
||
|
||
**3. Add Real-time Notifications with Supabase Realtime**
|
||
|
||
```typescript
|
||
// Client-side (Web/Mobile)
|
||
import { supabase } from '@picture/shared';
|
||
|
||
// Subscribe to generation updates
|
||
const subscription = supabase
|
||
.channel('generations')
|
||
.on(
|
||
'postgres_changes',
|
||
{
|
||
event: 'UPDATE',
|
||
schema: 'public',
|
||
table: 'image_generations',
|
||
filter: `user_id=eq.${userId}`
|
||
},
|
||
(payload) => {
|
||
const generation = payload.new;
|
||
if (generation.status === 'completed') {
|
||
showNotification('Image ready!');
|
||
fetchImage(generation.id);
|
||
} else if (generation.status === 'failed') {
|
||
showError(generation.error_message);
|
||
}
|
||
}
|
||
)
|
||
.subscribe();
|
||
```
|
||
|
||
**4. Add Database Functions for Better Logic**
|
||
|
||
```sql
|
||
-- Function to get next queued generation
|
||
CREATE OR REPLACE FUNCTION get_next_queued_generation()
|
||
RETURNS TABLE (
|
||
id uuid,
|
||
user_id uuid,
|
||
model_id uuid,
|
||
prompt text,
|
||
width int,
|
||
height int
|
||
) AS $$
|
||
BEGIN
|
||
RETURN QUERY
|
||
SELECT
|
||
ig.id,
|
||
ig.user_id,
|
||
ig.model_id,
|
||
ig.prompt,
|
||
ig.width,
|
||
ig.height
|
||
FROM image_generations ig
|
||
WHERE ig.status = 'queued'
|
||
ORDER BY ig.priority DESC, ig.created_at ASC
|
||
LIMIT 1
|
||
FOR UPDATE SKIP LOCKED;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
|
||
-- Function to update generation with retry logic
|
||
CREATE OR REPLACE FUNCTION update_generation_status(
|
||
p_generation_id uuid,
|
||
p_status text,
|
||
p_error_message text DEFAULT NULL
|
||
)
|
||
RETURNS void AS $$
|
||
BEGIN
|
||
UPDATE image_generations
|
||
SET
|
||
status = p_status,
|
||
error_message = p_error_message,
|
||
completed_at = CASE WHEN p_status IN ('completed', 'failed') THEN NOW() ELSE NULL END,
|
||
retry_count = CASE WHEN p_status = 'failed' THEN retry_count + 1 ELSE retry_count END
|
||
WHERE id = p_generation_id;
|
||
|
||
-- If failed and retry count < 3, requeue
|
||
IF p_status = 'failed' AND (SELECT retry_count FROM image_generations WHERE id = p_generation_id) < 3 THEN
|
||
UPDATE image_generations
|
||
SET status = 'queued'
|
||
WHERE id = p_generation_id;
|
||
END IF;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
```
|
||
|
||
**5. Add pg_cron for Cleanup Tasks**
|
||
|
||
```sql
|
||
-- Install pg_cron extension
|
||
CREATE EXTENSION IF NOT EXISTS pg_cron;
|
||
|
||
-- Schedule cleanup of old failed generations (every day at 2 AM)
|
||
SELECT cron.schedule(
|
||
'cleanup-failed-generations',
|
||
'0 2 * * *',
|
||
$$
|
||
DELETE FROM image_generations
|
||
WHERE status = 'failed'
|
||
AND created_at < NOW() - INTERVAL '30 days';
|
||
$$
|
||
);
|
||
|
||
-- Schedule recovery of stale processing generations (every 10 minutes)
|
||
SELECT cron.schedule(
|
||
'recover-stale-generations',
|
||
'*/10 * * * *',
|
||
$$
|
||
UPDATE image_generations
|
||
SET status = 'queued', retry_count = retry_count + 1
|
||
WHERE status = 'processing'
|
||
AND created_at < NOW() - INTERVAL '15 minutes'
|
||
AND retry_count < 3;
|
||
$$
|
||
);
|
||
```
|
||
|
||
#### Pros:
|
||
- **Keep Supabase Benefits:** Database, Auth, Storage, RLS all stay
|
||
- **Better Architecture:** Proper separation of concerns
|
||
- **Job Queue:** Inngest provides enterprise-grade queue
|
||
- **Real-time:** Use Supabase Realtime for notifications
|
||
- **Low Migration Effort:** Refactor existing code, no rewrite
|
||
- **Cost Effective:** Inngest free tier + Supabase Pro
|
||
- **Better Monitoring:** Inngest dashboard shows all jobs
|
||
- **Retry Logic:** Built into Inngest
|
||
- **Type Safety:** Keep existing TypeScript types
|
||
|
||
#### Cons:
|
||
- **Still on Supabase:** Some vendor lock-in remains
|
||
- **Edge Function Limitations:** Still 10-minute timeout
|
||
- **External Dependency:** Rely on Inngest (but stable)
|
||
- **Complexity:** More moving parts than current setup
|
||
|
||
#### Cost Estimate (Monthly):
|
||
- Supabase Pro: $25
|
||
- Inngest: $0 (free tier) or $50 (Team plan for more runs)
|
||
- **Total: $25-75/month**
|
||
|
||
---
|
||
|
||
### Option F: Hybrid/Multi-Cloud
|
||
|
||
**Philosophy:** Use the best tool for each job, keep flexibility.
|
||
|
||
#### Architecture:
|
||
|
||
```
|
||
┌─────────────┐
|
||
│ Web App │────┐
|
||
│ Mobile App │ │
|
||
└─────────────┘ │
|
||
▼
|
||
┌──────────────────────────────────────────────────────────┐
|
||
│ API Gateway / Router │
|
||
│ (Cloudflare Workers / Hono) │
|
||
│ - Route requests to appropriate services │
|
||
│ - JWT verification │
|
||
│ - Rate limiting │
|
||
└──────────────────────────────────────────────────────────┘
|
||
│ │ │ │
|
||
▼ ▼ ▼ ▼
|
||
┌─────────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
||
│ Supabase │ │ Cloudflare│ │ Inngest │ │ Replicate│
|
||
│ │ │ │ │ │ │ API │
|
||
│ - Database │ │ - Storage │ │ - Queue │ │ │
|
||
│ - Auth │ │ (R2) │ │ - Orchestr│ │ │
|
||
│ - RLS │ │ - CDN │ │ │ │ │
|
||
└─────────────┘ └──────────┘ └──────────┘ └──────────┘
|
||
```
|
||
|
||
#### Implementation:
|
||
|
||
**Cloudflare Workers Router:**
|
||
```typescript
|
||
// workers/src/index.ts
|
||
import { Hono } from 'hono';
|
||
import { cors } from 'hono/cors';
|
||
import { jwt } from 'hono/jwt';
|
||
|
||
const app = new Hono();
|
||
|
||
app.use('/*', cors());
|
||
app.use('/api/*', jwt({ secret: Deno.env.get('JWT_SECRET')! }));
|
||
|
||
// Route to Supabase for database operations
|
||
app.get('/api/images', async (c) => {
|
||
const userId = c.get('jwtPayload').sub;
|
||
|
||
const supabase = createClient(
|
||
c.env.SUPABASE_URL,
|
||
c.env.SUPABASE_SERVICE_KEY
|
||
);
|
||
|
||
const { data, error } = await supabase
|
||
.from('images')
|
||
.select('*')
|
||
.eq('user_id', userId);
|
||
|
||
return c.json(data);
|
||
});
|
||
|
||
// Route to Inngest for generation
|
||
app.post('/api/generate', async (c) => {
|
||
const userId = c.get('jwtPayload').sub;
|
||
const { prompt, modelId } = await c.req.json();
|
||
|
||
// Create generation record in Supabase
|
||
const generation = await createGenerationRecord(userId, prompt, modelId);
|
||
|
||
// Trigger Inngest workflow
|
||
await c.env.INNGEST.send({
|
||
name: 'generation/queued',
|
||
data: { generationId: generation.id, userId, prompt, modelId }
|
||
});
|
||
|
||
return c.json({ generationId: generation.id });
|
||
});
|
||
|
||
// Serve images from R2 with cache
|
||
app.get('/images/:userId/:filename', async (c) => {
|
||
const { userId, filename } = c.req.param();
|
||
|
||
const object = await c.env.R2_BUCKET.get(`${userId}/${filename}`);
|
||
|
||
if (!object) {
|
||
return c.notFound();
|
||
}
|
||
|
||
return new Response(object.body, {
|
||
headers: {
|
||
'Content-Type': object.httpMetadata.contentType,
|
||
'Cache-Control': 'public, max-age=31536000',
|
||
'ETag': object.httpEtag
|
||
}
|
||
});
|
||
});
|
||
|
||
export default app;
|
||
```
|
||
|
||
**Database:** Keep Supabase PostgreSQL (best managed Postgres)
|
||
**Auth:** Keep Supabase Auth (excellent, no reason to change)
|
||
**Storage:** Migrate to Cloudflare R2 (S3-compatible, cheaper)
|
||
**Queue:** Use Inngest (serverless job orchestration)
|
||
**CDN:** Cloudflare (automatic for Workers + R2)
|
||
|
||
#### Migration Path:
|
||
|
||
1. **Phase 1: Add Cloudflare Workers router** (Week 1)
|
||
- Deploy workers as API gateway
|
||
- Route all requests through it
|
||
- Keep all backends the same
|
||
|
||
2. **Phase 2: Migrate storage to R2** (Week 2)
|
||
- Copy existing images from Supabase Storage to R2
|
||
- Update new uploads to go to R2
|
||
- Update database URLs
|
||
- Keep Supabase Storage as fallback
|
||
|
||
3. **Phase 3: Add Inngest for queue** (Week 3)
|
||
- Implement Inngest workflows
|
||
- Refactor Edge Function into Inngest functions
|
||
- Test thoroughly
|
||
- Switch traffic
|
||
|
||
4. **Phase 4: Optimize** (Week 4)
|
||
- Add caching layers
|
||
- Optimize database queries
|
||
- Monitor performance
|
||
- Fine-tune costs
|
||
|
||
#### Pros:
|
||
- **Best of All Worlds:** Pick best service for each task
|
||
- **Cost Optimization:** R2 is 90% cheaper than S3/Supabase Storage
|
||
- **Performance:** Cloudflare CDN is fastest
|
||
- **Flexibility:** Easy to swap services
|
||
- **Scalability:** Each component scales independently
|
||
- **No Single Point of Failure:** Distributed architecture
|
||
|
||
#### Cons:
|
||
- **Complexity:** Most complex architecture
|
||
- **Multiple Services:** More to manage and monitor
|
||
- **Integration Overhead:** Services need to communicate
|
||
- **Debugging:** Harder with distributed system
|
||
- **Learning Curve:** Need to know multiple platforms
|
||
|
||
#### Cost Estimate (Monthly):
|
||
- Supabase Pro: $25 (database + auth)
|
||
- Cloudflare Workers: $5 (paid plan)
|
||
- Cloudflare R2: $0-5 (10GB free, $0.015/GB after)
|
||
- Inngest: $0-50 (free or team plan)
|
||
- **Total: $30-85/month**
|
||
|
||
---
|
||
|
||
## 3. Detailed Comparison Matrix
|
||
|
||
| Criteria | Current (Supabase) | A: Supabase + Backend | B: AWS Amplify | C: Firebase | D: Custom (NestJS) | E: Refactored Supabase | F: Hybrid |
|
||
|----------|-------------------|----------------------|----------------|-------------|-------------------|----------------------|-----------|
|
||
| **COST** |
|
||
| Initial Setup | $0-25/mo | $7-55/mo | $40-160/mo | $21-70/mo | $39-215/mo | $25-75/mo | $30-85/mo |
|
||
| Scaling Cost | Medium | Medium-Low | High | Medium | Low-Medium | Medium | Low |
|
||
| Predictability | Medium | High | Low | Medium | High | High | High |
|
||
| **PERFORMANCE** |
|
||
| API Latency | Good (PostgREST) | Very Good | Good | Good | Excellent | Good | Excellent (Edge) |
|
||
| Cold Starts | Yes (Edge Fn) | Minimal | Yes (Lambda) | Yes | No (if always-on) | Yes (Edge Fn) | Minimal (Workers) |
|
||
| Database Speed | Excellent (Postgres) | Excellent | Good (DynamoDB) | Good (Firestore) | Excellent | Excellent | Excellent |
|
||
| File Serving | Good | Good | Excellent (CloudFront) | Good | Excellent (CDN) | Good | Excellent (CF) |
|
||
| **SCALABILITY** |
|
||
| Horizontal Scaling | Auto (Supabase) | Manual/Auto | Auto (AWS) | Auto (Firebase) | Manual/Auto | Auto (Supabase) | Auto (Multi) |
|
||
| Geographic Distribution | Limited | Medium | Excellent (AWS Global) | Excellent | Medium | Limited | Excellent (CF) |
|
||
| Concurrent Jobs | Limited | Excellent (Queue) | Good | Good | Excellent | Excellent (Inngest) | Excellent |
|
||
| Max Request Size | 50MB | Configurable | 6MB (Lambda) | 10MB | Configurable | 50MB | Configurable |
|
||
| **DEVELOPER EXPERIENCE** |
|
||
| Learning Curve | Easy | Medium | Steep | Easy | Steep | Easy-Medium | Medium-Steep |
|
||
| Local Development | Good (CLI) | Excellent | Medium | Good | Excellent | Good | Medium |
|
||
| Type Safety | Excellent | Excellent | Medium | Medium | Excellent | Excellent | Excellent |
|
||
| Debugging | Medium | Good | Hard (AWS) | Good | Excellent | Medium | Medium |
|
||
| Testing | Medium | Excellent | Medium | Medium | Excellent | Good | Medium |
|
||
| Documentation | Excellent | Good | Excellent | Excellent | Good | Excellent | Good |
|
||
| **VENDOR LOCK-IN** |
|
||
| Database Portability | Medium (Postgres) | High (Postgres) | Low (DynamoDB) | Low (Firestore) | High (Standard DB) | Medium (Postgres) | High (Postgres) |
|
||
| Auth Portability | Medium | High (JWT) | Low (Cognito) | Low (Firebase Auth) | High (Custom) | Medium | High (JWT) |
|
||
| Storage Portability | Medium | High (S3-compatible) | Medium (S3) | Low (Firebase Storage) | High (S3-compatible) | Medium | High (R2/S3) |
|
||
| API Portability | Medium | High (Custom) | Low (AppSync) | Low (Firebase SDK) | High (Standard) | Medium | High (Standard) |
|
||
| Exit Difficulty | Medium | Low | High | High | Very Low | Medium | Low |
|
||
| **MAINTENANCE** |
|
||
| Ongoing Effort | Low | Medium | Medium-High | Low | High | Medium | Medium-High |
|
||
| Security Updates | Auto | Manual (Backend) | Auto | Auto | Manual | Auto | Mixed |
|
||
| Monitoring | Built-in | Need setup | Excellent (CloudWatch) | Good | Need setup | Built-in + Inngest | Mixed |
|
||
| Backup/Recovery | Auto | Manual (DB auto) | Manual (auto available) | Auto | Manual | Auto | Mixed |
|
||
| **FEATURES** |
|
||
| Real-time Updates | Yes (Realtime) | Need WebSockets | Yes (AppSync) | Yes (built-in) | Need WebSockets | Yes (Realtime) | Need WebSockets |
|
||
| Job Queue | No | Yes (BullMQ) | Yes (SQS) | Yes (Tasks) | Yes (BullMQ) | Yes (Inngest) | Yes (Inngest) |
|
||
| File Uploads | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||
| Image Processing | Basic | Full control | Lambda | Functions | Full control | Basic | Full control |
|
||
| Caching | Limited | Full control | CloudFront | Firebase CDN | Full control | Limited | Cloudflare |
|
||
| Analytics | Basic | Custom | CloudWatch | Firebase Analytics | Custom | Basic + Custom | Custom |
|
||
| **MIGRATION EFFORT** |
|
||
| Code Changes | N/A | 20% | 90% | 80% | 95% | 40% | 50% |
|
||
| Data Migration | N/A | None | Complex | Complex | Medium | None | Minimal (Storage) |
|
||
| Time Estimate | N/A | 2-3 weeks | 8-12 weeks | 6-10 weeks | 10-16 weeks | 3-5 weeks | 4-6 weeks |
|
||
| Risk Level | N/A | Low | High | Medium-High | High | Low-Medium | Medium |
|
||
| Rollback Difficulty | N/A | Easy | Hard | Hard | Medium | Easy | Medium |
|
||
| **SPECIFIC NEEDS** |
|
||
| Long-running Tasks | Poor | Excellent | Good | Good | Excellent | Excellent | Excellent |
|
||
| Third-party APIs | Poor | Excellent | Good | Good | Excellent | Excellent | Excellent |
|
||
| Large File Handling | Medium | Excellent | Good | Medium | Excellent | Medium | Excellent |
|
||
| Real-time Progress | No | Yes | Yes | Yes | Yes | Yes | Yes |
|
||
| Cost at Scale | Medium | Low | High | Medium | Low | Medium | Low |
|
||
| Multi-platform SDK | Excellent | Good | Excellent | Excellent | Medium | Excellent | Good |
|
||
|
||
### Summary Scores (1-10, higher is better)
|
||
|
||
| Option | Cost | Performance | Scalability | DX | Low Lock-in | Low Maintenance | Features | Migration Ease | **TOTAL** |
|
||
|--------|------|-------------|-------------|----|-----------|----|----------|----------------|-----------|
|
||
| Current | 9 | 6 | 5 | 8 | 5 | 9 | 5 | 10 | **57** |
|
||
| A: Supabase + Backend | 7 | 8 | 8 | 7 | 7 | 6 | 8 | 8 | **59** |
|
||
| B: AWS Amplify | 4 | 7 | 9 | 5 | 3 | 6 | 9 | 3 | **46** |
|
||
| C: Firebase | 6 | 7 | 8 | 7 | 3 | 8 | 8 | 4 | **51** |
|
||
| D: Custom (NestJS) | 6 | 9 | 8 | 7 | 10 | 4 | 10 | 2 | **56** |
|
||
| E: Refactored Supabase | 7 | 7 | 8 | 8 | 6 | 7 | 8 | 9 | **60** ⭐ |
|
||
| F: Hybrid | 7 | 9 | 10 | 6 | 8 | 5 | 9 | 6 | **60** ⭐ |
|
||
|
||
**Top 3 Options:**
|
||
1. **Option E: Refactored Supabase** (60 points) - Best balance
|
||
2. **Option F: Hybrid** (60 points) - Best performance/scale
|
||
3. **Option A: Supabase + Backend** (59 points) - Simplest improvement
|
||
|
||
---
|
||
|
||
## 4. Specific Concerns for Picture App
|
||
|
||
### 4.1 Long-Running Processes (2-120 seconds)
|
||
|
||
**Current Problem:**
|
||
- Edge Function blocks entire duration
|
||
- User waits for response
|
||
- Timeout risk if >10 minutes
|
||
|
||
**Solution Comparison:**
|
||
|
||
| Option | Approach | Quality Score |
|
||
|--------|----------|---------------|
|
||
| Current | Synchronous blocking | ❌ Poor (1/10) |
|
||
| A | BullMQ queue, async workers | ✅ Excellent (9/10) |
|
||
| B | SQS queue, Lambda workers | ✅ Good (8/10) |
|
||
| C | Cloud Tasks, Cloud Functions | ✅ Good (8/10) |
|
||
| D | BullMQ queue, dedicated workers | ✅ Excellent (10/10) |
|
||
| E | Inngest orchestration | ✅ Excellent (9/10) |
|
||
| F | Inngest + distributed workers | ✅ Excellent (9/10) |
|
||
|
||
**Recommended Solution:** Queue-based architecture (Options A, D, E, F)
|
||
|
||
**Implementation Pattern:**
|
||
```typescript
|
||
// Client request
|
||
POST /api/generate { prompt, modelId }
|
||
↓
|
||
// Immediate response
|
||
{ generationId: "abc123", status: "queued" }
|
||
↓
|
||
// Backend processes asynchronously
|
||
Queue Worker → Replicate API → Poll → Download → Upload → Update DB
|
||
↓
|
||
// Client polls or receives webhook/WebSocket update
|
||
GET /api/generations/abc123 → { status: "completed", imageUrl: "..." }
|
||
```
|
||
|
||
### 4.2 Third-Party API Integration (Replicate)
|
||
|
||
**Challenges:**
|
||
- Replicate uses async prediction model (not instant)
|
||
- Need to poll for completion
|
||
- Different models have different response formats
|
||
- Rate limiting on Replicate side
|
||
- Cost per generation
|
||
|
||
**Best Practices:**
|
||
|
||
1. **Abstraction Layer**
|
||
```typescript
|
||
// Create a ReplicateService that handles all models
|
||
class ReplicateService {
|
||
async generate(model: Model, input: any): Promise<Prediction> {
|
||
// Normalize input based on model
|
||
const normalizedInput = this.normalizeInput(model, input);
|
||
|
||
// Call Replicate
|
||
const prediction = await this.client.predictions.create({
|
||
version: model.replicateId,
|
||
input: normalizedInput
|
||
});
|
||
|
||
return prediction;
|
||
}
|
||
|
||
async waitForCompletion(predictionId: string): Promise<Output> {
|
||
return await this.client.wait(predictionId, {
|
||
interval: 2000,
|
||
maxAttempts: 120
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
2. **Retry Strategy**
|
||
```typescript
|
||
// Exponential backoff for failed API calls
|
||
const retryConfig = {
|
||
retries: 3,
|
||
factor: 2,
|
||
minTimeout: 1000,
|
||
maxTimeout: 10000
|
||
};
|
||
```
|
||
|
||
3. **Cost Tracking**
|
||
```typescript
|
||
// Log every Replicate call for cost analysis
|
||
await db.generation_costs.insert({
|
||
generation_id,
|
||
model_id,
|
||
cost: modelConfig.costPerGeneration,
|
||
timestamp: new Date()
|
||
});
|
||
```
|
||
|
||
**Best Options for API Integration:**
|
||
- **Option D** (Custom): Full control over retry, caching, error handling
|
||
- **Option E** (Inngest): Built-in retry and monitoring
|
||
- **Option A** (Backend + BullMQ): Good queue-based handling
|
||
|
||
### 4.3 File Handling (Image Downloads/Uploads)
|
||
|
||
**Challenges:**
|
||
- Images can be 1-20MB
|
||
- Need to download from Replicate
|
||
- Need to upload to storage
|
||
- Format conversions (JPEG → WebP)
|
||
- Metadata extraction
|
||
|
||
**Current Issues:**
|
||
- Everything in memory in Edge Function
|
||
- No streaming
|
||
- No optimization
|
||
|
||
**Improved Architecture:**
|
||
|
||
```typescript
|
||
// Option E/F: Inngest with streaming
|
||
const downloadAndUpload = inngest.createFunction(
|
||
{ id: 'download-upload-image' },
|
||
{ event: 'image/process' },
|
||
async ({ event, step }) => {
|
||
// Download with streaming
|
||
const imageStream = await step.run('download', async () => {
|
||
const response = await fetch(event.data.imageUrl);
|
||
return response.body;
|
||
});
|
||
|
||
// Process image (resize, optimize, format conversion)
|
||
const processedBuffer = await step.run('process', async () => {
|
||
return await sharp(imageStream)
|
||
.resize(2048, 2048, { fit: 'inside', withoutEnlargement: true })
|
||
.webp({ quality: 90 })
|
||
.toBuffer();
|
||
});
|
||
|
||
// Upload to storage
|
||
const { publicUrl } = await step.run('upload', async () => {
|
||
return await storage.upload(
|
||
`${event.data.userId}/${event.data.generationId}.webp`,
|
||
processedBuffer,
|
||
{ contentType: 'image/webp' }
|
||
);
|
||
});
|
||
|
||
return { publicUrl };
|
||
}
|
||
);
|
||
```
|
||
|
||
**Storage Comparison:**
|
||
|
||
| Storage | Cost/GB/mo | Bandwidth | Speed | Best For |
|
||
|---------|-----------|-----------|-------|----------|
|
||
| Supabase Storage | $0.021 | $0.09/GB | Good | Current (integrated) |
|
||
| AWS S3 | $0.023 | $0.09/GB | Good | Option A, B, D |
|
||
| Cloudflare R2 | $0.015 | Free egress! | Excellent | Option F (winner!) |
|
||
| Google Cloud Storage | $0.020 | $0.12/GB | Good | Option C |
|
||
|
||
**Recommendation:**
|
||
- **Short-term:** Keep Supabase Storage (simplicity)
|
||
- **Long-term:** Migrate to Cloudflare R2 (90% cheaper bandwidth)
|
||
|
||
### 4.4 Real-Time Updates
|
||
|
||
**Current Problem:**
|
||
- Client must poll database for status updates
|
||
- Inefficient (many queries)
|
||
- Delayed notifications (depends on poll interval)
|
||
|
||
**Solution Options:**
|
||
|
||
**Option 1: Supabase Realtime (Options E, Current)**
|
||
```typescript
|
||
// Client subscribes to database changes
|
||
const subscription = supabase
|
||
.channel('generations')
|
||
.on('postgres_changes', {
|
||
event: 'UPDATE',
|
||
schema: 'public',
|
||
table: 'image_generations',
|
||
filter: `user_id=eq.${userId}`
|
||
}, (payload) => {
|
||
if (payload.new.status === 'completed') {
|
||
showNotification('Image ready!');
|
||
}
|
||
})
|
||
.subscribe();
|
||
```
|
||
- **Pros:** Built-in, easy, no extra cost
|
||
- **Cons:** Limited to database changes, not custom events
|
||
|
||
**Option 2: WebSockets (Options A, D)**
|
||
```typescript
|
||
// Server pushes updates
|
||
socket.emit('generation:complete', { generationId, imageUrl });
|
||
|
||
// Client listens
|
||
socket.on('generation:complete', (data) => {
|
||
showImage(data.imageUrl);
|
||
});
|
||
```
|
||
- **Pros:** Full control, custom events, instant
|
||
- **Cons:** Need to manage WebSocket server
|
||
|
||
**Option 3: AppSync Subscriptions (Option B)**
|
||
```graphql
|
||
subscription OnGenerationComplete($userId: ID!) {
|
||
onUpdateGeneration(userId: $userId) {
|
||
id
|
||
status
|
||
image { url }
|
||
}
|
||
}
|
||
```
|
||
- **Pros:** GraphQL subscriptions, built-in
|
||
- **Cons:** AWS-specific
|
||
|
||
**Option 4: Firestore Real-time (Option C)**
|
||
```typescript
|
||
firestore()
|
||
.collection('generations')
|
||
.doc(generationId)
|
||
.onSnapshot(doc => {
|
||
if (doc.data().status === 'completed') {
|
||
// Update UI
|
||
}
|
||
});
|
||
```
|
||
- **Pros:** Excellent for mobile, offline support
|
||
- **Cons:** NoSQL limitations
|
||
|
||
**Recommendation:**
|
||
- **Best for current architecture:** Supabase Realtime (Option E)
|
||
- **Best for custom backend:** WebSockets (Option D)
|
||
- **Best for mobile:** Firebase (Option C) or Supabase Realtime
|
||
|
||
### 4.5 Cost Optimization
|
||
|
||
**Current Costs Breakdown:**
|
||
|
||
Assuming 1,000 generations/month:
|
||
- Replicate API: $50-100 (main cost, 15+ models)
|
||
- Supabase Pro: $25
|
||
- Edge Function invocations: Free (under limit)
|
||
- Storage: $5
|
||
- Bandwidth: $10
|
||
- **Total: $90-140/month**
|
||
|
||
**Cost Optimization Strategies:**
|
||
|
||
1. **Caching Generated Images**
|
||
```typescript
|
||
// Check if similar prompt was already generated
|
||
const cached = await db.images
|
||
.where('prompt', prompt)
|
||
.where('model_id', modelId)
|
||
.where('created_at', '>', thirtyDaysAgo)
|
||
.first();
|
||
|
||
if (cached) {
|
||
return cached; // Save Replicate API call
|
||
}
|
||
```
|
||
- **Savings:** 10-30% reduction in Replicate costs
|
||
|
||
2. **Batch Processing**
|
||
```typescript
|
||
// Group similar generations together
|
||
const batch = await getBatchGenerations(userId);
|
||
if (batch.length >= 5) {
|
||
// Process batch on Replicate (some models offer discounts)
|
||
await processGenerationBatch(batch);
|
||
}
|
||
```
|
||
- **Savings:** 5-15% with bulk discounts
|
||
|
||
3. **Model Selection**
|
||
```typescript
|
||
// Recommend cheaper, faster models for simple prompts
|
||
const modelRecommendation = analyzePrompt(prompt);
|
||
if (modelRecommendation.complexity === 'low') {
|
||
suggestModel('flux-schnell'); // Cheaper and faster
|
||
}
|
||
```
|
||
- **Savings:** 20-40% by using appropriate models
|
||
|
||
4. **Storage Optimization**
|
||
```typescript
|
||
// Convert all to WebP, compress aggressively
|
||
await sharp(imageBuffer)
|
||
.webp({ quality: 85, effort: 6 })
|
||
.toBuffer();
|
||
```
|
||
- **Savings:** 50-70% storage costs
|
||
|
||
5. **Intelligent Caching with CDN**
|
||
```typescript
|
||
// Use Cloudflare R2 + CDN (Option F)
|
||
// Free egress = no bandwidth costs
|
||
```
|
||
- **Savings:** 90% bandwidth costs
|
||
|
||
**Projected Costs with Optimizations:**
|
||
|
||
| Option | Without Optimization | With Optimization | Savings |
|
||
|--------|---------------------|-------------------|---------|
|
||
| Current | $90-140/mo | $65-100/mo | 28% |
|
||
| A: Supabase + Backend | $107-155/mo | $75-110/mo | 30% |
|
||
| E: Refactored Supabase | $75-125/mo | $50-85/mo | 33% |
|
||
| F: Hybrid (R2) | $80-135/mo | $45-75/mo | 44% ⭐ |
|
||
|
||
**Best for Cost:** Option F (Hybrid with Cloudflare R2)
|
||
|
||
### 4.6 Multi-Platform Support (Web + Mobile)
|
||
|
||
**Requirements:**
|
||
- Shared authentication
|
||
- Same API across platforms
|
||
- Optimized for mobile (offline, push notifications)
|
||
- Type-safe clients
|
||
|
||
**Platform-Specific Considerations:**
|
||
|
||
**Web (SvelteKit):**
|
||
```typescript
|
||
// Current: Direct Supabase client - works great
|
||
import { supabase } from '$lib/supabase';
|
||
|
||
// With custom backend: REST or tRPC client
|
||
import { api } from '$lib/api';
|
||
await api.generations.create({ prompt });
|
||
```
|
||
|
||
**Mobile (React Native/Expo):**
|
||
```typescript
|
||
// Current: Supabase client with AsyncStorage
|
||
import { supabase } from '@picture/shared';
|
||
|
||
// Ideal: Mobile-optimized SDK
|
||
// - Offline support
|
||
// - Background uploads
|
||
// - Push notifications
|
||
// - File system access
|
||
```
|
||
|
||
**Comparison:**
|
||
|
||
| Platform | Current | A: Backend | B: AWS | C: Firebase | D: Custom | E: Refactored | F: Hybrid |
|
||
|----------|---------|-----------|--------|-------------|-----------|---------------|-----------|
|
||
| Web SDK | ✅ Excellent | ✅ Good | ✅ Good | ✅ Good | ⚠️ Manual | ✅ Excellent | ✅ Good |
|
||
| Mobile SDK | ✅ Excellent | ⚠️ Manual | ✅ Excellent | ✅ Excellent | ⚠️ Manual | ✅ Excellent | ⚠️ Manual |
|
||
| Offline Support | ✅ Yes | ❌ No | ✅ Yes (DataStore) | ✅ Excellent | ❌ No | ✅ Yes | ⚠️ Partial |
|
||
| Push Notifications | ⚠️ Manual | ⚠️ Manual | ✅ SNS | ✅ FCM | ⚠️ Manual | ⚠️ Manual | ⚠️ Manual |
|
||
| Type Safety | ✅ Excellent | ✅ Good | ⚠️ Manual | ⚠️ Manual | ✅ Excellent | ✅ Excellent | ✅ Good |
|
||
| Auth Persistence | ✅ Auto | ⚠️ Manual | ✅ Auto | ✅ Auto | ⚠️ Manual | ✅ Auto | ⚠️ Manual |
|
||
|
||
**Best for Multi-Platform:**
|
||
1. **Option E** (Refactored Supabase) - Best SDK support
|
||
2. **Option C** (Firebase) - Excellent mobile SDKs
|
||
3. **Option B** (AWS Amplify) - Comprehensive but complex
|
||
|
||
**Recommendation:**
|
||
Keep Supabase client for both platforms (Options A, E, F) - already works perfectly.
|
||
|
||
---
|
||
|
||
## 5. Migration Strategies
|
||
|
||
### 5.1 Option A: Supabase + Custom Backend
|
||
|
||
**Timeline:** 2-3 weeks
|
||
|
||
#### Week 1: Setup & Development
|
||
**Day 1-2: Infrastructure Setup**
|
||
- Set up Node.js backend (Fastify or Express)
|
||
- Deploy to Railway/Render (use free tiers initially)
|
||
- Set up Redis (Upstash free tier)
|
||
- Install BullMQ
|
||
|
||
**Day 3-4: Extract Generation Logic**
|
||
- Create `replicate.service.ts` - extract Replicate integration
|
||
- Create `storage.service.ts` - Supabase Storage wrapper
|
||
- Create `generation.worker.ts` - port Edge Function logic
|
||
- Add comprehensive error handling and logging
|
||
|
||
**Day 5: API Development**
|
||
- Create `POST /api/generate` endpoint
|
||
- Implement JWT verification (verify Supabase token)
|
||
- Add queue job creation
|
||
- Return immediate response with `generation_id`
|
||
|
||
**Day 6-7: Testing**
|
||
- Unit tests for services
|
||
- Integration tests for queue workers
|
||
- End-to-end test full generation flow
|
||
- Load testing with multiple concurrent generations
|
||
|
||
#### Week 2: Integration & Migration
|
||
**Day 8-9: Client Integration**
|
||
- Update web app to call new backend
|
||
- Update mobile app to call new backend
|
||
- Implement retry logic in clients
|
||
- Add better error handling
|
||
|
||
**Day 10-11: Parallel Running**
|
||
- Run old (Edge Function) and new (Backend) in parallel
|
||
- Route 10% of traffic to new backend
|
||
- Monitor errors, performance, costs
|
||
- Fix any issues found
|
||
|
||
**Day 12-13: Full Cutover**
|
||
- Route 100% traffic to new backend
|
||
- Keep Edge Function as fallback for 1 week
|
||
- Monitor closely for any issues
|
||
- Update documentation
|
||
|
||
**Day 14: Cleanup**
|
||
- Remove old Edge Function
|
||
- Clean up test data
|
||
- Document new architecture
|
||
- Update deployment instructions
|
||
|
||
#### Rollback Plan:
|
||
1. **If issues detected:** Flip environment variable to route back to Edge Function
|
||
2. **Rollback time:** < 5 minutes
|
||
3. **Data consistency:** No data loss (same database)
|
||
|
||
**Risks & Mitigation:**
|
||
- **Risk:** Backend downtime
|
||
- **Mitigation:** Deploy to multiple regions, use health checks
|
||
- **Risk:** Queue failures
|
||
- **Mitigation:** BullMQ retries, dead letter queue
|
||
- **Risk:** JWT verification issues
|
||
- **Mitigation:** Thorough testing, use official Supabase JWT library
|
||
|
||
**Estimated Cost During Migration:** $15-30/mo (running both systems)
|
||
|
||
---
|
||
|
||
### 5.2 Option E: Refactored Supabase Architecture
|
||
|
||
**Timeline:** 3-5 weeks
|
||
|
||
#### Phase 1: Setup Inngest (Week 1)
|
||
**Day 1-2: Inngest Account & Configuration**
|
||
```bash
|
||
# Install Inngest SDK
|
||
npm install inngest
|
||
|
||
# Set up Inngest account (free tier)
|
||
# Configure environment variables
|
||
INNGEST_EVENT_KEY=xxx
|
||
INNGEST_SIGNING_KEY=xxx
|
||
```
|
||
|
||
**Day 3-5: Create Inngest Functions**
|
||
```typescript
|
||
// supabase/functions/shared/inngest-functions.ts
|
||
export const processGeneration = inngest.createFunction(
|
||
{ id: 'process-generation', retries: 3 },
|
||
{ event: 'generation/queued' },
|
||
async ({ event, step }) => {
|
||
// Implement step-by-step generation workflow
|
||
// (see code example in Option E section)
|
||
}
|
||
);
|
||
```
|
||
|
||
**Day 6-7: Testing Inngest Locally**
|
||
- Use Inngest Dev Server
|
||
- Test full workflow
|
||
- Verify retries work
|
||
- Check error handling
|
||
|
||
#### Phase 2: Refactor Edge Functions (Week 2)
|
||
**Day 8-10: Extract to Smaller Functions**
|
||
- Create `queue-generation` - Main entry point (HTTP)
|
||
- Create `prepare-model-input` - Model parameter mapping
|
||
- Create `call-replicate` - Replicate API wrapper
|
||
- Create `download-process-image` - Image handling
|
||
- Create `finalize-generation` - DB updates
|
||
|
||
**Day 11-12: Integrate with Inngest**
|
||
- Update `queue-generation` to send Inngest event
|
||
- Test integration
|
||
- Verify Inngest triggers correctly
|
||
|
||
**Day 13-14: Deploy to Supabase**
|
||
```bash
|
||
supabase functions deploy queue-generation
|
||
supabase functions deploy --no-verify-jwt prepare-model-input
|
||
supabase functions deploy --no-verify-jwt call-replicate
|
||
# etc.
|
||
```
|
||
|
||
#### Phase 3: Database Enhancements (Week 3)
|
||
**Day 15-16: Add Database Functions**
|
||
```sql
|
||
-- See SQL examples in Option E section
|
||
CREATE FUNCTION get_next_queued_generation();
|
||
CREATE FUNCTION update_generation_status();
|
||
```
|
||
|
||
**Day 17-18: Set Up pg_cron**
|
||
```sql
|
||
-- Enable extension
|
||
CREATE EXTENSION pg_cron;
|
||
|
||
-- Schedule cleanup and recovery jobs
|
||
SELECT cron.schedule(...);
|
||
```
|
||
|
||
**Day 19-20: Implement Real-time Subscriptions**
|
||
```typescript
|
||
// Client-side: Subscribe to Supabase Realtime
|
||
const subscription = supabase.channel('generations')
|
||
.on('postgres_changes', { /* config */ }, handler)
|
||
.subscribe();
|
||
```
|
||
|
||
#### Phase 4: Migration & Testing (Week 4-5)
|
||
**Day 21-23: Parallel Running**
|
||
- Deploy new architecture to staging
|
||
- Run smoke tests
|
||
- Load test with realistic data
|
||
- Monitor Inngest dashboard
|
||
|
||
**Day 24-26: Gradual Rollout**
|
||
- Route 10% of traffic to new system
|
||
- Monitor for 24 hours
|
||
- Increase to 50% if stable
|
||
- Monitor for another 24 hours
|
||
- Route 100% to new system
|
||
|
||
**Day 27-28: Optimization**
|
||
- Analyze Inngest metrics
|
||
- Identify slow steps
|
||
- Optimize database queries
|
||
- Tune retry strategies
|
||
|
||
**Day 29-30: Documentation & Cleanup**
|
||
- Document new architecture
|
||
- Update README
|
||
- Create runbooks for common issues
|
||
- Train team on new system
|
||
|
||
#### Rollback Plan:
|
||
1. **Staged rollback:**
|
||
- Stop sending events to Inngest
|
||
- Re-enable old Edge Function
|
||
- Rollback time: ~10 minutes
|
||
2. **Data consistency:** No data loss (same database, atomic updates)
|
||
3. **In-flight generations:** Let Inngest complete or mark as failed
|
||
|
||
**Coexistence Strategy:**
|
||
```typescript
|
||
// Feature flag to route to new or old system
|
||
const USE_NEW_ARCHITECTURE = Deno.env.get('USE_NEW_ARCHITECTURE') === 'true';
|
||
|
||
if (USE_NEW_ARCHITECTURE) {
|
||
// Send to Inngest
|
||
await inngest.send({ name: 'generation/queued', data: {...} });
|
||
} else {
|
||
// Call old Edge Function
|
||
await processGenerationSync({...});
|
||
}
|
||
```
|
||
|
||
**Risks & Mitigation:**
|
||
- **Risk:** Inngest downtime
|
||
- **Mitigation:** Inngest has 99.9% uptime SLA, can queue events
|
||
- **Risk:** Edge Function → Inngest event fails
|
||
- **Mitigation:** Retry logic, dead letter queue
|
||
- **Risk:** Database function bugs
|
||
- **Mitigation:** Thorough testing in staging, gradual rollout
|
||
|
||
**Estimated Cost During Migration:** $30-50/mo (running both, Inngest usage)
|
||
|
||
---
|
||
|
||
### 5.3 Option F: Hybrid Multi-Cloud
|
||
|
||
**Timeline:** 4-6 weeks
|
||
|
||
#### Phase 1: Cloudflare Workers Setup (Week 1-2)
|
||
**Week 1: Infrastructure**
|
||
```bash
|
||
# Create Cloudflare Workers project
|
||
npm create cloudflare@latest picture-api -- --framework=hono
|
||
|
||
# Set up R2 bucket
|
||
wrangler r2 bucket create generated-images
|
||
|
||
# Configure environment
|
||
wrangler secret put SUPABASE_URL
|
||
wrangler secret put SUPABASE_SERVICE_KEY
|
||
wrangler secret put JWT_SECRET
|
||
```
|
||
|
||
**Week 2: API Router Development**
|
||
```typescript
|
||
// workers/src/index.ts
|
||
import { Hono } from 'hono';
|
||
|
||
const app = new Hono();
|
||
|
||
// Health check
|
||
app.get('/health', (c) => c.json({ status: 'ok' }));
|
||
|
||
// Auth middleware
|
||
app.use('/api/*', async (c, next) => {
|
||
const token = c.req.header('Authorization')?.replace('Bearer ', '');
|
||
const user = await verifySupabaseToken(token);
|
||
c.set('user', user);
|
||
await next();
|
||
});
|
||
|
||
// Proxy to Supabase for reads
|
||
app.get('/api/images', async (c) => {
|
||
const user = c.get('user');
|
||
const { data } = await supabase
|
||
.from('images')
|
||
.select('*')
|
||
.eq('user_id', user.id);
|
||
return c.json(data);
|
||
});
|
||
|
||
// Generation endpoint
|
||
app.post('/api/generate', async (c) => {
|
||
// Implementation
|
||
});
|
||
|
||
export default app;
|
||
```
|
||
|
||
#### Phase 2: Storage Migration to R2 (Week 3)
|
||
**Day 15-17: Set Up Migration Script**
|
||
```typescript
|
||
// migrate-storage.ts
|
||
import { createClient } from '@supabase/supabase-js';
|
||
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
||
|
||
async function migrateToR2() {
|
||
// 1. List all images in Supabase Storage
|
||
const { data: files } = await supabase.storage
|
||
.from('generated-images')
|
||
.list();
|
||
|
||
// 2. Download and re-upload to R2
|
||
for (const file of files) {
|
||
const { data: blob } = await supabase.storage
|
||
.from('generated-images')
|
||
.download(file.name);
|
||
|
||
await r2.send(new PutObjectCommand({
|
||
Bucket: 'generated-images',
|
||
Key: file.name,
|
||
Body: blob
|
||
}));
|
||
|
||
// 3. Update database with new URLs
|
||
await supabase
|
||
.from('images')
|
||
.update({ public_url: `https://images.picture.com/${file.name}` })
|
||
.eq('storage_path', file.name);
|
||
}
|
||
}
|
||
```
|
||
|
||
**Day 18-19: Run Migration**
|
||
- Test with 10 images
|
||
- Run full migration overnight
|
||
- Verify all URLs work
|
||
- Keep Supabase Storage as fallback for 1 week
|
||
|
||
**Day 20-21: Update Upload Logic**
|
||
```typescript
|
||
// New uploads go to R2
|
||
app.post('/api/upload', async (c) => {
|
||
const file = await c.req.formData();
|
||
|
||
// Upload to R2
|
||
await c.env.R2_BUCKET.put(key, file);
|
||
|
||
// Save to database
|
||
await supabase.from('images').insert({...});
|
||
});
|
||
```
|
||
|
||
#### Phase 3: Integrate Inngest (Week 4)
|
||
**Day 22-24: Set Up Inngest**
|
||
```typescript
|
||
// workers/src/inngest.ts
|
||
import { Inngest } from 'inngest';
|
||
|
||
export const inngest = new Inngest({ id: 'picture' });
|
||
|
||
export const processGeneration = inngest.createFunction(
|
||
{ id: 'process-generation' },
|
||
{ event: 'generation/queued' },
|
||
async ({ event, step }) => {
|
||
// Generation workflow
|
||
}
|
||
);
|
||
|
||
// Serve Inngest functions
|
||
app.post('/api/inngest', async (c) => {
|
||
return await serve({
|
||
client: inngest,
|
||
functions: [processGeneration],
|
||
signingKey: c.env.INNGEST_SIGNING_KEY
|
||
})(c.req.raw);
|
||
});
|
||
```
|
||
|
||
**Day 25-26: Deploy Inngest Functions**
|
||
```bash
|
||
wrangler deploy
|
||
```
|
||
|
||
**Day 27-28: Test End-to-End**
|
||
- Trigger generation from client
|
||
- Verify Cloudflare Workers route correctly
|
||
- Check Inngest processes job
|
||
- Confirm image uploads to R2
|
||
- Validate database updates
|
||
|
||
#### Phase 4: Cutover & Optimization (Week 5-6)
|
||
**Week 5: Gradual Cutover**
|
||
- Update DNS to point to Cloudflare Workers
|
||
- Start with 10% traffic
|
||
- Monitor logs and errors
|
||
- Increase to 50%, then 100%
|
||
|
||
**Week 6: Optimization**
|
||
- Add caching headers for R2 images
|
||
- Optimize database queries
|
||
- Set up Cloudflare Analytics
|
||
- Fine-tune Inngest retries
|
||
- Load test at scale
|
||
|
||
#### Rollback Plan:
|
||
1. **DNS rollback:** Point back to Supabase (5 minutes)
|
||
2. **Storage fallback:** Images still accessible on Supabase Storage
|
||
3. **Inngest rollback:** Stop sending events, use old Edge Function
|
||
|
||
**Coexistence Strategy:**
|
||
```typescript
|
||
// Dual-write during migration
|
||
await Promise.all([
|
||
uploadToSupabase(file),
|
||
uploadToR2(file)
|
||
]);
|
||
|
||
// Read from R2, fallback to Supabase
|
||
let imageUrl = `https://images.picture.com/${key}`;
|
||
try {
|
||
await fetch(imageUrl, { method: 'HEAD' });
|
||
} catch {
|
||
imageUrl = supabaseStorageUrl; // Fallback
|
||
}
|
||
```
|
||
|
||
**Risks & Mitigation:**
|
||
- **Risk:** DNS propagation issues
|
||
- **Mitigation:** Test with hosts file, gradual DNS update
|
||
- **Risk:** R2 migration data loss
|
||
- **Mitigation:** Keep Supabase Storage for 30 days, verify all files
|
||
- **Risk:** Multiple services = higher complexity
|
||
- **Mitigation:** Comprehensive monitoring, runbooks
|
||
|
||
**Estimated Cost During Migration:** $50-80/mo (running both storage systems)
|
||
|
||
---
|
||
|
||
## 6. Final Recommendations
|
||
|
||
### 6.1 Short-Term Recommendation (0-3 months)
|
||
|
||
**Recommended: Option E - Refactored Supabase Architecture**
|
||
|
||
#### Why Option E?
|
||
|
||
1. **Lowest Risk**
|
||
- Same database, auth, storage
|
||
- No data migration required
|
||
- Easy rollback
|
||
|
||
2. **Best Cost/Benefit**
|
||
- $25-75/mo (affordable)
|
||
- Solves major pain points
|
||
- Keeps existing benefits
|
||
|
||
3. **Reasonable Effort**
|
||
- 3-5 weeks of development
|
||
- Team already knows Supabase
|
||
- Clear migration path
|
||
|
||
4. **Immediate Improvements**
|
||
- Proper job queue (Inngest)
|
||
- Better separation of concerns
|
||
- Real-time updates
|
||
- Retry logic and monitoring
|
||
- No more monolithic Edge Function
|
||
|
||
#### Implementation Priorities:
|
||
|
||
**Phase 1 (Week 1): Quick Wins**
|
||
- Set up Inngest account
|
||
- Create basic queue function
|
||
- Deploy alongside existing Edge Function
|
||
- Route 10% of traffic
|
||
|
||
**Phase 2 (Week 2-3): Full Refactor**
|
||
- Break apart monolithic Edge Function
|
||
- Add database functions
|
||
- Implement real-time subscriptions
|
||
- Improve error handling
|
||
|
||
**Phase 3 (Week 4-5): Polish & Optimize**
|
||
- Add comprehensive monitoring
|
||
- Optimize database queries
|
||
- Document new architecture
|
||
- Train team
|
||
|
||
#### Success Metrics:
|
||
- ✅ Average generation time < 30 seconds
|
||
- ✅ Error rate < 1%
|
||
- ✅ Successfully handle 10 concurrent generations
|
||
- ✅ Real-time status updates working
|
||
- ✅ Cost remains under $75/mo
|
||
|
||
---
|
||
|
||
### 6.2 Long-Term Recommendation (6-12 months)
|
||
|
||
**Recommended: Option F - Hybrid Multi-Cloud Architecture**
|
||
|
||
#### Why Option F?
|
||
|
||
After running Option E for 6+ months, you'll have:
|
||
- Stable, working queue system
|
||
- Better understanding of scaling needs
|
||
- More revenue to justify optimization
|
||
|
||
Option F provides:
|
||
1. **Best Performance**
|
||
- Cloudflare Workers = edge computing
|
||
- Sub-50ms API latency globally
|
||
- R2 = free egress bandwidth
|
||
|
||
2. **Best Cost at Scale**
|
||
- R2 storage: 90% cheaper than S3
|
||
- No bandwidth costs
|
||
- Pay-per-request pricing
|
||
|
||
3. **Best Flexibility**
|
||
- Keep Supabase for DB + Auth (works great)
|
||
- Use Cloudflare for compute + storage
|
||
- Easy to add more services later
|
||
|
||
4. **Future-Proof**
|
||
- Can scale to millions of users
|
||
- Can add new features easily
|
||
- No vendor lock-in (use standard APIs)
|
||
|
||
#### When to Migrate:
|
||
|
||
**Trigger Conditions:**
|
||
- Reaching 10,000+ generations/month
|
||
- Bandwidth costs > $50/month
|
||
- Need for global distribution
|
||
- Team comfortable with current architecture
|
||
|
||
#### Migration Path from Option E to Option F:
|
||
|
||
**Quarter 1 (Months 1-3): Run Option E**
|
||
- Stable production system
|
||
- Gather metrics and learnings
|
||
- Identify bottlenecks
|
||
|
||
**Quarter 2 (Months 4-6): Plan & Prepare**
|
||
- Set up Cloudflare Workers account
|
||
- Create R2 bucket
|
||
- Build API router prototype
|
||
- Test in staging
|
||
|
||
**Quarter 3 (Months 7-9): Migrate Storage**
|
||
- Dual-write to Supabase Storage + R2
|
||
- Migrate old images to R2
|
||
- Update URLs in database
|
||
- Switch reads to R2
|
||
|
||
**Quarter 4 (Months 10-12): Migrate Compute**
|
||
- Deploy Cloudflare Workers router
|
||
- Route API calls through Workers
|
||
- Keep Inngest for orchestration
|
||
- Optimize and polish
|
||
|
||
#### Expected Outcomes:
|
||
- 📈 50% reduction in bandwidth costs
|
||
- ⚡ 3x faster API response times (edge computing)
|
||
- 🌍 Global distribution (Cloudflare's 200+ data centers)
|
||
- 💰 Total cost: $30-85/mo (even at 10x scale)
|
||
|
||
---
|
||
|
||
### 6.3 Alternative Recommendation (Cost-Conscious)
|
||
|
||
**If budget is tight: Option A - Supabase + Lightweight Backend**
|
||
|
||
#### Why Option A?
|
||
|
||
- **Simplest upgrade:** Just add a backend service
|
||
- **Cheapest:** $7-55/mo (can start with $7/mo on Railway)
|
||
- **Fast to implement:** 2-3 weeks
|
||
- **Easy to understand:** Traditional REST API architecture
|
||
|
||
#### Use Option A if:
|
||
- Budget is under $50/month
|
||
- Team is small (1-2 developers)
|
||
- Don't need global distribution
|
||
- Prefer simplicity over advanced features
|
||
|
||
---
|
||
|
||
### 6.4 What NOT to Do
|
||
|
||
**Avoid Option B (AWS Amplify)**
|
||
- Too complex for current needs
|
||
- Steep learning curve
|
||
- High migration cost (time + risk)
|
||
- Vendor lock-in to AWS
|
||
- Only makes sense if already deep in AWS ecosystem
|
||
|
||
**Avoid Option C (Firebase)**
|
||
- NoSQL database = major rewrite
|
||
- Firestore query limitations
|
||
- Good for mobile-first, but Supabase is better for Picture's use case
|
||
- Migration effort not justified
|
||
|
||
**Avoid Option D (Custom NestJS) - For Now**
|
||
- Too much work for short-term
|
||
- Requires experienced backend team
|
||
- High maintenance burden
|
||
- Consider only if scaling to 100k+ users
|
||
|
||
---
|
||
|
||
### 6.5 Action Plan Summary
|
||
|
||
#### **Immediate (Next 30 days):**
|
||
1. ✅ Set up Inngest account (free tier)
|
||
2. ✅ Create proof-of-concept queue function
|
||
3. ✅ Test with 10 generations
|
||
4. ✅ Measure performance improvements
|
||
|
||
#### **Short-Term (Months 1-3): Implement Option E**
|
||
1. ✅ Refactor Edge Function into smaller functions
|
||
2. ✅ Integrate Inngest for job orchestration
|
||
3. ✅ Add real-time subscriptions
|
||
4. ✅ Improve error handling and monitoring
|
||
5. ✅ Deploy to production gradually
|
||
|
||
#### **Mid-Term (Months 4-6): Optimize & Scale**
|
||
1. ✅ Analyze performance metrics
|
||
2. ✅ Optimize database queries
|
||
3. ✅ Add caching where beneficial
|
||
4. ✅ Prepare for Option F migration
|
||
|
||
#### **Long-Term (Months 7-12): Migrate to Option F**
|
||
1. ✅ Set up Cloudflare Workers + R2
|
||
2. ✅ Migrate storage to R2 (90% cost savings)
|
||
3. ✅ Deploy API router on Workers
|
||
4. ✅ Optimize for global scale
|
||
|
||
#### **Success Metrics to Track:**
|
||
- Average generation time
|
||
- Error rate
|
||
- Cost per generation
|
||
- User satisfaction
|
||
- System uptime
|
||
- Concurrent generation capacity
|
||
|
||
---
|
||
|
||
## Appendices
|
||
|
||
### Appendix A: Code Examples Repository
|
||
|
||
All code examples from this document are available in a companion repository:
|
||
```
|
||
picture-backend-examples/
|
||
├── option-a-backend/ # Node.js + Fastify + BullMQ
|
||
├── option-b-amplify/ # AWS Amplify with GraphQL
|
||
├── option-c-firebase/ # Firebase with Cloud Functions
|
||
├── option-d-nestjs/ # NestJS custom backend
|
||
├── option-e-refactored/ # Refactored Supabase + Inngest
|
||
├── option-f-hybrid/ # Cloudflare Workers + Hono
|
||
└── README.md
|
||
```
|
||
|
||
### Appendix B: Cost Calculators
|
||
|
||
**Replicate API Cost Calculator:**
|
||
```typescript
|
||
function estimateReplicateCost(generations: number, avgModel: string): number {
|
||
const costPerGeneration = {
|
||
'flux-schnell': 0.003,
|
||
'flux-dev': 0.025,
|
||
'sdxl': 0.004,
|
||
'ideogram': 0.08,
|
||
'imagen-4': 0.04,
|
||
};
|
||
|
||
return generations * (costPerGeneration[avgModel] || 0.01);
|
||
}
|
||
|
||
// Example: 1000 generations/month, mostly FLUX Schnell
|
||
console.log(estimateReplicateCost(1000, 'flux-schnell')); // $3
|
||
```
|
||
|
||
**Storage Cost Calculator:**
|
||
```typescript
|
||
function estimateStorageCost(
|
||
images: number,
|
||
avgSizeKB: number,
|
||
service: 'supabase' | 's3' | 'r2'
|
||
): number {
|
||
const totalGB = (images * avgSizeKB) / 1024 / 1024;
|
||
const avgBandwidthGB = totalGB * 2; // Assume 2x bandwidth
|
||
|
||
const costs = {
|
||
supabase: totalGB * 0.021 + avgBandwidthGB * 0.09,
|
||
s3: totalGB * 0.023 + avgBandwidthGB * 0.09,
|
||
r2: totalGB * 0.015 + 0 // Free egress!
|
||
};
|
||
|
||
return costs[service];
|
||
}
|
||
|
||
// Example: 1000 images, 2MB each
|
||
console.log(estimateStorageCost(1000, 2000, 'supabase')); // $180.04/mo
|
||
console.log(estimateStorageCost(1000, 2000, 'r2')); // $30.00/mo
|
||
// R2 saves $150/month!
|
||
```
|
||
|
||
### Appendix C: Migration Checklists
|
||
|
||
**Pre-Migration Checklist:**
|
||
- [ ] Full database backup
|
||
- [ ] Document current API endpoints
|
||
- [ ] List all environment variables
|
||
- [ ] Export all images from storage
|
||
- [ ] Test rollback procedure
|
||
- [ ] Set up monitoring and alerts
|
||
- [ ] Create staging environment
|
||
- [ ] Inform users of potential downtime
|
||
|
||
**Post-Migration Checklist:**
|
||
- [ ] Verify all endpoints work
|
||
- [ ] Check error rates in logs
|
||
- [ ] Monitor performance metrics
|
||
- [ ] Test generation flow end-to-end
|
||
- [ ] Verify real-time updates work
|
||
- [ ] Check costs are as expected
|
||
- [ ] Update documentation
|
||
- [ ] Train team on new architecture
|
||
|
||
### Appendix D: Monitoring & Observability
|
||
|
||
**Key Metrics to Track:**
|
||
|
||
1. **Generation Metrics:**
|
||
- Average time per generation
|
||
- Success rate
|
||
- Error rate by model
|
||
- Queue depth
|
||
- Retry count
|
||
|
||
2. **API Metrics:**
|
||
- Request latency (p50, p95, p99)
|
||
- Error rate
|
||
- Request rate
|
||
- Concurrent users
|
||
|
||
3. **Infrastructure Metrics:**
|
||
- Database query time
|
||
- Storage usage
|
||
- Bandwidth usage
|
||
- Function execution time
|
||
|
||
4. **Cost Metrics:**
|
||
- Cost per generation
|
||
- Daily/monthly spend by service
|
||
- Cost per user
|
||
|
||
**Recommended Tools:**
|
||
|
||
**Option E (Supabase + Inngest):**
|
||
- Inngest Dashboard (built-in monitoring)
|
||
- Supabase Dashboard (database metrics)
|
||
- PostHog or Mixpanel (user analytics)
|
||
- Sentry (error tracking)
|
||
|
||
**Option F (Hybrid):**
|
||
- Cloudflare Analytics (Workers + R2)
|
||
- Inngest Dashboard
|
||
- Grafana + Prometheus (custom metrics)
|
||
- Sentry (error tracking)
|
||
|
||
### Appendix E: Security Considerations
|
||
|
||
**Authentication:**
|
||
- Keep using Supabase Auth (all options)
|
||
- Implement JWT token expiration
|
||
- Use refresh tokens properly
|
||
- Add rate limiting per user
|
||
|
||
**Authorization:**
|
||
- Keep RLS enabled on Supabase tables
|
||
- Verify user owns resource before operations
|
||
- Use service role key only in backend
|
||
- Never expose service key to clients
|
||
|
||
**API Security:**
|
||
- CORS configuration
|
||
- Input validation
|
||
- SQL injection prevention (use Prisma/Drizzle)
|
||
- File upload limits
|
||
|
||
**Storage Security:**
|
||
- Private buckets by default
|
||
- Signed URLs for temporary access
|
||
- Virus scanning for uploads
|
||
- Content-type validation
|
||
|
||
### Appendix F: Testing Strategy
|
||
|
||
**Unit Tests:**
|
||
```typescript
|
||
// Test model input preparation
|
||
describe('prepareModelInput', () => {
|
||
it('should format FLUX Schnell parameters correctly', () => {
|
||
const input = prepareModelInput('flux-schnell', {
|
||
prompt: 'test',
|
||
width: 1024,
|
||
height: 768
|
||
});
|
||
|
||
expect(input.aspect_ratio).toBe('4:3');
|
||
expect(input.num_inference_steps).toBe(4);
|
||
});
|
||
});
|
||
```
|
||
|
||
**Integration Tests:**
|
||
```typescript
|
||
// Test full generation flow
|
||
describe('Generation Flow', () => {
|
||
it('should complete generation end-to-end', async () => {
|
||
const { generationId } = await createGeneration({
|
||
prompt: 'test prompt',
|
||
modelId: 'flux-schnell'
|
||
});
|
||
|
||
// Wait for completion
|
||
await waitForGeneration(generationId, { timeout: 60000 });
|
||
|
||
const generation = await getGeneration(generationId);
|
||
expect(generation.status).toBe('completed');
|
||
expect(generation.image).toBeDefined();
|
||
});
|
||
});
|
||
```
|
||
|
||
**Load Tests:**
|
||
```typescript
|
||
// k6 load test script
|
||
import http from 'k6/http';
|
||
import { check } from 'k6';
|
||
|
||
export let options = {
|
||
stages: [
|
||
{ duration: '2m', target: 10 }, // Ramp up to 10 users
|
||
{ duration: '5m', target: 10 }, // Stay at 10 users
|
||
{ duration: '2m', target: 0 }, // Ramp down
|
||
],
|
||
};
|
||
|
||
export default function () {
|
||
let response = http.post('https://api.picture.com/api/generate', {
|
||
prompt: 'test prompt',
|
||
modelId: 'flux-schnell'
|
||
});
|
||
|
||
check(response, {
|
||
'status is 200': (r) => r.status === 200,
|
||
'response time < 500ms': (r) => r.timings.duration < 500,
|
||
});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Conclusion
|
||
|
||
Picture app has solid foundations with Supabase but needs architectural improvements to scale and handle long-running image generation tasks efficiently.
|
||
|
||
**The winning strategy is a two-phase approach:**
|
||
|
||
1. **Phase 1 (Now - 3 months): Option E - Refactored Supabase**
|
||
- Low risk, reasonable effort
|
||
- Solves immediate pain points
|
||
- Keeps what works, fixes what doesn't
|
||
|
||
2. **Phase 2 (6-12 months): Option F - Hybrid Architecture**
|
||
- Best performance and cost at scale
|
||
- Leverage multiple best-in-class services
|
||
- Future-proof for growth
|
||
|
||
This strategy provides:
|
||
- ✅ Immediate improvements without high risk
|
||
- ✅ Clear path to scale when needed
|
||
- ✅ Cost optimization at every step
|
||
- ✅ Flexibility to adapt as requirements change
|
||
|
||
**Next Steps:**
|
||
1. Review this document with your team
|
||
2. Set up Inngest account and test basic workflow
|
||
3. Create project timeline for Option E migration
|
||
4. Begin refactoring Edge Function
|
||
|
||
Good luck with your architecture evolution! 🚀
|
||
|
||
---
|
||
|
||
**Document Metadata:**
|
||
- **Version:** 1.0
|
||
- **Last Updated:** 2025-10-09
|
||
- **Next Review:** 2025-11-09
|
||
- **Maintained By:** Development Team
|
||
- **Contact:** [Your Email]
|