Arquitectura Segura de Cold-Start con Expo + Supabase
De Wikiprompt, la enciclopedia libre de prompts
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.
Discusión
0 comentarios