リレーショナル層は「固定コアは堅く、拡張は誰でも」。core は手書き SQL migration + 人間承認でしか 変わらず、extension は MCP の db_schema_addField で ALTER TABLE なしに即追加できる。両者を統べるのが Field Manifest(単一真実源)。実装は @event/crm-schema + @event/mcp の db ツール群。
二層構造
| レイヤ | 置き場所 | 変更方法 | 誰が書ける |
|---|---|---|---|
| core | 実カラム(手書き migration) | db_migration_propose → SQL 生成 → 人間承認 PR | system のみ(構造変更) |
| extension | meta jsonb のキー | db_schema_addField(即時・ALTER TABLE 不要) | admin / mcp |
全テーブルは標準 core フィールドを持つ: id / createdAt / updatedAt / deletedAt(soft-delete)/ version(楽観ロック)。これらは STANDARD_CORE_FIELDS として固定され、MCP/admin からは書けない。
Field Manifest — 3 消費者の単一真実源
FieldManifest = { version, entities: EntityDef[] }。1 つの manifest を 3 者が読む:
- Admin UI —
@event/cmsの FieldSchema 生成に使う - MCP server —
db_schema_describeで全 entity×field を返し、書き込みガードに使う - Manual — DB リファレンスの自動生成元。生成結果は /reference/db に全 entity × field の表として公開される
createManifestIndex(manifest) が lookup とガードを提供する。中核は splitPatch(entity, patch, writer): patch のキーを core カラム書き込み と meta(extension) 書き込み に振り分け、未登録キー / 権限なし / 型・制約違反を errors に列挙する。layer(構造の区分)と writableBy(値の書き込み権限)は独立 —— core カラムでも writableBy に writer が含まれれば値は更新できるが、構造変更は常に migration_propose 経由。
「カラムを足す」= addExtensionField
addExtensionField(manifest, "organization", {
name: "leadScore", type: "number",
writableBy: ["admin", "mcp"], description: "商談確度スコア",
});
// → { ok: true, manifest: { version: +1, ... } } ※非破壊。version が上がる
field 名は ^[a-zA-Z][a-zA-Z0-9_]{0,62}$、既存 core/extension との重複は弾く。layer は常に "extension"。 この操作は DB に触れない(manifest jsonb への追記のみ)。だから Neon 未接続の dev でも即座に効く。
書き込みガード — guardedUpdate
行の更新は必ず guardedUpdate を通す:
await guardedUpdate({
db, table, manifest, // manifest = createManifestIndex(...)
entity: "organization", id,
patch: { leadScore: 82 }, // splitPatch で core/meta に自動振り分け
writer: "mcp", // "system" | "admin" | "mcp"
expectedVersion, // 不一致は STALE で拒否(楽観ロック)
actor: "mcp:claude", // audit に残る
});
version 楽観ロック + updatedAt 自動刻印 + 追記専用 auditLog への記録がまとまって効く。物理削除は不可 (guardedArchive が deletedAt を刻む soft-delete のみ)。
MCP db ツール群(dbToolGroup)
dbToolGroup({ db, manifestStore, actor, entities? }) が返す EventTool[]:
| tool | 役割 | Neon 要否 |
|---|---|---|
db_schema_describe | Field Manifest を返す(書き込み前に必ず参照) | 不要 |
db_schema_addField | 拡張レイヤに field 追加 = カラムを足す | 不要 |
db_migration_propose | 固定コア変更の SQL を提案(自動実行しない) | 不要 |
db_entity_list / _get | 行の照会(soft-delete 除外) | 要 |
db_entity_update | guardedUpdate 経由の更新 | 要 |
db_entity_archive | soft-delete | 要 |
db_audit_query | 監査ログ照会 | 要 |
ManifestStore = { read(): Promise<FieldManifest>; write(next): Promise<void> } を注入する。 starter は content/db-manifest.json に持たせる(@event/core の contentStore = dev は fs、本番は GitHub commit =「カラム追加」が PR 履歴に残る)。だから schema 系ツールは Neon なしで完動し、行データ操作だけが DATABASE_URL を要求する。
固定コアの適用
@event/crm-schema/migrations/0001_init.sql を pg で非対話適用する(drizzle-kit push は TTY 要求 / neon-http は multi-statement 非対応のため使わない)。org / user / member / entity_lp / audit_log + compliance テーブル + 追記専用 trigger が作られる。冪等。手順は recipe add-database に完全なコード付きで ある(scripts/apply-migrations.mjs)。
配線の要点
dbは lazy Proxy(@event/crm-schema)。DATABASE_URL 未設定でも import では落ちず、実クエリ時に throw- HTTP MCP(
/api/mcp)と stdio(scripts/mcp.mjs)は 同一dbToolGroupを共有 — URL 経由でも
ローカル接続でも同じ「カラムを足す」操作ができる
/admin/dbが manifest を可視化(core/extension・writableBy・version)。MCP の addField が書く先そのもの