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

採用の狙い: Why this shape: 配布済みモバイルバイナリと DB スキーマを疎結合に保ち、サーバーキャッシュ・レート制限・観測性を BFF 層に一元化するために Cloudflare Workers + Hono を採用している。詳細は Cloudflare WorkersHono 参照。 Cloudflare Workers + Hono keep shipped mobile binaries decoupled from DB schema changes and centralize caching, rate-limiting, and observability at the BFF layer. See Cloudflare Workers and Hono for background.

プロダクトスコープProduct scope

Parky は 「駐車場検索ポータル + 情報メディア + オーナー掲載 + 外部送客」を中核に据えたサービスです。 駐車場予約機能 / 駐車場決済機能は実装対象外(2026-04-26 確定)。 資金決済法・媒介行為リスクの低減と、SEO・メディア型ビジネスへの集約、競合(Akippa / 特P / B-Times)との差別化を 「予約以外の軸」で取る方針のため、以降のドキュメントでも 予約フロー / Stripe / GMV / テイクレート 等は登場しません。

Parky's product core is parking discovery + information media + owner listings + outbound referral. Parking reservation and parking payment are explicitly out of scope (decided 2026-04-26). We intentionally compete on search UX, content depth, and owner experience instead of GMV / take-rate, so booking flows, Stripe billing, and reservation-side concepts do not appear in this documentation.

In scopeIn scope Out of scopeOut of scope
駐車場検索 / 地図 / 比較 / レビュー / オーナー掲載 / コンテンツメディア / 外部送客 Discovery, map, comparison, reviews, owner listings, content media, outbound referral 駐車場の予約成立・在庫ロック・決済処理・キャンセル / 返金フロー Slot reservation, inventory locking, payment processing, cancellation / refund flows
マネタイズ: 掲載料 / 広告 / 送客フィー Monetization via listing fees / ads / referral fees マネタイズ: テイクレート / GMV Monetization via take-rate / GMV
外部予約サービス(Akippa / 特P / 軒先 / オーナー直接連絡先)への送客導線 Outbound links to external booking services (Akippa / Toku-P / Nokisaki / direct owner contact) Parky 内での予約・決済 SDK 統合 In-app booking or payment SDK integration

