Discusión

Arquitectura Segura de Cold-Start con Expo + Supabase

De Wikiprompt, la enciclopedia libre de prompts

Tugrul Erdem
Contribuido porTugrul ErdemFuente

25 dic 2025

Arquitectura Segura de Cold-Start con Expo + Supabase Un prompt completo para diseñar una arquitectura de aplicación móvil escalable y segura ante arranques en frío, utilizando Expo, Supabase y un servicio de trabajador separado para la generación de IA.

Contenido del PromptGuardar

🌐
A) Architecture summary The system uses a cold-start-safe pattern: Expo client never blocks on Edge Functions, Edge Functions only gate and enqueue, and a separate Worker handles heavy AI generation and storage writes. Supabase Postgres stores jobs and generations, Storage holds generated assets, and Realtime pushes job updates to clients. Failed jobs still produce a generation record with original input for transparency. B) Migrations (SQL) ```sql -- extensions create extension if not exists "pgcrypto"; -- jobs table create table 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_payload jsonb not null, error_message text, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create index jobs_user_id_idx on public.jobs(user_id); create index jobs_status_idx on public.jobs(status) where status = 'queued'; -- generations table create table 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, original_input jsonb not null, storage_path text, status text not null default 'pending' check (status in ('pending','completed','failed')), created_at timestamptz not null default now() ); create index generations_user_id_idx on public.generations(user_id); create index generations_job_id_idx on public.generations(job_id); -- entitlements table create table public.entitlements ( user_id uuid primary key references auth.users(id) on delete cascade, credits integer not null default 0 check (credits >= 0), is_paid boolean not null default false, updated_at timestamptz not null default now() ); -- 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 select own jobs" on public.jobs for select using (auth.uid() = user_id); create policy "users insert own jobs" on public.jobs for insert with check (auth.uid() = user_id); create policy "users select own generations" on public.generations for select using (auth.uid() = user_id); create policy "users select 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 ``` **ping/index.ts** ```typescript export async function handler(req: Request) { if (req.method === 'HEAD' || req.method === 'GET') { return new Response('ok', { status: 200 }); } return new Response('method not allowed', { status: 405 }); } ``` **enqueue_generation/index.ts** ```typescript import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'; export async function handler(req: Request) { if (req.method !== 'POST') return new Response('method not allowed', { status: 405 }); 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 { input_payload } = await req.json(); if (!input_payload) return new Response('missing input', { status: 400 }); // check entitlement 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('no credits', { status: 403 }); } // create job const { data: job, error: jobError } = await supabase .from('jobs') .insert({ user_id: user.id, input_payload }) .select() .single(); if (jobError) return new Response('job creation failed', { status: 500 }); // decrement credit if not paid if (!entitlement.is_paid) { await supabase .from('entitlements') .update({ credits: entitlement.credits - 1 }) .eq('user_id', user.id); } return new Response(JSON.stringify({ jobId: job.id }), { status: 200 }); } ``` **get_job_status/index.ts** ```typescript import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'; export async function handler(req: Request) { const url = new URL(req.url); const jobId = url.searchParams.get('jobId'); if (!jobId) return new Response('missing jobId', { status: 400 }); 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 } } = await supabase.auth.getUser(authHeader); if (!user) return new Response('unauthorized', { status: 401 }); const { data: job } = await supabase .from('jobs') .select('status, error_message') .eq('id', jobId) .eq('user_id', user.id) .single(); if (!job) return new Response('not found', { status: 404 }); return new Response(JSON.stringify(job), { status: 200 }); } ``` D) Expo integration notes + key code blocks - On app start, fire a non-blocking `ping` to warm the Edge Function (no await, no UI impact). - Generate button: optimistic UI with placeholder, then enqueue via Edge Function. - Subscribe to Realtime on `jobs` table filtered by user; fallback to polling `get_job_status` every 5s. - On job completion, replace placeholder in gallery with final generation from `generations` table. ```typescript // warm ping on app start (non-blocking) useEffect(() => { fetch('https://your-project.supabase.co/functions/v1/ping', { method: 'HEAD', }).catch(() => {}); }, []); // generate button const handleGenerate = async (input: string) => { const placeholderId = `placeholder-${Date.now()}`; setGallery(prev => [...prev, { id: placeholderId, status: 'pending', input }]); const { data: { session } } = await supabase.auth.getSession(); const res = await fetch(`${SUPABASE_URL}/functions/v1/enqueue_generation`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session?.access_token}`, }, body: JSON.stringify({ input_payload: { text: input } }), }); if (res.ok) { const { jobId } = await res.json(); subscribeToJob(jobId, placeholderId); } else { setGallery(prev => prev.map(item => item.id === placeholderId ? { ...item, status: 'failed' } : item )); } }; // realtime subscription with polling fallback const subscribeToJob = (jobId: string, placeholderId: string) => { 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') { fetchGeneration(jobId, placeholderId); supabase.removeChannel(channel); } else if (payload.new.status === 'failed') { setGallery(prev => prev.map(item => item.id === placeholderId ? { ...item, status: 'failed' } : item )); supabase.removeChannel(channel); } }) .subscribe(); // fallback polling setTimeout(() => pollJobStatus(jobId, placeholderId), 5000); }; ``` E) Worker outline + pseudo-code **Interface**: A standalone Node/Deno service with access to Supabase service-role key. Exposes no public endpoints; runs as a loop. **Logic**: ``` while (true) { jobs = supabase.from('jobs').select('*').eq('status', 'queued').limit(10) for each job: try: update job status to 'processing' result = runAI(job.input_payload) storagePath = uploadToStorage(result, job.id) insert generation (job_id, user_id, original_input, storage_path, status='completed') update job status to 'completed' catch error: insert generation (job_id, user_id, original_input, status='failed') update job status to 'failed', error_message = error.message // retry policy: max 3 attempts, exponential backoff (30s, 2m, 10m) // idempotency: check if generation already exists for job_id before insert sleep(1s) } ``` **Key points**: - Worker uses service-role key, bypasses RLS. - Failed jobs always create a generation record with original input visible. - Retry only for transient errors (timeouts, 5xx); permanent errors (invalid input) mark failed immediately. - Idempotency: before inserting generation, check `generations` for existing `job_id`; skip if present.

Iniciá sesión para ver el prompt completo

Continuar con:

Al iniciar sesión, aceptás nuestros Términos de uso y Política de privacidad

Uso

Este prompt está diseñado para usarse con coding. Copiá el contenido de arriba y pegalo en tu herramienta de IA preferida.

Para mejores resultados, personalizá los marcadores (indicados con corchetes o mayúsculas) con tus requisitos específicos.

Referencias

Categorías:coding| prompts.chat| expo| supabase

Discusión