📐 DESIGN DOC & PRODUCT ROADMAP

設計書 & ロードマップ

Staff Manager のデータモデル・アーキテクチャ・5 フェーズ商品化ロードマップ。2026-05-09 礒貝CEO 決裁 により、自前 SaaS として AWS 上で企業向け展開する方針が確定済み。

📋 目次

0. 戦略転換と決定経緯 (2026-05-09 礒貝CEO 決裁)

当初は「LocalStorage プロトタイプ → Notion バックエンドに本番移行」というプランで進めていましたが、世界 78カ国マルチテナント展開・課金・SOC2 までを見据えた結果、自前 SaaS として AWS 上で育てる 方針に転換しました。

検討した 3 パターン

却下

B. Notion 一本化

UI を廃棄し、Notion DB に全移行。短期は楽だが、ブランド・課金・マルチテナントを Notion に握られる。

却下: SaaS として商品化できない。
却下

C. ハイブリッド

フロントは自前、Notion をバックエンド DB として併用。

却下: データ二重管理リスク + Notion API 制約に縛られる。
💡 この設計書は「採用案 A」を前提に、Phase 2 (Supabase) → Phase 4 (Aurora Serverless v2) まで一貫して使えるよう設計されています。 NoSQL は使わず、全フェーズで Postgres を採用するため、SQL・スキーマ・ORM が無傷でフェーズ間移行できます。

1. 5 フェーズ・ロードマップ

Phase期間内容ゴール
1. プロトタイプ完成 〜数週間 UI/UX・運用フロー確定 (LocalStorage のまま) 「これで運用できる」確信を礒貝CEOが持つ
2. バックエンド導入 1-2週間 Supabase (Postgres + Auth + Realtime + RLS) データ永続化、複数デバイス同期、$0/月
3. マルチテナント化 1-2週間 tenant_id + Postgres RLS で会社単位完全分離 他社にも貸せる SaaS 状態
4. AWS 移行 2-4週間 Aurora Serverless v2 + Cognito + Lambda + CDK 本番 SLA、数千社スケール
5. 商品化 継続 Stripe 課金 / SOC2 / オンボーディング / マーケサイト エンタープライズ正式販売開始

2. 確定アーキテクチャ

レイヤーPhase 2-3Phase 4 (本番)
Frontend静的 HTML/JS (現状) → React or Next.jsS3 + CloudFront
APISupabase Edge FunctionsAPI Gateway + Lambda (Node/TypeScript)
DBSupabase PostgresAurora Serverless v2 (Postgres 互換)
AuthSupabase Auth + Google federationAWS Cognito + Google federation
StorageSupabase StorageS3 + CloudFront
RealtimeSupabase RealtimeAppSync (または polling)
IaCPhase 4 で本格化AWS CDK (TypeScript)
CI/CDGitHub Actions → SupabaseGitHub Actions → AWS (OIDC)
💡 Postgres 一貫の理由: Supabase = Postgres、Aurora Serverless v2 も Postgres 互換 → SQL・スキーマ・ORM (Prisma or Drizzle) が無傷で Phase 2 → Phase 4 へ移行できる。

3. データモデル (6 テーブル + tenants)

マスター系 2 (employees / projects) + トランザクション系 4 (tasks / daily_reports / time_blocks / leave_requests)。全テーブルに tenant_id (UUID) を Phase 3 で追加し、Postgres Row Level Security で会社単位完全分離します。