クライアント接続層の一元化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"] OP["Owner Portal
React + Vite"] MP["Marketing 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
5 schemas / 128 base tables + 12 views")] RT["Realtime"] end subgraph Storage["Object storage"] WSB["Cloudflare R2
parky bucket"] end subgraph External["External services"] MB["Mapbox"] FCM["FCM"] LLM["LLM API
(Claude/Gemini/OpenAI)"] STORE["Play/App Store"] SENTRY["Sentry
Issues / Performance"] end CRON["CF Cron Triggers"] MA -->|"/v1/*"| CFW WA -->|"/v1/*"| CFW AP -->|"/v1/admin/*"| CFW OP -->|"/v1/owner/*"| CFW MP -->|"/v1/marketing/*"| CFW MA -.->|Auth SDK| AUTH WA -.->|Auth SDK| AUTH AP -.->|Auth SDK| AUTH OP -.->|Auth SDK| AUTH MP -.->|Auth SDK| AUTH MA -.->|Realtime| RT AP -.->|Realtime| RT MA -.->|PUT presigned| WSB WA -.->|GET public| WSB AP -.->|PUT presigned| WSB MP -.->|PUT presigned| WSB CRON --> CFW CFW --> DB CFW -->|mint presign| WSB CFW --> FCM CFW --> LLM CFW --> STORE CFW -->|"capture (5xx/unhandled)"| SENTRY MA -.->|"capture (Dart)"| SENTRY WA -.->|"capture (Astro/Island)"| SENTRY AP -.->|"capture (React)"| SENTRY OP -.->|"capture (React)"| SENTRY MP -.->|"capture (React)"| SENTRY WA --> MB MA --> MB AP --> MB %% クリック時はハンドラ(app.js の initInteractiveArchitecture)で %% 関連シェイプをハイライト表示する。ページ遷移はしない。

制御面と データ面の分離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 すべての永続データを保持(5 schema = public / admin / marketing / analytics / extensions、合計 128 base tables + 12 views。さらに 2 matviews と RPC 専用の bff_only schema、cron 管理用の infra schema を併用) The single source of truth — 5 schemas (public / admin / marketing / analytics / extensions) totalling 128 base tables + 12 views. Two materialized views plus a RPC-only bff_only schema and a cron-management infra schema sit alongside. PostGIS で位置検索、RLS で多租户分離(Service Role でも二重防御として維持)。schema 分離の役割と GRANT 体系は DB schema 構成 参照 PostGIS for geo queries; RLS for isolation, kept as defense-in-depth even under Service Role. Schema role / GRANT details: see DB schema layout
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 定期ジョブのスケジューラ(wrangler.admin.toml 等の [triggers] crons に集約、pg_cron は不使用)。現在配線中のスケジュールは下表「Cron スロット」を参照。Worker 分離は ADR-0010 Scheduler for recurring jobs — schedules live in [triggers] crons across the per-channel wrangler.*.toml files (no pg_cron). See the "Cron slots" table below; Worker split per ADR-0010 現状 6 スロット稼働(admin worker) Currently 6 slots wired on the admin worker
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
Sentry Sentry エラー監視・issue grouping・リアルタイム通知。BFF (Workers) は toucan-jserror-handler から 5xx と未処理例外を capture(api/src/lib/sentry.ts / middleware/error-handler.ts)。各クライアント SPA / Astro / Flutter も SDK で連携 Error monitoring, issue grouping, and real-time alerts. The BFF (Workers) captures 5xx and unhandled exceptions from the error-handler via toucan-js (api/src/lib/sentry.ts + middleware/error-handler.ts). Every SPA / Astro / Flutter client wires its own SDK DSN / release は Workers binding (SENTRY_DSN / SENTRY_RELEASE)。request body / cookies / IP は redact、許可ヘッダーは user-agent / x-app-version / x-request-id / accept-language のみ。詳細は ops / sentry-setup DSN / release flow as Workers bindings (SENTRY_DSN / SENTRY_RELEASE). Request body, cookies, and IP are redacted; only user-agent / x-app-version / x-request-id / accept-language headers are forwarded. See ops / sentry-setup

DB schema 構成DB schema layout

Supabase Postgres は用途別に 5 schema に分離(2026-04-21 reorg)。新規テーブル作成時は必ず schema を判定し、 CREATE TABLE <schema>.<table> で明示する。admin / marketing / analytics は anon / authenticated 不可 (schema レベルで REVOKE)、アプリから触るには必ず BFF 経由。

Supabase Postgres is split into 5 schemas by purpose (reorg 2026-04-21). Every new table must explicitly target a schema with CREATE TABLE <schema>.<table>. admin / marketing / analytics deny anon / authenticated at the schema level — clients can only reach them through the BFF.

SchemaSchema 役割Role アクセスAccess 代表テーブルRepresentative tables
publicpublic 業務ドメインの中核(end-user 向け機能・core business) Core business domain (end-user features) anon / authenticated + RLSanon / authenticated + RLS parking_lots / parking_lot_pricing_groups / parking_spots / app_users / articles / user_notifications parking_lots / parking_lot_pricing_groups / parking_spots / app_users / articles / user_notifications
adminadmin 内部運営者データ Internal operator data service_role onlyservice_role only admins / admin_tasks / admin_activity_logs / roles / role_permissions admins / admin_tasks / admin_activity_logs / roles / role_permissions
marketingmarketing マーケサブプロダクト(Marketing Portal 駆動) Marketing sub-product (driven by Marketing Portal) service_role onlyservice_role only marketing_campaigns / x_posts / newsletter_broadcasts / store_sales_daily marketing_campaigns / x_posts / newsletter_broadcasts / store_sales_daily
analyticsanalytics append-only テレメトリ(retention 戦略が public と別) Append-only telemetry (independent retention from public) service_role onlyservice_role only client_events / error_reports / boost_*_logs / sns_follower_snapshots client_events / error_reports / boost_*_logs / sns_follower_snapshots
extensionsextensions 拡張機能の encapsulation 先 Encapsulation namespace for extensions USAGE は全 roleUSAGE granted to all roles postgis / pg_net / pg_stat_statements / pgcrypto / citext / uuid-ossp / pgtap postgis / pg_net / pg_stat_statements / pgcrypto / citext / uuid-ossp / pgtap

さらに RPC 専用の bff_only schema(ADR-0012: client 直叩き禁止、SECURITY DEFINER)と pg_cron ジョブ管理用の infra schema が併存する。auth_helpers は Supabase Auth との JWT 解決ユーティリティ専用。

Two more schemas live alongside the five: bff_only (RPC-only / SECURITY DEFINER, no client GRANT — see ADR-0012) and infra (pg_cron job tracking). auth_helpers houses Supabase Auth JWT resolution utilities.

詳細・テーブル一覧は データモデル、列挙値の codes マスター方針は 共通規約 を参照。 DB 関数 (RPC) は default で bff_only schema 配置、client は BFF endpoint 経由でしか叩けない(4 層防御 L1〜L4)。

See Data model for the full table list and Conventions for the codes master policy. DB functions (RPCs) default to the bff_only schema — clients reach them only via BFF endpoints (a 4-layer L1–L4 defense).

Cron スロット(Cloudflare Cron Triggers)Cron slots (Cloudflare Cron Triggers)

api/wrangler.admin.toml[triggers] crons で配線(マルチ Worker は ADR-0010)。現状 6 スロット稼働。ハンドラは api/src/cron/、cron 式の SSoT は api/src/cron-constants.tsCRON_JOBS、実行分岐は api/src/app-core.tsdispatchScheduled()

Wired in api/wrangler.admin.toml under [triggers] crons (multi-worker split per ADR-0010). Currently 6 slots are active. Handlers live under api/src/cron/, the cron-expression SSoT is the CRON_JOBS constant in api/src/cron-constants.ts, and dispatch happens in dispatchScheduled() in api/src/app-core.ts.

スケジュールSchedule ハンドラHandler 役割Purpose
*/15 * * * **/15 * * * * WARMERwarmerWARMERwarmer Workers isolate と Hyperdrive 接続のウォーム維持 Keeps the Workers isolate and Hyperdrive connection warm
* * * * ** * * * * EVERY_MINUTE → X 予約投稿 + X 定期ルール + newsletter dispatch + session-notifications fireEVERY_MINUTE → X scheduled posts + X schedule rules + newsletter dispatch + session-notifications fire X の予約投稿送信 / 定期ルール発火 / ニュースレター配信 / セッション通知 (price/time trigger) を waitUntil で並列実行 X scheduled posts, X recurring rule fire, newsletter delivery, and session-notification triggers (price/time targets) — parallelized via waitUntil
*/5 * * * **/5 * * * * X_LISTENxListenX_LISTENxListen X 監視ルール評価(検索クエリ / 言及検知 → x_automation_log Evaluates X listening rules (search / mentions → x_automation_log)
*/10 * * * **/10 * * * * X_INSIGHTSxInsightsX_INSIGHTSxInsights X 投稿のインサイト(impression / engagement)を取得して x_posts へ反映 Pulls X post insights (impressions / engagement) into x_posts
0 * * * *0 * * * * HOURLYreviewReminder + snsFollowerSnapshot (UTC 00:00 のみ) + r2TempCleanup (UTC 03:00 のみ) + notificationFailuresDigest (月曜 UTC 09:00 のみ)HOURLYreviewReminder + snsFollowerSnapshot (UTC 00:00 only) + r2TempCleanup (UTC 03:00 only) + notificationFailuresDigest (Mon UTC 09:00 only) 毎時: ストアレビュー促進 push を評価。UTC 00:00 (= 09:00 JST) のみ sns_follower_snapshots へ日次スナップショット、UTC 03:00 (= 12:00 JST) のみ R2 temp/ prefix の 24h 超オブジェクトを削除(旧独立スロット 0 3 * * * を Cloudflare 無料プラン cron 上限 5 のため吸収)、月曜 UTC 09:00 (= JST 月曜 18:00) のみ Notification DLQ digest を Discord へ投稿 Hourly: store-review-prompt push evaluation. At UTC 00:00 (= 09:00 JST) it also writes a daily snapshot into sns_follower_snapshots; at UTC 03:00 (= 12:00 JST) it deletes objects older than 24 h under R2 temp/ (folded in from the former standalone 0 3 * * * slot to fit the Cloudflare free-plan cron cap of 5); at Mon UTC 09:00 (= Mon 18:00 JST) it posts the weekly Notification DLQ digest to Discord
0 17 * * 6 (UTC) = 02:00 JST 日0 17 * * 6 (UTC) = 02:00 JST Sun PLACES_WEEKLYplacesQueueProducerPLACES_WEEKLYplacesQueueProducer 週次 Google Places 取込ジョブを parky-places-refresh queue に投入 Weekly Google Places import — enqueues into the parky-places-refresh queue

近接検知(スポンサー付近の通知)はモバイル側のネイティブジオフェンスで行うため、サーバー側 cron では扱わない。 Google Places 取込は PLACES_WEEKLY cron(毎週土曜 17:00 UTC)と POST /v1/admin/places/import/all の両ルートで parky-places-refresh queue を駆動する。

Sponsor proximity detection runs on-device via native geofences, so no cron slot covers it. Google Places import is driven by both the PLACES_WEEKLY cron (Sat 17:00 UTC) and the manual POST /v1/admin/places/import/all trigger — both feed the parky-places-refresh queue.

Queue 構成(Cloudflare Queues)Queue topology (Cloudflare Queues)

QueueQueue ProducerProducer ConsumerConsumer 役割Purpose
parky-store-syncparky-store-sync 管理者の手動実行 / 将来的な自動ジョブ Admin manual runs / future automated jobs src/queue/store-sync.ts(DLQ: *-dlq, retries 3) src/queue/store-sync.ts (DLQ: *-dlq, retries 3) Google Play / App Store Connect API から売上・レビュー・メトリクスを取得し、store_* テーブルへ upsert Pulls sales / reviews / metrics from Google Play and App Store Connect APIs and upserts into store_* tables
parky-fcm-dispatchparky-fcm-dispatch POST /v1/admin/user-notifications/:id/send(トークンを 500 件/バッチで投入) POST /v1/admin/user-notifications/:id/send (tokens chunked 500/batch) src/queue/fcm-dispatch.ts(DLQ: *-dlq, retries 3) src/queue/fcm-dispatch.ts (DLQ: *-dlq, retries 3) FCM v1 OAuth2 JWT(RS256)を Web Crypto で署名して並列 fetch。access_token は KV(parky-cache)で 2 層キャッシュ Signs FCM v1 OAuth2 JWTs (RS256) via Web Crypto and fans out. The access_token is 2-tier cached (isolate Map + KV parky-cache)
parky-places-refreshparky-places-refresh PLACES_WEEKLY cron + POST /v1/admin/places/import/all PLACES_WEEKLY cron + POST /v1/admin/places/import/all src/queue/places-refresh-consumer.ts(DLQ 配線済) src/queue/places-refresh-consumer.ts (DLQ wired) Google Places API から近隣施設をバッチ取得し area_places へ upsert Pulls nearby places from Google Places API in batches and upserts into area_places
parky-x-ai-generateparky-x-ai-generate Marketing Portal の X 投稿生成リクエスト X post generation requests from the Marketing Portal src/queue/x-ai-generate-consumer.ts(DLQ 配線済) src/queue/x-ai-generate-consumer.ts (DLQ wired) LLM で X 投稿草案を生成し x_posts に保存。長時間処理を Worker 本体から切り離す Generates X post drafts via LLM and stores them in x_posts; offloads long-running calls from the request worker

チャネル別 API マウントChannels and route mounts

Workers は単一 API サーフェス(/v1/*)の中で、クライアント種別ごとにサブツリーを切り、認可ミドルウェアで型安全に分離しています。

Under the single /v1/* surface, Workers carve out per-client subtrees and isolate each with its own authorization middleware.

チャネルChannel マウントMount 認可Authz OpenAPIOpenAPI
モバイルアプリMobile app /v1/me/*, /v1/parking-lots/*, /v1/search/*, /v1/reviews, /v1/ratings, /v1/notifications, /v1/subscriptions, /v1/themes, /v1/vehicles /v1/me/*, /v1/parking-lots/*, /v1/search/*, etc. requireUser (Supabase JWT) requireUser (Supabase JWT) mobile-app/openapi.json mobile-app/openapi.json
Web 版Web app /v1/parking-lots/*, /v1/articles, /v1/ads, /v1/sponsors, /v1/hubs, /v1/tags, /v1/codes, /v1/newsletter-track /v1/parking-lots/*, /v1/articles, /v1/ads, etc. optionalUser + cachePublicRead optionalUser + cachePublicRead web-app/openapi.json web-app/openapi.json
管理者ポータルAdmin portal /v1/admin/*(40+ モジュール) /v1/admin/* (40+ modules) requireAdminadmins + role_permissions requireAdmin (admins + role_permissions) portal-admin/openapi.json portal-admin/openapi.json
オーナーポータルOwner portal /v1/owner/parking-lots/mine, /v1/owner/authority-requests, /v1/owner/reviews/mine, /v1/owner/boosts, /v1/owner/credits/balance /v1/owner/parking-lots/mine, /v1/owner/authority-requests, /v1/owner/reviews/mine, /v1/owner/boosts, /v1/owner/credits/balance etc. requireOwnerownersstatus=active で照合) requireOwner (owners.status = active) portal-owner/openapi.json portal-owner/openapi.json
マーケティングポータルMarketing portal /v1/marketing/dashboard/sns-metrics, /v1/marketing/newsletter/broadcasts, /v1/marketing/x/posts, /v1/marketing/integrations, /v1/marketing/analytics/summary, /v1/marketing/campaigns, /v1/marketing/assets, /v1/marketing/calendar, /v1/marketing/notifications, /v1/marketing/activity, /v1/marketing/brand, /v1/marketing/content-pool, /v1/marketing/article-categories /v1/marketing/dashboard/sns-metrics, /v1/marketing/newsletter/broadcasts, /v1/marketing/x/posts, /v1/marketing/integrations, /v1/marketing/analytics/summary, /v1/marketing/campaigns, /v1/marketing/assets, /v1/marketing/calendar, /v1/marketing/notifications, /v1/marketing/activity, /v1/marketing/brand, /v1/marketing/content-pool, /v1/marketing/article-categories etc. requireMarketingmarketing:* 権限 or super admin) requireMarketing (marketing:* permission or super admin) portal-marketing/openapi.json portal-marketing/openapi.json

ミドルウェア順序: requestIdcachePublicReadcors → [routes] → errorHandler。エラーは {error: {code, message, request_id}} で統一、Zod バリデーション失敗は 422 を返す。詳細は エラーカタログ

Middleware order: requestIdcachePublicReadcors → [routes] → errorHandler. Errors come back as {error: {code, message, request_id}}; Zod failures return 422. See the error catalog.

Worker 分割と route の対応Worker split and route mapping

単一 /v1/* サーフェスはチャネル別の Worker(wrangler.public.toml / wrangler.admin.toml / wrangler.marketing.toml)にデプロイされ、CPU 上限 / 障害ドメイン / 機密境界を分離する(ADR-0010)。binding ID の SSoT は wrangler.shared-bindings.toml。2026-05-11 に store-sync 専用 Worker (skeleton) は削除済、queue consumer は admin Worker に集約。

The single /v1/* surface is deployed across channel-specific Workers (wrangler.public.toml / wrangler.admin.toml / wrangler.marketing.toml) to isolate CPU limits, blast radius, and secret boundaries (ADR-0010). Binding-ID SSoT lives in wrangler.shared-bindings.toml. The dedicated store-sync Worker skeleton was removed on 2026-05-11; its queue consumer now runs inside the admin Worker.

flowchart LR
  subgraph Hosts["Public hostnames"]
    HPUB["api.parky.co.jp"]
    HADM["admin-api.parky.co.jp"]
    HMKT["marketing-api.parky.co.jp"]
  end

  subgraph Workers["Cloudflare Workers"]
    WPUB["parky-api-public
wrangler.public.toml"] WADM["parky-api-admin
wrangler.admin.toml
+ all crons (6 slots)
+ all queue consumers
(store-sync / FCM / Places / X_AI)"] WMKT["parky-api-marketing
wrangler.marketing.toml"] end HPUB --> WPUB HADM --> WADM HMKT --> WMKT WPUB -->|/v1/parking-lots, /v1/articles, /v1/me/*, /v1/search/*, ...| ROUTES1[end-user routes] WADM -->|/v1/admin/*| ROUTES2[admin routes] WMKT -->|/v1/marketing/*| ROUTES3[marketing routes] WADM -.->|consumes parky-store-sync, parky-fcm-dispatch ...| Q[(Cloudflare Queues)]

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

Mobile → BFF は ViewEnvelope 契約Mobile → BFF uses the ViewEnvelope contract

モバイルアプリ(Flutter)が Mobile BFF の View Endpoint/v1/views/*)を叩くと、 画面描画に必要な情報が ViewEnvelope という統一エンベロープで返ります。Parky は Server-Driven UI Level 3(Data + Validation + Navigation + UI Config)を採用しており、 UI コンポーネント構造はサーバー配信しない(そこは Flutter の責任)。 サーバーが担うのは data(ドメインデータ)/ui_config(文言・feature flag・theme hint)/ navigation(次画面 Hint)/validation(サーバー駆動ルール)/ states(error / empty / skeleton 指示)/fallback_behavior(オフライン / 認証エラー / バージョン不整合の振る舞い)/ metaserver_time / cache_key / min_app_version / sunset_date)。 クライアントは全 /v1/* 呼び出しで X-App-Version ヘッダーを必ず送信し、BFF が meta.min_app_version と突き合わせて force_update / degrade / ignore を画面単位で指示する。

When the Flutter mobile app hits a Mobile BFF View endpoint (/v1/views/*), the response is wrapped in the unified ViewEnvelope. Parky runs on Server-Driven UI Level 3 (Data + Validation + Navigation + UI Config); UI component structure is never shipped from the server — that stays with Flutter. The server owns data (domain data), ui_config (messages, feature flags, theme hint), navigation (next-screen hints), validation (server-driven rules), states (error / empty / skeleton directives), fallback_behavior (offline / auth error / version mismatch behavior), and meta (server_time / cache_key / min_app_version / sunset_date). The client must send X-App-Version on every /v1/* call, and the BFF compares it with meta.min_app_version to pick force_update / degrade / ignore per screen.

詳細: Details: フィールド仕様・バージョン互換ポリシー・Flutter 呼び出し例は API レスポンス構造 を参照。モバイル視点は Mobile / システム全体像 See API response structure for field-level specs, compatibility policy, and Flutter usage. The mobile-side take is at Mobile / system architecture.

クライアント別の特徴What each client does differently

プロダクト別の詳細: Product details: 各プロダクトの詳しいアーキテクチャは、上部タブから対象セクションへ。 See each product's own architecture page via the tabs above.

監視・観測性 (Sentry + Logpush)Monitoring & observability (Sentry + Logpush)

Parky の観測性は Cloudflare Workers Logs (Logpush → R2)Sentry の役割分担で構成しています。

Observability is split between Cloudflare Workers Logs (Logpush → R2) and Sentry.

仕組みMechanism 担当範囲Responsibility 保存先・通知先Sink / channel
Workers Logs (Logpush → R2) Workers Logs (Logpush → R2) 全 invocation の構造化ログ(request_id / route / status / latency / user_id)。長期保存と事後解析が目的 Structured log for every invocation (request_id / route / status / latency / user_id). Long-term archive for forensic analysis R2(parky-logs)。dashboard 化・retention は Logpush 側で制御 R2 (parky-logs). Dashboards / retention configured on the Logpush side
SentrySentry 5xx / 未処理例外 / クライアント SDK 例外。stack trace・user_id・request_id 付きの error context をリアルタイム通知し、issue grouping / release tracking で再発を追跡 5xx / unhandled exceptions / client SDK errors. Real-time alerting with full context (stack trace, user_id, request_id), issue grouping, and release tracking for regression follow-up Sentry プロジェクト(SENTRY_DSN)。Slack / メール通知 (Sentry 側設定) Sentry project (SENTRY_DSN). Slack / email alerts wired in Sentry

BFF (Workers) では api/src/lib/sentry.tstoucan-js をラップし、middleware/error-handler.ts が 5xx と未処理例外を capture。app-core.ts で初期化します。 PII 保護: request body / cookies / クライアント IP は redact。許可ヘッダーは user-agent / x-app-version / x-request-id / accept-language のみ送信します(api/src/lib/sentry.ts L52–60)。 各クライアントは Mobile (sentry_flutter) / Web (@sentry/astro) / Admin・Owner・Marketing Portal (@sentry/react) の SDK で連携。詳細は ops / sentry-setup、Logpush との段階展開は ops / sentry-logpush-rollout、役割分担の運用詳細は ops / logging を参照。

On the BFF (Workers), api/src/lib/sentry.ts wraps toucan-js, and middleware/error-handler.ts captures 5xx + unhandled exceptions. Initialization happens in app-core.ts. PII protection: request body, cookies, and client IP are redacted. Only user-agent / x-app-version / x-request-id / accept-language headers are forwarded (api/src/lib/sentry.ts L52–60). Clients wire their own SDKs: Mobile (sentry_flutter) / Web (@sentry/astro) / Admin · Owner · Marketing Portal (@sentry/react). See ops / sentry-setup, ops / sentry-logpush-rollout, and ops / logging.

BFF 内部コード構造BFF internal code structure

Parky の BFF(parky/api/)は 4 層構造(bff / core / data / schema) + shared / app で構成する。 トップレベルを層で切り、各層の内部は層に自然な軸でさらに分割する。 単一 Cloudflare Workers デプロイのモジュラーモノリスで、層間通信は関数呼び出し。

The BFF (parky/api/) is organised as a 4-layer structure (bff / core / data / schema) + shared / app. The top level is layered; each layer is subdivided by an axis natural to that layer. A single-deploy modular monolith on Cloudflare Workers; inter-layer communication is by function calls.

採用の狙い: Why this shape: テーブル構造の変更が BFF 層に一切影響しない保証を機械的に担保するため、 schema/row(DB 列名 snake_case)と schema/domain(意味論 camelCase)を物理的に分離し、 data 層が row → domain 変換を担う。これにより列名変更は data + schema/row 内に閉じ、 corebff に染み出さない。 The layering mechanically guarantees that table changes never leak into the BFF layer. schema/row (snake_case DB columns) and schema/domain (semantic camelCase) are physically split, and data translates row → domain. A column rename is bounded to data + schema/row — it never reaches core or bff.

層とその責務Layers and responsibilities

Layer 役割Role 層内の分類軸Internal axis
bff/ bff/ HTTP I/O + ViewEnvelope 整形 HTTP I/O + ViewEnvelope shaping channel × screen(例: bff/mobile/views/ channel × screen (e.g. bff/mobile/views/)
core/ core/ ユースケース / ビジネスルール Use cases / business rules capability (例: core/parking-lots/, core/pricing/) capability (e.g. core/parking-lots/, core/pricing/)
data/ data/ 永続化 / SQL の閉じ込め Persistence / SQL encapsulation table cluster(1 ファイル 1 table、関数 export のみ) table cluster (1 file per cluster, functions only — no repository classes)
schema/domain/ schema/domain/ core が使う意味論型 Domain entity types for core entity(camelCase) entity (camelCase)
schema/view/ schema/view/ bff が返す画面固有型 View Model types for bff screen × channel screen × channel
schema/row/ schema/row/ data が使う DB 列型 DB row types for data table(snake_case) table (snake_case)

依存方向(ESLint で機械強制)Dependency direction (enforced by ESLint)

flowchart TD
  BFF[bff/] --> CORE[core/]
  BFF --> SV[schema/view/]
  BFF --> SD[schema/domain/]
  CORE --> DATA[data/]
  CORE --> SD
  CORE --> SR[schema/row/]
  DATA --> SR
  DATA --> SHARED[shared/]
  CORE --> SHARED
  BFF --> SHARED
        

data 層の実装方針 (postgres.js + raw SQL hybrid)data layer (postgres.js + raw SQL hybrid)

data 層の永続化は postgres.jslib/db.ts)+ raw SQL タグ付きテンプレート + Supabase 自動生成型schema/row/ = Supabase が list_tables から生成した DB 列型)の hybrid 構成です。 Drizzle 等の TypeScript ORM は採用しない(2026-04-27 確定)。

The data layer combines postgres.js (lib/db.ts) + raw SQL tagged templates + Supabase-generated row types (schema/row/, generated from list_tables). No TypeScript ORM (Drizzle and friends are explicitly off the table) — finalized 2026-04-27.

Multi-channel BFFMulti-channel BFF

同じ core 機能を異なる client(mobile / admin / owner / marketing)で提供する時は、 bff/<channel>/ を並置する。ビジネスルールは core に一元化され、 channel ごとに異なる View Model だけが bff に出る。

To expose the same core capability to different clients (mobile / admin / owner / marketing), place bff/<channel>/ folders side-by-side. Business rules stay centralized in core; only channel-specific View Models live in bff.

詳細: Details: 層規律・アンチパターン・ESLint 設定例は コード規約 / 層規律 を、 ViewEnvelope フィールド仕様は BFF / ViewEnvelope を参照。 See Code conventions / Layer discipline for layer rules, anti-patterns, and ESLint examples; see API response structure for envelope field specs.