🏗️ アーキテクチャ

📅 最終更新: 2026-05-13
🔗 関連: design.md (index) / requirements.md / sitemap.md / conventions.md / operations.md

🛠️ 1. 技術スタック

レイヤ採用技術備考
言語TypeScript
フレームワークNext.js (App Router)React 19 / RSC / Server Actions 中心
UI ライブラリHeroUITailwind ベース、React Aria 製
スタイリングTailwind CSS
フォームReact Hook FormHeroUI の controlled コンポーネントと組む
バリデーションZodserver / client 両側で同一スキーマ
認証better-authEmail + Password のみ (Phase 1)、Open registration
ORMDrizzle ORM
DBPostgreSQL本番: Neon (Serverless) / 開発: ローカル Docker
デプロイVercel
Linter / FormatterBiome
テストVitest (Unit) + Playwright (E2E)
パッケージマネージャpnpm
CIGitHub Actions型チェック + Biome + Vitest
レート制限Upstash RatelimitVercel Middleware 経由

📱 1.1 PWA 方針

PWA 化はしない(純 Web として運用)。

🌐 1.2 ランタイム / Locale

📚 1.3 ライブラリの仕様確認方針

better-auth / HeroUI / Drizzle は更新が活発なため、実装着手前および主要バージョン更新時に context7 で最新仕様を取得して確認 すること。


🗄️ 2. データモデル(Phase 1)

🔗 2.1 ER 概要