TABLE 1 / 6
👥
employees (従業員)
カラム名用途・候補値
idUUID PK自動採番
tenant_idUUID FKtenants.id 参照(Phase 3 追加)
nameTEXT「礒貝 浩資」
kanaTEXT「いそがい ひろし」
roleTEXTCEO / CFO / CTO / COO / CMO / CSO / CHRO / 一般
deptTEXT経営 / 財務 / 技術 / 業務 / マーケ / 営業 / 人事
emailCITEXTxxx@ganecci.net 社用メール
gmailCITEXT個人 Gmail (Google ログインで両方マッチ)
joined_atDATE入社日
statusTEXTactive / leave / retired
colorTEXTアバター背景用 (10色)
role_levelSMALLINT1=社長(LV.MAX) / 2=管理者(LV.MIDDLE) / 3=一般(STANDARD)
cognito_subTEXTAWS Cognito 紐付け用 sub クレーム(Phase 4)
google_calendar_idTEXTカレンダー API 取得対象
created_at / updated_atTIMESTAMPTZ
GANECCI 初期 10 名: 礒貝浩資(CEO) / 岩瀬智子(CFO) / 梅村圭司(CTO) / 降田淨福(COO) / 橋本あん子(CMO) / 藤澤択弥(CSO) / 森茂薫(CHRO) / 田中美咲(デザイナー) / 山本大輔(エンジニア) / 佐藤結衣(営業AS)
TABLE 2 / 6
📂
projects (プロジェクト)
カラム名用途
idUUID PK
tenant_idUUID FKtenants.id 参照
nameTEXT「SightClean」「isoAi」「世界78カ国展開」など
statusTEXTidea / active / paused / done
lead_emp_idUUID FKemployees.id (プロジェクト責任者)
starts_onDATE開始日
due_onDATE期限
colorTEXTUI 上の色分け
created_at / updated_atTIMESTAMPTZ
メンバー(複数)は project_members(project_id, emp_id) 中間テーブルで管理(Phase 2 で追加)。
TABLE 3 / 6
tasks (タスク)
カラム名用途
idUUID PK
tenant_idUUID FKtenants.id 参照
titleTEXT
assignee_emp_idUUID FKemployees.id (担当者 1名)
project_idUUID FKprojects.id (1件)
priorityTEXThigh / mid / low
statusTEXTtodo / doing / done / blocked
due_startDATE期間タスクの開始日
due_atDATE期限
start_time / end_timeTIME時間範囲(任意)
effort_hNUMERIC(5,2)工数 (h)
progressSMALLINT0〜100 進捗率
parent_task_idUUID self-FK階層化用
related_report_idUUID FKdaily_reports.id
assigned_by_emp_idUUID FK委任元(自分以外なら依頼)
acknowledged_atTIMESTAMPTZ受領日時
created_at / updated_atTIMESTAMPTZ
TABLE 4 / 6
📝
daily_reports (日報)
カラム名用途
idUUID PK
tenant_idUUID FK
emp_idUUID FKemployees.id
report_dateDATE
done_todayTEXT今日やったこと
do_tomorrowTEXT明日やること
blockersTEXT課題・ブロッカー
moodSMALLINT 1-55=絶好調 / 4=良好 / 3=普通 / 2=やや不調 / 1=不調
submitted_atTIMESTAMPTZ
⚠️ UNIQUE 制約: (tenant_id, emp_id, report_date) の複合 UNIQUE で、同一テナント×同一従業員×同一日は 1 件のみ(DB レベルで担保)。
TABLE 5 / 6
time_blocks (タイムブロック)
カラム名用途
idUUID PK
tenant_idUUID FK
emp_idUUID FK
titleTEXT「朝会」「コーディング」など
block_dateDATE
starts_at / ends_atTIME開始/終了 時刻
descriptionTEXT
project_idUUID FK任意
kindTEXTwork / meeting / travel / break / focus
created_atTIMESTAMPTZ
💡 Google カレンダー連携: time_blocks は当アプリ内のタイムブロック管理用。同時に Google Calendar API で取得した個人予定を別キャッシュ gcal_event_cache(Phase 2)に保存して、「アプリで作ったブロック」と「Google で作った予定」を区別表示します。
TABLE 6 / 6
🏖
leave_requests (休暇申請)
カラム名用途
idUUID PK
tenant_idUUID FK
applicant_emp_idUUID FKemployees.id
starts_on / ends_onDATE単日 or 範囲
kindTEXTpaid / sick / special / condolence
reasonTEXT
statusTEXTpending / approved / rejected
approver_emp_idUUID FK承認者 (CHRO / CEO)
decided_atTIMESTAMPTZ承認/却下時刻
created_atTIMESTAMPTZ申請日時

4. マルチテナント設計 (Phase 3)

「他社にも貸せる」状態を Phase 3 で達成。tenants テーブル新設 + 全テーブルに tenant_id + Postgres Row Level Security で会社単位完全分離します。tenant_id を後付けで足すと地獄になるため、データモデルの初期段階から織り込み済み。

tenants テーブル(新設)

カラム名用途
idUUID PK自動採番
nameTEXT「株式会社GANECCI」など会社名
planTEXTfree / starter / business / enterprise
settingsJSONBテナント単位設定(タイムゾーン、ブランド色、etc.)
created_atTIMESTAMPTZ

RLS ポリシーの基本パターン

JWT クレームから tenant_id を取り出し、API ミドルウェア(Edge Function or Lambda)で Postgres セッション変数に設定 → RLS が自動でフィルタする。

-- API リクエスト先頭で実行(JWT から取り出した tenant_id を埋め込む)
SET app.tenant_id = '550e8400-e29b-41d4-a716-446655440000';

-- 各テーブルで RLS を有効化 + ポリシー設定
ALTER TABLE employees ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_employees ON employees
  USING (tenant_id = current_setting('app.tenant_id', true)::uuid);

