求職者管理の次設計 · 2026-07-15

LINE API と DB を分ける。
候補者の状態は「証拠から提案」する。

LINE APIかTursoか、の二択ではありません。LINE APIは受信・送信の入口、SQLite系DBは会話と判断根拠の保管庫です。無料枠・Worker接続・削除運用まで比較した結果、この仕事の正本はD1に集約するのが安全です。Tursoは「何ができ、なぜ今は正本にしないか」までを併記します。

まず押さえる3点

LINE API 入口

Webhookで受ける。受信したテキストはあとからAPIで取り直せないため、受信時に署名を確認して保存する。

D1 当面の文脈DB

既存の実行経路を活かす。WebhookとLINE Readerが既にD1を使っている。まずここに候補者との紐付け・事実・監査を揃える。

管理表 人の業務ビュー

会話全文を置かない。次の行動、LINE対応要否、AI提案だけを見せ、会話文脈は必要な時だけ開く。

結論:LINE APIとTursoは代替関係ではない

文脈をLINE APIに任せることはできません。APIはイベントを届ける経路であり、判断に使う履歴・状態・監査は自分たちのDBで持つ必要があります。

推奨 D1へ一本化

  • LINE API:受信・送信・LINE user ID取得
  • Cloudflare D1:候補者状態、会話、事実、削除保留
  • スプレッドシート:日々の仕事を動かす業務ビュー
  • candidate:D1を読んで提案・承認を扱う唯一の入口

Tursoを選ぶ条件 オフラインが主目的

  • 複数端末でローカルSQLiteを主に使う
  • ネットワークなしの閲覧・下書きが不可欠
  • 同期競合を設計・運用できる
  • WorkerとDBの外部接続を管理できる

無料枠と接続形態では、現行構成にD1が合う

候補者LINEの規模では両方とも無料枠に収まりやすいです。差は「数字」より、Webhookの保存経路とデータを一つに保てるかです。

観点D1Turso
Worker接続同一WorkerのDB bindingHTTPS+認証トークン
無料読取500万行/日5億行/月
無料書込10万行/日1,000万行/月
無料容量合計5GB合計5GB
時点復元Free 7日Free 1日
端末SQLite同期標準なし明示 push / pull

現状の矛盾を先に解く

Tursoを採用するなら「追加」ではなく「置換」です。先に正本を決めれば、AIがどの状態を信じるかも一つにできます。

現在の計画との差分

既存のactive計画は、管理表を現在状態の正本、TursoをPIIなしの監査台帳と定めています。一方、古いSQLにはSupabase正本の記述があり、実稼働Webhook/LINE ReaderはCloudflare D1を読んでいます。Tursoへ候補者・LINE本文を置くなら、既存計画を更新し、D1/Supabaseからの一回限りの移行と切替を別計画にします。

実行フロー:会話から直接ステータスを変えない

LINEの一文は証拠です。AIはまず事実を抽出し、既存状態と照合して「変更案」を出すだけにします。承認済みの変更だけが管理表とKPIに届きます。

LINE 受信・送信API / webhook 紐付け判定LINE user ID → 候補者 D1に保存イベント・会話・時刻 事実を抽出AI・信頼度・根拠 状態変更案次の行動・KPI候補 人が確認candidate の L2 承認 業務ビュー更新管理表・KPI・実行ログ 紐付かない・矛盾する・低信頼度の会話は「要確認キュー」へ。自動で候補者の状態を変えない。
LINE本文 → 候補者の状態、という直結は禁止。途中に「紐付け」「事実」「提案」「人の承認」を置く。

使用感:管理表は「今日動くための画面」に絞る

今の管理表のステータス・戦略/方針・次の行動・ニーズ・金額はそのまま仕事の中心に置きます。LINEの全文をセルに入れず、対応判断に必要な短い派生情報だけを横に見せます。

管理表に足す候補の表示

  • LINE連携:未連携 / 確認待ち / 連携済み
  • 最終LINE:相対時刻と方向だけ
  • 返信要否:未読 / 返信案あり / 対応不要
  • AI提案:次の行動または確認したい論点
  • 同期状態:反映済み / 要確認

表示しないもの

  • 会話全文:誤読・横スクロールを防ぐ
  • AIの確信だけの状態変更:承認前は案に留める
  • LINE表示名だけの自動紐付け:同姓同名を避ける
  • 複数DBの別々のステータス:正本を増やさない

実際の1件はこう流れる

