API仕様
API spec
モバイルアプリが利用する API を、接続先ごとに分類して一覧化します。
現行設計では Cloudflare Workers BFF(/v1/*、OpenAPI 3.1)がクライアント向けの唯一の API 層で、
複雑なロジックは PostgreSQL RPC(Hyperdrive 経由で Workers から呼び出し)に集約しています。
Supabase は Auth / Realtime / PostgreSQL のみ(SDK 直接使用は Auth と Realtime の 2 系統に限定)。
This page catalogs every API the mobile app talks to, grouped by backend.
In the current design the Cloudflare Workers BFF (/v1/*, OpenAPI 3.1) is the single client-facing API surface,
with complex logic consolidated into PostgreSQL RPCs (invoked from Workers via Hyperdrive).
Supabase is used only for Auth / Realtime / PostgreSQL — direct SDK use is limited to Auth and Realtime.
6.1 API設計方針API design principles
- 契約駆動:
parky/api/openapi.json(OpenAPI 3.1)が正本。Workers 実装は @hono/zod-openapi で spec と連動し、CI で openapi.json を自動再生成する
- Contract-first:
parky/api/openapi.json (OpenAPI 3.1) is the source of truth. Workers use @hono/zod-openapi to stay in sync with the spec; CI regenerates openapi.json
- 単一 API 層:クライアントが叩くのは
/v1/*(Cloudflare Workers BFF)のみ。supabase.from() / rpc() / storage の直叩きは禁止(Auth と Realtime のみ SDK 直接利用)
- Single API surface: clients hit only
/v1/* (the Cloudflare Workers BFF). Calling supabase.from() / rpc() / storage directly is forbidden — only Auth and Realtime use the SDK
- 認証:Supabase JWT を
Authorization: Bearer <token> で BFF に送る。Workers が JWKS で検証してユーザースコープを立てる
- Authentication: send the Supabase JWT as
Authorization: Bearer <token>. Workers verify via JWKS and scope the request to the user
- 認可:Workers がコード層で user_id スコープを徹底。RLS は Service Role でも二重防御として有効
- Authorization: Workers enforce user_id scoping in code. RLS stays on as defense-in-depth even under Service Role
- 複雑ロジック:トランザクション・集計は PostgreSQL RPC に切り出し、Workers から Hyperdrive 経由で呼び出す
- Complex logic: transactions and aggregations live as PostgreSQL RPCs, invoked from Workers via Hyperdrive
- 冪等性:副作用のある RPC は
p_client_request_id (uuid) を引数に取り、二重発火を検知して no-op にする
- Idempotency: mutating RPCs take a
p_client_request_id (uuid) and collapse duplicate calls into a no-op
- バージョニング:
/v1/ プレフィックス。破壊的変更は /v2/ で並行運用し、旧版は最低 180 日維持
- Versioning:
/v1/ prefix. Breaking changes ship as /v2/ and run in parallel for at least 180 days
- エラー:Workers は統一 JSON エラー形式(
{ error: { code, message, request_id, issues? } })。422 は Zod validation 失敗
- Errors: Workers return a unified JSON shape (
{ error: { code, message, request_id, issues? } }). 422 means Zod validation failed
- インタラクティブ仕様:Swagger UI(Dev 固定)・Redoc(read-only)で直接叩ける
- Interactive spec: Swagger UI (Dev-only) / Redoc (read-only) let you explore the spec directly
6.2 API一覧API inventory
モバイルが叩く BFF エンドポイント(代表)BFF endpoints the mobile app calls (highlights)
完全な一覧と試し打ちは Swagger UI / Redoc を参照。
ここでは画面との対応を把握しやすい代表的な公開/自分向けエンドポイントのみ抜粋します。
For the full list and interactive tryouts use Swagger UI / Redoc.
This table highlights the public and "me" endpoints that map 1-to-1 to screens.
| メソッドMethod |
パスPath |
用途Purpose |
| GET | /v1/parking-lots | 駐車場検索(フィルタ + ページング)Search parking lots (filter + paging) |
| GET | /v1/parking-lots/nearby | GPS 周辺検索(PostGIS RPC)Nearby by GPS (PostGIS RPC) |
| GET | /v1/parking-lots/{id} | 駐車場詳細(画像・タグ・属性)Parking-lot detail (images, tags, attributes) |
| GET | /v1/parking-lots/{id}/pricing-rules | 料金ルールPricing rules |
| POST | /v1/parking-lots/{id}/calc-fee | 料金試算(calc_parking_fee)Fee estimate (calc_parking_fee) |
| GET | /v1/parking-lots/{id}/reviews | 承認済みレビュー一覧Approved reviews |
| POST | /v1/parking-lots/{id}/reviews | レビュー投稿(status=pending で作成)Post a review (created as status=pending) |
| POST | /v1/parking-sessions | 駐車開始(create_parking_session)Start session (create_parking_session) |
| POST | /v1/parking-sessions/{id}/finalize | 駐車終了+料金確定End session + confirm fee |
| POST | /v1/parking-sessions/{id}/cancel | セッションキャンセル(5 分以内)Cancel session (within 5 min) |
| GET | /v1/me | 自分のプロフィール+権限スコープOwn profile + permission scopes |
| PATCH | /v1/me | プロフィール更新Update profile |
| POST | /v1/me/withdraw | 退会(匿名化+ステータス変更)Withdraw account (anonymize + status change) |
| GET/POST | /v1/me/saved-parking-lots | お気に入り一覧・追加List / add favorites |
| DELETE | /v1/me/saved-parking-lots/{lotId} | お気に入り解除Unfavorite |
| GET | /v1/me/parking-sessions | 自分の駐車履歴Own parking history |
| PATCH | /v1/me/parking-sessions/{id} | メモ・個人評価の更新Update memo / personal rating |
| GET | /v1/me/vehicles | 車両一覧Own vehicles |
| POST/PATCH/DELETE | /v1/me/vehicles[/{id}] | 車両登録・更新・ソフト削除Register / update / soft-delete vehicles |
| GET | /v1/me/search-presets | 検索条件プリセット一覧(sort_order 昇順)List saved search presets |
| POST/PATCH/DELETE | /v1/me/search-presets[/{id}] | プリセット作成(最大 20 件)・更新・ソフト削除。16 項目の query_json を保持Create (max 20) / update / soft-delete a preset holding the 16-field query_json |
| POST | /v1/me/search-presets/{id}/set-default | デフォルトプリセット切替(起動時に自動適用される 1 件)Flip the default preset (auto-applied on app launch) |
| GET | /v1/me/notifications | 通知一覧(Realtime 購読対象)Notification list (subject to Realtime) |
| POST | /v1/me/notifications/mark-read | 一括既読(IDs or before)Bulk mark read (IDs or before) |
| PUT | /v1/me/push-tokens | FCM デバイストークン upsertUpsert FCM device token |
| GET | /v1/me/exp | 自分の EXP / レベルOwn EXP / level |
| GET | /v1/me/badges | 獲得バッジEarned badges |
| GET | /v1/me/badge-progress | 進捗中バッジIn-progress badges |
| GET | /v1/me/themes | 所有テーマOwned themes |
| POST | /v1/me/themes/{id}/apply | テーマ適用Apply a theme |
| GET | /v1/me/subscription | 現行サブスクCurrent subscription |
| POST | /v1/me/subscription/verify-iap | IAP レシート検証Verify IAP receipt |
| GET | /v1/codes | コードマスター一括(edge キャッシュ)Bulk code master (edge-cached) |
| GET | /v1/subscription-plans | プランマスター(公開)Plan master (public) |
| GET | /v1/tags | タグマスターTag master |
| GET | /v1/articles | 公開記事Published articles |
| GET | /v1/ads | 配信広告Active ads |
| GET | /v1/sponsors | 公開スポンサーPublic sponsors |
| GET | /v1/sponsors/nearby | 周辺スポンサー(PostGIS)Nearby sponsors (PostGIS) |
| POST | /v1/sponsors/{id}/checkin | スポンサーチェックインCheck in at sponsor |
| POST | /v1/support/tickets | サポート起票Create support ticket |
| GET | /v1/support/tickets | 自分のサポートチケットOwn support tickets |
| POST | /v1/error-reports | 誤情報報告Report incorrect info |
| POST | /v1/storage/upload-url | R2 presigned PUT URL(バイトは Workers を経由しない)R2 presigned PUT URL (bytes skip Workers) |
| POST | /v1/search/ai | AI 自然言語検索(AI Gateway 経由、10 req/60 s 制限)AI natural-language search (via AI Gateway, 10 req/60 s) |
RPC(PostgreSQL 関数)RPC (PostgreSQL functions)
| 関数Function |
目的Purpose |
主要引数Key arguments |
戻り値Return value |
nearby_parking_lots |
指定座標の半径内で駐車場を取得Fetch parking lots within a given radius |
p_lat, p_lng, p_radius_m, p_filters jsonb, p_limit int |
setof parking_lots + distance_m |
calc_parking_fee |
料金シミュレーション(サーバー側整合用)Fee simulation (server-side reconciliation) |
p_lot_id, p_entry_at, p_exit_at, p_vehicle_type |
jsonb (total_amount, breakdown) |
create_parking_session |
駐車セッション生成(冪等)Create parking session (idempotent) |
p_lot_id, p_vehicle_type, p_start_lat, p_start_lng, p_client_request_id |
uuid (session_id) |
finalize_parking_session |
駐車終了・料金確定End parking & confirm fee |
p_session_id, p_exit_at, p_client_request_id |
jsonb (total_amount, breakdown) |
cancel_parking_session |
5分以内のキャンセルCancel within 5 minutes |
p_session_id |
boolean |
mark_notifications_read |
通知を一括既読Bulk mark notifications read |
p_ids uuid[] or p_before timestamptz |
int (更新行数)int (rows updated) |
withdraw_account |
退会処理(匿名化+ステータス変更)Account withdrawal (anonymize + status change) |
なしnone |
void |
Cloudflare Workers BFF エンドポイント(旧 Edge Functions を吸収)Cloudflare Workers BFF endpoints (absorbed the legacy Edge Functions)
| エンドポイントEndpoint |
目的Purpose |
入力Input |
出力Output |
POST /v1/search/ai |
自然言語→検索条件(LLM 呼び出し、AI Gateway 経由)Natural language → search conditions (LLM via AI Gateway) |
{ query, locale } |
{ location, entry_at?, exit_at?, max_price?, filters[] } |
POST /v1/admin/user-notifications/{id}/send |
FCM 配信を parky-fcm-dispatch キューに enqueue(即時 202 返却、consumer で fan-out)Enqueues FCM fan-out into parky-fcm-dispatch (returns 202 immediately; consumer handles delivery) |
{ notification_id } |
{ enqueued, batches } |
POST /v1/verify-iap |
Apple/Google のレシート検証Apple / Google receipt verification |
{ platform, receipt, product_id } |
{ subscription, status } |
POST /v1/storage/upload-url |
R2 presigned PUT URL 発行+assets INSERTMints an R2 presigned PUT URL and inserts into assets |
{ file_name, mime_type, file_size, category } |
{ asset_id, upload_url, s3_key, public_url, expires_in } |
report-crash |
クラッシュレポート補完(Sentryと二重化)Supplemental crash report (doubled up with Sentry) |
{ stack, device, app_version } |
{ ok } |
Realtime 購読Realtime subscriptions
| チャネルChannel |
用途Purpose |
user_notifications:user_id=eq.{uid} | 通知リアルタイム受信Realtime notification receipt |
parking_sessions:user_id=eq.{uid} | 複数端末間の駐車状態同期Cross-device parking state sync |
user_badges:user_id=eq.{uid} | バッジ獲得演出トリガBadge-earned animation trigger |
外部APIExternal APIs
| サービスService |
エンドポイントEndpoint |
用途Purpose |
| Mapbox | api.mapbox.com/geocoding/v5/… | 住所→座標address → coordinates |
| Mapbox | モバイルSDK (タイル)Mobile SDK (tiles) | 地図描画map rendering |
| Cloudflare R2 | 署名付きURL (Edge発行)Signed URL (issued by Edge) | 画像アップロードimage upload |
| FCM | SDK | Push受信push receipt |
| App Store / Play Store | StoreKit2 / Play Billing | IAP |
6.3 API詳細仕様API details
RPC: nearby_parking_lots
定義Definition
create or replace function public.nearby_parking_lots(
p_lat numeric,
p_lng numeric,
p_radius_m int default 500,
p_filters jsonb default '{}'::jsonb,
p_limit int default 50
) returns table (
id uuid,
name text,
address text,
lat numeric,
lng numeric,
distance_m numeric,
avg_rating numeric,
review_count int,
main_image text,
tags text[]
) language sql stable
as $$
select pl.id, pl.name, pl.address, pl.lat, pl.lng,
st_distance(pl.location, st_makepoint(p_lng, p_lat)::geography) as distance_m,
...
$$;
フィルタ(p_filters JSONB)Filter (p_filters JSONB)
{
"max_height_m": 2.1,
"tags": ["24h", "ev"],
"open_now": true,
"max_price": 2000,
"entry_at": "2026-04-14T23:00:00+09:00",
"exit_at": "2026-04-15T09:00:00+09:00"
}
性能Performance
- PostGIS GiST インデックス必須(
CREATE INDEX ON parking_lots USING gist(location);)
- A PostGIS GiST index is required (
CREATE INDEX ON parking_lots USING gist(location);).
- 目標:500m 半径で P95 < 200ms
- Target: P95 < 200ms for a 500m radius query.
RPC: create_parking_session
定義Definition
create or replace function public.create_parking_session(
p_lot_id uuid,
p_vehicle_type text,
p_start_lat numeric,
p_start_lng numeric,
p_client_request_id uuid
) returns uuid language plpgsql
security definer as $$
declare v_session_id uuid;
begin
-- 冪等性チェック
select id into v_session_id
from parking_sessions
where client_request_id = p_client_request_id
and user_id = auth.uid();
if v_session_id is not null then
return v_session_id;
end if;
-- 同時多重駐車防止
if exists (select 1 from parking_sessions
where user_id = auth.uid() and status = 'parking') then
raise exception 'concurrent_session_not_allowed' using errcode = 'P0001';
end if;
insert into parking_sessions (...)
returning id into v_session_id;
-- 活動ログ
insert into user_activity_logs (...) values (...);
return v_session_id;
end$$;
エラーErrors
concurrent_session_not_allowed:既に駐車中セッションが存在
concurrent_session_not_allowed: an active parking session already exists.
lot_not_found:駐車場が存在しないか非公開
lot_not_found: the parking lot does not exist or is not public.
RPC: finalize_parking_session
動作Operation
- セッションが自分のもので
status = 'parking' であることを確認
- Verify the session belongs to the caller and is in
status = 'parking'.
calc_parking_fee を内部呼出して料金を算出
- Internally call
calc_parking_fee to compute the fee.
parking_sessions を ended に更新、ended_at と total_amount を記録
- Update
parking_sessions to ended, recording ended_at and total_amount.
user_activity_logs に session_end を INSERT(バッジ/EXPトリガ)
- INSERT a
session_end row into user_activity_logs (triggers badges / EXP).
- 算出された金額と内訳を JSONB で返却
- Return the computed amount and breakdown as JSONB.
Cloudflare Workers: ai-search-parse
リクエストRequest
POST /functions/v1/ai-search-parse
Authorization: Bearer <jwt>
Content-Type: application/json
{ "query": "池袋で23時から朝9時まで2000円以下", "locale": "ja" }
レスポンスResponse
{
"location": "池袋",
"entry_at": "2026-04-14T23:00:00+09:00",
"exit_at": "2026-04-15T09:00:00+09:00",
"max_price": 2000,
"filters": [],
"confidence": 0.94
}
エラー形式Error format
{ "error": { "code": "unparseable", "message": "解釈できませんでした" } }
Cloudflare Workers: verify-iap-receipt
シーケンスSequence
sequenceDiagram
participant App
participant EF as Cloudflare Workers
participant Apple
participant PG as PostgreSQL
App->>EF: { platform, receipt, product_id }
alt platform=ios
EF->>Apple: verifyReceipt
Apple-->>EF: { valid, expires_at }
else platform=android
EF->>Google: Play Developer API
Google-->>EF: { valid, expires_at }
end
EF->>PG: upsert user_subscriptions
EF->>PG: update app_users.premium
EF-->>App: { subscription, status }
6.4 エラー仕様Error spec
統一エラー形式(Workers BFF)Unified error format (Workers BFF)
{
"error": {
"code": "string_code", // スネークケースの機械可読コード
"message": "日本語メッセージ",
"details": { ... }, // 任意
"request_id": "uuid"
}
}
{
"error": {
"code": "string_code", // snake_case machine-readable code
"message": "human-readable message",
"details": { ... }, // optional
"request_id": "uuid"
}
}
主要エラーコードKey error codes
| コードCode |
HTTPステータスHTTP status |
意味Meaning |
unauthorized | 401 | JWT無効・未認証Invalid JWT / unauthenticated |
forbidden | 403 | RLSで拒否Denied by RLS |
not_found | 404 | 対象リソース不在Target resource not found |
conflict | 409 | 状態遷移不正・冪等違反Invalid state transition / idempotency violation |
validation_error | 422 | 入力不正Invalid input |
rate_limited | 429 | レート超過Rate limit exceeded |
unparseable | 422 | AI検索で解釈不能AI search could not interpret input |
iap_invalid | 400 | IAPレシート不正Invalid IAP receipt |
server_error | 500 | 内部エラーInternal error |
service_unavailable | 503 | メンテナンス・外部依存ダウンMaintenance / external dependency down |
DB 由来のエラーコード(Workers が翻訳して返す)DB-derived error codes (translated by Workers)
Workers は PostgreSQL の errcode を上記統一形式の error.code にマップしつつ、HTTP ステータスも適切に変換します。代表的なクラス:
Workers translate PostgreSQL's errcode into the unified shape above (error.code) and choose an appropriate HTTP status. Typical classes:
23505 (unique_violation) → 「既に存在します」
23505 (unique_violation) → "Already exists"
23514 (check_violation) → 「入力値が不正です」+制約名を日本語化
23514 (check_violation) → "Invalid input value" plus a localized constraint name
42501 (insufficient_privilege) → forbidden 扱い
42501 (insufficient_privilege) → treated as forbidden
PGRST116 (Supabase: 行が見つからない) → not_found 扱い
PGRST116 (Supabase: row not found) → treated as not_found
6.5 レート制限Rate limiting
| 対象Target |
制限Limit |
実装Implementation |
| Workers BFF 全般Workers BFF general | Cloudflare 側のバースト制限(未チューニング)Cloudflare burst protection (untuned) | CF 標準。ユーザー単位に絞りたい場合は Rate Limiting binding で別途設定CF default. Per-user limits use the dedicated Rate Limiting binding |
| 認証系(signIn/signUp/OTP)Auth endpoints (signIn / signUp / OTP) | IP単位 10 req/min10 req/min per IP | Supabase Auth 設定Supabase Auth settings |
| AI検索AI search | ユーザー 10 req/min(Free)/ 60 req/min(Plus)10 req/min per user (Free) / 60 req/min (Plus) | Cloudflare Workers 内で Redis相当のカウンタRedis-equivalent counter inside the Cloudflare Workers |
| Push 送信Push dispatch | ユーザー 5通/min(同種別)5 msgs/min per user (same type) | DBトリガ+前回送信時刻チェックDB trigger + last-send timestamp check |
| サポート送信Support submissions | ユーザー 5件/日5 per user per day | RPC内チェックChecked inside RPC |