Expo + Supabase Cold-Start sichere Architektur
Von Wikiprompt, der freien Prompt-Enzyklopädie
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.
Diskussion
0 Kommentare