Arquitetura Segura para Cold-Start com Expo + Supabase
De Wikiprompt, a enciclopédia livre de prompts
Arquitetura Segura para Cold-Start com Expo + Supabase Um prompt abrangente para projetar uma arquitetura de aplicativo móvel escalável e segura para cold start, usando Expo, Supabase e um serviço de worker separado para geração de IA.
Conteúdo do PromptSalvar
🌐
A) Architecture summary
The architecture separates concerns: Expo client handles UI and optimistic updates, Supabase Edge Functions provide lightweight gating and job enqueueing, and a separate Worker service performs heavy AI generation and storage writes. This ensures cold-start safety by keeping Edge Functions minimal and non-blocking, while the Worker handles resource-intensive tasks asynchronously.
B) Migrations (SQL)
```sql
-- 001_create_tables.sql
create table if not exists public.jobs (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id) on delete cascade,
status text not null default 'queued' check (status in ('queued', 'processing', 'completed', 'failed')),
input_prompt text not null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table if not exists public.generations (
id uuid primary key default gen_random_uuid(),
job_id uuid not null references public.jobs(id) on delete cascade,
user_id uuid not null references auth.users(id) on delete cascade,
status text not null default 'pending' check (status in ('pending', 'completed', 'failed')),
original_input text not null,
output_url text,
error_message text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table if not exists public.entitlements (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id) on delete cascade,
credits integer not null default 0,
is_paid boolean not null default false,
updated_at timestamptz not null default now(),
unique(user_id)
);
-- Indexes
create index if not exists idx_jobs_user_status on public.jobs(user_id, status);
create index if not exists idx_jobs_created_at on public.jobs(created_at);
create index if not exists idx_generations_user on public.generations(user_id);
create index if not exists idx_generations_job on public.generations(job_id);
-- RLS
alter table public.jobs enable row level security;
alter table public.generations enable row level security;
alter table public.entitlements enable row level security;
create policy "users can view own jobs" on public.jobs for select using (auth.uid() = user_id);
create policy "users can insert own jobs" on public.jobs for insert with check (auth.uid() = user_id);
create policy "users can view own generations" on public.generations for select using (auth.uid() = user_id);
create policy "users can view own entitlements" on public.entitlements for select using (auth.uid() = user_id);
```
C) Edge function file structure + key code blocks
```
supabase/functions/
ping/index.ts
enqueue_generation/index.ts
get_job_status/index.ts
```
```typescript
// ping/index.ts
export const handler = async (req: Request) => {
if (req.method === 'HEAD' || req.method === 'GET') {
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
status: 200
});
}
return new Response('Method not allowed', { status: 405 });
};
```
```typescript
// enqueue_generation/index.ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
export const handler = async (req: Request) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!
);
const authHeader = req.headers.get('Authorization');
if (!authHeader) return new Response('Unauthorized', { status: 401 });
const { data: { user }, error: authError } = await supabase.auth.getUser(authHeader);
if (authError || !user) return new Response('Unauthorized', { status: 401 });
const { prompt } = await req.json();
if (!prompt) return new Response('Missing prompt', { status: 400 });
// Check entitlements
const { data: entitlement } = await supabase
.from('entitlements')
.select('credits, is_paid')
.eq('user_id', user.id)
.single();
if (!entitlement || (!entitlement.is_paid && entitlement.credits <= 0)) {
return new Response('Insufficient credits', { status: 403 });
}
// Create job
const { data: job, error: jobError } = await supabase
.from('jobs')
.insert({ user_id: user.id, input_prompt: prompt, status: 'queued' })
.select()
.single();
if (jobError) return new Response('Job creation failed', { status: 500 });
return new Response(JSON.stringify({ jobId: job.id }), {
headers: { 'Content-Type': 'application/json' },
status: 200
});
};
```
```typescript
// get_job_status/index.ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
export const handler = async (req: Request) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!
);
const authHeader = req.headers.get('Authorization');
if (!authHeader) return new Response('Unauthorized', { status: 401 });
const { data: { user }, error: authError } = await supabase.auth.getUser(authHeader);
if (authError || !user) return new Response('Unauthorized', { status: 401 });
const url = new URL(req.url);
const jobId = url.searchParams.get('jobId');
if (!jobId) return new Response('Missing jobId', { status: 400 });
const { data: job } = await supabase
.from('jobs')
.select('id, status, updated_at')
.eq('id', jobId)
.eq('user_id', user.id)
.single();
if (!job) return new Response('Job not found', { status: 404 });
return new Response(JSON.stringify(job), {
headers: { 'Content-Type': 'application/json' },
status: 200
});
};
```
D) Expo integration notes + key code blocks
```typescript
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
import AsyncStorage from '@react-native-async-storage/async-storage';
export const supabase = createClient(
process.env.EXPO_PUBLIC_SUPABASE_URL!,
process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!,
{
auth: { storage: AsyncStorage, autoRefreshToken: true },
}
);
```
```typescript
// hooks/useGeneration.ts
import { useState, useEffect, useCallback } from 'react';
import { supabase } from '../lib/supabase';
export function useGeneration() {
const [generations, setGenerations] = useState([]);
const [isGenerating, setIsGenerating] = useState(false);
// Non-blocking warm ping on app start
useEffect(() => {
fetch(`${process.env.EXPO_PUBLIC_SUPABASE_URL}/functions/v1/ping`, {
method: 'HEAD',
}).catch(() => {}); // Silently fail
}, []);
const generate = useCallback(async (prompt: string) => {
setIsGenerating(true);
// Optimistic UI - add placeholder immediately
const placeholderId = `temp-${Date.now()}`;
setGenerations(prev => [...prev, { id: placeholderId, status: 'pending', original_input: prompt }]);
try {
const { data: { session } } = await supabase.auth.getSession();
const response = await fetch(
`${process.env.EXPO_PUBLIC_SUPABASE_URL}/functions/v1/enqueue_generation`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session?.access_token}`,
},
body: JSON.stringify({ prompt }),
}
);
const { jobId } = await response.json();
// Subscribe to realtime updates
const channel = supabase
.channel(`job-${jobId}`)
.on('postgres_changes',
{ event: 'UPDATE', schema: 'public', table: 'jobs', filter: `id=eq.${jobId}` },
(payload) => {
if (payload.new.status === 'completed' || payload.new.status === 'failed') {
// Fetch final generation
fetchGeneration(jobId);
channel.unsubscribe();
}
}
)
.subscribe();
// Polling fallback (every 5s, max 2min)
const pollInterval = setInterval(async () => {
const statusRes = await fetch(
`${process.env.EXPO_PUBLIC_SUPABASE_URL}/functions/v1/get_job_status?jobId=${jobId}`,
{ headers: { 'Authorization': `Bearer ${session?.access_token}` } }
);
const job = await statusRes.json();
if (job.status === 'completed' || job.status === 'failed') {
clearInterval(pollInterval);
fetchGeneration(jobId);
}
}, 5000);
setTimeout(() => clearInterval(pollInterval), 120000);
} catch (error) {
console.error('Generation failed:', error);
// Update placeholder to failed state
setGenerations(prev => prev.map(g =>
g.id === placeholderId ? { ...g, status: 'failed' } : g
));
} finally {
setIsGenerating(false);
}
}, []);
const fetchGeneration = useCallback(async (jobId: string) => {
const { data } = await supabase
.from('generations')
.select('*')
.eq('job_id', jobId)
.single();
if (data) {
setGenerations(prev => prev.map(g =>
g.id.startsWith('temp-') ? data : g
));
}
}, []);
return { generations, generate, isGenerating };
}
```
E) Worker outline + pseudo-code
```
Worker Service (Node.js/TypeScript)
- Polls Supabase for queued jobs every 5 seconds
- Processes jobs sequentially or with limited concurrency
- Uses service role key for privileged access
Pseudo-code:
```
```typescript
// worker/index.ts
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
async function processJobs() {
const { data: jobs } = await supabase
.from('jobs')
.select('*')
.eq('status', 'queued')
.order('created_at', { ascending: true })
.limit(5);
for (const job of jobs || []) {
await processJob(job);
}
}
async function processJob(job: any) {
// Mark as processing
await supabase.from('jobs').update({ status: 'processing' }).eq('id', job.id);
try {
// Run AI generation (e.g., call external AI API)
const outputUrl = await runAIGeneration(job.input_prompt);
// Upload to storage
const { data: uploadData } = await supabase.storage
.from('generations')
.upload(`${job.user_id}/${job.id}.png`, outputUrl);
const publicUrl = supabase.storage.from('generations').getPublicUrl(uploadData.path).data.publicUrl;
// Update job and create generation record
await supabase.from('jobs').update({ status: 'completed' }).eq('id', job.id);
await supabase.from('generations').insert({
job_id: job.id,
user_id: job.user_id,
status: 'completed',
original_input: job.input_prompt,
output_url: publicUrl,
});
} catch (error) {
// Failed job still creates generation record with original input
await supabase.from('jobs').update({ status: 'failed' }).eq('id', job.id);
await supabase.from('generations').insert({
job_id: job.id,
user_id: job.user_id,
status: 'failed',
original_input: job.input_prompt,
error_message: error.message,
});
// Retry policy: max 3 attempts, exponential backoff
if (job.retry_count < 3) {
await supabase.from('jobs').update({
status: 'queued',
retry_count: (job.retry_count || 0) + 1
}).eq('id', job.id);
}
}
}
// Idempotency: check if generation already exists for job before creating
async function ensureIdempotency(jobId: string) {
const { data: existing } = await supabase
.from('generations')
.select('id')
.eq('job_id', jobId)
.single();
return !!existing;
}
// Main loop
setInterval(processJobs, 5000);
```
Entre para ver o prompt completo
Continuar com:
Ao entrar, você concorda com nossos Termos de uso e Política de privacidade
Uso
Este prompt foi projetado para uso com coding. Copie o conteúdo acima e cole na sua ferramenta de IA preferida.
Para melhores resultados, personalize os marcadores (indicados por colchetes ou maiúsculas) com seus requisitos específicos.
Discussão
0 comentários