ZeBridge – NATS経由でデバイス上のPostgreSQLレプリカを同期
ZeBridge – PostgreSQL replicas on devices, synchronized over NATS (github.com)
要約
ZeBridgeは、デバイス上でローカルなPostgreSQLレプリカを維持し、オフライン時には書き込みをキューイングし、再接続時にNATS経由で同期するシステムです。PostgreSQLが権威を持ち続け、ルールを適用し、競合する書き込みを解決します。このシステムは、特にB2Bの運用アプリや、接続が不安定な環境でのフィールドチームなどのユースケースに適しています。
全文翻訳
ZeBridge
NATS経由で、アプリやサービスのためのローカルPostgreSQLレプリカ。
[画像: Zig support] [画像: License] [画像: Tests]
ZeBridge は各デバイスにローカルデータベースを保持し、オフライン時には書き込みをキューイングし、再接続時に NATS を介して同期します。PostgreSQL は権威を持ち続け、ルールを適用し、競合する書き込みを解決し、アプリに結果を返します。
クライアントライブラリは、ローカルレプリカの読み取り、ミューテーションの送信、ルーティングや分析サービスなどのリモートレスポンダーへのクエリのための単一インターフェイスを提供します。オプションのリージョナル NATS リーフは、デバイスの近くにスナップショットキャッシュを配置し、これらのサービスをホストします。
最も強力なユースケース: 各顧客組織がテナント(スキーマの列)であり、そのデバイスが作業データセットを共有する B2B 運用アプリ: フィールドチーム、フリート、デポ、サイト。これらは、接続状態が悪い場合やチームが孤立している間でも作業を継続でき、すべての書き込みは最終的に PostgreSQL からの権威ある決定で終わります。これは、ユーザーごとに1つのテナントを持つコンシューマー(B2C)アプリ向けには構築されていません。
実行中のデモを見る: https://bridge.zebridge.eu/demo。フランスの16,173箇所の充電ポイントの地図上で5台のトラックが移動します。トラックと目的地を選択すると、すべてのオープン画面で道路に沿って移動します。いずれかの画面から目的地を変更すると、その場所で方向転換します。充電器はブラウザ自身のレプリカに対するクエリであり、ルートは同じ NATS 接続を介してルーティングサービスから提供され、PostgreSQL には決定のみが保持されます。2つのタブ、または電話とラップトップで開いてください。各訪問は、共有デモテナント内で1時間の招待として独自のIDを取得します。構築方法: depot example。
Docker で試してみる
Docker Compose が必要です。リポジトリのクローンやローカルビルドは不要です。
curl -O https://raw.githubusercontent.com/ndrean/ZeBridge/main/docker-compose.quickstart.yml
docker compose -f docker-compose.quickstart.yml up
これにより、PostgreSQL、NATS、ブリッジ、そのトゥームストーンクリーンアッププロセス(スイーパー)、および公開されたイメージを使用する2つのブラウザデモが起動します。この評価用 Compose ファイルには、個別のミューテーションアドミッションサービスはありません。本番環境のレイアウトではこれが含まれます。Compose ファイルはイメージバージョンを選択し、現在のリポジトリチェックアウトをビルドしません。
init サービスが招待リンクを表示したら、それらを別々のタブで開きます。
| リンク | デバイス |
|---|---|
| http://localhost:5173/?invite=quickstart-alice-1 | Alice, テナント acme |
| http://localhost:5173/?invite=quickstart-alice-2 | 2番目の Alice デバイス, テナント acme |
| http://localhost:5173/?invite=quickstart-bob-1 | Bob, テナント globex |
各招待は一度だけ使用できます。タブは独自のデバイスキーを作成し、ブリッジに登録し、スコープされた NATS クレデンシャルを受け取ります。4つのフェーズバッジが緑色になるまで待ちます。最初のスナップショットには最大1分かかる場合があります。
テナントの分離を確認する。 Alice のタブで counter_tenant を変更すると、もう一方の Alice タブも追従しますが、Bob のタブは追従しません。counter_public を変更すると、3つすべてが追従します。このページは、外部キー、拒否された書き込み、協調編集もデモンストレーションします。
オフライン書き込みを試す。 タブを開いたまま、サービスを停止します。
docker compose -f docker-compose.quickstart.yml down
切断中にカウンターをクリックします。各タブは自身のローカルレプリカを読み取り、保留中の書き込みをアウトボックスに保持します。サービスを再度起動します。
docker compose -f docker-compose.quickstart.yml up
タブは再接続し、キューに入れられた書き込みを送信して同期します。カウンターはラストライターウィン(最後に書き込んだものが勝つ)を使用します。最終値は、オフラインクリックの合計ではなく、勝った書き込みです。
初期化ログには、ポート5174の共有レコードデモへのリンクも表示されます。これは、複数のエディターが同じレコードの別々のフィールドを変更する様子を示します。
この評価スタックを消去してやり直すには、docker compose -f docker-compose.quickstart.yml down -v を実行します。これにより、データベースと NATS のボリューム、およびこれらの招待が依存するIDが削除されます。クイックスタートはローカル評価設定を使用し、NATS TLS は使用しません。サーバーにはデプロイガイドを使用してください。
アプリを構築する
3つの部分: インストールするハブ、ユーザーにサインインさせ招待を発行するあなたのバックエンド、そしてローカルデータベースで使用するクライアントライブラリを持つアプリ。アプリが問い合わせできるサービス(ルーティングや分析など)はオプションであり、同じライブラリ上で別個のプロセスとして実行されます。
ハブをインストールする
ハブは NATS、ブリッジ、ミューテーションアドミッション、およびスイーパーを1台のサーバーに集約したものです。PostgreSQL は同じサーバーで実行することも、別の場所で管理することもできます(論理レプリケーションが必要です)。2つのルートがすぐに使用できます。
-
Docker Compose: サーバー上の2つのファイル。HAProxy と Let's Encrypt が含まれており、PostgreSQL はオプションです。
V=0.2.11 mkdir zebridge && cd zebridge curl -fsSLO https://raw.githubusercontent.com/ndrean/ZeBridge/v$V/deploy/compose/compose.yml curl -fsSL -o .env https://raw.githubusercontent.com/ndrean/ZeBridge/v$V/deploy/compose/.env.example chmod 600 .env # .env で: 3つのドメイン名、Let's Encrypt のメールアドレス、およびパスワード(ガイドを参照)。 docker compose up -d最初の起動で証明書が発行され、NATS ID が生成され、ZeBridge の SQL オブジェクト(ブリッジが後述するログインに使用する
zb_inviterを含む)がインストールされます。Compose ガイドを参照してください: the Compose guide。 -
Ansible: systemd サービスを備えた Debian VPS 用: インベントリとボールトを記入し、
ansible-playbook site.yml --ask-vault-passを実行します。いつでも再実行でき、変更されたものだけを変更します。Ansible ガイドを参照してください: the Ansible guide。
次にテーブルを有効にし(テーブルの準備)、bridge --diagnose でハブを確認します。手動インストールや Supabase などの他のルートは、デプロイ にリストされています。
あなたのバックエンド: サインイン、そして招待
あなたのバックエンドの仕事は1つだけです: ユーザーにサインインさせ、招待を発行することです。 ZeBridge はユーザーを認証せず、どのように行うかにも関心がありません: OAuth、パスワード、マジックリンク、任意の言語またはフレームワーク。あなたのバックエンドがユーザーにサインインさせた後、ユーザーのプリンシパルとテナントを決定し、PostgreSQL に1回限りの招待コードを要求します。これは、招待を作成できるログインのみを持ち、それ以外の権限はありません。あなたが書くサーバーコードはこれだけです。構築すべき書き込み API や同期エンドポイントはありません。なぜなら、書き込みはライブラリを介して行われ、PostgreSQL 自身のルールがそれを判断するからです。
// あなたのバックエンド、ユーザーにサインインさせた後。(そのデータベースログインは zebridge_inviter のみを持つ)
const { rows } = await db.query(
"SELECT zebridge_invite($1, $2, interval '1 hour') AS code",
[user.id, user.tenant],
);
return { invite: rows[0].code }; // ログインレスポンス内
アプリ
アプリは、最初の起動時にコードをライブラリに渡します。ライブラリはデバイスを登録し、そのIDを保存し、レプリカにシードし、変更を追跡します。後続の起動ではコードは不要で、ライブラリはデバイスのクレデンシャルを自動的に更新します。コードの配信方法(リンク、メール)や登録手順については、ユーザー: 招待 を参照してください。
ブラウザアプリの場合:
npm install @zebridge/client
import { ZeBridge } from "@zebridge/client";
// あなた自身のログインフローから取得します。最初の起動のみが必要です。DBA は orders テーブルを有効にしました。
const { invite } = await signIn(email, password);
const zb = new ZeBridge({
bridgeUrl: "https://bridge.example.com",
invite,
tables: ["orders"],
});
zb.onChange("orders", () => refreshOrders());
zb.onVerdict((verdict) => showWriteOutcome(verdict));
await zb.connect();
// SQL はローカルデータを読み取ります。オフライン時でも同様です。
const orders = await zb.query(
"SELECT id, status FROM orders WHERE status = ?",
"pending",
);
// ミューテーションは楽観的に適用され、耐久性のあるアウトボックスに入ります。
if (orders.length > 0) {
await zb.mutate("orders", "UPDATE", { id: orders[0].id }, { status: "done" });
}
query() はローカルデータを読み取ります。mutate() は書き込みパスを所有します: ローカル変更、バージョン管理、アウトボックス、リトライ、および結果。mutate() の完了は、PostgreSQL が書き込みを受け入れたことを意味するわけではありません。ライブラリを介してその結果を処理してください。リモートレスポンダーにクエリするには request() を使用し、その応答をローカルに保持する必要がある場合は ingest() を使用します。
TypeScript クライアントは、独自の同期ループを駆動します。ネイティブ libzb では、ホストが sync() と poll() を駆動します。スレッドまたはワーカーモデルについては、バインディングの例を参照してください。
ローカルデータベースを選択する
レプリカはデバイス上の実際のデータベースであり、アプリはそのデータベースの SQL でそれを読み取ります。
- SQLite: どこでもデフォルトです。電話、デスクトップ、Node ではファイル、ブラウザでは OPFS。
- PGlite: WebAssembly にコンパイルされた PostgreSQL。ブラウザまたは Node (
engine: 'pglite') で使用できます。サーバーと同じ SQL 方言です。 - DuckDB: libzb を介して使用 (
engine: "duckdb")。ダッシュボードやサービス用の分析レプリカで、libzb が閉じると DuckDB が直接開くことができます。 - PostgreSQL: libzb を介して使用 (
dbUrl)。サーバーサイドレプリカで、完全な PostgreSQL を必要とするサービス向けです。
以下の表は、どのホストがどれを提供するかを示しています。CLIENTS には、各エンジンのオプションと型マッピングが含まれています。
| ホスト | パッケージ | ローカルデータベース |
|---|---|---|
| ブラウザ | @zebridge/client | OPFS の SQLite または PGlite |
| Node | @zebridge/client, better-sqlite3, @nats-io/transport-node | SQLite または PGlite |
| Python | zebridge | libzb を介した SQLite, PostgreSQL または DuckDB |
| Dart / Flutter | zebridge | libzb; バインディングのプラットフォームサポートを参照 |
| React Native | @zebridge/react-native | libzb を介した SQLite; ネイティブビルドが必要 |
| Android / Kotlin / Java | eu.zebridge:zebridge-android | libzb を介した SQLite |
| その他のネイティブホスト | libzb の C ABI | SQLite, PostgreSQL または DuckDB |
ネイティブコアは Zig で書かれています。TypeScript クライアントは WebAssembly を介して共有コアルールを再利用します。プラットフォームバインディングは、別の同期エンジンを実装するのではなく、libzb をラップします。
サービスを接続する(必要な場合)
レスポンダーは、アプリが質問できるサービスです: ルーティング、検索、分析、外部 API。これは同じクライアントライブラリ上に構築された別個のプロセスです。独自のIDで NATS に接続し、データが必要な場合は独自のレプリカを保持し、query.<tenant>.<name> に応答します。そのテナントのどのクライアントでも呼び出すことができ、HTTP エンドポイント、URL、または別のクレデンシャルは必要ありません。
// サービス(独自のプロセス内): 招待ではなく、`bridge --mint-responder --name pois --tenant acme` で発行されたクレデンシャル `credsPath: 'pois.creds'` で開かれたクライアント。
await service.serve({
tenants: ["acme"],
handlers: { nearby: ({ lat, lon }, tenant) => findNearby(lat, lon) },
});
// テナント acme の任意のアプリ。
zb.request("query.acme.nearby", { lat: 47.2, lon: -1.55 });
複数のレスポンダーインスタンスが負荷を共有し(1つの NATS キューグループ)、レスポンダーはリージョナルリーフで実行され、ユーザーの近くに配置できます。読み取りのみが可能で、書き込みはできません: NATS はそのIDからのミューテーションを拒否します。ingest() は応答をアプリのレプリカに保持します。ID については Services and sensors を参照し、Valhalla をこの方法で提供するルーティング例については routing example を参照してください。
各言語での最初の呼び出し から始め、CLIENTS でオプション、ライフサイクル、クライアント間の違いを確認してください。インストールとプラットフォームアーティファクトは DISTRIBUTION でカバーされています。
PowerSync および ElectricSQL との比較
ZeBridge、PowerSync、および Electric's Postgres Sync はすべて PostgreSQL データをクライアントに同期します。PowerSync と ZeBridge はローカル SQLite レプリカを中心としたクライアントライブラリを提供します。Electric は、クライアントライブラリまたは統合がローカルストアに具体化する HTTP Shapes を公開します。これらは、書き込みパスの場所、実行するもの、およびアプリケーションが他のサービスに接続する方法が異なります。
flowchart LR
subgraph ZB["ZeBridge: you run one hub"]
direction LR
PG[(PostgreSQL<br/>local or managed)] <--> B[Bridge]
S[Sweeper] --> PG
B <--> N[NATS JetStream]
A[Mutation admission] <--> N
end
N --> C[libzb or zb-client-ts<br/>+ local replica]
Loading
flowchart LR
subgraph PS["PowerSync: hosted or self-hosted service"]
direction LR
PG2[(PostgreSQL)] --> P[PowerSync service]
P <--> BS[(Bucket storage<br/>PostgreSQL or MongoDB)]
end
P --> C2[PowerSync SDK<br/>+ local SQLite]
C2 -->|"uploadData(): your code"| API[Your backend API]
API --> PG2
AUTH["Your auth: signs the JWT"] -.-> C2
Loading
Electric の列は、文書化されたアーキテクチャを要約しています。ここでは TanStack DB および Node SQLite を使用して、読み取りサイドのシードパスをテストしました。書き込み動作、テナント認証、またはその他のクライアントプラットフォームはテストしていません。
| ZeBridge | PowerSync | ElectricSQL / Postgres Sync (documented) | |
|---|---|---|---|
| 実行するもの | NATS、ブリッジ、アドミッション、スイーパーを1台のサーバーで実行 | PowerSync サービスとそのバケットストレージ、またはそれらのホストされたクラウド | Electric Sync (self-hosted or Electric Cloud); Postgres に接続し、HTTP Shapes を提供 |
| あなたが書くサーバーコード | あなたのサインイン、その後招待ごとの1つの SQL 呼び出し | あなたのサインイン、サービスが信頼する JWT サインヤー、および各アップロードを適用する書き込み API | テナントスコープアクセスの場合、ID を検証し Shapes を制約する認証プロキシ/ゲートキーパー。あなたの API が書き込みを処理します (auth, writes) |
| クライアント上のテーブルスキーマ | ブリッジによって送信される | アプリで再度宣言される | Shape の行は動的である可能性があります。TanStack の Electric アダプターはキーアクセサーを必要としますが、その検証スキーマはオプションです (adapter docs) |
| 読み取り | ローカルレプリカ上の SQL | ローカルレプリカ上の SQL | クライアントによって具体化されるか、ローカルストアに統合される HTTP Shapes |
| 書き込み | mutate(); ライブラリが送信し、PostgreSQL が判断して結果を返します | ローカルテーブル上のプレーン SQL。あなたの uploadData() が あなたのバックエンド に送信し、それが独自のルールを適用します | Electric Sync には組み込みの書き込みパスはありません。API を使用してください。TanStack DB は楽観的なミューテーションを追加し、同期された変更と調整できます |
| ちょうど1回、認証、拒否 | 組み込み: 受領、テナントクレデンシャルと行レベルセキュリティ、書き込みごとの結果 | あなたのバックエンドの仕事。SDK は uploadData() が成功するまでリトライします | あなたの API が書き込み保証を所有します。プロキシ/ゲートキーパーは Shapes の認証を強制する必要があります |
| クライアントプラットフォーム | ブラウザ、Node、Python、Dart/Flutter、React Native、Android、C ABI | Web、React Native、Flutter、Kotlin、Swift、Node など、多くの成熟した SDK | |
| ZeBridge | PowerSync | ElectricSQL / Postgres Sync (documented) |
読み取りサイドのコールドシード、同じローカル PostgreSQL: システムあたり10クライアント、約200バイトの行が50,000行、各クライアントは空の SQLite データベースから開始します。タイマーには転送とローカル具体化が含まれ、すべての行がクエリ可能になった時点で停止します。すべてのレプリカは同じ SHA-256 チェックサムに一致しました。
| クライアントパス | 中央値/クライアントシード、2ラウンド | 最も遅いクライアント、2ラウンド |
|---|---|---|
| ZeBridge / libzb | 3.076 秒 / 2.959 秒 | 3.094 秒 / 2.963 秒 |
| PowerSync / Node SDK | 12.76 秒 / 12.99 秒 | 13.00 秒 / 13.14 秒 |
| Electric / TanStack DB / Node SQLite | 12.052 秒 / 11.481 秒 | 18.548 秒 / 17.636 秒 |
これらの実行では、ZeBridge の中央値シード時間は PowerSync の約 4.2〜4.4倍、Electric の約 3.7〜4.1倍 低くなりました。これはクライアントサイドのシード結果であり、一般的なスループットの主張ではありません。サーバーサイドの状態は、タイミングされたラウンドでウォームでした: ZeBridge のスナップショットチェーンは準備完了、PowerSync は1クライアントの準備完了シード、Electric の2ラウンド目は最初のラウンドで作成された形状を再利用しました。クライアントは OrbStack 上の同じ8コア Mac で実行され、Docker に約4GB を公開しました。同期サービスは一度に1つずつ実行されました。PowerSync のテストストリームは固定テナント述語 tenant_id = 'cmp' を使用したため、これはユーザーごとの認証を比較するものではありません。完全な方法論、バージョン、チェックサム、および起動感度: local cold seed comparison。
測定済み、同じワークロード: 500クライアント、1つの Supabase データベース、両方で同じテーブルルール(2026-11-02)。
| ZeBridge | PowerSync | |
|---|---|---|
| 配信: 500クライアントがコールドスタートで10,000行のテーブルを受信、中央値(ローカル適用を除く) | 5.3–5.7 秒: 1つの共有圧縮スナップショット、クライアントあたり0.23 MB | 61–76 秒: バケットのすべての操作、クライアントあたり4.63 MB |
| 書き込み、 1,000/秒 / 2,000/秒 で提供 | 990/秒、結果中央値 0.69 秒 / 1,572/秒 | 879/秒(PostgREST 経由)、確認中央値 4.8 秒 / 1,702/秒 |
| 1つの書き込みがすべてのクライアントに到達する、500件すべてへの1つの書き込み、中央値 | 0.4–0.8 秒 | 0.7–1.6 秒 |
設定、両方のサーバーの数値、実行で測定されないもの、および切断が各書き込みに与える影響は COMPARISON にあります。
仕組み
ハブは NATS と3つの ZeBridge プロセスを実行します。これらは別々の責任を持っています。
| プロセス | 責任 |
|---|---|
Bridge (bridge) | PostgreSQL の変更を読み取り、スナップショットを構築し、登録を管理し、許可されたミューテーションを PostgreSQL に適用する。 |
Mutation admission (bridge --mutation-ingress) | 受信したミューテーションリクエストを検証し、処理のために保存する前に、プリンシパルごとのキュー予算を強制する。 |
Sweeper (bridge_sweeper) | データベースの GC フロアを進め、それより古いトゥームストーンを回収する。データベースあたり1つのアクティブなスイーパー。 |
flowchart LR
PG[(PostgreSQL)] <--> B[Bridge]
B <--> H[NATS hub]
H -->|Mutation requests| A[Mutation admission]
A -->|Admitted queue| H
S[Sweeper] -->|Tombstone cleanup| PG
H <--> C[Client library]
H <--> L[Optional regional leaf]
L <--> C2[Client library]
L <--> R[Routing / analytics responders]
C <--> DB[(Local replica)]
C2 <--> DB2[(Local replica)]
Loading
- 変更: ブリッジは PostgreSQL の論理レプリケーションフィードを読み取り、スキーマと変更を NATS JetStream に発行します。接続されたクライアントは、これらの変更をローカルデータベースに適用します。
- シーディングとリカバリ: ブリッジは、テーブルとテナントごとに圧縮されたスナップショットチェーン(フルとデルタ)を構築します。クライアントはこれらを NATS からフェッチし、チェーンのカット後に CDC を追跡します。適格な再接続は、デルタを使用してより大きな CDC バックログを置き換えることができます。
- 書き込み: クライアントはミューテーションリクエストを NATS に発行します。別個のアドミッションプロセスがそれらを検証し、各プリンシパルのキューに入れられた作業を制限します。ブリッジは、許可された書き込みを消費し、PostgreSQL に適用し、結果を含む耐久性のある領収書を記録します。一致するリトライは、ミューテーションを再度コミットするのではなく、領収書を再利用します。
- ハウスキーピング: スイーパーはデータベース全体のロックを保持し、GC フロア(決して下がらないウォーターマーク: それより前にスタンプされた書き込みは拒否され、リプレイされない)を進め、その後、データベース全体で1つの保持期間(
GC_THRESHOLD_MS)の下で、それより古いトゥームストーンを回収します。2番目のスイーパーはアイドル状態です。フロアについては the floor を参照してください。 - サービス: レスポンダーは NATS を介してリクエストに応答し、独自のレプリカまたは外部サービスを使用します。ルーティングレスポンダーは Valhalla をラップでき、分析レスポンダーは DuckDB をクエリできます。
デバイスは、PostgreSQL を個別にクエリするのではなく、共有スナップショットオブジェクトからシードされます。これにより、接続ストームがプライマリデータベースのスナップショットクエリストームになるのを防ぎます。PostgreSQL は引き続きミューテーション実行とブリッジのスナップショット構築作業を処理します。
オプションのリーフ
ハブから始めます。リージョナル接続、シードトラフィック、またはサービスが正当化する場合にリーフを追加します。
シードリーフは独自の JetStream ドメインを実行し、承認されたテナントのスナップショットデータをミラーリングします。クライアントはローカルでフルおよびデルタをダウンロードできます。権威ある CDC ストリーム、ミューテーション処理、および結果はハブに残ります。リーフはまた、ユーザーの近くにレスポンダーをホストすることもできます。
これにより、スナップショット提供とクライアント接続の負荷がオフロードされます。ハブの可用性が高まるわけではなく、権威ある書き込みがリージョンに移動するわけでもありません。登録は、割り当てられたリーフをハブが追跡するように供給します。これは順序付けられた接続フェイルオーバーのためです。リーフへのライブ接続とハブリンクの破損は、別の障害ケースです。リーフ境界、設定、およびフォールバック動作については leaf boundaries, configuration and fallback behavior を参照してください。
デプロイ
論理レプリケーションを備えた PostgreSQL と、ZeBridge の SQL オブジェクトをインストールするために必要な権限、さらに NATS、ブリッジ、ミューテーションアドミッション、スイーパー用のサーバーが必要です。PostgreSQL は別のマシンで管理・実行することもできます。ブリッジへのネットワーク遅延を考慮してください。
| ルート | ガイド |
|---|---|
| Docker Compose を使用したハブ | Compose deployment |
| systemd を使用した Debian VPS 上のハブ | Ansible deployment |
| 手動インストールと管理 PostgreSQL | DEPLOYMENT |
| 管理 PostgreSQL、空のプロジェクトから(Supabase) | SUPABASE_TEST |
| オプションのリージョナルリーフ | LEAF_NODES |
セットアップにより、NATS ID と TLS が確立され、データベース関数とロールがインストールされ、テーブルが有効化され、アドミッション、ブリッジ、スイーパーが起動し、デバイス招待が発行されます。初期化コマンドを異なるレイアウトから組み合わせるのではなく、選択したデプロイガイドに従ってください。
起動前または設定変更後に、サービスと同じ環境とクレデンシャルで bridge --diagnose を実行します。これはデータベース、NATS、およびブリッジの状態を読み取りますが、変更はしません。Compose では:
docker compose exec bridge sh -c \
'set -a; . /etc/zebridge/.env.nats; set +a; bridge --diagnose'
レポートの解釈には OPERATIONS を使用してください。これには、サービスが利用できないためにスキップされたチェックも含まれます。診断は設定とヘルスチェックであり、すべてのデバイスが収束したことの証明ではありません。アップグレードの場合、receipt upgrade order に従ってください。新しいブリッジワーカーを起動する前に、対応する SQL をインストールしてください。
テーブルの準備
zebridge_enable() を使用して、各テーブルのレプリケーションと書き込みルールを宣言します。その dry_run モードは、テーブルを有効にする前にスキーマ要件を報告します。デプロイ全体の確認には bridge --diagnose を実行します。
書き込み可能なテーブルには、キー、バージョン、テナント、および削除に関する慎重な選択が必要です。クライアント生成 UUID はオフライン挿入をサポートします。バージョンスタンプは競合する書き込みを解決します。トゥームストーンは、削除されたデバイスのために削除を保持します。行の幅とスキーマ記述子は、ブリッジの設定バッファと NATS ペイロード制限に適合する必要があります。
データベーススキーマ要件、マイグレーション、および 設定とサイジング を参照してください。
スケーリングと可用性
サポートされているアーキテクチャは、オフライン対応クライアント、オプションのリージョナルシードリーフ、および 単一のアクティブハブ(PostgreSQL、NATS、ブリッジ、ミューテーションアドミッション、スイーパー)で、監視付き再起動下で行われます。これは自動高可用性ではなく、重複コンテナを実行しても安全に高可用性にはなりません。オフライン操作は時間を稼ぎますが、以下の保持およびストレージ制限をなくすわけではありません。
データベースあたり1つのブリッジデプロイが推奨設定です。 それ以上は制約付きの設定であり、異なるテーブルセット、分離されたワークロードまたはデプロイメントライフサイクル、または1つの PostgreSQL データベースからの別個の NATS デプロイメントなどの利点があります。これは HA ではなく、同じテーブルを追跡する2番目のブリッジは有用なスループットを追加せず、追加のブリッジごとにレプリケーション、スナップショット、および調整コストが増加します。制約(データベースあたり1つの共有 GC ポリシーと1つのスイーパー、テーブルあたり1つの発行)は DEPLOYMENT にあります。
本番環境のレイアウト(すべての3つの ZeBridge プロセスを実行する Compose および Ansible ハブ。上記の評価用クイックスタートにはアドミッションプロセスがありません。MUTATION_ADMISSION=1 がない場合、ブリッジは直接書き込みを受け取り、Admission 行はそれに適用されません)で、1つのコンポーネントの障害がどのように影響するか。各行は scripts/scenarios/outage_matrix.py によって再生されます(コンポーネントがダウンしている間の影響と、それが復帰したときのハブの収束方法)。ダウンしている期間は、キューの保持とストレージの制限に依存しますが、これはまだ負荷下での検証が必要です。
| 利用不可 | 予想される影響 |
|---|---|
| Bridge | PostgreSQL の変更がクライアントに到達しなくなり、ミューテーション実行と生成ビルドが一時停止する。 |
| Admission | 新しいミューテーションがキューに入らなくなり、クライアントはそれらをローカルに保持してリトライする。 |
| Sweeper | クリーンアップが一時停止し、障害の長さとともにストレージ圧力が上昇する。 |
| Hub NATS | ハブ CDC およびミューテーション転送が停止する。リーフは、信頼性-陳腐化ポリシー (LEAF_NODES) の範囲内で、キャッシュされたシードと到達可能なリージョナルレスポンダーを提供できる。 |
監視、バックアップ、ハブの別マシンでの再構築、および自動フェイルオーバーに必要なもの: A supported deployment。リーフはシード容量を追加しますが、ハブの可用性は追加しません。
フィットと境界
ZeBridge は、モバイルアプリ、断続的に接続されるデバイス、フィールド機器、およびローカル SQL 読み取りから恩恵を受けるサービスに適しています。depot example はトラック、共有計画、ルーティングを使用します。同じパターンが、接続不良期間中に動作する機器にも適用されます。
- レプリケーションはテナントベースです。 フォローされたテーブルは、テナントの行をレプリカにロードします。ローカル SQL の
WHERE句は、デバイスが受信するものだけでなく、アプリが読み取るものをフィルタリングします。B2C スケールでのコンシューマーあたりのテナントは、意図されたトポロジーの範囲外です。 - PostgreSQL が権威です。 オフライン書き込みは、サーバーによって判断されるまで暫定的なものです。これはピアツーピアまたはマルチマスターレプリケーションではありません。
- 競合セマンティクスは明示的です。 通常の書き込みはラストライターウィンを使用します。協調編集はフィールドごとのレジスタをマージできます。これは協調テキスト編集ではありません。COOPERATIVE_EDITING を参照してください。
- リカバリには保持要件があります。 CDC 保持、チェーン可用性、トゥームストーン GC、およびミューテーション領収書保持は、異なる目的を果たします。読み取りが回復できるオフライン期間は、すべての古い保留中の書き込みが実行できることを自動的に許可するわけではありません。GC フロアより前にスタンプされた書き込みは拒否されます。なぜなら、それを上書きするトゥームストーンが失われている可能性があるからです。catching up および indeterminate write recovery を参照してください。
- ローカル永続性が重要です。 レプリカの削除やアプリの再インストールは、送信されていない書き込みを破壊する可能性があります。デバイス ID とストレージをアプリケーションデータとして保護してください。
- NATS と PostgreSQL を意図的に操作してください。 クレデンシャル、レプリケーションスロット、ディスク予算、バックアップ、および復元手順は、デプロイメントの一部です。
境界については SCOPE を、テナント分離、権限、および取り消しについては SECURITY を参照してください。
例
| 探索 | 例 |
|---|---|
| ローカル読み取り、楽観的書き込み、および拒否されたミューテーション | Tables demo |
| 大規模レプリカとモバイルシーディング | Large table |
| DuckDB レプリカ | DuckDB |
| 負荷下でのミューテーションと分析クエリ | Events |
| レスポンダーを介したルーティング | Valhalla routing |
| 共有計画、地図、ルーティング | Depot, running live |
| 1つのレコードを複数の場所から編集 | Shared record |
| 読み取りを Parquet にアーカイブ | Archive |
テストとパフォーマンス
TEST_SCENARIOS は、シナリオとその内容を説明しています。リポジトリには、クライアントフィクスチャ、データベースと認証チェック、リカバリシナリオ、および負荷ハーネスが含まれています。実装について参照ドキュメントと意見が異なる場合は、コードとテストを参照してください。
CDC の配布、スナップショットのダウンロード/適用、ミューテーションコミット、およびアウトボックスリカバリを個別に測定してください。これらは異なるボトルネックを持っています。結果は、ビルド、データベースの遅延、ディスク、レーン数、プリンシパル数、およびリカバリ負荷に依存します。過去のピークはデプロイメント容量の保証ではありません。event ramp および storm harness は、トポロジーの評価の出発点を提供します。SPEED_TEST は、そのマシンと負荷が記載された測定例です。
測定済み
各数値は、指定されたハードウェアでの1回の実行であり、容量の保証ではありません。
本番ハブ、0.2.10 で。 ブリッジ、アドミッション、スイーパー、および NATS を1つの VPS(6 vCPU、11.7 GB、4 イングレスレーン)で実行。PostgreSQL はロンドンの Supabase で実行(ハブからのクエリあたり17 ms)。クライアントは1台の Mac で実行(ハブから23 ms)、公開されている Python パッケージを使用。約200バイトの行が10,000行のテーブル。測定日: 2026-11-02。
| 方向 | 内容 | 数値 |
|---|---|---|
| PostgreSQL → クライアント | 10クライアントがテーブルをシード | 0.98 秒(中央値)、レプリカが既にシードされていた場合は0.35秒で同期 |
| PostgreSQL → クライアント | 100,000行を1つのステートメントに挿入 | ブリッジはそれらを約46,700イベント/秒で発行。コミット後2.95秒で1クライアントがすべてを保持 |
| 両方向 | 1クライアントの書き込みが PostgreSQL を介して全10クライアントに到達 | 中央値 117–121 ms |
| クライアント → PostgreSQL | 書き込みの判断と応答、1クライアント | 263 書き込み/秒 |
| クライアント → PostgreSQL | 10クライアント | 1,414 書き込み/秒 |
| クライアント → PostgreSQL | 50クライアント、それぞれ400件を一度に送信 | 1,447 書き込み/秒、中央値結果 3.2 秒 |
書き込み数値の意味。 これらはハブの合計であり、すべてのテナントで共有されます。すべての書き込みは同じイングレスレーンを通過し、それぞれが PostgreSQL への接続は1つなので、テナントが増えても容量は増えません。イングレスレーンが増えると容量が増える可能性がありますが、データベースのラウンドトリップが支配的になるまでです(比較では8レーンを測定)。実行では1つのテナントを使用しましたが、これは設計されたトポロジーではありませんが、他のものが書き込んでいない場合に1つのテナントがどれだけ処理できるかを示しています。実行中、PostgreSQL とブリッジはほぼアイドル状態でした。書き込みバッチはデータベースへのラウンドトリップを必要とするため、ブリッジと PostgreSQL の間の距離が上限を設定します。1,450 書き込み/秒 は、14,500台のデバイスが10秒ごとに1つの書き込みを送信することに相当します。
レート制限は、1つのテナントが取得できるものを決定します。 デフォルトでは制限はありません(MUTATION_RATE_PER_PRINCIPAL=0)、テナントはハブが処理できる上限全体を使用できます。設定されている場合、これは各プリンシパルおよび各テナントに適用されます。毎秒100件(バースト200件、このハブの設定)の場合、1つのテナントはハブが処理できる量に関わらず、毎秒約100件の書き込みに制限されます。これらの実行では2,000件(バースト4,000件)を使用し、書き込みは遅延しませんでした。Write limits, as a client sees them を参照してください。
1台のマシン、および以前のデプロイメントで。
| 内容 | 数値 | 条件 | 詳細 |
|---|---|---|---|
| ブリッジ、PostgreSQL → NATS | Docker で約10万イベント/秒、ネイティブで20万以上 | Apple M2 Pro、200万行のバースト、PostgreSQL と NATS が同じホストで実行 | SPEED_TEST |
| ライブでビジーなテーブルを適用するクライアント | libzb 約5万イベント/秒、zb-client-ts 約3.3万 | 1台の Mac、安定した書き込み負荷下のテーブル | |
| レプリカへの変更到達 | 中央値 12 ms | DuckDB エンジン上の libzb、1台の Mac 上の開発スタック | |
| 電話での大規模テーブルのシード | libzb 35.0 秒(React Native)、70.8 秒(Flutter)。JavaScript クライアント 725 秒 | iPhone 12、同じテーブル | Large table |
| クライアントが PostgreSQL に書き込む | 1つのイングレスレーンで約8,500書き込み/秒、4つで約19,500 | 1台の Mac、同じマシンで PostgreSQL 実行、0.2.7 より前の耐久性のあるミューテーション領収書 | the ramp |
| 500クライアントが同時に起動、空のレプリカ | すべて同期完了。最も遅いものは8.6〜12秒。NATS は140〜165% CPU、クライアントあたり約1 MB。PostgreSQL は16% CPU未満。ブリッジはほぼアイドル状態。 | 2 vCPU、3.7 GB VPS、クライアントはハブから100 ms 離れている | storm |
| その500クライアントに1つの書き込みが到達する | 中央値 288 ms、p95 491 ms(ネットワークで100 ms) | 同上 | |
| 500クライアント下で NATS を再起動 | なし損失。半分は6秒以内に復帰、最後は23秒。 | 同上 | |
| 大西洋を越えたリーフ経由の100クライアント | コールドシード中央値 13.2 秒、最遅 14.7 秒。ウォームシード 1.7 秒。NATS 再起動後、最長ギャップ 2.9 秒、損失なし。 | カナダの0.2.8テストハブ、クライアントはヨーロッパの Mac 上(約100 ms) | |
| 安定したストリーム下でのブリッジのメモリ | 約8 MB 常駐、70分間割り当てはフラット | 20,246回の挿入、更新、削除のティック | scripts/scenarios/drip.py |
PowerSync との比較は Compared with PowerSync にあります。
測定されていないもの: 4つ以上のイングレスレーンを持つ本番ハブの書き込み上限(比較サーバーでは8レーンのみ測定)。リカバリに再シードが必要になる前に各コンポーネントがダウンしている可能性のある時間。安定した負荷下での NATS メモリ(保存データではなく最近の負荷に従います)。負荷下での長時間のネットワークパーティション。
ドキュメント
| タスク | リファレンス |
|---|---|
| PostgreSQL のインストールと接続 | DEPLOYMENT |
| 診断、チューニング、アップグレード、バックアップ、リカバリ | OPERATIONS |
| クライアントライブラリの統合 | CLIENTS |
| ID の登録、更新、取り消し | AUTHENTICATION |
| 権限とテーブルルールの理解 | SECURITY |
| リージョナルキャッシュとレスポンダーの追加 | LEAF_NODES |
| スキーマとデータセットの変更 | MIGRATIONS |
| メトリクスとログの設定 | OBSERVABILITY_TELEMETRY |
| ワイヤーとリカバリ契約の理解 | PROTOCOL, GENERATION (テナントおよびパブリックストリームにおけるクライアントの位置) |
| プラットフォームアーティファクトとパッケージングの確認 | DISTRIBUTION |
| スコープとテスト済み動作の評価 | SCOPE, TEST_SCENARIOS |
| 複数の場所から1つのレコードを編集 | COOPERATIVE_EDITING |
設計ログを読む(コードコメントが参照する番号付き § セクション) | NOTES |
ソースからのビルド
公開されているパッケージとイメージが最も簡単な出発点です。実装に取り組むには、リポジトリの Zig 0.17 ツールチェーンの要件と build.zig.zon の依存関係ピンを使用してください。NATS クライアントはサブモジュールです: フォーク ndrean/nats.zig、ブランチ zebridge(アップストリームにパッチを追加、各コミット1つ)。プラットフォームビルドとパッケージングについては DISTRIBUTION を、ブリッジがデコードする pgoutput バイナリ形式については PG を、ツリー、ビルド、テストコマンドのマッピングと実際のテストとして機能するシナリオについては CLAUDE.md を参照してください。
ZeBridge は Apache 2.0 ライセンスの下で提供されます。