NOTE · 2026-05-07
kokan-nikki の技術スタック選定理由を全部書く
kokan-nikki (Next.js 16 / Auth.js v5 / Prisma 6 / Postgres 16 / zod 4 / Tailwind v4 / Vitest + Testcontainers / Playwright / Resend / Vercel + Neon) は、要件と制約から逆算してツールを選んだ。次に新しい Web アプリを 1 人で組むときに同じ判断をするか確認できるよう、各層で『何を求めて、何を捨てて、何を選んだか』を全部書き残す。
kokan-nikki (グループで回す Y2K カワイイ系の交換日記アプリ) を 1 人で組むときに、ツールスタックを真面目に選び直した。各層で「何を求めて、何を捨てて、何を選んだか」を残しておくと、次の Web アプリを作るときに同じ判断ができる (or 違う判断をするときに分岐点が見える) ので、スタックを丸ごと書き出す。
前提として、このアプリの制約は次の 4 つ:
- 1 人開発。レビュアー = 自分、運用 = 自分。判断負荷を下げる選択を優先。
- DB 密結合のドメインロジック (ターン制、招待の atomic claim、ナッジの 24h クールダウン)。型と DB をまとめて検証できないと壊れる。
- Vercel デプロイ前提。Edge / Node の境界、cron、preview 環境はプラットフォームの都合に従う。
- エージェント (Claude Code) と並走する開発。AGENTS.md / CLAUDE.md にルールを集約して、人間とエージェントが同じ前提で動く。
この 4 つを軸に、レイヤごとの選定を見ていく。
フレームワーク: Next.js 16 (App Router) + React 19
求めた条件:
- Server Component と Server Actions を一級市民として書きたい (DB 操作の薄いラッパーを Express で書きたくない)
- ファイルシステムベースのルーティング
- Vercel との統合
- 静的生成と動的生成の混在
候補は Remix (React Router v7) と Next.js 14 (Pages Router) と Next.js 16 (App Router) と SvelteKit。SvelteKit は React のエコシステムを捨てる代償が大きすぎる。Remix は Server Action 相当 (action) は強いが、Vercel の cron や preview を素のまま使うなら Next.js のほうが摩擦が少ない。Pages Router は型システムと Server Actions の組み合わせが App Router ほど洗練されていない。
選んだのは Next.js 16 (App Router) + React 19。Server Action から revalidatePath を呼ぶだけで cache 無効化が完結し、use server で関数 1 つを RPC エンドポイント化できる。型は zod の safeParse で入力を絞り込めば、TS の絞り込みが server / client の境界を越えて効く。
落とし穴:
- 「これは知っている Next.js ではない」。AGENTS.md の冒頭に書いてあるように、API・規約・ファイル構成が訓練データの想定から外れている可能性が高い。実装前に
node_modules/next/dist/docs/を読む習慣をつけないと、エージェントが古い知識を引きずってgetServerSideProps相当を書こうとする。 error.tsx/not-found.tsx/global-error.tsxの三層構造 (別記事 Next.js 16 のエラーページ三層を別物として扱う)。一枚岩で書こうとすると詰む。- TS plugin
rules/client-boundary.tsの whitelist がresetと*Actionで止まっていて、unstable_retryを直接 typed すると 71007 誤警告が出る。interface RetryFn { (): void }で interface 経由にすれば回避できる。
認証: Auth.js v5 (next-auth) + Resend Email Provider + Prisma Adapter + DB セッション
求めた条件:
- Magic link が一級市民。F-AUTH-02 で「パスワード機能は持たない」と決めたので、magic link を bolt-on で雑に足せるサービスは選べない。
- Prisma Adapter があること。スキーマを Prisma で管理しているので、auth テーブルも同じ schema.prisma に乗せたい。
- Session を DB 側に持てる。JWT セッションだと invalidate が難しい (logout や user merge で session 即時失効が要件)。
- Server Component から
auth()だけで session を取れる。
候補は Clerk、Supabase Auth、自作 (lucia-auth)、Auth.js v5。
- Clerk は magic link を 1 行で足せて UI まで揃うが、user データを Clerk に置くため Prisma スキーマと外部キーで結べない。F-AUTH-04 の「同じメアドのアカウントは統合する」を実装するときに Clerk の API を経由する手数が増える。SaaS 課金も発生する。
- Supabase Auth は Postgres 上に乗るが、Supabase の RLS (Row Level Security) を使う前提で設計が引っ張られる。Prisma で書いた application logic と RLS の責務が二重化する。
- lucia-auth は薄くて好きだが、自分で adapter を書く時間を削減したい。
- Auth.js v5 (beta) は magic link が provider 1 つの設定で動き、Prisma Adapter が公式に存在し、
session: { strategy: "database" }で DB セッションになり、Server Component からauth()を呼ぶだけで session が取れる。
選んだのは Auth.js v5 + Resend Email Provider + Prisma Adapter + DB セッション。
落とし穴:
- v5 はまだ beta (
next-auth: 5.0.0-beta.31)。breaking change を覚悟して採用する必要がある。 - 結合テストで
next-authの ESM bundle がnext/serverを拡張子なしで import するため、Node の resolver がERR_MODULE_NOT_FOUNDを出す。vitest.config.tsのserver.deps.inline: ["next-auth", "@auth/core"]で回避する。 - 本番環境では cookie 名が
__Secure-authjs.session-token(https のとき自動で__Secure-prefix が付く)、dev ではauthjs.session-tokenという違いがある。E2E や手動テスト時に意識する。
DB / ORM: Postgres 16 + Prisma 6
求めた条件:
- 並行性の制約を atomic に書きたい (F-INV-08 招待の同時受諾)
- 型がスキーマから自動生成される
- migration を git で管理できる
- 本番でもローカルでも結合テストでも同じ DB を使いたい
候補は SQLite (with Turso / D1)、Postgres + Drizzle、Postgres + Prisma、PlanetScale (MySQL) + Drizzle。
- SQLite は単一プロセスとデータ規模の前提が違う。
SELECT FOR UPDATEのような明示ロックがなく、並行性の検証手段が貧弱。 - PlanetScale (MySQL) は外部キー制約を伝統的に避ける文化があり、F-INV-08 の atomic claim を「
updateManyの WHERE 条件で expressed する」設計と相性が悪い。 - Drizzle は SQL に近い書き味で速いが、関連 (
include) を書くときに毎回with句を組み立てる必要がある。1 人開発で MVP を出す段階では Prisma のincludeの書き心地のほうが先に進める。
選んだのは Postgres 16 + Prisma 6。本番は Neon、ローカルは Docker Compose、結合テストは Testcontainers の Postgres 16 イメージ。3 環境で 同じ DB エンジン が動くので「テストでは通るが本番で落ちる」が発生しない。
落とし穴:
- Prisma 6 で
postinstallフックが消えた。pnpm installだけでは usable な client が生成されない。プロジェクト側のpackage.jsonに"postinstall": "prisma generate"を書いて全環境 (local / CI / Vercel) で回るようにする必要がある。pnpm install --ignore-scriptsを使う場面 (一部 CI ジョブ) では明示的にpnpm exec prisma generateを呼ぶ。 - Prisma 7 への major bump はインフラ移行を伴う。driver adapter (
@prisma/adapter-pg)、directUrlのprisma.config.tsへの移動、new PrismaClient(...)の全 call site の書き換え、これだけ書き換えが要る。dependabot で自動 PR にすると判断負荷が高すぎるので、.github/dependabot.ymlで Prisma の major だけignoreにしている。
バリデーション: zod 4
求めた条件:
- Server Actions の入力を 1 行で型ごと検証したい
- 結合テストで境界値検証 (本文 1〜5,000 文字) を書きやすい
- form state と Server Action の入力検証が二重化しない
候補は zod、valibot、yup、ajv。
- valibot は tree-shake が効いてバンドルが小さい。新規 SPA で起動速度を厳密に詰めるなら強い。kokan-nikki は SSR 中心で初回 JS を切り詰める優先度が低いので、利点が活きない。
- yup / ajv は型推論の表現力で zod に劣る。
選んだのは zod 4。schema.safeParse(formData) で TS の絞り込みが効き、エラー側は result.error.issues で field エラーを列挙できる。@conform-to/zod を挟めば form の field レベルでも同じスキーマが使える。
落とし穴:
- zod 4 の
.string()は v3 から API がいくつか変わっている (.email()の deprecation など)。dependabot の minor bump で警告が増えるので、リリースノートを軽く読んでから merge する。
スタイル: Tailwind CSS v4 + デザインハンドオフ
求めた条件:
- デザイントークンをコードに落としたい (kokan-nikki は "Dreamy Pixel" というハンドオフがある)
prefers-reduced-motion連動を後付けで切り替えたい (NF-A11Y-01)- スコープ汚染を避ける (Server Component と Client Component が混在するので、grobals.css 1 枚で足を引っ張られたくない)
候補は CSS Modules、vanilla-extract、Tailwind CSS、Panda CSS、Stitches (deprecated)。
- CSS Modules は素直だが、デザイントークンを type-safe に共有する仕組みは別途用意する必要がある。
- vanilla-extract は型安全で最高だが、build 設定の追加コストと、エージェント (Claude Code) が CSS-in-TS の構文に詳しくない場面がある。
- Panda CSS は方向性は良いが、エコシステムが Tailwind に比べて薄い。
選んだのは Tailwind CSS v4 + @tailwindcss/postcss + prettier-plugin-tailwindcss。Tailwind v4 は config を CSS の @theme で書くスタイルになり、デザイントークンを CSS variables で持てる。reduce-motion クラス連動は data- 属性 + arbitrary variant で書ける。prettier-plugin-tailwindcss で className の utility 順序を自動整列するので、エージェントとの差分レビューが楽になる。
落とし穴:
flex-colの親で子のmax-widthが効かない罠。これはハンドオフ実装中に踏んだ別記事 flex-col 親で子の max-width が効かなかったので width: 100% を当てた を参照。- Tailwind v4 のドキュメントは v3 から大きく変わっているので、エージェントが v3 の知識で書こうとする場面があり、
@themeブロックで補正する。
メール: Resend
求めた条件:
- magic link 送信が確実 (deliverability)
- API が薄い (
emails.send1 関数で済む) - DKIM / SPF / DMARC が運用できる
- テスト中に絶対に本番 API を叩かない仕組みが組める
候補は SendGrid、AWS SES、Resend、Postmark。
- SendGrid は古典で安定だが、API の冗長さと UI の重さがある。
- AWS SES は安いが、bounce / complaint の自前管理が要求される (SNS topic を購読して suppression list を作る) ので、1 人開発の運用負荷が増える。
- Postmark は transactional に強いが、価格モデルが Resend より高い。
選んだのは Resend。理由は resend.emails.send({ from, to, subject, react }) 1 行で React コンポーネントから email が送れる体験が速い。SPF / DKIM の DNS 設定は Resend の管理画面が誘導する。kokannikki.app の DMARC は Porkbun に DNS を置きつつ、p=quarantine; pct=25 まで段階的に締める計画を残してある。
落とし穴:
- テスト中の事故防止。
tests/全体でRESEND_API_KEYを空にして、lib/mailer.tsの dev fallback (tmp/dev-mailbox に書き出す) に倒す。E2E では MSW を Node 側で立ててhttps://api.resend.com/emailsを 200 で握り、呼び出し履歴を assert に使う。「実 API を叩く CI を作らない」がアカウント停止と料金事故の両方を避ける。
ファイルアップロード: @vercel/blob
ユーザーが画像を貼る要件 (将来) のためだけに先回りして導入。S3 + presigned URL 自前実装の代わり。Vercel に乗っているなら、Edge Function からアップロード token を発行する API を 1 ファイルで書ける。
別 hosting に移す可能性が高いプロジェクトでは選ばないが、kokan-nikki は Vercel で完結させる前提なので採用。
テスト: Vitest 4 + Testcontainers + Playwright + axe-core
ここは別記事 kokan-nikki の CI/CD とテスト運用を解説する で詳しく書いた。要点だけ:
- Vitest 4 で unit と integration の
projectsを分離。ESM ネイティブで Next.js との相性が良い。Jest を選ばないのは ESM の取り回し。 @testcontainers/postgresqlで本番と同じ Postgres 16 を結合テストごとに起動。SQLite で代替しない。- Playwright は Chromium 1 ブラウザに絞る (MVP)。
webServerではなくglobal-setup.tsで Postgres + migrate + build + next start を直列に立ち上げる (環境変数 race を避ける割り切り)。 @axe-core/playwrightで E2E のシナリオ末尾に axe を流して a11y を見る。serious 以上を fail に。- Playwright MCP は CI に載せない。人または Claude が対話的に dev サーバを操作する別レイヤとして制度化する。
候補から落としたもの: Cypress (Playwright のほうが multi-tab / multi-context が素直)、Jest (ESM の対応が後手)、Storybook (コンポーネント数が少なく、E2E と axe のほうが情報量が多い)。
静的解析: ESLint 9 + Prettier 3 + TypeScript 6
ESLint 9 は flat config (eslint.config.mjs) に揃え、eslint-config-next を base にしてプロジェクト固有のルールを足す。eslint-config-prettier を最後に並べて formatting 関連のルールを潰す。
Prettier 3 と prettier-plugin-tailwindcss で utility 順序まで含めた整形が走る。CI の lint job で pnpm lint (eslint) と pnpm typecheck (tsc --noEmit) を直列に流し、red のときは feature branch に追加 commit を打つ。
候補から落としたもの: Biome (速いが eslint-config-next の責務との重複が大きい)、dprint (採用してもいいがエコシステムの薄さ)。
デプロイ: Vercel + Neon (本番) / Docker Compose (ローカル)
求めた条件:
- preview 環境が PR ごとに自動で立つ
- cron が宣言的に書ける
- DB が低コストでスケールする
- ローカルが Docker Compose 1 行で起動する
選んだのは Vercel (アプリ + cron + preview) + Neon (本番 Postgres)。
- Vercel Preview は PR ごとに別 URL で立ち、エージェントが MCP で実機検証するときに
https://kokan-nikki-git-<branch>-...vercel.appを踏んで確認できる。 - Neon は Postgres branching が独立した DB として preview と本番を分けやすい (今は MVP なので 1 DB だが、将来 preview ごとに branch を切る運用に伸ばせる)。
- ローカルは
docker/docker-compose.yml1 ファイルで Postgres を立てる。pnpm db:up1 行。
候補から落としたもの: Fly.io (region が世界中に散る利点は kokan-nikki に過剰)、Railway (preview 環境の自動化が Vercel に劣る)、Render (Vercel と決定的な差はない)。
Cron: Vercel Crons
vercel.json の "crons" 配列に { "path": "/api/cron/nudge", "schedule": "0 0 * * *" } を書くだけ。エンドポイント側は Authorization: Bearer ${CRON_SECRET} を要求するように作って、未認証なら 401 を返す (NF-SEC-02)。
候補は Upstash QStash、GitHub Actions の schedule:、ホスト側 cron。Upstash QStash は強力だが kokan-nikki の cron は 1 本だけなので、Vercel に寄せる方が安い。GitHub Actions の schedule は遅延が大きい (5〜15 分) ので magic link や日次リマインダー用途には向かない。
エージェント協業: AGENTS.md / CLAUDE.md
これは「ツール」というよりは規約だが、ツールスタックの一部として扱う価値がある。
AGENTS.md (= CLAUDE.md で取り込まれる) に「これは知っている Next.js ではない」「実装前に node_modules/next/dist/docs/ を読む」「Prisma 6 の postinstall フック消失」などの prior knowledge を集約する。エージェント (Claude Code) は session ごとに training data の古い知識を持ち込むので、project-local な事実を 1 ファイルに集めておくと、毎セッション説明する手間が消える。
docs/testing.md を別ファイルに切ったのも同じ思想で、テスト戦略を「暗黙知」にしておくとセッションごとに迷子になる。
採用しなかった主な選択肢
- tRPC: Server Actions で十分。RPC レイヤを別途立てる利点が出るのは複数 client (mobile + web) が同じ API を共有するときで、kokan-nikki は web 1 本。
- Storybook: コンポーネント数が少なく、E2E + axe + Playwright MCP の組み合わせのほうが情報量が多い。
- Sentry: MVP では Vercel の log と Auth.js v5 の
error.digestだけで足りる。production verification を MCP で踏むので、observability の必要量がまだ低い。1.0 以降で再評価する。 - i18n (next-intl): F-i18n は要件外 (日本語ユーザー対象)。
- PWA / Service Worker: 要件にオフライン対応がなく、複雑度に対する利得が小さい。
このスタックが向くケース・向かないケース
向くケース:
- 1 〜 数人 の開発チーム
- Postgres で完結する relational data 中心のアプリ
- Vercel に乗せて preview / cron まで使い倒す前提
- エージェント (Claude Code) との並走 を前提にして、ルールを project-local に集約できる
- Magic link 認証 が許される (B2B 法人ログインのような複雑な要件がない)
向かないケース:
- 多テナント B2B (Clerk + Stripe + RBAC のほうが速い)
- 超低レイテンシ要件 (Edge runtime での Prisma は制約がある、Drizzle + neon-http のほうが向く)
- mobile / desktop の native app が同居する (Expo + tRPC のほうが共有しやすい)
- Vercel から離れる可能性 が高いプロジェクト (Vercel Crons / Vercel Blob / Vercel Preview を使うほど移行コストが上がる)
ツール選定で意識した 3 つの視点
スタックを決める作業を振り返って、選定時に意識していた共通の軸が 3 つあった。
- 判断負荷が低い側に倒す。1 人開発なので、reviewer = 自分。「迷ったときに正解が出やすい」ツールを選ぶと、無駄な再考が減る。Tailwind の utility 順序自動整列、Prisma の
includeの書きやすさ、Next.js の Server Actions、これらは判断を消すので速い。 - 本番と同じものをローカルで動かす。Postgres、Vercel Preview、Resend MSW、これらは「本番でだけ落ちる」を消す。1 人で書いている以上、本番でだけ起きる事象に気づける目が少ないので、環境差を生む選択は最初に潰す。
- エージェントと共有できる前提を文書化する。AGENTS.md、
docs/testing.md、docs/blog-pipeline.md、これらは「Claude Code とペアで作るときの暗黙知」を明示知に格上げする。1 人 + エージェントの開発体験は、ルールを文章化した量に比例して安定する。
これらは抽象的な原則ではなく、各層の選定で具体的に効いた。次の Web アプリを作るとき、まずこの 3 つの視点で要件を眺めると、ツール選定の半分くらいは自動的に決まる。
参考リンク
- kokan-nikki (public)
- kokan-nikki の CI/CD とテスト運用を解説する: テスト戦略と CI 5 jobs の詳細
- Next.js 16 のエラーページ三層を別物として扱う
- dependabot 11 本の triage で踏んだ 6 つの罠