-- 結果: 以降の SELECT/UPDATE/DELETE は自動的に
-- WHERE tenant_id = '550e8400-...' でフィルタされる
💡 Phase 2 (Supabase) では: auth.jwt() -> 'tenant_id' を直接 RLS ポリシーで参照できるため、SET 文不要。
💡 Phase 4 (Aurora) では: Lambda の RDS Data API or Prisma で API 入口に SET app.tenant_id を実行する pattern が一般的。

5. 権限設計

role_level (1/2/3) を employees テーブルに保持。アプリ層 + API 層の両方で検証。RLS だけでは「自分のデータのみ」までしか守れないので、「LV.MIDDLE は他人の電話番号も見える」のような細かい制御は API 層(認可ミドルウェア)で実装します。

role_level呼称employeestasksdaily_reportsleave_requests
1LV.MAX 社長 (CEO)全閲覧編集全閲覧編集全閲覧全閲覧承認
2LV.MIDDLE 管理者 (役員)全閲覧 / 自分編集全閲覧 / 自分編集全閲覧全閲覧承認
3STANDARD 一般自分のみ自分のみ編集自分のみ作成 / 全閲覧自分申請のみ
💡 日報 DB の閲覧: 透明性のため 全員閲覧 OK にするのを推奨(各テナントの settings で切り替え可能に)。

6. Postgres DDL (実装用 / Phase 2 Supabase で初期投入)

以下の DDL を Supabase SQL Editor で実行すれば Phase 2 のスキーマが構築されます。Phase 4 で Aurora Serverless v2 に移行する際も pg_dump でそのまま持ち込めます。

-- ===== 拡張機能 =====
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "citext";

