Expo + Supabase コールドスタート安全アーキテクチャ
フリーのプロンプト百科事典 Wikiprompt より
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 ツールに貼り付けてください。
最良の結果を得るには、プレースホルダー(角括弧や大文字で示された部分)を具体的な要件に置き換えてください。
ノート
0 件のコメント