ノート

Expo + Supabase コールドスタート安全アーキテクチャ

フリーのプロンプト百科事典 Wikiprompt より

Tugrul Erdem

2025年12月25日

Expo + Supabase コールドスタート安全アーキテクチャ Expo、Supabase、およびAI生成用の別個のワーカーサービスを使用した、スケーラブルでコールドスタートに安全なモバイルアプリアーキテクチャを設計するための包括的なプロンプト。

プロンプト内容保存

🌐
A) アーキテクチャ概要 このアーキテクチャは、Expoクライアント、Supabase(Postgres、Storage、Realtime)、軽量なゲーティングとジョブエンキュー用のSupabase Edge Functions、そして重いAI生成とストレージ書き込みを処理する別のWorkerサービスで構成されます。コールドスタートの安全性を確保するため、アプリ起動時にEdge Functionをブロックせず、AI呼び出しはEdge Functions内では実行せず、失敗したジョブでも元の入力が表示されたgenerationレコードが作成されるようにします。 B) マイグレーション(SQL) ```sql -- 拡張機能 create extension if not exists "pgcrypto"; -- ジョブテーブル 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 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, input_payload jsonb not null, output_url text, status text not null default 'pending' check (status in ('pending', 'completed', 'failed')), created_at timestamptz not null default now() ); -- エンタイトルメントテーブル create table public.entitlements ( id uuid primary key default gen_random_uuid(), user_id uuid not null references auth.users(id) on delete cascade, credits_remaining integer not null default 0, is_paid boolean not null default false, updated_at timestamptz not null default now() ); -- インデックス create index idx_jobs_user_id on public.jobs(user_id); create index idx_jobs_status on public.jobs(status); create index idx_generations_user_id on public.generations(user_id); create index idx_generations_job_id on public.generations(job_id); create index idx_entitlements_user_id on public.entitlements(user_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 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 insert own generations" on public.generations for insert with check (auth.uid() = user_id); create policy "users can select own entitlements" on public.entitlements for select using (auth.uid() = user_id); ``` C) Edge Functionsのファイル構造と主要コード ``` supabase/functions/ ├── ping/index.ts ├── enqueue_generation/index.ts └── get_job_status/index.ts ``` **ping/index.ts** ```typescript import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; serve(async () => { return new Response(JSON.stringify({ ok: true }), { headers: { "Content-Type": "application/json" }, }); }); ``` **enqueue_generation/index.ts** ```typescript import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; serve(async (req) => { const supabase = createClient( Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_ANON_KEY")! ); const authHeader = req.headers.get("Authorization"); const { data: { user }, error: authError } = await supabase.auth.getUser(authHeader); if (authError || !user) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 }); } const { input_payload } = await req.json(); // エンタイトルメントチェック const { data: entitlement } = await supabase .from("entitlements") .select("credits_remaining, is_paid") .eq("user_id", user.id) .single(); if (!entitlement || (!entitlement.is_paid && entitlement.credits_remaining <= 0)) { return new Response(JSON.stringify({ error: "Insufficient credits" }), { status: 403 }); } // ジョブ作成 const { data: job, error: jobError } = await supabase .from("jobs") .insert({ user_id: user.id, input_payload }) .select() .single(); if (jobError) { return new Response(JSON.stringify({ error: jobError.message }), { status: 500 }); } // クレジット減算 await supabase .from("entitlements") .update({ credits_remaining: entitlement.credits_remaining - 1 }) .eq("user_id", user.id); return new Response(JSON.stringify({ jobId: job.id }), { status: 200 }); }); ``` **get_job_status/index.ts** ```typescript import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; serve(async (req) => { const supabase = createClient( Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_ANON_KEY")! ); const authHeader = req.headers.get("Authorization"); const { data: { user }, error: authError } = await supabase.auth.getUser(authHeader); if (authError || !user) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 }); } const url = new URL(req.url); const jobId = url.searchParams.get("jobId"); const { data: job } = await supabase .from("jobs") .select("status, error_message") .eq("id", jobId) .eq("user_id", user.id) .single(); return new Response(JSON.stringify(job), { status: 200 }); }); ``` D) Expo統合ノートと主要コード **アプリ起動時の非ブロッキングping** ```typescript // app/_layout.tsx useEffect(() => { // 非ブロッキングでウォームアップ fetch(`${SUPABASE_URL}/functions/v1/ping`, { method: "GET", headers: { "Authorization": `Bearer ${anonKey}` } }).catch(() => { // 失敗しても無視 }); }, []); ``` **生成ボタン(楽観的UI + プレースホルダー)** ```typescript // components/GenerateButton.tsx const handleGenerate = async () => { // 楽観的UI const tempId = `temp-${Date.now()}`; setGallery(prev => [...prev, { id: tempId, status: "pending", input: prompt }]); const res = await fetch(`${SUPABASE_URL}/functions/v1/enqueue_generation`, { method: "POST", headers: { "Authorization": `Bearer ${session.accessToken}`, "Content-Type": "application/json" }, body: JSON.stringify({ input_payload: { prompt } }) }); const { jobId } = await res.json(); setJobId(jobId); }; ``` **Realtimeサブスクリプション + ポーリングフォールバック** ```typescript // hooks/useJobSubscription.ts useEffect(() => { if (!jobId) return; // Realtimeサブスクリプション const channel = supabase .channel(`job-${jobId}`) .on("postgres_changes", { event: "UPDATE", schema: "public", table: "jobs", filter: `id=eq.${jobId}` }, (payload) => { handleJobUpdate(payload.new); }) .subscribe(); // ポーリングフォールバック(30秒ごと) const pollInterval = setInterval(async () => { const res = await fetch(`${SUPABASE_URL}/functions/v1/get_job_status?jobId=${jobId}`, { headers: { "Authorization": `Bearer ${session.accessToken}` } }); const data = await res.json(); handleJobUpdate(data); }, 30000); return () => { supabase.removeChannel(channel); clearInterval(pollInterval); }; }, [jobId]); ``` **ギャラリーリストのプレースホルダー置換** ```typescript const handleJobUpdate = (job) => { if (job.status === "completed") { // 生成レコードを取得してプレースホルダーを置換 fetchGeneration(job.id).then(gen => { setGallery(prev => prev.map(item => item.id === tempId ? { ...gen, status: "completed" } : item )); }); } else if (job.status === "failed") { setGallery(prev => prev.map(item => item.id === tempId ? { ...item, status: "failed", error: job.error_message } : item )); } }; ``` E) Workerアウトライン + 疑似コード **Workerサービス(別プロセス)** ``` worker/ ├── index.ts # メインループ ├── ai_generator.ts # AI生成ロジック ├── storage.ts # ストレージアップロード └── db.ts # データベース操作 ``` **疑似コード** ```typescript // index.ts async function main() { while (true) { // キューからジョブを取得 const { data: jobs } = await supabase .from("jobs") .select("*") .eq("status", "queued") .limit(5); for (const job of jobs) { await processJob(job); } await sleep(5000); // 5秒待機 } } async function processJob(job) { // 冪等性チェック const { data: existing } = await supabase .from("generations") .select("id") .eq("job_id", job.id) .single(); if (existing) return; // 既に処理済み try { // ジョブを処理中に更新 await supabase.from("jobs").update({ status: "processing" }).eq("id", job.id); // AI生成 const output = await generateAI(job.input_payload); // ストレージにアップロード const { data: upload } = await supabase.storage .from("generations") .upload(`${job.user_id}/${job.id}.png`, output); // ジョブ完了 + 生成レコード作成 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, input_payload: job.input_payload, output_url: upload.path, status: "completed" }); } catch (error) { // 失敗時も生成レコードを作成(元の入力を表示) await supabase.from("jobs").update({ status: "failed", error_message: error.message }).eq("id", job.id); await supabase.from("generations").insert({ job_id: job.id, user_id: job.user_id, input_payload: job.input_payload, status: "failed" }); // リトライポリシー(最大3回) if (job.retry_count < 3) { await supabase.from("jobs").update({ status: "queued", retry_count: (job.retry_count || 0) + 1 }).eq("id", job.id); } } } ```

ログインして完全なプロンプトを表示

次で続行:

ログインすると、次に同意したことになります: 利用規約 と プライバシーポリシー

使い方

このプロンプトは coding 向けに設計されています。上の内容をコピーして、お好みの AI ツールに貼り付けてください。

最良の結果を得るには、プレースホルダー(角括弧や大文字で示された部分)を具体的な要件に置き換えてください。

参考資料

カテゴリ:coding| prompts.chat| expo| supabase

ノート