アーキテクチャ Architecture
Parky は Cloudflare Workers(Hono 製 BFF)をクライアントと Supabase の間に挟むマルチクライアント構成のサービスです。
クライアント(Flutter / Web)が叩く API エンドポイントは /v1/* の 1 経路に一元化し、
Supabase(PostgreSQL + Auth + Realtime)はバックエンドの中核として利用します。
Supabase Edge Functions は使用せず、すべてのサーバーサイドロジックは Workers に集約します。
画像・PDF などのバイナリアセットは Cloudflare R2 (S3 互換)に格納し、
Supabase は assets テーブルでメタデータと s3_key のみを保持します。
Parky is a multi-client service with Cloudflare Workers (a Hono-based BFF) sitting between clients and Supabase.
Clients (Flutter / Web) hit a single unified /v1/* API surface. Supabase
(PostgreSQL + Auth + Realtime) remains the core backend.
Supabase Edge Functions are not used — all server-side logic is consolidated in Workers.
Binary assets (images, PDFs) live in Cloudflare R2 (S3-compatible);
Supabase holds only metadata and the s3_key via the assets table.
クライアント接続層の一元化Unified client-facing API layer
クライアントが直接叩く API は Cloudflare Workers の /v1/* の 1 経路のみ。
Supabase Edge Functions は使わず、FCM 配信・LLM 呼び出し・ストア同期・定期ジョブまですべて Workers 内で完結させる。
spec(OpenAPI)も Workers 側 1 枚に統一。
Clients call exactly one public API surface: Cloudflare Workers at /v1/*.
Supabase Edge Functions are not used — FCM delivery, LLM calls, store sync, and scheduled jobs all live inside Workers.
The OpenAPI spec is authored once, on the Workers side.
| 呼び出し元 | Caller | 呼び先 | Target | 分類 | Kind | 理由 | Reason |
|---|---|---|---|---|---|---|---|
| Flutter / Web | Flutter / Web | Cloudflare Workers (/v1/*) |
Cloudflare Workers (/v1/*) |
唯一の API 層 | The single API surface | 契約 / 認証 / キャッシュ / レート制限 / 観測性をここに集約 | Contract, auth, cache, rate-limiting, observability all live here |
| Flutter / Web | Flutter / Web | Supabase Auth | Supabase Auth | 例外① (data plane) | Exception 1 (data plane) | OAuth / メール確認 / セッション更新は supabase-flutter に委任 | OAuth, email confirm, session refresh handled by supabase-flutter |
| Flutter / Web | Flutter / Web | Supabase Realtime | Supabase Realtime | 例外② (data plane) | Exception 2 (data plane) | WebSocket プロキシは Workers で重く、恩恵が小さい | WebSocket proxying at the edge has little upside |
| Flutter / Web | Flutter / Web | Cloudflare R2 (direct PUT) | Cloudflare R2 (direct PUT) | 例外③ (data plane) | Exception 3 (data plane) | Workers が発行した presigned URL に直接 PUT。バイト列を Workers 経由で流さない | Direct PUT using a presigned URL minted by Workers — bytes skip the edge |
| Cloudflare Workers | Cloudflare Workers | Supabase Postgres | Supabase Postgres | 内部呼び出し | Internal only | Service Role で接続、user_id スコープをコード層で徹底、RLS は二重防御。重トランザクションは RPC として DB 内で実装 | Service Role with code-level user_id scoping; RLS as defense-in-depth. Heavy transactions live as PG RPCs inside the DB |
| Cloudflare Workers | Cloudflare Workers | FCM / LLM / ストア API / R2 | FCM / LLM / Store APIs / R2 | 外部 API 呼び出し | External API calls | FCM の OAuth2 JWT 署名は Web Crypto、プレサイン URL は SigV4、定期ジョブは CF Cron Triggers。Edge Functions は不使用 | Workers sign FCM OAuth2 JWTs via Web Crypto, mint R2 SigV4 presigns, and run scheduled jobs via CF Cron Triggers. No Edge Functions |
システム全体像System context
flowchart LR
subgraph Clients["Clients"]
MA["Mobile App
Flutter"]
WA["Web App
Astro + Islands"]
AP["Admin Portal
React + Vite"]
end
subgraph BFF["BFF (Control Plane)"]
CFW["Cloudflare Workers
Hono + OpenAPI /v1/*"]
end
subgraph Supabase["Supabase (core backend)"]
AUTH["Auth"]
DB[("PostgreSQL
66 tables + RLS")]
RT["Realtime"]
end
subgraph Storage["Object storage"]
WSB["Cloudflare R2
parky bucket"]
end
subgraph External["External services"]
MB["Mapbox"]
FCM["FCM"]
STRIPE["Stripe
(planned)"]
LLM["LLM API
(Claude/Gemini/OpenAI)"]
STORE["Play/App Store"]
end
CRON["CF Cron Triggers"]
MA -->|"/v1/*"| CFW
WA -->|"/v1/*"| CFW
AP -->|"/v1/*"| CFW
MA -.->|Auth SDK| AUTH
WA -.->|Auth SDK| AUTH
AP -.->|Auth SDK| AUTH
MA -.->|Realtime| RT
AP -.->|Realtime| RT
MA -.->|PUT presigned| WSB
WA -.->|GET public| WSB
AP -.->|PUT presigned| WSB
CRON --> CFW
CFW --> DB
CFW -->|mint presign| WSB
CFW --> FCM
CFW --> LLM
CFW --> STORE
WA --> MB
MA --> MB
AP --> MB
制御面と データ面の分離Control plane vs data plane
「ロジックと認可(制御面)」は Workers に完全集約し、「大容量バイト列・WebSocket・Auth フロー(データ面)」だけは経路最適化のため Workers を経由させない、という二分割で整理しています。
Storage の例外(presigned URL)は、認可の意味では Workers が門番です(/v1/storage/upload-url で JWT 検証・user_id スコープで URL を発行)。
We split the system into control plane (all business logic, auth, validation — centralized in Workers)
and data plane (bulk bytes, WebSockets, auth flows — bypassing Workers for efficiency).
Even the Storage exception is gated by Workers: /v1/storage/upload-url validates the JWT and mints a scoped presigned URL.
コンポーネントの役割Component responsibilities
| コンポーネント | Component | 役割 | Responsibility | 備考 | Notes |
|---|---|---|---|---|---|
| Cloudflare Workers (Hono) | Cloudflare Workers (Hono) | BFF。OpenAPI 駆動で /v1/* を提供。JWT 検証、入力バリデーション(Zod)、キャッシュ(Cache API + KV)、レート制限、ログ出力を一手に担う |
BFF. OpenAPI-driven /v1/*. Handles JWT verify, input validation (Zod), cache (Cache API + KV), rate-limit, logging |
東京 PoP で Supabase ap-northeast-1 と至近 | Tokyo PoP colocated near Supabase ap-northeast-1 |
| Supabase Auth | Supabase Auth | 管理者はメール+パスワードで稼働中。エンドユーザー認証は未実装(フェーズ以降で電話番号/SNS導入予定) | Email+password is live for admins. End-user auth is not yet implemented (phone / OAuth planned for a later phase) | Workers は SUPABASE_JWT_SECRET で JWT 検証、RLS と admins / app_users を連携 |
Workers verify JWT with SUPABASE_JWT_SECRET; RLS links admins / app_users |
| PostgreSQL | PostgreSQL | すべての永続データを保持(66テーブル) | The single source of truth — 66 tables | PostGIS で位置検索、RLS で多租户分離(Service Role でも二重防御として維持) | PostGIS for geo queries; RLS for isolation, kept as defense-in-depth even under Service Role |
| Realtime | Realtime | 管理者通知・駐車セッション状態のライブ更新(クライアント直接接続) | Live updates for admin notifications and parking sessions — clients connect directly | admin_notifications, parking_sessions |
admin_notifications, parking_sessions |
| Cloudflare Cron Triggers | Cloudflare Cron Triggers | 定期ジョブのスケジューラ。スポンサー近傍通知を 10 分毎に Workers ハンドラで実行 | Scheduler for recurring jobs — sponsor proximity scans run every 10 min as a Worker handler | pg_cron は使わず wrangler.toml [triggers] crons に集約 |
Replaces pg_cron. Schedules live in [triggers] crons inside wrangler.toml |
| Cloudflare Hyperdrive | Cloudflare Hyperdrive | Workers → Supabase Postgres の接続プーリング+グローバルキャッシュ。lib/db.ts の postgres.js が env.HYPERDRIVE.connectionString で接続 |
Connection pooling and global cache for Workers → Supabase Postgres. lib/db.ts uses postgres.js with env.HYPERDRIVE.connectionString |
Supavisor と二重プーリングになるため Supabase Pooler は使わず Direct Connection 経由 | Connects via Supabase Direct Connection — the Supavisor pooler would double-pool |
| Cloudflare Queues | Cloudflare Queues | 非同期バッチ処理。parky-store-sync(Google Play / App Store Connect API から sales/reviews 取得)、parky-fcm-dispatch(FCM 通知の fan-out)。各キューに DLQ(*-dlq)配線済み、max_retries=3 |
Async batch processing. parky-store-sync (pulls sales/reviews from Google Play / App Store Connect) and parky-fcm-dispatch (FCM fan-out). Each has a DLQ and max_retries=3 |
consumer は src/queue/*.ts、producer は src/lib/queue.ts |
Consumers in src/queue/*.ts, producers in src/lib/queue.ts |
Cloudflare KV (parky-cache) |
Cloudflare KV (parky-cache) |
isolate 跨ぎキャッシュ。現状は FCM OAuth access_token の 2 層キャッシュ(isolate Map + KV / TTL 55分)に利用 | Cross-isolate cache. Currently backs the 2-tier FCM OAuth access_token cache (isolate Map + KV / TTL 55 min) | 将来的に AI 応答キャッシュ等にも拡張予定 | Planned for AI response caching later |
| Cloudflare AI Gateway | Cloudflare AI Gateway | /v1/search/ai の LLM 呼出を Gateway 経由にしてキャッシュ・ログ・フォールバックを取得。gateway slug parky-ai-gateway |
Routes LLM calls through parky-ai-gateway for caching, logging, and fallback |
プロバイダ別パス(/openai, /anthropic, /google-ai-studio)を AI_GATEWAY_BASE_URL で制御 |
Provider-specific paths (/openai, /anthropic, /google-ai-studio) driven by AI_GATEWAY_BASE_URL |
| Cloudflare Analytics Engine | Cloudflare Analytics Engine | AI 呼出のテレメトリ集計(dataset: parky_ai_usage)。現行は PG ai_usage_logs との dual write、後続で AE 単独に寄せる |
Telemetry for AI calls (dataset: parky_ai_usage). Dual-writes with PG ai_usage_logs today; PG write will be removed later |
90 日保持・10M イベント/日まで無料 | 90-day retention, 10M events/day free tier |
| Cloudflare Workers AI | Cloudflare Workers AI | エッジ推論。Instagram tool で DETR 物体検出を走らせ、顔/ナンバープレート候補の領域をヒューリスティックで返す | Edge inference. Used by the Instagram tool to run DETR object detection and heuristically return face / license-plate candidate regions | binding は env.AI([env.dev.ai]) |
Bound as env.AI via [env.dev.ai] |
| LLM API (Claude / Gemini / OpenAI) | LLM API (Claude / Gemini / OpenAI) | 自然言語の駐車場検索クエリを構造化 JSON にパース。Workers の /v1/search/ai ハンドラがマルチプロバイダー優先順位でフォールバックしながら呼び出す |
Parses natural-language parking queries into structured JSON. Called by Workers via /v1/search/ai with provider-priority fallback |
API キーは Supabase Vault に暗号化保存し、vault_read_secret RPC 経由で取得 |
API keys stay encrypted in Supabase Vault and are fetched via the vault_read_secret RPC |
| Cloudflare R2 (object storage) | Cloudflare R2 (object storage) | 駐車場画像・ユーザーアバター・バッジ/テーマアセット・管理者アップロード等のバイナリ | Parking images, avatars, badge/theme assets, admin uploads — all binaries | Workers の /v1/storage/upload-url が presigned PUT URL を発行、クライアントは直接 PUT。assets テーブルに s3_key をメタデータ登録。公開 URL https://cdn.parky.co.jp/{key} で参照 |
Workers mint presigned PUT URLs via /v1/storage/upload-url; clients PUT directly. Metadata with s3_key lives in assets. Public URL: https://cdn.parky.co.jp/{key} |
| Mapbox GL JS | Mapbox GL JS | 地図表示・マーカー・経路。全クライアントで統一利用 | Map rendering, markers, and routing — used by every client | 独自タイル/スタイルは未採用 | No custom tiles or styles yet |
| FCM (サーバ側完成 / Flutter 未配線) | FCM (server complete / Flutter not wired) | ユーザー向けプッシュ配信。POST /v1/admin/user-notifications/{id}/send は受信者トークンを 500 件/バッチで parky-fcm-dispatch キューに投入、consumer が Workers 内で OAuth2 JWT(RS256)を Web Crypto で署名し FCM v1 API を並列 fetch。OAuth access_token は KV(parky-cache)にキャッシュ。Flutter クライアントはトークン取得・受信をまだ実装していない |
End-user push. POST /v1/admin/user-notifications/{id}/send chunks tokens 500/batch into parky-fcm-dispatch; the consumer signs the OAuth2 JWT (RS256) via Web Crypto and fans out to the FCM v1 API. The OAuth access_token lives in KV (parky-cache). The Flutter client is not yet wired for token registration / receive |
トークン格納先 user_push_tokens テーブルはスキーマに存在 |
Token table user_push_tokens exists in schema |
| Stripe (予定) | Stripe (planned) | プレミアムプランの課金に使用予定。現状は DB の revenue_transactions に stripe_payment_intent_id / stripe_status カラムのみ |
Planned for premium-plan billing. Today only DB columns exist — no actual Stripe SDK calls yet | planned | planned |
認証フローAuth flow
1. Flutter / Web: supabase-flutter.auth.signIn*() → JWT 取得
2. Flutter / Web: BFF 呼び出し時 Authorization: Bearer <JWT>
3. Workers: SUPABASE_JWT_SECRET で署名検証 → user_id 抽出
4. Workers: Service Role Key で Supabase Postgres へ接続
(user_id でスコープされたクエリをコード層で徹底)
5. RLS は二重防御として維持
(Service Role でも明示的に .eq('user_id', ...) を書く)
契約定義 (OpenAPI)Contract (OpenAPI)
API 契約は parky/packages/api-spec/openapi.yaml(OpenAPI 3.1)を Single Source of Truth として管理し、
Workers 実装は @hono/zod-openapi で spec と連動、TypeScript クライアントは openapi-typescript、
Flutter 向け Dart クライアントは openapi-generator-cli の dart-dio ジェネレータで CI 自動生成します。
破壊的変更は URL バージョニング(/v1/ → /v2/)で吸収し、旧版は最低 180 日のサンセット期間を維持します。
The API contract lives at parky/packages/api-spec/openapi.yaml (OpenAPI 3.1) as the single source of truth.
Workers use @hono/zod-openapi to stay in sync with the spec; CI regenerates TypeScript clients via
openapi-typescript and Dart clients via openapi-generator-cli (dart-dio).
Breaking changes move behind /v2/; prior versions are maintained for at least 180 days.
クライアント別の特徴What each client does differently
- モバイルアプリ(Flutter): 自動生成 Dart クライアントで
/v1/*を呼ぶ。supabase-flutter は Auth と Realtime のみ利用(from()/rpc()/storageの直接利用は Dart analyzer で lint エラー化)。位置情報・プッシュ・カメラのネイティブ機能を活用 - Mobile app (Flutter): Calls
/v1/*through the auto-generated Dart client. Uses supabase-flutter only for Auth and Realtime — directfrom()/rpc()/storagecalls are lint-errored. Uses native GPS, push, camera - Web版(Astro): 静的 SSG + Islands。SEO 最重視。検索・地図は島(Island)として JS 化。公開データアクセスは全面 BFF (
/v1/*) 経由に移行済み(2026-04-19 時点でsupabase.from()直叩きは 0 件)。ビルド時に API のヘルスチェックを挟んで SSG を焼く - Web app (Astro): SSG with Islands. SEO-first; search/map hydrate as islands. All public-read paths now go through the BFF (
/v1/*) — as of 2026-04-19 there are 0 directsupabase.from()calls. Build-time API health check gates SSG rendering - 管理者ポータル(React + Vite): SPA。BFF 移行完了(2026-04-19 時点で BFF クライアント (
@parky/bff-client) 経由の呼び出しが 252 箇所、supabase.from()直叩きは 0 件)。Realtime は直接購読のまま継続(データプレーン例外)。グラスモーフィズムのデザイン - Admin portal (React + Vite): SPA. BFF migration complete — as of 2026-04-19 there are 252 BFF client (
@parky/bff-client) calls and 0 directsupabase.from()calls. Realtime subscriptions stay direct (data-plane exception). Glassmorphism design
移行履歴(完了済)Migration log (completed)
- Phase 2-A(モバイル先行・完了):
packages/api-spec/作成、Workers (api/) 初期化、Flutter は最初から BFF 経由で実装 - Phase 2-A (mobile first — done): Scaffolded
packages/api-spec/, bootstrapped Workers (api/), built Flutter against the BFF from day one - Phase 2-B(Web 移行・完了):
@parky/bff-clientを新設、web/home は全ページを BFF 経由に、portal/admin も 100% BFF 経由に書き換え完了 - Phase 2-B (web migration — done): introduced
@parky/bff-client; web/home is 100% BFF-backed and portal/admin has been fully rewritten against the BFF - Phase 2-C(成熟化・完了): 2026-04-19 時点で Supabase Edge Functions はすべて Workers に吸収済み(
admin-auth→/v1/admin/admins、ai-search→/v1/search/ai、upload-asset→/v1/storage/upload-url、send-push-notification→parky-fcm-dispatchキュー、sync-store-data→parky-store-syncキュー、check-sponsor-proximity→ CF Cron Triggers)。Wasabi も廃止し R2 へ完全移行。Supabase 側に残るのは Auth / Realtime / PostgreSQL のみ。ホスティングも全面 Cloudflare Pages 化 - Phase 2-C (hardening — done): As of 2026-04-19, all Supabase Edge Functions have been absorbed into Workers (
admin-auth→/v1/admin/admins,ai-search→/v1/search/ai,upload-asset→/v1/storage/upload-url,send-push-notification→parky-fcm-dispatchqueue,sync-store-data→parky-store-syncqueue,check-sponsor-proximity→ CF Cron Triggers). Wasabi is retired in favor of R2. Supabase retains only Auth / Realtime / PostgreSQL. Hosting fully migrated to Cloudflare Pages
.work/ 配下)を参照。
The full design doc — including hosting rationale, cache strategy, observability, and verification steps — lives at the backend architecture spec under .work/.