Diskussion

Expo + Supabase Cold-Start sichere Architektur

Von Wikiprompt, der freien Prompt-Enzyklopädie

Tugrul Erdem
Beigetragen vonTugrul ErdemQuelle

25. Dez. 2025

Expo + Supabase Cold-Start sichere Architektur Ein umfassender Prompt für das Entwerfen einer skalierbaren, kaltstart-sicheren mobilen App-Architektur mit Expo, Supabase und einem separaten Worker-Dienst für die KI-Generierung.

Prompt-InhaltSpeichern

🌐
A) Architecture summary The system uses a three-tier architecture: - **Expo client**: Handles UI, optimistic updates, and real-time subscriptions. Never blocks on network calls. - **Supabase**: Provides Postgres for state, Storage for generated assets, Realtime for job updates, and Edge Functions for lightweight gating/enqueueing. - **Worker service**: A separate Node.js (or similar) process that polls for queued jobs, runs AI generation, uploads results, and updates state. This keeps heavy compute off the edge. Flow: Client calls `enqueue_generation` → Edge validates auth/credits → creates job → Worker picks up job → generates → uploads → updates job + inserts generation → Client receives Realtime event → replaces placeholder. 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_prompt text not null, input_params jsonb not null default '{}'::jsonb, error_message text, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create index jobs_status_idx on public.jobs (status) where status = 'queued'; create index jobs_user_created_idx on public.jobs (user_id, created_at desc); -- 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, storage_path text, original_prompt text not null, status text not null default 'pending' check (status in ('pending','ready','failed')), created_at timestamptz not null default now() ); create index generations_user_created_idx on public.generations (user_id, created_at desc); -- 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 can select 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 select own generations" on public.generations for select using (auth.uid() = user_id); create policy "users can 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** (lightweight health check) ```ts import { corsHeaders } from "../_shared/cors.ts"; Deno.serve(async (req) => { if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders }); return new Response(JSON.stringify({ ok: true, ts: Date.now() }), { headers: { ...corsHeaders, "Content-Type": "application/json" }, }); }); ``` **enqueue_generation/index.ts** (validate + create job) ```ts import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; Deno.serve(async (req) => { if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders }); 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("Unauthorized", { status: 401 }); const { prompt, params } = await req.json(); if (!prompt || prompt.length > 500) return new Response("Invalid prompt", { status: 400 }); const { data: ent, error: entError } = await supabase .from("entitlements") .select("credits, is_paid") .eq("user_id", user.id) .single(); if (entError || (!ent.is_paid && ent.credits < 1)) { return new Response("Insufficient credits", { status: 403 }); } const { data: job, error: jobError } = await supabase .from("jobs") .insert({ user_id: user.id, input_prompt: prompt, input_params: params }) .select() .single(); if (jobError) return new Response("Job creation failed", { status: 500 }); if (!ent.is_paid) { await supabase.from("entitlements").update({ credits: ent.credits - 1 }).eq("user_id", user.id); } return new Response(JSON.stringify({ jobId: job.id }), { headers: { ...corsHeaders, "Content-Type": "application/json" } }); }); ``` **get_job_status/index.ts** (light read) ```ts Deno.serve(async (req) => { 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")!, { global: { headers: { Authorization: req.headers.get("Authorization")! } }, }); const { data, error } = await supabase.from("jobs").select("status, error_message").eq("id", jobId).single(); if (error) return new Response("Not found", { status: 404 }); return new Response(JSON.stringify(data), { headers: { ...corsHeaders, "Content-Type": "application/json" } }); }); ``` D) Expo integration notes + key code blocks - Use `@supabase/supabase-js` and `@supabase/realtime-js` (or the built-in Realtime from supabase-js). - On app start, fire a `fetch` to `/functions/v1/ping` with `catch(() => {})` - never await it. - Generate button: immediately add a placeholder generation to local state, then call `enqueue_generation`. - Subscribe to Realtime on `jobs` table filtered by `user_id` and `status = 'completed'` or `'failed'`. - Fallback: poll `get_job_status` every 5s for jobs older than 10s without a Realtime event. ```tsx // app/generate.tsx const [generations, setGenerations] = useState<Generation[]>([]); // warm ping - non-blocking useEffect(() => { fetch(`${SUPABASE_URL}/functions/v1/ping`, { method: "HEAD" }).catch(() => {}); }, []); // realtime subscription useEffect(() => { const channel = supabase .channel("jobs-updates") .on("postgres_changes", { event: "UPDATE", schema: "public", table: "jobs", filter: `user_id=eq.${user.id}` }, (payload) => { const job = payload.new; if (job.status === "completed" || job.status === "failed") { // fetch generation or update placeholder refreshGenerations(); } }) .subscribe(); return () => { supabase.removeChannel(channel); }; }, [user.id]); // generate button const handleGenerate = async (prompt: string) => { const tempId = `temp-${Date.now()}`; setGenerations(prev => [{ id: tempId, status: "pending", original_prompt: prompt }, ...prev]); try { const { data, error } = await supabase.functions.invoke("enqueue_generation", { body: { prompt, params: { model: "flux", size: "1024x1024" } }, }); if (error) throw error; // store jobId mapping for polling fallback setJobMapping(prev => ({ ...prev, [data.jobId]: tempId })); } catch (e) { setGenerations(prev => prev.map(g => g.id === tempId ? { ...g, status: "failed" } : g)); } }; ``` E) Worker outline + pseudo-code **Interface**: A standalone Node.js service with: - `GET /health` - liveness probe - Polling loop: every 2s, fetch `jobs` where `status = 'queued'` (limit 5) - No public endpoints needed; it only talks to Supabase. **Pseudo-code**: ```js // worker.js const POLL_INTERVAL = 2000; const MAX_RETRIES = 3; async function poll() { const { data: jobs } = await supabase .from("jobs") .select("*") .eq("status", "queued") .limit(5); for (const job of jobs) { await processJob(job); } } async function processJob(job) { // idempotency: check if already processing (use a lock or updated_at) const { data: locked } = await supabase .from("jobs") .update({ status: "processing", updated_at: new Date() }) .eq("id", job.id) .eq("status", "queued") .select() .single(); if (!locked) return; // someone else took it try { const result = await runAI(job.input_prompt, job.input_params); const { path } = await uploadToStorage(result, `generations/${job.user_id}/${job.id}.png`); 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, storage_path: path, original_prompt: job.input_prompt, status: "ready", }); } catch (err) { const retryCount = (job.retry_count || 0) + 1; if (retryCount <= MAX_RETRIES) { await supabase.from("jobs").update({ status: "queued", retry_count: retryCount }).eq("id", job.id); } else { await supabase.from("jobs").update({ status: "failed", error_message: err.message }).eq("id", job.id); // always create a generation record so the user sees the failure await supabase.from("generations").insert({ job_id: job.id, user_id: job.user_id, original_prompt: job.input_prompt, status: "failed", }); } } } setInterval(poll, POLL_INTERVAL); ``` **Retry policy**: Exponential backoff (e.g., 2s, 4s, 8s) via `retry_count` and a `next_retry_at` timestamp. **Idempotency**: Use the `status` transition from `queued` → `processing` as a lock; only one worker can claim a job.

Melde dich an, um den vollständigen Prompt zu sehen

Weiter mit:

Mit der Anmeldung akzeptierst du unsere Nutzungsbedingungen und Datenschutz

Verwendung

Dieser Prompt ist für die Verwendung mit coding gedacht. Kopiere den Inhalt oben und füge ihn in dein bevorzugtes KI-Tool ein.

Für beste Ergebnisse passe die Platzhalter (eckige Klammern oder Großbuchstaben) an deine Anforderungen an.

Referenzen

Kategorien:coding| prompts.chat| expo| supabase

Diskussion