取り組みの背景 — なぜ Edge Functions × RLS が重要なのか
Rork Max でアプリを開発していると、クライアントサイドだけでは処理しきれないロジックに直面する場面が出てきます。外部 API との連携、決済処理、複雑なデータ集計、メール送信——こうしたサーバーサイドの処理を安全かつ効率的に実装するのが Supabase Edge Functions です。
一方、データベースのアクセス制御において「誰がどのデータを読み書きできるか」を厳密に管理するのが Row Level Security(RLS) です。RLS はデータベースレイヤーで動作するため、クライアントから直接 Supabase にアクセスされても不正なデータ参照を防ぐことができます。
この2つを組み合わせることで、Rork Max アプリのバックエンドは「軽量でありながら堅牢」な構成になります。ここで扱うのはEdge Functions と RLS を活用したセキュアな API 設計の全体像を、実装コード付きで解説します。
対象読者は、Rork Max で本格的なアプリをリリースしたい中上級者です。基本的な Supabase の操作(認証・CRUD)については Rork × Supabase 認証&リアルタイム機能実装ガイド で解説していますので、そちらを先にご覧ください。
Supabase Edge Functions の基本アーキテクチャ
Edge Functions とは
Supabase Edge Functions は、Deno ランタイム上で動作するサーバーレス関数です。Supabase のインフラストラクチャ内で実行されるため、データベースとの接続が高速で、コールドスタートも最小限に抑えられています。
主な特徴は以下の通りです。
Deno ランタイム : TypeScript をネイティブサポートし、セキュアなサンドボックス環境で実行される
グローバルエッジ配信 : ユーザーに近いリージョンから応答するため低レイテンシ
Supabase クライアント組み込み : supabase-js が環境内で利用可能
シークレット管理 : 環境変数として API キーやトークンを安全に保存
Edge Functions のディレクトリ構造
supabase/
├── functions/
│ ├── process-payment/
│ │ └── index.ts # 決済処理
│ ├── send-notification/
│ │ └── index.ts # プッシュ通知送信
│ ├── aggregate-analytics/
│ │ └── index.ts # 分析データ集計
│ └── _shared/
│ ├── cors.ts # CORS ヘルパー
│ ├── auth.ts # 認証ヘルパー
│ └── response.ts # レスポンスヘルパー
└── config.toml
_shared/ ディレクトリに共通ユーティリティを配置することで、各 Edge Function のコードを DRY(Don't Repeat Yourself)に保てます。
最小構成の Edge Function
以下は、認証済みユーザーのプロフィールを返すシンプルな Edge Function の例です。
// supabase/functions/get-profile/index.ts
import { serve } from "https://deno.land/std@0.177.0/http/server.ts" ;
import { createClient } from "https://esm.sh/@supabase/supabase-js@2" ;
// CORS ヘッダー定義
const corsHeaders = {
"Access-Control-Allow-Origin" : "*" ,
"Access-Control-Allow-Headers" :
"authorization, x-client-info, apikey, content-type" ,
};
serve ( async ( req : Request ) => {
// preflight リクエスト対応
if (req.method === "OPTIONS" ) {
return new Response ( "ok" , { headers: corsHeaders });
}
try {
// リクエストヘッダーから JWT を取得
const authHeader = req.headers. get ( "Authorization" );
if ( ! authHeader) {
return new Response (
JSON . stringify ({ error: "認証が必要です" }),
{ status: 401 , headers: { ... corsHeaders, "Content-Type" : "application/json" } }
);
}
// Supabase クライアントを認証済みユーザーとして初期化
const supabase = createClient (
Deno.env. get ( "SUPABASE_URL" ) ?? "" ,
Deno.env. get ( "SUPABASE_ANON_KEY" ) ?? "" ,
{ global: { headers: { Authorization: authHeader } } }
);
// 認証済みユーザーの情報を取得
const { data : { user }, error : userError } = await supabase.auth. getUser ();
if (userError || ! user) {
return new Response (
JSON . stringify ({ error: "無効なトークンです" }),
{ status: 401 , headers: { ... corsHeaders, "Content-Type" : "application/json" } }
);
}
// RLS が適用された状態でプロフィールを取得
const { data : profile , error } = await supabase
. from ( "profiles" )
. select ( "*" )
. eq ( "id" , user.id)
. single ();
if (error) throw error;
return new Response (
JSON . stringify ({ profile }),
{ status: 200 , headers: { ... corsHeaders, "Content-Type" : "application/json" } }
);
} catch (err) {
return new Response (
JSON . stringify ({ error: err.message }),
{ status: 500 , headers: { ... corsHeaders, "Content-Type" : "application/json" } }
);
}
});
// 期待する出力例:
// { "profile": { "id": "uuid-xxx", "display_name": "Masaki", "avatar_url": "https://..." } }
このコードのポイントは、Authorization ヘッダーを Supabase クライアントに渡すことで、RLS ポリシーがユーザーコンテキストで評価される 点です。
Row Level Security(RLS)の設計パターン
RLS の仕組み
RLS は PostgreSQL のネイティブ機能で、テーブルの各行に対してアクセスポリシーを定義します。Supabase ではこれがデフォルトで有効化されており、ポリシーが定義されていないテーブルには誰もアクセスできません。
RLS ポリシーは4つの操作に対して定義できます。
SELECT : データの読み取り
INSERT : データの挿入
UPDATE : データの更新
DELETE : データの削除
パターン1: ユーザー所有データの保護
最も基本的なパターンは「自分のデータだけ操作できる」というルールです。
-- profiles テーブルの RLS ポリシー
-- 自分のプロフィールのみ閲覧可能
CREATE POLICY "Users can view own profile"
ON profiles FOR SELECT
USING ( auth . uid () = id);
-- 自分のプロフィールのみ更新可能
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
USING ( auth . uid () = id)
WITH CHECK ( auth . uid () = id);
-- 新規作成時は自分の ID でのみ作成可能
CREATE POLICY "Users can insert own profile"
ON profiles FOR INSERT
WITH CHECK ( auth . uid () = id);
auth.uid() は Supabase が提供する関数で、現在の JWT トークンからユーザー ID を自動的に抽出します。
パターン2: マルチテナント対応
SaaS アプリやチーム機能を持つアプリでは、「同じ組織のメンバーだけがデータを共有できる」というパターンが必要です。
-- 組織メンバーシップテーブル
CREATE TABLE org_members (
org_id UUID REFERENCES organizations(id),
user_id UUID REFERENCES auth . users (id),
role TEXT CHECK ( role IN ( 'owner' , 'admin' , 'member' , 'viewer' )),
PRIMARY KEY (org_id, user_id)
);
-- プロジェクトテーブルの RLS
-- 同じ組織のメンバーのみプロジェクトを閲覧可能
CREATE POLICY "Org members can view projects"
ON projects FOR SELECT
USING (
org_id IN (
SELECT org_id FROM org_members
WHERE user_id = auth . uid ()
)
);
-- admin 以上のみプロジェクトを作成・更新可能
CREATE POLICY "Admins can manage projects"
ON projects FOR ALL
USING (
org_id IN (
SELECT org_id FROM org_members
WHERE user_id = auth . uid ()
AND role IN ( 'owner' , 'admin' )
)
);
パターン3: 公開 × 非公開の混在
ブログやポートフォリオのように「公開コンテンツは誰でも閲覧可能、下書きは本人のみ」という設計パターンです。
-- 記事テーブルの RLS
-- 公開記事は全員が閲覧可能
CREATE POLICY "Anyone can view published posts"
ON posts FOR SELECT
USING ( status = 'published' );
-- 下書きは作成者のみ閲覧可能
CREATE POLICY "Authors can view own drafts"
ON posts FOR SELECT
USING (author_id = auth . uid () AND status = 'draft' );
-- 作成・更新は本人のみ
CREATE POLICY "Authors can manage own posts"
ON posts FOR INSERT
WITH CHECK (author_id = auth . uid ());
CREATE POLICY "Authors can update own posts"
ON posts FOR UPDATE
USING (author_id = auth . uid ())
WITH CHECK (author_id = auth . uid ());
RLS パフォーマンスの最適化
RLS ポリシーのサブクエリが複雑になると、クエリのパフォーマンスが低下します。以下の最適化テクニックを活用してください。
-- ❌ 悪い例: 毎回サブクエリが走る
CREATE POLICY "slow_policy" ON items FOR SELECT
USING (
owner_id IN (
SELECT user_id FROM team_members
WHERE team_id IN (
SELECT team_id FROM user_teams WHERE user_id = auth . uid ()
)
)
);
-- ✅ 良い例: セキュリティ定義関数でキャッシュ効果を得る
CREATE OR REPLACE FUNCTION auth .user_team_ids()
RETURNS SETOF UUID
LANGUAGE sql
SECURITY DEFINER
STABLE -- 同一トランザクション内でキャッシュされる
AS $$
SELECT team_id FROM user_teams WHERE user_id = auth . uid ();
$$;
CREATE POLICY "fast_policy" ON items FOR SELECT
USING (team_id IN ( SELECT auth . user_team_ids ()));
STABLE マーカーを付けることで、PostgreSQL は同一トランザクション内でこの関数の結果をキャッシュします。複数のテーブルで同じ条件を使う場合に効果的です。
Rork Max から Edge Functions を呼び出す
基本的な呼び出しパターン
Rork Max アプリから Edge Function を呼び出すには、supabase-js の functions.invoke() メソッドを使います。
// Rork Max アプリ内での Edge Function 呼び出し
import { supabase } from "./lib/supabase" ;
// 決済処理を Edge Function に委譲する例
async function processPayment ( amount : number , currency : string ) {
try {
const { data , error } = await supabase.functions. invoke (
"process-payment" ,
{
body: {
amount,
currency,
// カード情報はクライアントに保持しない
// Stripe の PaymentIntent を使用
payment_method_id: "pm_xxx" ,
},
}
);
if (error) throw error;
return data;
} catch (err) {
console. error ( "決済エラー:" , err.message);
throw err;
}
}
// 期待する出力例:
// { "success": true, "payment_intent_id": "pi_xxx", "status": "succeeded" }
認証トークンの自動付与
supabase.functions.invoke() は、現在のセッションの JWT トークンを自動的に Authorization ヘッダーに含めます。そのため、Edge Function 側で auth.uid() を使ったユーザー識別が可能です。
エラーハンドリングのベストプラクティス
Edge Function のエラーを Rork Max アプリ側で適切にハンドリングするパターンを紹介します。
// lib/edge-functions.ts — 共通の呼び出しラッパー
import { supabase } from "./supabase" ;
type EdgeFunctionResult < T > = {
data : T | null ;
error : string | null ;
status : number ;
};
export async function callEdgeFunction < T >(
functionName : string ,
body ?: Record < string , unknown >
) : Promise < EdgeFunctionResult < T >> {
try {
const { data , error } = await supabase.functions. invoke (functionName, {
body: body ?? {},
});
if (error) {
// FunctionsHttpError: Edge Function が 4xx/5xx を返した場合
// FunctionsRelayError: ネットワークエラー
// FunctionsFetchError: fetch 自体の失敗
return {
data: null ,
error: error.message,
status: error.context?.status ?? 500 ,
};
}
return { data: data as T , error: null , status: 200 };
} catch (err) {
return {
data: null ,
error: err instanceof Error ? err.message : "不明なエラー" ,
status: 500 ,
};
}
}
// 使用例
const result = await callEdgeFunction <{ profile : Profile }>( "get-profile" );
if (result.error) {
// エラー表示(Toast、Alert など)
showToast ( `エラー: ${ result . error }` );
} else {
setProfile (result.data?.profile);
}
実践: マルチテナント対応のタスク管理 API を構築する
ここからは、実際のアプリ開発を想定した包括的な実装例を見ていきます。チーム向けタスク管理アプリの API を、Edge Functions × RLS で構築します。
データベーススキーマ
-- 組織テーブル
CREATE TABLE organizations (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY ,
name TEXT NOT NULL ,
slug TEXT UNIQUE NOT NULL ,
created_at TIMESTAMPTZ DEFAULT now ()
);
-- メンバーシップテーブル
CREATE TABLE memberships (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY ,
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE ,
user_id UUID REFERENCES auth . users (id) ON DELETE CASCADE ,
role TEXT NOT NULL CHECK ( role IN ( 'owner' , 'admin' , 'member' )),
created_at TIMESTAMPTZ DEFAULT now (),
UNIQUE (org_id, user_id)
);
-- タスクテーブル
CREATE TABLE tasks (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY ,
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE ,
title TEXT NOT NULL ,
description TEXT ,
status TEXT DEFAULT 'todo' CHECK ( status IN ( 'todo' , 'in_progress' , 'done' )),
priority INTEGER DEFAULT 0 ,
assignee_id UUID REFERENCES auth . users (id),
created_by UUID REFERENCES auth . users (id) NOT NULL ,
due_date DATE ,
created_at TIMESTAMPTZ DEFAULT now (),
updated_at TIMESTAMPTZ DEFAULT now ()
);
-- RLS を有効化
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY ;
ALTER TABLE memberships ENABLE ROW LEVEL SECURITY ;
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY ;
-- 組織: メンバーのみ閲覧可能
CREATE POLICY "Members can view org"
ON organizations FOR SELECT
USING (id IN ( SELECT org_id FROM memberships WHERE user_id = auth . uid ()));
-- メンバーシップ: 同じ組織のメンバーが閲覧可能
CREATE POLICY "Members can view memberships"
ON memberships FOR SELECT
USING (org_id IN ( SELECT org_id FROM memberships WHERE user_id = auth . uid ()));
-- タスク: 同じ組織のメンバーが CRUD 可能
CREATE POLICY "Members can view tasks"
ON tasks FOR SELECT
USING (org_id IN ( SELECT org_id FROM memberships WHERE user_id = auth . uid ()));
CREATE POLICY "Members can create tasks"
ON tasks FOR INSERT
WITH CHECK (
org_id IN ( SELECT org_id FROM memberships WHERE user_id = auth . uid ())
AND created_by = auth . uid ()
);
CREATE POLICY "Members can update tasks"
ON tasks FOR UPDATE
USING (org_id IN ( SELECT org_id FROM memberships WHERE user_id = auth . uid ()));
-- 削除は admin 以上のみ
CREATE POLICY "Admins can delete tasks"
ON tasks FOR DELETE
USING (
org_id IN (
SELECT org_id FROM memberships
WHERE user_id = auth . uid () AND role IN ( 'owner' , 'admin' )
)
);
Edge Function: タスク集計 API
// supabase/functions/task-summary/index.ts
import { serve } from "https://deno.land/std@0.177.0/http/server.ts" ;
import { createClient } from "https://esm.sh/@supabase/supabase-js@2" ;
const corsHeaders = {
"Access-Control-Allow-Origin" : "*" ,
"Access-Control-Allow-Headers" :
"authorization, x-client-info, apikey, content-type" ,
};
serve ( async ( req : Request ) => {
if (req.method === "OPTIONS" ) {
return new Response ( "ok" , { headers: corsHeaders });
}
try {
const authHeader = req.headers. get ( "Authorization" );
if ( ! authHeader) {
return new Response (
JSON . stringify ({ error: "Unauthorized" }),
{ status: 401 , headers: { ... corsHeaders, "Content-Type" : "application/json" } }
);
}
const { org_id } = await req. json ();
if ( ! org_id) {
return new Response (
JSON . stringify ({ error: "org_id は必須です" }),
{ status: 400 , headers: { ... corsHeaders, "Content-Type" : "application/json" } }
);
}
// Service Role キーではなく、ユーザーの JWT で初期化
// → RLS が適用され、権限のない組織のデータにはアクセスできない
const supabase = createClient (
Deno.env. get ( "SUPABASE_URL" ) ?? "" ,
Deno.env. get ( "SUPABASE_ANON_KEY" ) ?? "" ,
{ global: { headers: { Authorization: authHeader } } }
);
// タスクのステータス別集計
const { data : tasks , error } = await supabase
. from ( "tasks" )
. select ( "status, priority, assignee_id, due_date" )
. eq ( "org_id" , org_id);
if (error) throw error;
const summary = {
total: tasks. length ,
by_status: {
todo: tasks. filter (( t ) => t.status === "todo" ). length ,
in_progress: tasks. filter (( t ) => t.status === "in_progress" ). length ,
done: tasks. filter (( t ) => t.status === "done" ). length ,
},
overdue: tasks. filter (
( t ) => t.due_date && new Date (t.due_date) < new Date () && t.status !== "done"
). length ,
high_priority: tasks. filter (( t ) => t.priority >= 3 ). length ,
completion_rate:
tasks. length > 0
? Math. round (
(tasks. filter (( t ) => t.status === "done" ). length / tasks. length ) * 100
)
: 0 ,
};
return new Response (
JSON . stringify ({ summary }),
{ status: 200 , headers: { ... corsHeaders, "Content-Type" : "application/json" } }
);
} catch (err) {
return new Response (
JSON . stringify ({ error: err.message }),
{ status: 500 , headers: { ... corsHeaders, "Content-Type" : "application/json" } }
);
}
});
// 期待する出力例:
// {
// "summary": {
// "total": 42,
// "by_status": { "todo": 15, "in_progress": 12, "done": 15 },
// "overdue": 3,
// "high_priority": 8,
// "completion_rate": 36
// }
// }
Edge Functions のセキュリティベストプラクティス
1. Service Role キーの使い分け
Edge Function 内では2種類のクライアント初期化が可能です。使い分けを間違えると、RLS をバイパスしてしまう重大なセキュリティ事故につながります。
// ✅ ユーザーコンテキスト(RLS 適用)
// → ユーザーデータの CRUD に使用
const userClient = createClient (
Deno.env. get ( "SUPABASE_URL" ) ! ,
Deno.env. get ( "SUPABASE_ANON_KEY" ) ! ,
{ global: { headers: { Authorization: authHeader } } }
);
// ⚠️ Service Role(RLS バイパス)
// → 管理系処理(統計集計、バッチ処理)にのみ使用
// → ユーザー入力を直接渡さないこと
const adminClient = createClient (
Deno.env. get ( "SUPABASE_URL" ) ! ,
Deno.env. get ( "SUPABASE_SERVICE_ROLE_KEY" ) !
);
原則として、ユーザーのリクエストに基づくデータ操作には必ずユーザーコンテキスト(ANON_KEY + JWT)を使い 、Service Role は管理バッチ処理やシステム通知など、ユーザーコンテキストでは実行できない操作に限定してください。
2. 入力バリデーション
Edge Function に渡される入力は、クライアントが自由に改ざんできます。必ずサーバー側でバリデーションを行いましょう。
// Zod を使った入力バリデーション例
import { z } from "https://deno.land/x/zod@v3.22.4/mod.ts" ;
const CreateTaskSchema = z. object ({
org_id: z. string (). uuid (),
title: z. string (). min ( 1 ). max ( 200 ),
description: z. string (). max ( 5000 ). optional (),
priority: z. number (). int (). min ( 0 ). max ( 5 ). default ( 0 ),
assignee_id: z. string (). uuid (). optional (),
due_date: z. string (). datetime (). optional (),
});
// Edge Function 内での使用
const body = await req. json ();
const parsed = CreateTaskSchema. safeParse (body);
if ( ! parsed.success) {
return new Response (
JSON . stringify ({
error: "バリデーションエラー" ,
details: parsed.error. flatten (),
}),
{ status: 400 , headers: { ... corsHeaders, "Content-Type" : "application/json" } }
);
}
// parsed.data は型安全
const { org_id , title , description , priority } = parsed.data;
3. レートリミットの実装
Edge Functions にはデフォルトのレートリミットがありませんが、Supabase のテーブルを利用した簡易的なレートリミットを実装できます。
// _shared/rate-limit.ts
import { SupabaseClient } from "https://esm.sh/@supabase/supabase-js@2" ;
export async function checkRateLimit (
adminClient : SupabaseClient ,
userId : string ,
action : string ,
maxRequests : number ,
windowMinutes : number
) : Promise < boolean > {
const windowStart = new Date (
Date. now () - windowMinutes * 60 * 1000
). toISOString ();
// 直近のリクエスト数をカウント
const { count } = await adminClient
. from ( "rate_limits" )
. select ( "*" , { count: "exact" , head: true })
. eq ( "user_id" , userId)
. eq ( "action" , action)
. gte ( "created_at" , windowStart);
if ((count ?? 0 ) >= maxRequests) {
return false ; // レートリミット超過
}
// リクエストを記録
await adminClient
. from ( "rate_limits" )
. insert ({ user_id: userId, action });
return true ;
}
// Edge Function での使用例
const allowed = await checkRateLimit (adminClient, user.id, "create-task" , 100 , 60 );
if ( ! allowed) {
return new Response (
JSON . stringify ({ error: "リクエストが多すぎます。しばらく待ってから再試行してください。" }),
{ status: 429 , headers: corsHeaders }
);
}
Edge Functions のデバッグとデプロイ
ローカル開発環境
# Supabase CLI でローカルサーバーを起動
supabase start
# Edge Function をローカルで実行
supabase functions serve --env-file .env.local
# 別のターミナルからテスト
curl -i --location --request POST \
"http://localhost:54321/functions/v1/task-summary" \
--header "Authorization: Bearer YOUR_ANON_KEY" \
--header "Content-Type: application/json" \
--data '{"org_id": "test-org-id"}'
本番デプロイ
# 特定の関数をデプロイ
supabase functions deploy task-summary
# すべての関数を一括デプロイ
supabase functions deploy
# シークレットの設定
supabase secrets set STRIPE_SECRET_KEY=YOUR_API_KEY
supabase secrets set SENDGRID_API_KEY=YOUR_API_KEY
ログの確認とモニタリング
# リアルタイムログ(デバッグに便利)
supabase functions logs task-summary --tail
# 過去24時間のエラーログ
supabase functions logs task-summary --status error
Supabase ダッシュボードの Edge Functions セクションでは、呼び出し回数、平均レイテンシ、エラー率をグラフで確認できます。アプリの成長に合わせてパフォーマンスを監視する点が肝心です。
パフォーマンス最適化テクニック
1. コネクションプーリング
Edge Functions は短命なリクエストごとに実行されるため、データベース接続の効率化が重要です。
// Supabase クライアントはリクエストごとに作成するが、
// 内部的にコネクションプールが使われるため問題ない
// ただし、直接 PostgreSQL に接続する場合は pooler を使う
const supabase = createClient (
// pooler URL を使用(6543 ポート)
Deno.env. get ( "SUPABASE_URL" ) ! . replace ( ":5432" , ":6543" ),
Deno.env. get ( "SUPABASE_ANON_KEY" ) !
);
2. レスポンスキャッシュ
変更頻度が低いデータに対しては、Cache-Control ヘッダーを活用します。
// 5分間キャッシュを許可する例
return new Response ( JSON . stringify ({ data }), {
status: 200 ,
headers: {
... corsHeaders,
"Content-Type" : "application/json" ,
"Cache-Control" : "public, max-age=300, s-maxage=300" ,
},
});
3. 並列クエリの活用
複数のデータ取得が必要な場合は、Promise.all() で並列実行します。
// ❌ 直列実行(遅い)
const tasks = await supabase. from ( "tasks" ). select ( "*" ). eq ( "org_id" , orgId);
const members = await supabase. from ( "memberships" ). select ( "*" ). eq ( "org_id" , orgId);
// ✅ 並列実行(高速)
const [ tasksResult , membersResult ] = await Promise . all ([
supabase. from ( "tasks" ). select ( "*" ). eq ( "org_id" , orgId),
supabase. from ( "memberships" ). select ( "*, profiles(display_name, avatar_url)" ). eq ( "org_id" , orgId),
]);
個人開発者の視点から(実体験メモ)
まとめ
Supabase Edge Functions と Row Level Security を組み合わせることで、Rork Max アプリのバックエンドをシンプルかつ堅牢に構築できます。本記事で解説した内容を振り返ります。
Edge Functions は Deno ベースのサーバーレス関数で、外部 API 連携や複雑なロジックの実行に適している
RLS はデータベースレイヤーで認可を強制し、クライアントからの不正アクセスを防ぐ
ユーザーコンテキストと Service Role の使い分けがセキュリティの要
入力バリデーション、レートリミット、ログ監視を組み合わせて本番品質のバックエンドを実現する
セキュリティの基本については Rork Max セキュリティベストプラクティス も合わせてご参照ください。AI を活用したセマンティック検索の実装に興味がある方は Rork × Supabase pgvector セマンティック検索アプリ開発ガイド をお読みください。