マルチテナントと RLS
プロジェクトごとに独立したスペースを持ちます。分離はアプリケーションではなくデータベースで行われ、PostgreSQL の行レベルセキュリティが、アプリケーション層に不具合があってもテナント間のデータ漏れを防ぎます。
API ファーストのマルチテナント型ヘッドレス CMS。読み取りパスは API に一切届きません —— 訪問者が増えて上がるのは請求額ではなく、キャッシュヒット率です。
データは当社が運用するサーバー上に保管従量課金の外部サービスはゼロエンタープライズプランでは自社サーバーへ導入可能
すべてのコンポーネントを自社インフラで
機能
すべては API の背後にあり、管理画面に閉じ込められた機能はひとつもありません。
プロジェクトごとに独立したスペースを持ちます。分離はアプリケーションではなくデータベースで行われ、PostgreSQL の行レベルセキュリティが、アプリケーション層に不具合があってもテナント間のデータ漏れを防ぎます。
コンテンツタイプはコードに埋め込まれていません。管理画面からでも API からでも定義できます。フィールドは title、slug、body といったロールを持ち、ビルドパイプラインと SEO 生成はフィールド名ではなくロールを見ます。
メディア、検索、Webhook、SEO、GraphQL、インポート、アナリティクス —— それぞれ独立していて無効化できます。無効化してもデータは削除されず、再び有効にすれば中断した続きから動きます。
公開されたコンテンツはビルド時にイミュータブルなアドレスへ書き出され、nginx のエッジキャッシュから配信されます。コンテンツ件数が増えても訪問者が増えても、読み取りのコストは上がりません。
S3 互換のオブジェクトストレージ(Garage)、3 ゾーンに複製される画像、自動生成される派生画像。アップロードしたファイルは自社インフラに留まり、従量課金の CDN へ渡ることはありません。
Typesense による多言語全文検索。「Istanbul」を「Istanul」と打った訪問者にも正しいコンテンツが見つかり、応答は 50 ミリ秒未満で返ります。
公開、更新、削除 —— あらゆるイベントがお客様のシステムへ通知されます。配信は永続キューに保持され、指数バックオフで再試行されます。プロセスが落ちてもイベントは失われません。
スキーマは手書きしません。スペースのコンテンツタイプから導出されます。新しいタイプを追加すれば、その時点でクエリ可能になります。クエリ爆弾への防御として、深さと長さの制限は必須です。
@corpusctl/client と React・Vue・Next.js 向けアダプター。ブロックレンダラーはデフォルトスタイルをまったく持ちません —— CSS を 1 行も押し付けません。デザインは完全にお客様のものです。
コード例
管理画面でできることには、すべて API の対応があります。インターフェースに閉じ込められた機能はありません。
# 1) コンテンツタイプを定義 —— スキーマはコードに埋め込まれていません
curl -X PUT "$API/v1/content-types/post" \
-H "authorization: Bearer $TOKEN" \
-H "x-corpusctl-tenant: $TENANT" \
-H "content-type: application/json" \
-d '{
"name": "post",
"title": "Post",
"fields": [
{ "name": "title", "type": "text", "role": "title", "required": true },
{ "name": "slug", "type": "slug", "role": "slug", "required": true },
{ "name": "body", "type": "blocks", "role": "body" }
]
}'
# 2) 下書きを作成
curl -X POST "$API/v1/documents" \
-H "authorization: Bearer $TOKEN" \
-H "x-corpusctl-tenant: $TENANT" \
-d '{ "type": "post", "data": { "title": "Hello", "slug": "hello" } }'
# 3) 公開 —— 検証、ビルド、エッジへの反映を 1 回の呼び出しで
curl -X POST "$API/v1/documents/$ID/publish" \
-H "authorization: Bearer $TOKEN" \
-H "x-corpusctl-tenant: $TENANT"
# → { "data": { "version": 3, "contentHash": "…", "durationMs": 42 } }import { ReadClient } from '@corpusctl/client'
import { Content } from '@corpusctl/react'
// ReadClient はトークンを一切持たず、API を呼び出しません —— エッジから直接読み取ります。
const client = new ReadClient({
tenantId: '3f2b…',
edgeUrl: 'https://cdn.example.com',
manifestTtlMs: 60_000,
})
export default async function Page({ params }) {
// slug → シャード計算(ローカル)→ マニフェスト → イミュータブルなアドレス。
// HTTP リクエストは 2 回、どちらもキャッシュから。見つからない場合は undefined を返し、
// 例外は投げません —— 404 にするかどうかはお客様のページの判断です。
const post = await client.getBySlug(params.slug)
if (!post) notFound()
return (
<article>
<h1>{post.data.title}</h1>
{/* デフォルトスタイルはゼロ: どのブロックタイプも上書きできます */}
<Content value={post.data.body} components={{ ... }} />
</article>
)
}# スキーマは手書きしません: スペースのコンテンツタイプから導出されます。
# 「post」タイプを定義した瞬間からクエリできます。
query GetPost($slug: String!) {
post(slug: $slug) {
title
slug
body
# 計算フィールドもスキーマに含まれます: 読了時間、文字数など…
readingTime
}
}
# リストクエリ —— カーソルページネーション、オフセットなし
query LatestPosts {
postList(limit: 10) {
data { title slug }
cursor
}
}ReadClient は秘密のトークンを持たずブラウザでも動きます。ManagementClient はサーバー専用です。1 つのクラスにまとめていたら、トークンの流出は 1 行のミスで起きていたはずです。アーキテクチャ
公開ボタンはビルドを起動します。訪問者が読むのはそのビルドの成果物であって、システムそのものではありません。
コンテンツはハッシュでアドレス指定されます。同じアドレスが別の内容を返すことは決してないため 30 日間キャッシュでき、公開のタイミングでマニフェストが更新されます。
どのマニフェストシャードを読むかは、slug からクライアント側で計算されます。1 万件のコンテンツを抱えるスペースでも、ダウンロードされるのは小さなファイル 1 つだけです。
すべてのレコードはバージョン管理され、古いバージョンを下書きに復元できます。未来の日時を指定した公開も予約でき、スケジューラーが自らビルドを起動します。
比較
すべての行で当社が勝つわけではありません。この表は、お客様が何を選ぼうとしているかをはっきり示すためにあります。
| 評価項目 | corpusctl | Contentful | DatoCMS |
|---|---|---|---|
| 料金モデル | 定額プラン。読み取りリクエストが API に届かないため、計測される呼び出しがありません。 | API 呼び出し数・レコード数・ユーザー数で課金。超過分は追加料金。 | API 呼び出し数と転送量で課金。 |
| 複数プロジェクト管理 | コアに組み込み済み。管理画面は 1 つ、スペースは無制限。ユーザーはスペースごとに別のロールを持ちます。 | プロジェクトごとに「space」。多くのプランで space 単位の課金。 | プロジェクトごとに別環境。プロジェクト単位のプラン。 |
| テナント分離 | データベースレベル —— PostgreSQL の行レベルセキュリティ(RLS)。 | アプリケーションレベル。詳細は非公開。 | アプリケーションレベル。詳細は非公開。 |
| データの所在 | 当社が運用するサーバー。エンタープライズプランなら完全に自社サーバーへ。 | ベンダーのクラウド。リージョン選択は上位プラン。 | ベンダーのクラウド。リージョン選択は上位プラン。 |
| 従量課金の外部サービス | なし。PostgreSQL、Valkey、Typesense、Garage、nginx —— すべて自社インフラ上。 | あり。検索・画像・CDN の各レイヤーが従量課金。 | あり。画像 CDN と変換処理が従量課金。 |
| 押し付けられるレンダリング | なし。ブロックレンダラーはデフォルトスタイルを持たず、未知のブロックはスキップされ、落ちることはありません。 | リッチテキストレンダラーを提供。スタイルの判断はお客様側。 | 構造化テキストレンダラーを提供。スタイルの判断はお客様側。 |
| 機能の無効化 | すべての機能が無効化できるモジュール。無効化でもプラン変更でもデータは消えません。 | プラン上限により機能が使えなくなります。 | プラン上限により機能が使えなくなります。 |
| グローバルエッジネットワーク | 単一リージョンのエッジキャッシュ。グローバル PoP 網はありません。 | マルチリージョンのグローバル CDN。 | マルチリージョンのグローバル CDN。 |
| アプリマーケットプレイス | なし。連携は Webhook と API で書きます。 | 広範で成熟したアプリエコシステム。 | プラグインのエコシステムあり。 |
| エンタープライズ成熟度 | 若い製品。SSO と正式な SLA はロードマップ上。 | SOC 2、SSO、SLA —— 成熟。 | SSO と SLA は上位プラン。 |