担当者はセルを探さず、「誰から何が来たか」を起点に candidate へ任せます。AIは状況を要約して、変更ではなく確認しやすい提案を返します。

  1. 1

    候補者から日程に関するLINEが届く

    Webhookを検証してD1へ一度だけ保存する。重複イベントはイベントIDで除外する。

  2. 2

    本人と候補者を確定する

    LINE user ID → candidate_id → profile_url を基本キーにする。未連携・同姓同名・低信頼なら要確認キューへ送る。

  3. 3

    AIが「事実」を抽出する

    例:日程候補、意思、質問、辞退の兆候。本文ではなく根拠のメッセージIDと信頼度を一緒に持つ。

    抽出: 面談日程への回答あり
    候補: 「日程調整中」→「面談候補日を確認」
    次の行動案: 候補日を確認して面談を確定
    根拠: line_event_… / 信頼度: 高
  4. 4

    candidate が現在の管理表と照合して提案する

    既存のステータス、戦略、次の行動を読んで矛盾を出す。KPIになるイベントなら必要項目が揃っているかも確認する。

  5. 5

    担当者が「この内容で反映して」と承認する

    ここがL2ゲート。承認後にだけ、正本・管理表・KPI・実行ログを一つのイベントIDで更新し、再読込で確認する。

SQLiteの構造は「現在」「会話」「判断」「同期」を分ける

D1もTursoもSQLite系です。DB製品ではなく、候補者の現在値を一行に潰さず、会話とAI判断を別テーブルにすることが精度と削除しやすさを決めます。

candidates                          内部candidate_id・profile_url・確定状態・状態version
├── line_identity_links             LINE user ID・紐付け根拠・確信度・連携状態
├── line_messages                   webhook_event_id・方向・本文・受信時刻・削除期限
├── candidate_facts                 抽出事実・根拠message_id・信頼度・抽出version
├── candidate_change_proposals      状態/KPIの提案・承認/却下・操作event_id
├── sheet_projection_outbox         承認済み管理表反映・再読込状態
└── candidate_lifecycle              最終確認・保存停止・削除予定・保持保留

キー・制約・トランザクションで誤紐付けと二重反映を防ぐ

氏名やスプシ行番号は候補者キーにしません。内部IDと正式 profile_url、LINE user ID、WebhookイベントIDを役割ごとに一意にします。

候補者とLINEのキー

candidate_id は内部UUID、profile_url は候補者を一意に示す業務キー、LINE user ID はLINE側のキー。氏名・表示名は表示専用にし、紐付けが不確かなら要確認キューへ送る。

会話の重複と削除

line_messages は webhook_event_id を一意にする。再配信でも二重に保存しない。候補者削除時は外部キーの ON DELETE 方針に従い、本文・LINE紐付け・未送信予約を同じ削除単位にする。

承認済みの状態変更

事実、提案、確定状態、管理表outboxを一つのトランザクションで更新する。途中で失敗すれば確定状態だけが先に変わらず、外部表は同じoutboxを再実行できる。

フォルダは「実装・スキーマ・PIIなしのログ」だけを置く

候補者名、LINE本文、profile_urlをGit管理のフォルダへ出しません。実データはD1だけ、repoには再現可能なコード・SQL・匿名テストだけを残します。

仕事/
├── mcp/
│   ├── sql/
│   │   ├── d1-001-candidate-context.sql       追跡:テーブル・索引・制約だけ
│   │   ├── d1-002-sheet-projection.sql        追跡:outbox・寿命管理
│   │   └── d1-003-d1-migration.sql             追跡:一回限りの移行手順
│   ├── line-webhook/
│   │   ├── src/index.ts                        LINE署名確認・受信入口
│   │   ├── src/d1-writer.ts                    会話の書込み専用
│   │   ├── src/lifecycle.ts                    保存停止・猶予・削除の規則
│   │   └── scripts/snapshot-management-sheet.mjs 管理表の存在確認
│   └── line-reader/
│       ├── src/index.ts                        MCPの読み取り入口
│       └── src/d1-reader.ts                    最小権限の読取り専用
├── scripts/candidate/
│   ├── record_event.py                          既存:PIIを拒否する操作ログ
│   ├── tests/                                   追跡:匿名fixtureのみ
│   └── output/logs/                             非追跡:PIIなしの実行結果
├── loops/line-data-lifecycle/
│   └── loop.md                                  発火・dry-run・停止・rollbackの正本
└── areas/整備/.../plans/                        正本の計画。実データは置かない

Tursoを正本にするなら、Cloudだけを候補者DBにする

この案では、D1をLINE文脈の正本から外し、Turso CloudのSQLite互換DBへ直接書きます。Cloudflare Workerからはリモート接続用のSDKを使い、個人情報を持つローカルSQLiteファイルは作りません。

採用する Cloud正本

  • Webhook:LINEイベントをTursoへ一度だけ記録
  • candidate:Tursoの状態・事実を読んで提案
  • 承認操作:Tursoの一トランザクションで確定
  • 管理表:Tursoから投影する業務ビュー

採用しない ローカル同期

  • Turso Sync:候補者本文のMac複製には使わない
  • 理由:端末紛失・同期競合・データ寿命が複雑になる
  • 開発時:匿名fixtureだけをローカルSQLiteで使う
  • 実データ:Cloudへ直接接続し、ローカルに残さない