flowchart TD U["👤 User
better-auth 管理"]:::auth --> B["☕ Bean"]:::bean B --> Br["🫖 Brew"]:::brew Br --> P["💧 Pour"]:::pour classDef auth fill:#f0f6fc,stroke:#1e5a8e,stroke-width:2px,color:#1a2533 classDef bean fill:#eef3f8,stroke:#6b9bc7,color:#1a2533 classDef brew fill:#eef3f8,stroke:#6b9bc7,color:#1a2533 classDef pour fill:#f7f9fc,stroke:#c4d0dc,color:#1a2533

📊 2.2 テーブル定義

👤 user

better-auth が管理(user / session / account / verification 等)。

☕ bean

カラム備考
idtext PKcuid2(24 文字、@paralleldrive/cuid2
user_idtext FK user(id)better-auth user.id は string
nametext
roastertext
roast_leveltextPhase 1 はフリーテキスト、Phase 2 でマスタ FK 化
roast_datedate
origintext同上
varietytext同上
processtext同上
notestext
photo_urltext (nullable)Phase 2 で使用、カラムは Phase 1 から確保
archivedboolean default false飲み終わった豆
created_at / updated_at / deleted_attimestamp

🫖 brew

カラム備考
idtext PKcuid2(24 文字、@paralleldrive/cuid2
user_idtext FK user(id)クエリ全部スコープのため冗長保持
bean_idtext FK bean(id)
brewed_attimestamp
dose_gnumeric(5,1)
total_water_gnumeric(6,1)
water_temp_cnumeric(4,1)
grind_settingtextミル名と目盛を自由記述
dripper_nametext
total_time_secondsinteger
overall_ratingsmallint (1-5)
flavor_notestext自由記述
memotext
created_at / updated_at / deleted_attimestamp

💧 pour

カラム備考
idtext PKcuid2(24 文字、@paralleldrive/cuid2
brew_idtext FK brew(id)ON DELETE CASCADE (Brew が soft delete されると論理的に消える)
sequencesmallint注ぎ順 (1, 2, 3, ...)
time_secondsinteger抽出開始からの経過秒
water_gnumeric(6,1)この注ぎで入れた湯量
notetext (nullable)

💡 Pour は Brew にネストされた値オブジェクトの位置付け。(brew_id, sequence) で UNIQUE。

🏢 2.3 マルチテナント方針


📁 3. ディレクトリ構造

🧩 3.1 設計方針: 軽量モジュラーモノリス

項目採用
アーキテクチャ軽量モジュラーモノリス(DDD レイヤード構造は採用しない)
動機(a) Modern なモジュール設計の学習 + (b) Phase 2 への備え
モジュールBean / Brew / Auth の 3 つ(Pour は Brew にネストされた値オブジェクトとして同居)
モジュール内部構造フラット型(種類ごとに 1 ファイル)— Phase 1 規模で十分、肥大化時に分割
public API各モジュールの index.ts (barrel) 経由のみ許可
境界強制Biome noRestrictedImports で機械的に強制(規約ではなくツール強制)

🌲 3.2 全体ツリー

coffee-note-develop/
├── src/
│   ├── app/                              # Next.js App Router(中間層: data fetching + 組み立て)
│   │   ├── (auth)/
│   │   │   ├── layout.tsx
│   │   │   ├── sign-in/page.tsx
│   │   │   └── sign-up/page.tsx
│   │   ├── (app)/
│   │   │   ├── layout.tsx                # 認証ガード + ナビ
│   │   │   ├── page.tsx                  # /            ホーム / ダッシュボード
│   │   │   ├── beans/
│   │   │   │   ├── page.tsx              # /beans       豆一覧
│   │   │   │   ├── new/page.tsx          # /beans/new   豆追加
│   │   │   │   └── [id]/
│   │   │   │       ├── page.tsx          # /beans/[id]  豆詳細 + brew 履歴
│   │   │   │       └── edit/page.tsx     # /beans/[id]/edit
│   │   │   └── brews/
│   │   │       ├── page.tsx              # /brews       抽出一覧(絞り込み + ページング)
│   │   │       ├── new/page.tsx          # /brews/new   タイマー画面(コアフロー)
│   │   │       └── [id]/
│   │   │           ├── page.tsx          # /brews/[id]  抽出詳細
│   │   │           └── edit/page.tsx     # /brews/[id]/edit
│   │   ├── api/auth/[...all]/route.ts    # better-auth ハンドラ(薄いラッパー)
│   │   ├── layout.tsx
│   │   ├── page.tsx
│   │   ├── loading.tsx
│   │   ├── error.tsx
│   │   └── not-found.tsx
│   ├── modules/
│   │   ├── auth/                         # better-auth 設定 + サインイン/アップ UI
│   │   │   ├── index.ts                  # public API (barrel)
│   │   │   ├── schema.ts                 # better-auth CLI 生成テーブル
│   │   │   ├── config.ts                 # better-auth サーバー設定
│   │   │   ├── client.ts                 # better-auth クライアント
│   │   │   ├── actions.ts
│   │   │   ├── queries.ts
│   │   │   ├── components/
│   │   │   └── *.test.ts(x)              # Unit テスト colocate
│   │   ├── bean/
│   │   │   ├── index.ts                  # public API
│   │   │   ├── schema.ts                 # bean テーブル定義
│   │   │   ├── validations.ts            # Zod
│   │   │   ├── types.ts
│   │   │   ├── actions.ts                # "use server"
│   │   │   ├── queries.ts                # RSC からの読み取り('server-only')
│   │   │   ├── components/
│   │   │   │   ├── bean-list.tsx
│   │   │   │   ├── bean-form.tsx
│   │   │   │   ├── bean-card.tsx
│   │   │   │   └── bean-select.tsx       # Brew 画面で使う Bean 選択 UI
│   │   │   └── *.test.ts(x)
│   │   └── brew/
│   │       ├── index.ts
│   │       ├── schema.ts                 # brew + pour 両テーブル
│   │       ├── validations.ts
│   │       ├── types.ts
│   │       ├── actions.ts
│   │       ├── queries.ts
│   │       ├── components/
│   │       │   ├── brew-list.tsx
│   │       │   ├── brew-form.tsx
│   │       │   ├── brew-timer.tsx
│   │       │   └── pour-timeline.tsx
│   │       └── *.test.ts(x)
│   └── shared/                           # 横断基盤
│       ├── db/
│       │   ├── client.ts                 # Drizzle client (Neon adapter)
│       │   ├── schema.ts                 # 各モジュール schema を re-export して drizzle-kit に渡す
│       │   └── relations.ts              # 横断 relations を一括定義(循環参照回避)
│       ├── ui/                           # HeroUI ラッパー、共通 UI
│       │   ├── button.tsx
│       │   ├── form-field.tsx            # React Hook Form + HeroUI controlled wrapper
│       │   ├── app-header.tsx
│       │   └── app-shell.tsx
│       ├── lib/                          # 純粋関数 utility
│       │   ├── date.ts                   # Asia/Tokyo 変換
│       │   ├── format.ts
│       │   ├── ratelimit.ts              # Upstash Ratelimit ラッパー
│       │   └── server-only.ts
│       └── config/
│           ├── env.ts                    # zod で検証する env loader
│           └── constants.ts
├── tests/e2e/                            # Playwright(E2E のみ集約)
│   ├── fixtures/
│   └── *.spec.ts
├── drizzle/                              # Drizzle migrations 出力先
├── public/                               # 静的アセット(Next.js 仕様でルート固定)
├── docs/
│   ├── design.md                         # index
│   ├── requirements.md
│   ├── sitemap.md
│   ├── architecture.md
│   ├── conventions.md
│   └── operations.md
├── .github/workflows/ci.yml              # 型チェック + Biome + Vitest
├── .editorconfig
├── .env.example                          # commit する(.env.local は gitignore)
├── .gitignore
├── .nvmrc                                # Node バージョン固定
├── biome.json                            # noRestrictedImports でモジュール境界強制
├── docker-compose.yml                    # 開発用 Postgres
├── drizzle.config.ts
├── next.config.ts
├── package.json
├── playwright.config.ts
├── pnpm-lock.yaml
├── README.md
├── tsconfig.json                         # paths: { "@/*": ["./src/*"] }
└── vitest.config.ts

🚦 3.3 モジュール間通信のルール

📦 public API (barrel)

各モジュールの index.ts が export するもの:

種類用途
TypesBean, BeanInput, BeanView他モジュール・app/ で型として使う
QueriesgetBeans, getBeanById, getBeansByIdsRSC から読み取り
Server ActionscreateBean, updateBean, archiveBeanapp/ の form action から呼ぶ
ComponentsBeanList, BeanForm, BeanCard, BeanSelectapp/・他モジュールで使用
ValidationscreateBeanSchema, updateBeanSchemaフォーム resolver、Server Action 検証

index.ts に export しないもの(internal):

🔑 アクセスルール

アクセス先判定
自モジュール内テーブルへの SELECT/INSERT/UPDATEOK
他モジュールテーブルへの relations 経由の JOINOK 読み取りのみ
他モジュールテーブルへの直接 INSERT/UPDATE/DELETENG 必ず public action 経由
他モジュールテーブル名の直接 importNG relations が知っているのは Drizzle 内部のみ
他モジュールの type / public query 関数OK barrel 経由
他モジュールの UI componentOK barrel 経由
shared/modules/ の逆依存NG

➡️ 依存方向

flowchart LR Auth["🔐 auth
独立"]:::indep Bean["☕ bean"]:::bean Brew["🫖 brew"]:::brew Brew -->|"✅ OK"| Bean Bean -.->|"❌ NG (循環禁止)"| Brew classDef indep fill:#f0f6fc,stroke:#1e5a8e,stroke-width:2px classDef bean fill:#eef3f8,stroke:#6b9bc7 classDef brew fill:#eef3f8,stroke:#6b9bc7 linkStyle 0 stroke:#2f7a4a,stroke-width:2px linkStyle 1 stroke:#b13838,stroke-width:1.5px,stroke-dasharray: 5 5

🎯 3.4 app/ ディレクトリの責務

🧪 3.5 テスト配置

種類配置命名設定
Unit (Vitest)モジュール内 colocate*.test.ts(x)include: ['src/**/*.test.{ts,tsx}']
E2E (Playwright)tests/e2e/ に集約*.spec.tstestDir: './tests/e2e'

🗂️ 3.6 Drizzle スキーマ管理

🛡️ 3.7 モジュール境界の強制

Biome の noRestrictedImports で機械的に強制する。他モジュールへのアクセスは barrel(@/modules/<name> = index.ts)のみ許可、内部ファイルへの直接 import は CI でブロック。自モジュール内では barrel ではなく相対パス(./queries)を使うこと(循環参照リスク回避)。

🔌 3.8 Phase 2 への接続


🔐 4. 認証ガード構成

実装の詳細(requireSession() ヘルパ等のコード)は conventions.md に集約。ここでは責務分担のみ。

レイヤ責務
src/middleware.ts/api/auth/:path* への Upstash Ratelimit のみ。matcher も同パスに限定
src/app/(app)/layout.tsxauth.api.getSession() で session 本物検証 → 未認証は /sign-in へ redirect
Server Action冒頭で requireSession() を呼ぶ。直接 POST 経路の防御深度

設計上のキー判断:


🗺️ 5. Phase 2: マスタデータ化(origin / roast_level / variety / process)

💡 5.1 動機

入力時に選択肢として簡単に入力できるようにする(表記ゆれの抑制は副次効果)。

🧭 5.2 設計方針

🏗️ 5.3 マスタテーブル設計(4 つ、すべて同形)

CREATE TABLE origin_master (
  id                  TEXT PRIMARY KEY,                -- cuid2
  name                TEXT NOT NULL,
  name_ja             TEXT,
  display_order       INT  NOT NULL DEFAULT 0,
  created_by_user_id  TEXT REFERENCES "user"(id),      -- NULL = システム共通
  created_at          TIMESTAMP NOT NULL DEFAULT now(),
  updated_at          TIMESTAMP NOT NULL DEFAULT now(),
  deleted_at          TIMESTAMP,
  UNIQUE (name, created_by_user_id)
);
-- roast_level_master, variety_master, process_master も同形

👁️ 5.4 表示条件

WHERE deleted_at IS NULL
  AND (created_by_user_id IS NULL OR created_by_user_id = :current_user_id)

✏️ 5.5 Bean テーブル変更

ALTER TABLE bean
  ADD COLUMN origin_id       TEXT REFERENCES origin_master(id),
  ADD COLUMN roast_level_id  TEXT REFERENCES roast_level_master(id),
  ADD COLUMN variety_id      TEXT REFERENCES variety_master(id),
  ADD COLUMN process_id      TEXT REFERENCES process_master(id);
-- 既存 text カラムは移行完了後に DROP

🔄 5.6 マイグレーション手順

  1. マスタ 4 テーブル作成
  2. Seed 投入(created_by_user_id = NULL、システム共通プリセット)
  3. Bean に *_id カラム追加(nullable)
  4. 既存 Bean.{origin,roast_level,variety,process}_text の値を:
    • マスタと name 完全一致 → 対応する *_id に紐付け
    • 一致なし → 自分専用マスタとして INSERT

      created_by_user_id = bean.user_id でマスタに INSERT し、ID 取得後 Bean に紐付け。

  5. 旧 text カラムを DROP

⌨️ 5.7 入力 UI(Phase 2)

🌱 5.8 Seed プリセット(最低限の方針)