Recipe Stock は、レシピサイト、YouTube、SNS投稿、書籍、画像、スクリーンショットなどからレシピを取り込み、統一された形式で保存・検索・閲覧するための PWA です。
フロントエンドの静的アセットと API は、同一の Cloudflare Worker から配信します。
Browser / PWA
-> Cloudflare Worker
-> static Vite React SPA
-> /api/* Hono API
-> Neon PostgreSQL
-> Cloudflare R2
-> Better Auth
-> Resend
-> Stripe
-> Vercel AI SDK + Cloudflare AI Gateway/api/*: Hono API- それ以外: Vite React SPA の static assets / SPA fallback
| 領域 | 技術 |
|---|---|
| Frontend | Vite + React + TypeScript |
| Routing | TanStack Router |
| Server state | TanStack Query |
| Forms / validation | React Hook Form + Zod |
| API | Hono + Hono RPC client |
| Database | Neon PostgreSQL + Drizzle ORM |
| Storage / deploy | Cloudflare Workers + Cloudflare R2 |
| Auth | Better Auth |
| Resend | |
| Billing | Stripe |
| AI | Vercel AI SDK + Cloudflare AI Gateway |
| Monorepo | pnpm workspace + Turborepo |
| Lint / format | Biome |
| Tests | Vitest + Testing Library |
apps/
web/ Vite React SPA
api/ Hono API on Cloudflare Workers
packages/
db/ Drizzle schema, migrations, Neon client
schemas/ Zod schemas and API-facing types
shared/ deterministic logic shared by API and web
config/ shared TypeScript and tool configuration- Node.js 22 系
- pnpm via Corepack
- Cloudflare account and Wrangler access
- Neon project / database
- 必要に応じて Resend, Stripe, Cloudflare AI Gateway のアカウント・キー
corepack enable依存関係をインストールします。
pnpm installローカル用の環境変数ファイルを作成します。
cp .env.example .env
cp apps/api/.dev.vars.example apps/api/.dev.varsDATABASE_URL には Neon の接続文字列を設定してください。
VITE_IOS_SHARE_SHORTCUT_URL には、設定画面から追加する公開済みの iOS Shortcut URL
を設定してください。この値は Web アプリのビルド時にブラウザ向けコードへ埋め込まれます。
Shortcut のアクション列と API 契約は docs/shortcut/ios-share.md にあります。
Cloudflare にログインし、開発用 R2 bucket を作成します。
pnpm --filter @recipestock/api exec wrangler login
pnpm --filter @recipestock/api exec wrangler r2 bucket create recipestock-images-dev
pnpm --filter @recipestock/api exec wrangler r2 bucket cors set recipestock-images-dev --file apps/api/cors.example.json
pnpm --filter @recipestock/api exec wrangler r2 bucket lifecycle add recipestock-images-dev expire-tmp-uploads tmp/ --expire-days 1ライフサイクルルールは、保存されないまま残る一時アップロード(tmp/)を消します(ADR 0024)。
本番など別の bucket を作るときも同じ設定を適用してください。
API 固有のセットアップ詳細は apps/api/README.md を参照してください。
API と Web をまとめて起動します。
pnpm dev個別に起動する場合:
pnpm --filter @recipestock/api dev
pnpm --filter @recipestock/web devデフォルトの URL:
- Web: http://localhost:5173/
- API: http://localhost:8787/
画面の状態(レシピなし、フリープランのロック、取り込み失敗、接続不可など)を目視で確認するときは、 API を MSW に差し替えて起動します。wrangler も Neon も R2 も不要です。
pnpm dev:mockシナリオは画面左下のセレクタか、?scenario=<id> で切り替えます。
一度指定すると sessionStorage に残るので、画面遷移しても維持されます。
?delay=2000 を付けると全 API レスポンスが遅くなり、スケルトンを観察できます。
| 分類 | id | 内容 |
|---|---|---|
| 基本 | default |
Pro・26件(2ページ目あり)・タグあり |
| レシピ一覧・詳細 | empty |
レシピなし |
| レシピ一覧・詳細 | no-tags |
タグを持たない(チップ列なし・詳細で定番候補) |
| レシピ一覧・詳細 | list-error |
一覧の取得失敗 |
| レシピ一覧・詳細 | next-page-error |
2ページ目の取得失敗 |
| レシピ一覧・詳細 | no-cover |
カバー画像なし |
| レシピ一覧・詳細 | image-only |
画像だけの投稿(詳細が材料・手順なしで画像だけ。表紙はレシピ画像の1枚目と同じ。2件目は1枚だけ) |
| レシピ一覧・詳細 | broken-image |
画像の読み込み失敗(一覧のサムネイルと、2・5・8件目の詳細の画像) |
| プラン・上限 | free-locked |
Freeで末尾がロック |
| プラン・上限 | limit-reached |
Freeで保存上限ちょうど |
| プラン・上限 | import-limit |
Freeで今月のAI取り込みが上限 |
| プラン・上限 | pro-import-limit |
Proで今月のAI取り込みが上限 |
| プラン・上限 | checkout-pending |
決済から戻った直後(/settings/billing?checkout=success を開くと、数秒でProに変わる) |
| プラン・上限 | pro-canceling |
Proで解約予約中 |
| プラン・上限 | pro-canceling-no-lock |
Proで解約予約中だが、Freeの保存上限以内 |
| プラン・上限 | pro-past-due |
Proで支払いを確認できない |
| プラン・上限 | billing-status-error |
契約状態の取得失敗 |
| プラン・上限 | pro-price-error |
Pro価格の取得失敗 |
| プラン・上限 | checkout-error |
Checkoutの開始失敗 |
| プラン・上限 | billing-portal-error |
契約管理画面の開始失敗 |
| 設定・連携 | linked-devices |
iPhoneとiPadの2台と共有を連携中 |
| 設定・連携 | viewer-error |
プラン・利用状況の取得失敗 |
| 設定・連携 | tags-error |
タグの取得失敗 |
| 設定・連携 | shortcut-credentials-error |
連携端末の取得失敗 |
| 設定・連携 | push-subscriptions-error |
通知状態の取得失敗(Push対応ブラウザ向け) |
| 設定・連携 | shortcut-issue-error |
連携キーの発行失敗 |
| 設定・連携 | shortcut-revoke-error |
連携端末の解除失敗 |
| 取り込み | importing |
取り込み中 |
| 取り込み | import-failed |
取り込み失敗 |
| 取り込み | text-import-failed |
テキストの取り込み失敗(原文を直して再試行) |
| アカウント | google-login |
Googleだけでログイン(アカウント設定にパスワードとメールアドレスの変更が出ない) |
| アカウント | password-and-google |
パスワードとGoogleの両方でログイン可能 |
| アカウント | login-methods-error |
ログイン方法の取得失敗 |
| アカウント | account-write-error |
メールアドレスとパスワードの変更失敗 |
| アカウント | invalid-current-password |
パスワード変更時に現在のパスワードが不一致 |
| アカウント | sign-out-error |
ログアウト失敗 |
| セッション | signed-out |
未ログイン |
| セッション | offline |
接続不可 |
検索ヒットなしの表示は、default で一致しない語を検索すると出ます。
取り込みは URL やテキストを送信してから数秒で成功に変わるので、島の一連の流れをそのまま追えます。
レシピの作成・編集・削除も、リロードするまでは入力した内容で詳細と一覧に反映されます。
Freeで保存上限に達しているシナリオ(limit-reached / free-locked)では、作成と URL・テキストの取り込みが本番と同じく recipe_limit_exceeded で失敗します。
Freeのシナリオでプランのページから「Proにする」を押すと、決済から戻った画面になりますが、Proには変わらず待ちきれなかったときの表示になります。
checkout-error、billing-portal-error、shortcut-issue-error、shortcut-revoke-error、アカウントの更新失敗は、対象の設定ページでボタンを押すとエラー表示を確認できます。
signed-out でメールアドレスによるログインや新規登録(OTP 検証)をすると、そのままログイン状態になります。
Google ログインはリロードを伴うので、戻り先で default シナリオに切り替わります。
ハンドラのない API は実 API に流さず、501 を返してコンソールにエラーを出します。
API を追加したら apps/web/src/mocks/handlers.ts にハンドラを足してください。
設定画面の通知の有効化は、モックモードでは試せません(失敗の表示になります)。
Service Worker の scope / を MSW が使っているためです。
シナリオとフィクスチャは apps/web/src/mocks/ にあります。
フィクスチャは @recipestock/schemas の型で縛ってあり、src/mocks/scenarios.test.ts が
Zod スキーマとの整合を検証するので、API 契約が変わればテストが落ちます。
http://<LAN-IP>:5173 のように localhost 以外を http で開くと Service Worker が使えません。
この場合 MSW はページ内の fetch だけを差し替えるフォールバックで動くので、API のモックは効きますが、
<img> で読む画像は差し替わらず、すべて読み込み失敗の表示になります。
スマートフォンで画像まで確認するときは、trycloudflare などの HTTPS トンネル越しに開いてください。
| コマンド | 内容 |
|---|---|
pnpm dev |
Turborepo 経由で開発サーバーを起動 |
pnpm dev:mock |
API を MSW に差し替えた Web のみの開発サーバーを起動 |
pnpm build |
全 package/app を build |
pnpm typecheck |
TypeScript の型チェック |
pnpm lint |
Biome による lint / format check |
pnpm format |
Biome による format |
pnpm test |
Vitest を実行 |
pnpm test:db |
Neon ephemeral branchでDatabase統合テストを実行 |
pnpm test:all |
通常テストとDatabase統合テストを実行 |
pnpm db:generate |
Drizzle migration を生成 |
pnpm db:migrate |
Drizzle migration を適用 |
pnpm run deploy |
Web build 後に Cloudflare Worker へ deploy し、source map を Sentry へ上げる(apps/api/README.md) |
GitHub ActionsのCI workflowは、PRの作成・更新・再オープン、mainへのpush、手動実行で起動する。forkからのPRも同じチェックの対象とする。
checks jobはUbuntu 24.04、Node.js 22系、package.jsonに指定したpnpmで次を順に実行する。
pnpm install --frozen-lockfile
pnpm lint
pnpm typecheck
pnpm build
pnpm testビルドではPWA生成物も検証する。APIテストが参照するWebの静的アセットを生成するため、ビルドをテストより先に実行する。通常テストのCloudflare bindingsはローカルで実行し、CIにSecretsやローカル環境ファイルは不要。同じPR・ブランチへの更新では古い実行をキャンセルする。
pnpm test:dbはDockerでNeon Localを起動し、テスト専用Neon projectにephemeral branchを作成する。全migrationとDatabase統合テストを実行した後、成功・失敗にかかわらずbranchを削除する。通常の開発用または本番用DATABASE_URLは使用しない。
事前にDockerを起動し、次の環境変数を設定する。ローカルではgit管理外の.env.test.localから自動的に読み込む。
NEON_API_KEY: テスト専用projectでbranchを作成・削除できるAPI keyNEON_PROJECT_ID: 実データを含まないテスト専用Neon projectNEON_PARENT_BRANCH_ID: 空の親branchNEON_LOCAL_PORT: Neon Localの公開port。省略時は55432
cp .env.example .env.test.local
# .env.test.localへテスト専用projectの値を設定
pnpm test:db日常の高速テストにはpnpm testを使用し、Databaseまたはrepositoryを変更した場合は、CIに加えて必要に応じてローカルでもpnpm test:allを実行する。
Cloudflare Worker の deploy 前検証:
pnpm --filter @recipestock/web build
pnpm --filter @recipestock/api exec wrangler deploy --dry-runローカルでは apps/api/.dev.vars、本番では Cloudflare secrets / vars に設定します。
主な値:
DATABASE_URLVITE_IOS_SHARE_SHORTCUT_URL(Web ビルド時に公開される iOS Shortcut URL)VITE_SENTRY_DSN(Web ビルド時に埋め込む Sentry DSN)SENTRY_DSN(Worker の Sentry DSN)BETTER_AUTH_SECRETRESEND_API_KEYSTRIPE_SECRET_KEYSTRIPE_WEBHOOK_SECRETSTRIPE_PRO_PRICE_IDCLOUDFLARE_ACCOUNT_IDAI_GATEWAY_NAMEAI_TEXT_MODELAI_VISION_MODELIMPORT_TIMEOUT_MSIMPORT_JOB_TIMEOUT_MS
開発基盤の変更後は、少なくとも以下を実行します。
pnpm lint
pnpm typecheck
pnpm test
pnpm buildWorker 設定と static assets の確認には wrangler deploy --dry-run を使います。
pnpm --filter @recipestock/api exec wrangler deploy --dry-run