-- ===== tenants =====
CREATE TABLE tenants (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  plan TEXT NOT NULL DEFAULT 'free' CHECK (plan IN ('free','starter','business','enterprise')),
  settings JSONB NOT NULL DEFAULT '{}'::jsonb,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- ===== employees =====
CREATE TABLE employees (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  kana TEXT,
  role TEXT NOT NULL,
  dept TEXT,
  email CITEXT NOT NULL,
  gmail CITEXT,
  joined_at DATE,
  status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','leave','retired')),
  color TEXT,
  role_level SMALLINT NOT NULL DEFAULT 3 CHECK (role_level IN (1,2,3)),
  cognito_sub TEXT,
  google_calendar_id TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (tenant_id, email)
);
CREATE INDEX idx_employees_tenant ON employees(tenant_id);

-- ===== projects =====
CREATE TABLE projects (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('idea','active','paused','done')),
  lead_emp_id UUID REFERENCES employees(id),
  starts_on DATE,
  due_on DATE,
  color TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_projects_tenant ON projects(tenant_id);

-- ===== tasks =====
CREATE TABLE tasks (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  title TEXT NOT NULL,
  assignee_emp_id UUID REFERENCES employees(id),
  project_id UUID REFERENCES projects(id),
  priority TEXT NOT NULL DEFAULT 'mid' CHECK (priority IN ('high','mid','low')),
  status TEXT NOT NULL DEFAULT 'todo' CHECK (status IN ('todo','doing','done','blocked')),
  due_start DATE,
  due_at DATE,
  start_time TIME,
  end_time TIME,
  effort_h NUMERIC(5,2),
  progress SMALLINT NOT NULL DEFAULT 0 CHECK (progress BETWEEN 0 AND 100),
  parent_task_id UUID REFERENCES tasks(id),
  related_report_id UUID,
  assigned_by_emp_id UUID REFERENCES employees(id),
  acknowledged_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_tasks_tenant_assignee ON tasks(tenant_id, assignee_emp_id);
CREATE INDEX idx_tasks_due ON tasks(tenant_id, due_at);

-- ===== daily_reports =====
CREATE TABLE daily_reports (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  emp_id UUID NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
  report_date DATE NOT NULL,
  done_today TEXT,
  do_tomorrow TEXT,
  blockers TEXT,
  mood SMALLINT CHECK (mood BETWEEN 1 AND 5),
  submitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (tenant_id, emp_id, report_date)
);
CREATE INDEX idx_reports_date ON daily_reports(tenant_id, report_date);

-- ===== time_blocks =====
CREATE TABLE time_blocks (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  emp_id UUID NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
  title TEXT NOT NULL,
  block_date DATE NOT NULL,
  starts_at TIME NOT NULL,
  ends_at TIME NOT NULL,
  description TEXT,
  project_id UUID REFERENCES projects(id),
  kind TEXT CHECK (kind IN ('work','meeting','travel','break','focus')),
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_blocks_emp_date ON time_blocks(tenant_id, emp_id, block_date);

-- ===== leave_requests =====
CREATE TABLE leave_requests (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  applicant_emp_id UUID NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
  starts_on DATE NOT NULL,
  ends_on DATE NOT NULL,
  kind TEXT NOT NULL CHECK (kind IN ('paid','sick','special','condolence')),
  reason TEXT,
  status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','approved','rejected')),
  approver_emp_id UUID REFERENCES employees(id),
  decided_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_leaves_status ON leave_requests(tenant_id, status);

-- ===== Row Level Security =====
ALTER TABLE employees ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
ALTER TABLE daily_reports ENABLE ROW LEVEL SECURITY;
ALTER TABLE time_blocks ENABLE ROW LEVEL SECURITY;
ALTER TABLE leave_requests ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_iso_employees      ON employees      USING (tenant_id = current_setting('app.tenant_id', true)::uuid);
CREATE POLICY tenant_iso_projects       ON projects       USING (tenant_id = current_setting('app.tenant_id', true)::uuid);
CREATE POLICY tenant_iso_tasks          ON tasks          USING (tenant_id = current_setting('app.tenant_id', true)::uuid);
CREATE POLICY tenant_iso_daily_reports  ON daily_reports  USING (tenant_id = current_setting('app.tenant_id', true)::uuid);
CREATE POLICY tenant_iso_time_blocks    ON time_blocks    USING (tenant_id = current_setting('app.tenant_id', true)::uuid);
CREATE POLICY tenant_iso_leave_requests ON leave_requests USING (tenant_id = current_setting('app.tenant_id', true)::uuid);

-- ===== シードデータ(GANECCI テナント)=====
INSERT INTO tenants (id, name, plan)
  VALUES ('00000000-0000-0000-0000-00000000ga01', '株式会社GANECCI', 'enterprise');
-- 以降、employees / projects / etc. に上記 tenant_id を入れて投入

7. 移行手順 (LocalStorage → Supabase → Aurora)

  1. Phase 2: Supabase プロジェクト作成 + DDL 実行
    supabase.com で新規プロジェクト → SQL Editor で上記 DDL を流し込む。10分作業。
  2. Auth 連携
    Supabase Auth に Google を Provider として追加。signInWithOAuth({ provider: 'google' }) で動く。
    新規ユーザー登録時に employees 行を upsert するトリガーを設定(auth.usersemployees マッピング)。
  3. app.js のデータ層を Supabase Client に置き換え
    現状: localStorageloadData() / saveData()
    新規: supabase.from('tasks').select('*') ベースに置換。
    関数シグネチャは温存しつつ実装だけ差し替えるため UI 変更は不要。
  4. 初期データ投入(GANECCI 10名 + 6プロジェクト)
    本ひな形 app.jsSTAFF / PROJECTS 配列に対応するレコードを INSERT。tenant_id は GANECCI テナントの UUID。
  5. Phase 3: tenant_id を JWT クレームに埋め込む
    Supabase Auth Hook(または DB トリガー)で、ログイン時に auth.users.raw_app_meta_data.tenant_id を JWT に追加。RLS が自動で動き出す。
  6. Phase 4: AWS CDK で Aurora Serverless v2 + Cognito + Lambda 構築
    Supabase からダンプを取り Aurora に流し込む。Cognito User Pool に Google IdP を追加して JWT クレーム互換にする。
  7. Phase 5: Stripe 課金 + SOC2 準備 + マーケサイト
    tenants.plan に応じた機能制限。SOC2 Type 1 監査受審。

8. Google Workspace / Google カレンダー連携

8-1. ID 基盤を Google アカウントに統一

8-2. Google カレンダーを「真のスケジュール台帳」とする

本アプリの time_blocks はアプリ内で作成するタイムブロック専用。個人の予定は Google Calendar API で取得して 「Google カレンダー = 1次ソース」「アプリ DB = 集計・分析レイヤー」 の役割分担にします。

用途置き場所理由
個人の予定 (会議等)Google カレンダー既存の習慣 / モバイル通知 / 外部連携が強い
出社/退社の打刻Postgres (leave_requests)集計が必要
プロジェクトごとの工数Postgres (tasks.effort_h)レポート要件
日報Postgres (daily_reports)検索・横断分析
タイムブロックPostgres (time_blocks)アプリ内 UI で完結

8-3. 当アプリでの取り込み

Google Calendar API で取得したイベントを当アプリ UI に統合表示。実装は google-auth.jsfetchCalendarEvents(calendarId) / fetchManyCalendars(ids):

8-4. 招待・共有フロー

詳細手順は 📧 招待・参加フロー を参照。

設計者: isoAi (Claude Opus 4.7) / Reviewer: 礒貝浩資 (CEO) / Last Update: 2026-05-10 (戦略転換版)