SALON BOARD クローン — ディレクトリ構成(モノレポ)
SALON BOARD クローン — ディレクトリ構成(モノレポ)
Section titled “SALON BOARD クローン — ディレクトリ構成(モノレポ)”- モノレポ:Bun workspaces(タスクランナーは入れない。Bun
--filterと最小限のシェルスクリプトで運用) - ランタイム・パッケージマネージャ・テストランナーをBunに統一
- turbo/Nx/moon等は初期採用しない:パッケージ数・CI規模が育ってから必要に応じて導入判断
- 開発も本番も Docker Compose 経由で実行:ホストにBun/Nodeを入れない
- 常駐起動:
docker compose up - 単発コマンド:
docker compose run --rm api <cmd>
- 常駐起動:
- サーバーもクライアントもTypeScript単一言語にする(型・zodスキーマ共有の最大化)
- apps/ は実行単位、packages/ は共有コード
- 依存方向は一方向:apps → packages、packages同士は依存グラフを閉じる
- DDDの軽量版:ドメイン層を
packages/domainに純粋関数として切る(IO禁止) - RLSとマルチテナント規律を最初から強制:
- Phase 1最初のマイグレーションから全テナント対象テーブルでRLS有効
- DB接続を2本に分離(
postgressuperuser=マイグレーション用、app=アプリ接続・RLS適用対象) - リポジトリIFが
storeIdを必須引数に取る
1. トップレベル構成
Section titled “1. トップレベル構成”salon-clone/├── apps/│ ├── api/ # Hono APIサーバー│ ├── worker/ # graphile-worker プロセス│ ├── web/ # オペレーター向けSPA (Vite + React)│ └── liff/ # 顧客向けLIFFアプリ (Phase 2+)│├── packages/│ ├── db/ # Drizzleスキーマ + マイグレーション│ ├── domain/ # ドメインロジック(純粋TS、IO禁止)│ ├── contracts/ # zodスキーマ / OpenAPI型(FE/BE共有)│ ├── jobs/ # graphile-workerタスク定義(api/worker 両用)│ ├── auth/ # better-auth設定とプラグイン│ ├── ui/ # 共有Reactコンポーネント(shadcn primitives)│ └── config/ # 共有 tsconfig / biome 設定│├── infra/│ ├── docker/│ │ ├── Dockerfile # 本番用(multi-stage、oven/bun:1-slim)│ │ └── Dockerfile.dev # 開発用(bind mount前提、hot reload)│ └── sql/│ ├── bootstrap/ # DB初期化(app ユーザー作成、拡張有効化)│ │ └── 001_roles_and_extensions.sql│ └── policies/ # Drizzleから呼ぶRLSポリシー定義├── docker-compose.yml # 開発用(ルート直下)├── docker-compose.prod.yml # 本番用│├── docs/│ ├── SRS/│ │ ├── parent.md # 親SRS│ │ └── features/ # 子SRS置き場(実装前に都度書く)│ ├── ADR/ # アーキテクチャ判断記録│ ├── ERD.md│ └── SETUP.md│├── .env.example├── .gitignore├── biome.json # lint/format(ESLint + Prettier代替)├── bunfig.toml # Bun設定(install挙動、テスト設定)├── package.json # ルート。workspaces定義もここ├── tsconfig.base.json└── README.mdルート package.json の workspaces 指定:
{ "name": "salon-clone", "private": true, "workspaces": ["apps/*", "packages/*"]}2. apps 各パッケージ
Section titled “2. apps 各パッケージ”apps/api — Hono APIサーバー
Section titled “apps/api — Hono APIサーバー”apps/api/├── src/│ ├── index.ts # エントリ(Bun.serve ベース)│ ├── app.ts # Honoインスタンス組み立て│ ├── routes/│ │ ├── admin/ # /api/admin/* — オペレーター向け│ │ │ ├── reservation.ts│ │ │ ├── customer.ts│ │ │ ├── register.ts│ │ │ └── ...│ │ ├── portal/ # /api/portal/* — 顧客(LIFF)向け│ │ │ ├── booking.ts│ │ │ └── ...│ │ ├── mcp/ # /mcp — MCP over OAuth2.1│ │ │ ├── oauth.ts # /.well-known, /authorize, /token, /register│ │ │ └── server.ts # MCPサーバー本体│ │ └── webhooks/ # Stripe / LINE / HPB連携│ ├── middleware/│ │ ├── auth.ts # better-auth連携│ │ ├── tenant.ts # storeId解決 + SET app.current_store_id│ │ ├── rate-limit.ts│ │ └── audit.ts # operator_action_log 自動記録│ ├── openapi.ts # zod-openapi からOAS生成│ └── config.ts # 環境変数ロード(zod検証)├── test/├── tsconfig.json└── package.json責務:HTTPハンドラ層。ビジネスロジックは packages/domain、永続化は packages/db、ジョブ投入は packages/jobs に委譲。
apps/worker — graphile-worker
Section titled “apps/worker — graphile-worker”apps/worker/├── src/│ ├── index.ts # graphile-worker run() 起動│ └── health.ts # 小さなHTTPサーバーで /healthz 提供├── crontab # graphile-worker組み込みcron定義├── tsconfig.json└── package.json責務:タスク実行のみ。タスク定義自体は packages/jobs にある(api側が型安全にenqueueできるように)。
apps/web — オペレーターSPA
Section titled “apps/web — オペレーターSPA”apps/web/├── src/│ ├── main.tsx│ ├── App.tsx│ ├── router.tsx # TanStack Router or React Router│ ├── features/ # feature-first(画面単位ではなく機能単位)│ │ ├── reservation/│ │ │ ├── calendar/│ │ │ ├── detail/│ │ │ └── hooks/│ │ ├── customer/│ │ ├── register/│ │ └── settings/│ ├── components/ # 画面横断の純粋UI(feature非依存)│ ├── lib/│ │ ├── api.ts # openapi-fetch で生成型(paths)を喰わせた型安全クライアント│ │ └── auth.ts│ ├── styles/│ └── pwa/ # manifest + service worker├── public/├── vite.config.ts├── tsconfig.json└── package.json責務:オペレーター業務UI。packages/contracts/src/generated/schema.ts の paths 型を openapi-fetch に渡して型安全クライアントを作る。OpenAPIを単一契約ソースとし、サーバ変更が型エラーでFEに伝播する。
apps/liff — 顧客LIFF(Phase 2以降)
Section titled “apps/liff — 顧客LIFF(Phase 2以降)”apps/liff/├── src/│ ├── main.tsx│ ├── features/│ │ ├── booking/ # 空き枠・予約│ │ ├── history/ # 来店履歴│ │ └── profile/ # 会員情報│ └── lib/│ ├── liff.ts # LIFF SDK初期化│ └── api.ts└── ...3. packages 各パッケージ
Section titled “3. packages 各パッケージ”packages/db — Drizzle
Section titled “packages/db — Drizzle”packages/db/├── src/│ ├── client.ts # drizzle() ファクトリ(RLSセッション付き)│ ├── schema/│ │ ├── tenancy.ts # store, operator, session, passkey, totp│ │ ├── master.ts # staff, equipment, menu, business_hours│ │ ├── customer.ts # customer, note, tag│ │ ├── reservation.ts # reservation, reservation_menu, event, visit│ │ ├── register.ts # closing, cash_movement│ │ ├── messaging.ts # message, auto_message_rule, coupon│ │ ├── smartpay.ts # txn, point_ledger, settlement│ │ ├── listing.ts # HPB掲載系│ │ └── index.ts # 全 re-export│ └── repositories/ # 任意:よく使うクエリをまとめる├── drizzle.config.ts├── migrations/ # drizzle-kit generate の出力├── seed/│ └── dev.ts # 開発用シード├── tsconfig.json└── package.json規約
- 全テーブルに
store_id列 + 複合FK - Drizzleでは型安全のためリポジトリは必ず storeId を引数に取る
- RLS有効化とポリシー定義は
infra/sql/の手書きSQL(Drizzleマイグレーションでimport)
packages/domain — ドメインロジック
Section titled “packages/domain — ドメインロジック”packages/domain/├── src/│ ├── reservation/│ │ ├── state-machine.ts # 11状態遷移表 + ガード関数│ │ ├── conflict.ts # 二重予約判定(純粋関数)│ │ ├── pricing.ts # メニュー合計/割引/キャンセル料算定│ │ └── types.ts│ ├── customer/│ ├── register/│ └── shared/│ ├── money.ts # JPY整数、税抜/税込、端数処理│ ├── id.ts # uuidv7() ラッパ、型(Branded type: CustomerId, ReservationId等)│ └── time.ts # Asia/Tokyoのタイムゾーン扱い├── test/└── package.json鉄則:IO禁止。DB/HTTP/時刻取得すら引数で受け取る。テストはピュアユニット。
packages/contracts — API契約
Section titled “packages/contracts — API契約”packages/contracts/├── src/│ ├── admin/ # オペレーター向けAPI zodスキーマ(apps/api でimport)│ │ ├── reservation.ts│ │ └── ...│ ├── portal/ # 顧客向けAPI│ ├── mcp/ # MCPツール入出力スキーマ│ ├── shared/ # Paginated<T> など共通型│ ├── generated/ # ★ codegen出力(gitignore)│ │ └── schema.ts # openapi-typescript が出力する paths 型│ └── index.ts # re-export├── openapi.json # ★ apps/api の openapi:write が出力(gitignore)└── package.json使い道
apps/api:zodスキーマで zod-openapi ルート定義apps/web/apps/liff:generated/schema.tsのpaths型を openapi-fetch に喰わせてクライアント生成packages/jobs:ペイロード型定義- zodを起点にOpenAPIを出力 → そこからFE型生成する一方向パイプライン
packages/jobs — 非同期タスク定義
Section titled “packages/jobs — 非同期タスク定義”packages/jobs/├── src/│ ├── tasks/│ │ ├── send-reservation-reminder.ts│ │ ├── auto-cancel-expired-reservation.ts│ │ ├── charge-cancel-fee-smartpay.ts│ │ ├── send-message-batch.ts│ │ ├── aggregate-daily-sales.ts│ │ ├── generate-csv-export.ts│ │ └── resize-image.ts│ ├── enqueue.ts # 型安全 enqueue ヘルパー│ ├── handler-list.ts # worker 向け全ハンドラ束ね│ └── types.ts # タスク名↔ペイロード型マップ└── package.jsonパターン:各タスクファイルで name、Payload 型、handler をexport。apps/api は enqueue(name, payload) を型推論付きで呼ぶ。apps/worker は handler-list をまるごとregister。
packages/auth — 認証
Section titled “packages/auth — 認証”packages/auth/├── src/│ ├── index.ts # better-auth インスタンス構築│ ├── plugins/│ │ ├── passkey.ts│ │ ├── totp.ts│ │ ├── oauth-server.ts # MCP用のOAuth2.1 AS│ │ └── operator.ts # 独自:オペレーター×店舗の多対多│ └── types.ts└── package.jsonpackages/ui — 共有UI
Section titled “packages/ui — 共有UI”packages/ui/├── src/│ ├── primitives/ # shadcnベース│ ├── patterns/ # 複数primitive組み合わせ│ └── tailwind-preset.ts # apps/web, apps/liff で共用└── package.json注意:shadcnは”コピペで持つ”思想なので、厳密にはapps側に置く派閥もある。共有したいのは Button Dialog 等の薄い層に限定し、ドメイン色ついたUIはapp側に残す。
packages/config — 設定プリセット
Section titled “packages/config — 設定プリセット”packages/config/├── tsconfig/│ ├── base.json│ ├── node.json│ └── react.json├── biome.json└── package.json4. 依存グラフ(守る)
Section titled “4. 依存グラフ(守る)”apps/api → packages/{db, domain, contracts, jobs, auth}apps/worker → packages/{db, domain, jobs}apps/web → packages/{contracts, ui} + apps/api (型のみ)apps/liff → packages/{contracts, ui} + apps/api (型のみ)
packages/db → packages/{domain (型のみ), config}packages/domain → packages/config のみpackages/contracts→ packages/config のみpackages/jobs → packages/{contracts, domain, db}packages/auth → packages/{db, config}packages/ui → packages/config- packages/domain は何にも依存しない(Drizzleも知らない)
- packages/db は domain を知るが逆は不可
- circular import は biome + eslint-plugin-import 相当 or
madge --circularをCIで検出
5. タスク実行(Bun直、タスクランナーなし)
Section titled “5. タスク実行(Bun直、タスクランナーなし)”実行は全てdocker compose経由
# 常駐起動(api + worker + web + db)docker compose up
# 単発コマンド(以下、全部 docker compose run --rm api <cmd> で実行)bun run build # 全パッケージ+全アプリを順次ビルドbun run typecheck # 全型チェックbun run test # Vitestbun run db:generate # drizzle-kit generatebun run db:migrate # drizzle-kit migrate + RLSポリシー適用bun run codegen # OpenAPI → openapi-typescriptワークスペース絞り込み
docker compose run --rm api bun --filter api devdocker compose run --rm api bun --filter './packages/*' buildpackage.json scripts(ルート、Bun直)
{ "scripts": { "dev": "bun --filter '*' dev", "build": "bun --filter './packages/*' build && bun --filter './apps/*' build", "typecheck": "bun --filter '*' typecheck", "test": "bun --filter '*' test", "lint": "bunx biome check .", "format": "bunx biome format --write .", "db:bootstrap":"bun scripts/db-bootstrap.ts", "db:generate": "bun --filter db db:generate", "db:migrate": "bun --filter db db:migrate", "db:seed": "bun --filter db db:seed", "db:reset": "bun scripts/db-reset.ts", "codegen": "bun --filter api openapi:write && bunx -y openapi-typescript packages/contracts/openapi.json -o packages/contracts/src/generated/schema.ts", "predev": "bun run codegen", "prebuild": "bun run codegen" }}採否の判断基準(将来turbo/moon等を入れるとき)
- CIのbuild/testが2分以上かかるようになった
- パッケージ数が15超えた
- 複数人開発でリモートキャッシュ共有が効く体制になった
このいずれかが来たら採用検討、それまではBun直でいい。
6. 環境変数
Section titled “6. 環境変数”.env.exampleをルートに置く- 各appは
packages/configに共通zodスキーマ定義 → 各appがconfig.tsでparse - 本番はdocker-compose or systemd envで注入、開発は
.env.local(gitignore)
7. CI/CD 配置予定
Section titled “7. CI/CD 配置予定”.github/└── workflows/ ├── ci.yml # lint + typecheck + test(GitHub Actions キャッシュで node_modules 保持) ├── build.yml # Dockerイメージビルド └── deploy.yml # MiniPCへのデプロイ(SSH or Tailscale)8. 命名・ファイル配置ルール(短く)
Section titled “8. 命名・ファイル配置ルール(短く)”- ファイル名 kebab-case、コンポーネントだけ PascalCase
- テストは
*.test.ts同居(feature-firstなので) index.tsは re-export専用、ロジック書かない- 1ファイル1責務、300行超えたら分割検討
any禁止、unknown+ narrowenum使わない(const union 型で統一)
8.5 docker-compose.yml(開発用、想定形)
Section titled “8.5 docker-compose.yml(開発用、想定形)”services: db: image: postgres:16-alpine environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: salon_dev ports: ["5432:5432"] volumes: - pgdata:/var/lib/postgresql/data - ./infra/sql/bootstrap:/docker-entrypoint-initdb.d:ro healthcheck: test: ["CMD", "pg_isready", "-U", "postgres"] interval: 5s
api: build: context: . dockerfile: infra/docker/Dockerfile.dev command: bun --hot apps/api/src/index.ts working_dir: /work volumes: - .:/work - node_modules:/work/node_modules # named volumeで速度確保 ports: ["3000:3000"] env_file: [.env.local] depends_on: db: condition: service_healthy
worker: build: context: . dockerfile: infra/docker/Dockerfile.dev command: bun apps/worker/src/index.ts working_dir: /work volumes: - .:/work - node_modules:/work/node_modules env_file: [.env.local] depends_on: db: condition: service_healthy
web: build: context: . dockerfile: infra/docker/Dockerfile.dev command: bun --filter web dev working_dir: /work volumes: - .:/work - node_modules:/work/node_modules ports: ["3001:3001"] env_file: [.env.local] depends_on: [api]
volumes: pgdata: node_modules:Dockerfile.dev(想定形)
FROM oven/bun:1-slimRUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl \ && rm -rf /var/lib/apt/lists/*WORKDIR /work# 依存は docker compose run --rm api bun install で入れる(bind mountが支配的)infra/sql/bootstrap/001_roles_and_extensions.sql(想定形)
-- 拡張CREATE EXTENSION IF NOT EXISTS pgcrypto;CREATE EXTENSION IF NOT EXISTS btree_gist; -- 時間範囲排他で使用
-- アプリ用非superuserロール(RLS適用対象)CREATE ROLE app LOGIN PASSWORD 'app';GRANT CONNECT ON DATABASE salon_dev TO app;GRANT USAGE ON SCHEMA public TO app;
-- 以降のテーブル権限はマイグレーションで付与するALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app;ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO app;9. 後で追加する候補
Section titled “9. 後で追加する候補”apps/admin:社内管理ツール(Phase 3以降)apps/docs:公開ドキュメント(VitePress or Astro)packages/analytics:集計用クエリ(materialized view生成も含む)packages/telemetry:OpenTelemetryラッパー
決定一覧(ADRにも起こす)
Section titled “決定一覧(ADRにも起こす)”| 項目 | 決定 | 理由 |
|---|---|---|
| ランタイム | Bun 1.1+ | 起動・I/O・TS実行が速い、Node互換、DX良 |
| パッケージマネージャ | Bun(bun install) | ランタイムと統合、ワークスペース対応、高速 |
| タスクランナー | 採用しない(Bun直) | 現時点の規模では旨味が薄い。必要になれば turbo/moon 導入 |
| Linter/Formatter | Biome | 速い・単一ツール・configが少ない |
| テスト | Vitest | Vite統合、高速、Jest互換API(Bun上で動作) |
| 型共有方式 | OpenAPI → openapi-typescript + openapi-fetch(Docker codegen) | 契約を単一ソース化、FE実装がサーバ変更に追従しやすい |
| UIライブラリ | shadcn/ui + Tailwind | 依存最小、カスタム容易 |
| Router (web) | TanStack Router | 型安全、データロード統合 |
Bun採用時の注意点(最小限)
- Phase 1冒頭で
graphile-workerの smoke test(addJob→ LISTEN/NOTIFY即時ピックアップ)をCIに入れる - APM/Tracingを入れるときはOTel手動計装推奨(自動計装はNode前提のもの多い)
- 本番Docker baseは
oven/bun:1-slim
型共有パイプライン
Section titled “型共有パイプライン”apps/api (zod-openapi) └─ openapi:write スクリプトで packages/contracts/openapi.json を出力 └─ scripts/codegen.sh が Docker経由で openapi-typescript を実行 └─ packages/contracts/src/generated/schema.ts を生成 └─ apps/web / apps/liff が openapi-fetch + paths 型で利用- 生成物
packages/contracts/src/generated/は gitignore predev/prebuildhookで自動再生成(手動忘れ防止)- ランタイムクライアントは
openapi-fetch(約1KB、薄い) - ReactQuery hook自動生成が欲しくなったら
OrvalorKubbに差し替え可
以上。ディレクトリ掘る段階で迷わない粒度まで決めてあります。