アーキテクチャ 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.
プロダクトスコープ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 scope | In scope | Out of scope | Out 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 / 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"]
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-js で error-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.
| Schema | Schema | 役割 | Role | アクセス | Access | 代表テーブル | Representative tables |
|---|---|---|---|---|---|---|---|
public | public |
業務ドメインの中核(end-user 向け機能・core business) | Core business domain (end-user features) | anon / authenticated + RLS | anon / 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 |
admin | admin |
内部運営者データ | Internal operator data | service_role only | service_role only | admins / admin_tasks / admin_activity_logs / roles / role_permissions |
admins / admin_tasks / admin_activity_logs / roles / role_permissions |
marketing | marketing |
マーケサブプロダクト(Marketing Portal 駆動) | Marketing sub-product (driven by Marketing Portal) | service_role only | service_role only | marketing_campaigns / x_posts / newsletter_broadcasts / store_sales_daily |
marketing_campaigns / x_posts / newsletter_broadcasts / store_sales_daily |
analytics | analytics |
append-only テレメトリ(retention 戦略が public と別) | Append-only telemetry (independent retention from public) | service_role only | service_role only | client_events / error_reports / boost_*_logs / sns_follower_snapshots |
client_events / error_reports / boost_*_logs / sns_follower_snapshots |
extensions | extensions |
拡張機能の encapsulation 先 | Encapsulation namespace for extensions | USAGE は全 role | USAGE 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.ts の CRON_JOBS、実行分岐は api/src/app-core.ts の dispatchScheduled()。
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 * * * * |
WARMER → warmer | WARMER → warmer |
Workers isolate と Hyperdrive 接続のウォーム維持 | Keeps the Workers isolate and Hyperdrive connection warm |
* * * * * | * * * * * |
EVERY_MINUTE → X 予約投稿 + X 定期ルール + newsletter dispatch + session-notifications fire | EVERY_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_LISTEN → xListen | X_LISTEN → xListen |
X 監視ルール評価(検索クエリ / 言及検知 → x_automation_log) |
Evaluates X listening rules (search / mentions → x_automation_log) |
*/10 * * * * | */10 * * * * |
X_INSIGHTS → xInsights | X_INSIGHTS → xInsights |
X 投稿のインサイト(impression / engagement)を取得して x_posts へ反映 |
Pulls X post insights (impressions / engagement) into x_posts |
0 * * * * | 0 * * * * |
HOURLY → reviewReminder + snsFollowerSnapshot (UTC 00:00 のみ) + r2TempCleanup (UTC 03:00 のみ) + notificationFailuresDigest (月曜 UTC 09:00 のみ) | HOURLY → reviewReminder + 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_WEEKLY → placesQueueProducer | PLACES_WEEKLY → placesQueueProducer |
週次 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)
| Queue | Queue | Producer | Producer | Consumer | Consumer | 役割 | Purpose |
|---|---|---|---|---|---|---|---|
parky-store-sync | parky-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-dispatch | parky-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-refresh | parky-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-generate | parky-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 | OpenAPI | OpenAPI |
|---|---|---|---|---|---|---|---|
| モバイルアプリ | 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) |
requireAdmin(admins + 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. |
requireOwner(owners を status=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. |
requireMarketing(marketing:* 権限 or super admin) |
requireMarketing (marketing:* permission or super admin) |
portal-marketing/openapi.json |
portal-marketing/openapi.json |
ミドルウェア順序: requestId → cachePublicRead → cors → [routes] → errorHandler。エラーは {error: {code, message, request_id}} で統一、Zod バリデーション失敗は 422 を返す。詳細は エラーカタログ。
Middleware order: requestId → cachePublicRead → cors → [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-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.
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(オフライン / 認証エラー / バージョン不整合の振る舞い)/
meta(server_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.
クライアント別の特徴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, and camera - Web 版(Astro): 静的 SSG + Islands。SEO 最重視。検索・地図は島(Island)として JS 化。公開データアクセスは
@parky/bff-client経由で/v1/*を呼ぶ。ビルド時に API のヘルスチェックを挟んで SSG を焼く - Web app (Astro): SSG with Islands, SEO-first; search and map hydrate as islands. Public reads go through
@parky/bff-clientagainst/v1/*. A build-time API health check gates SSG rendering - 管理者ポータル(React + Vite): SPA。データアクセスは
@parky/bff-client経由で/v1/admin/*を呼ぶ。Realtime は直接購読(データプレーン例外)。グラスモーフィズムのデザイン - Admin portal (React + Vite): SPA. Data access goes through
@parky/bff-clientagainst/v1/admin/*. Realtime subscriptions stay direct as a data-plane exception. Glassmorphism design - オーナーポータル(React + Vite): SPA。駐車場オーナー向けのセルフサービス画面(13 画面)。Supabase Auth(メール + PW、招待トークン経由で新規作成)で認証し、BFF の
/v1/owner/*をrequireOwner経由で呼ぶ。デプロイは Cloudflare Pages(dev-owner.parky.co.jp) - Owner portal (React + Vite): SPA — 13 self-service screens for parking owners. Supabase Auth (email + password, onboarded via invitation tokens) gates access; the BFF path is
/v1/owner/*behindrequireOwner. Deployed to Cloudflare Pages (dev-owner.parky.co.jp) - マーケティングポータル(React + Vite): SPA。SNS(X / Instagram)運用・ニュースレター配信・キャンペーン管理・アナリティクス・コンテンツ管理を担う。
adminsテーブル +marketing:*権限で認可。OAuth リフレッシュトークンは Supabase Vault(pgcrypto)で暗号化保管。デプロイは Cloudflare Pages(dev-marketing.parky.co.jp) - Marketing portal (React + Vite): SPA. Owns SNS ops (X / Instagram), newsletter delivery, campaigns, analytics, and content. Authorized via the
adminstable +marketing:*permissions. OAuth refresh tokens are encrypted in Supabase Vault (pgcrypto). Deployed to Cloudflare Pages (dev-marketing.parky.co.jp)
監視・観測性 (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 |
| Sentry | Sentry | 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.ts が toucan-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.
schema/row(DB 列名 snake_case)と schema/domain(意味論 camelCase)を物理的に分離し、
data 層が row → domain 変換を担う。これにより列名変更は data + schema/row 内に閉じ、
core と bff に染み出さない。
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
bff → core → dataが正方向。逆参照は禁止- Forward flow is
bff → core → data; reverse imports are forbidden schema/viewは bff 専用(core / data は import 禁止)schema/viewbelongs to bff only — core / data cannot import itschema/rowは data 専用(core / bff は import 禁止)schema/rowbelongs to data only — core / bff cannot import it- 単純な read-only CRUD は core 省略可(
bff → data直呼び)。ロジック出現時に core 追加 - Pure read-only CRUD may skip core (
bff → datadirect); add core once business logic appears
data 層の実装方針 (postgres.js + raw SQL hybrid)data layer (postgres.js + raw SQL hybrid)
data 層の永続化は postgres.js(lib/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.
- 接続:
lib/db.tsのpostgres(env.HYPERDRIVE.connectionString, ...)。Hyperdrive がプーリング+グローバルキャッシュを担う - Connection:
postgres(env.HYPERDRIVE.connectionString, ...)inlib/db.ts. Hyperdrive provides pooling + global cache - クエリ: タグ付きテンプレート
sql`SELECT ... FROM public.parking_lots WHERE id = ${id}`で SQL injection 安全+型推論 - Queries use tagged templates —
sql`SELECT ... FROM public.parking_lots WHERE id = ${id}`— for injection safety and inference - 列型は
schema/row/(Supabase MCP で再生成可能)。data層がschema/row → schema/domainの変換を担う - Column types live in
schema/row/(regenerated via Supabase MCP).datahandles row → domain translation SELECT *禁止 lint とcallRpc型推論ヘルパで保守漏れを防止([Phase 1+2+3 完了](https://...) で残違反 0)- A
SELECT *ESLint gate and acallRpctype-inference helper guard against drift (Phase 1+2+3 closed all violations) - 重トランザクション・行レベルロックが必要なロジックは PG 関数 (
bff_only.*) として DB 内に実装し、data層からcallRpc経由で呼ぶ - Heavy transactional logic / row-level locking lives as PG functions (
bff_only.*) and is invoked fromdataviacallRpc
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.