同期は「DB間」ではなく、責務ごとに片方向にする

Tursoのローカル同期を候補者管理の仕組みにしません。同期対象を混ぜず、入口ごとに一方向の流れを固定します。

  1. 1

    LINE → Turso 即時・直接

    Webhookの署名確認後、webhook_event_id の一意制約で重複を防ぎ、受信本文・時刻・LINE user IDをTursoへ保存する。これは同期ではなく、原本の受入れである。

  2. 2

    candidate → Turso 承認後

    AIが抽出した事実と変更案を保存し、人がL2承認した時だけ候補者状態を確定する。同じトランザクションで管理表投影用のoutboxを作る。

  3. 3

    Turso → 管理表 投影

    承認済みoutboxだけを管理表の対応行へ反映し、読み直して完了にする。スプシのセルをTursoへ無制限に書き戻さない。

  4. 4

    管理表 → Turso 限定入力

    初回移行と、管理対象に存在するかの毎日スナップショットだけを入力にする。候補者の正式キーは行番号・氏名ではなく profile_url にする。

管理表から消えた時は、即削除でなく「停止 → 猶予 → 削除」にする

管理表にいない候補者のLINE本文を持ち続けないことには賛成です。ただし、行の誤削除・並べ替え・一時移動まで消さないよう、新規保存の停止既存データの破棄を分けます。

管理対象管理表に存在 保存を停止有効な欠落を検知 削除保留例:30日・復帰可 完全削除本文・紐付け・送信予約 同じ profile_url が管理表に戻れば、保留を解除して管理対象へ復帰
削除対象を名前や行番号で決めず、候補者の正式 profile_url を照合キーにする。同期元が0件・見出し不明・急減の場合は何も削除せず停止する。

このループは機械的な「掃除」に限定する

候補者の状態をAIが判断する処理と、LINEデータを整理する処理は別責務です。後者はAI runnerではなく、一定のルールだけを実行するrepo-localのscript loopにします。

毎日の同期 読み取り → 差分

管理表をライブに読み、正式 profile_url の集合を作る。固定の行・列・氏名で判定しない。空の取得、見出し異常、前回比の大幅減少はfail closedで、DBに一切書かない。

欠落を検知した直後 新規保存を止める

LINE本文の新規保存と保留中の送信を停止する。既存本文は削除保留へ移し、担当者が誤って行を消しても猶予期間中なら同じ profile_url で復帰できる。

猶予満了後 完全削除

LINE本文、LINE user IDとの紐付け、未送信予約を消す。実行ログには人数・日時・理由だけを残し、氏名・profile_url・本文を残さない。保持義務がある候補者は retention_hold で除外する。

既存の毎分削除は、この寿命ループへ置き換える

今のWebhookは毎分の定期実行で、allowlistにいない候補者のLINE本文を削除します。管理表同期が固定範囲・名前中心のため、このままでは誤削除を戻せません。

置換時の安全条件

新しい同期がdry-runで十分に一致するまで、既存の即時削除を広げない。まず削除候補の件数だけを出し、人が確認してから「保存停止」「猶予」「完全削除」の順に切り替える。launchdではなく既存のCloudflare定期実行を使う場合も、発火変更と削除ポリシーは明示承認が必要です。

導入は三段階に分ける

一度に正本移行をしないことが、今の管理表運用を止めずに精度を上げる鍵です。

  1. A

    監査と正本決定 先に行う

    D1・Supabase・管理表のどれが実際に更新されているかを読み取りだけで照合する。期間中は管理表を業務上の正本として維持する。

  2. B

    LINE連携と「事実抽出」だけを追加

    候補者状態はまだ自動変更しない。連携済み率、要確認件数、提案と人の判断の差を測る。

  3. C

    承認済み反映を一本化

    candidate のL2承認から、正本・管理表・KPI・ログを一つの操作IDで反映して再読込確認する。十分な照合後にだけ、DB正本化を判断する。

次に人が決める3点

ここを決めれば、実装計画を安全に確定できます。いまはまだDB migrationやスプシ列追加をしません。

1. 最終的なクラウド正本

推奨は、実稼働中のD1へ統合すること。Supabaseの古い設計は監査後に統合または停止対象として扱い、Tursoは正本にしない。既存 mcp/sql にはSupabase用のSQLも混ざるため、D1へ流用せずSQLite互換の新migrationを別にする。

2. LINE本文の保持ルール

管理表から消えた後の猶予日数、最長保存、削除保留の条件を先に決める。既存Webhookには3か月で削除する処理があるため、業務要件に合うかを確認する。

3. 人の承認を外す範囲

当面は状態・KPI反映をL2承認のままにする。未読表示や要確認キューまでを自動化し、送信・状態変更・KPIは担当者が確定する。

根拠・確認したもの