アーキテクチャ 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.

設計変更の記録: Design decision log: 2026-04-17、Phase 2(モバイルアプリ本格実装)を前に、従来の「全クライアントが Supabase を直叩き」設計から 「Cloudflare Workers を BFF として挟む」構成へ移行することを決定。 モバイル配布済みアプリと DB スキーマを疎結合にし、サーバーキャッシュ・レート制限・観測性を一元化するため。 詳細は Cloudflare WorkersHono 参照。 2026-04-17: Ahead of Phase 2 (full mobile-app implementation), we switched from the original "clients talk to Supabase directly" design to a Cloudflare Workers BFF architecture. This decouples shipped mobile binaries from DB schema evolution and unifies caching, rate-limiting, and observability.

クライアント接続層の一元化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 / WebFlutter / Web Cloudflare Workers (/v1/*) Cloudflare Workers (/v1/*) 唯一の API 層 The single API surface 契約 / 認証 / キャッシュ / レート制限 / 観測性をここに集約 Contract, auth, cache, rate-limiting, observability all live here
Flutter / WebFlutter / Web Supabase AuthSupabase Auth 例外① (data plane) Exception 1 (data plane) OAuth / メール確認 / セッション更新は supabase-flutter に委任 OAuth, email confirm, session refresh handled by supabase-flutter
Flutter / WebFlutter / Web Supabase RealtimeSupabase Realtime 例外② (data plane) Exception 2 (data plane) WebSocket プロキシは Workers で重く、恩恵が小さい WebSocket proxying at the edge has little upside
Flutter / WebFlutter / 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 WorkersCloudflare Workers Supabase PostgresSupabase 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 WorkersCloudflare Workers FCM / LLM / ストア API / R2FCM / 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

使い方: Tip: 図のシェイプをクリックすると、関連するコネクターと接続先だけがハイライトされます。もう一度クリックするか「リセット」で解除。 Click any shape to highlight only the connectors and nodes related to it. Click again or hit "Reset" to clear.
シェイプをクリックして関連を強調 Click any shape to focus its connections
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_transactionsstripe_payment_intent_id / stripe_status カラムのみ Planned for premium-plan billing. Today only DB columns exist — no actual Stripe SDK calls yet plannedplanned

認証フロー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-clidart-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

移行履歴(完了済)Migration log (completed)

  1. Phase 2-A(モバイル先行・完了): packages/api-spec/ 作成、Workers (api/) 初期化、Flutter は最初から BFF 経由で実装
  2. Phase 2-A (mobile first — done): Scaffolded packages/api-spec/, bootstrapped Workers (api/), built Flutter against the BFF from day one
  3. Phase 2-B(Web 移行・完了): @parky/bff-client を新設、web/home は全ページを BFF 経由に、portal/admin も 100% BFF 経由に書き換え完了
  4. 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
  5. Phase 2-C(成熟化・完了): 2026-04-19 時点で Supabase Edge Functions はすべて Workers に吸収済み(admin-auth/v1/admin/adminsai-search/v1/search/aiupload-asset/v1/storage/upload-urlsend-push-notificationparky-fcm-dispatch キュー、sync-store-dataparky-store-sync キュー、check-sponsor-proximity → CF Cron Triggers)。Wasabi も廃止し R2 へ完全移行。Supabase 側に残るのは Auth / Realtime / PostgreSQL のみ。ホスティングも全面 Cloudflare Pages 化
  6. 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-notificationparky-fcm-dispatch queue, sync-store-dataparky-store-sync queue, 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
詳細方針書: Full spec: ホスティング選定・キャッシュ戦略・観測性・検証手順を含む完全な設計方針書は バックエンド設計方針書.work/ 配下)を参照。 The full design doc — including hosting rationale, cache strategy, observability, and verification steps — lives at the backend architecture spec under .work/.
プロダクト別の詳細: Product details: 各プロダクトの詳しいアーキテクチャは、上部タブから対象セクションへ。 See each product's own architecture page via the tabs above.