コンテンツにスキップ

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本に分離(postgres superuser=マイグレーション用、app=アプリ接続・RLS適用対象)
    • リポジトリIFが storeId を必須引数に取る

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/*"]
}

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/
├── src/
│ ├── index.ts # graphile-worker run() 起動
│ └── health.ts # 小さなHTTPサーバーで /healthz 提供
├── crontab # graphile-worker組み込みcron定義
├── tsconfig.json
└── package.json

責務:タスク実行のみ。タスク定義自体は packages/jobs にある(api側が型安全にenqueueできるように)。

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.tspaths 型を 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
└── ...

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/
├── 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/liffgenerated/schema.tspaths 型を 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

パターン:各タスクファイルで namePayload 型、handler をexport。apps/apienqueue(name, payload) を型推論付きで呼ぶ。apps/workerhandler-list をまるごとregister。

packages/auth/
├── src/
│ ├── index.ts # better-auth インスタンス構築
│ ├── plugins/
│ │ ├── passkey.ts
│ │ ├── totp.ts
│ │ ├── oauth-server.ts # MCP用のOAuth2.1 AS
│ │ └── operator.ts # 独自:オペレーター×店舗の多対多
│ └── types.ts
└── package.json
packages/ui/
├── src/
│ ├── primitives/ # shadcnベース
│ ├── patterns/ # 複数primitive組み合わせ
│ └── tailwind-preset.ts # apps/web, apps/liff で共用
└── package.json

注意:shadcnは”コピペで持つ”思想なので、厳密にはapps側に置く派閥もある。共有したいのは Button Dialog 等の薄い層に限定し、ドメイン色ついたUIはapp側に残す。

packages/config/
├── tsconfig/
│ ├── base.json
│ ├── node.json
│ └── react.json
├── biome.json
└── package.json

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 # Vitest
bun run db:generate # drizzle-kit generate
bun run db:migrate # drizzle-kit migrate + RLSポリシー適用
bun run codegen # OpenAPI → openapi-typescript

ワークスペース絞り込み

docker compose run --rm api bun --filter api dev
docker compose run --rm api bun --filter './packages/*' build

package.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直でいい。


  • .env.example をルートに置く
  • 各appは packages/config に共通zodスキーマ定義 → 各appが config.ts でparse
  • 本番はdocker-compose or systemd envで注入、開発は .env.local(gitignore)

.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.tsre-export専用、ロジック書かない
  • 1ファイル1責務、300行超えたら分割検討
  • any 禁止、unknown + narrow
  • enum 使わない(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-slim
RUN 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;

  • apps/admin:社内管理ツール(Phase 3以降)
  • apps/docs:公開ドキュメント(VitePress or Astro)
  • packages/analytics:集計用クエリ(materialized view生成も含む)
  • packages/telemetry:OpenTelemetryラッパー

項目決定理由
ランタイムBun 1.1+起動・I/O・TS実行が速い、Node互換、DX良
パッケージマネージャBun(bun install)ランタイムと統合、ワークスペース対応、高速
タスクランナー採用しない(Bun直)現時点の規模では旨味が薄い。必要になれば turbo/moon 導入
Linter/FormatterBiome速い・単一ツール・configが少ない
テストVitestVite統合、高速、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
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 / prebuild hookで自動再生成(手動忘れ防止)
  • ランタイムクライアントは openapi-fetch(約1KB、薄い)
  • ReactQuery hook自動生成が欲しくなったら Orval or Kubb に差し替え可

以上。ディレクトリ掘る段階で迷わない粒度まで決めてあります。