Arquitetura Segura para Cold-Start com Expo + Supabase

De Wikiprompt, a enciclopédia livre de prompts

Tugrul Erdem
Contribuído porTugrul ErdemFonte

25 de dez. de 2025

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.

Referências

Categorias:coding| prompts.chat| expo| supabase

Discussão

0 comentários