Serverpod 4「Jetstream」正式リリース — フルスタックホットリロードとCRDTオフライン同期の実像

はじめに

2026年9月14日、Dart 製フルスタックフレームワーク Serverpod 4「Jetstream」 が正式リリースされた。7月の public beta から2ヶ月、100以上の新機能を積んでのメジャーバージョンだ。r/FlutterDev でも「Serverpod 4 is out!」のスレッドが伸びている。

見出しに並ぶのは3つ——フルスタックホットリロード、エージェンティックコーディング対応、そしてオフライン同期。前者2つは開発体験の話で、分かりやすい代わりに評価が難しい。この記事で掘り下げたいのは3つ目のほうだ。Flutter アプリのオフライン対応は長らく「自前でキューを書くか、PowerSync のような外部同期エンジンを買うか」の二択だった。Serverpod 4 はそこに delta-CRDT という解を持ち込んだ。何が解けたのか、そして代わりに何を諦めることになるのかを整理する。

serverpod start が全部の面倒を見る

まず開発体験側から。Serverpod 4 の入口は serverpod start という単一コマンドに統合された。これ1つでバックエンド・データベース・Flutter アプリが1つの環境として起動する。

地味だが効くのが埋め込み PostgreSQL だ。従来 Serverpod のローカル開発は Docker Compose で Postgres と Redis を立てる前提だったが、4.0 では dataPath を指定すればバンドルされた Postgres が起動する。dart test を回すときの前準備も消える。Docker を入れていない環境に「とりあえず動かしてみて」と渡せるようになったのは、OSS フレームワークとしては大きい。

ホットリロードは、データモデルを変更した際にコード再生成とデータベース更新までが走ったまま行われる。公式ブログの表現はこうだ。

In most cases, Serverpod can even preserve your application and backend state throughout the process.

“In most cases” と条件つきで書かれている点は正直に読んでおきたい。状態が常に保たれるわけではなく、保たれない条件は明記されていない。Flutter のホットリロードと同じ温度感——大半は通るが、通らないケースがある——で期待値を置くのが妥当だろう。

エージェンティックコーディング対応は、MCP サーバーと agent skills が同梱されるという形で実装されている。AI エージェントがサーバーログを読み、エンドポイントを追加し、Flutter 側の UI を変更して、その結果をホットリロードで即座に確認する、というループが回る想定だ。エージェントにとって「変更 → 反映 → 観測」の待ち時間が消えることの価値は、人間が感じるそれより大きい。

技術的な核 — 同じコードを SQLite と Postgres の両方で走らせる

本題のオフライン同期。serverpod_offline_sync パッケージが担当し、モデル定義に1行加えるだけで有効になる。

  • database: sync … クライアント(SQLite)とサーバー(Postgres)の両方にテーブルを生成。主キーは UuidValue(defaultPersist=random_v7、つまり UUIDv7)に固定され、offline_sync_spaces への cascade 関係として spaceId 列が自動で足される
  • database: client … クライアント側のみに生成される非同期テーブル

設計上の肝は、クライアントとサーバーで同一のコードが走るという点にある。従来の自前同期レイヤーで最も厄介なのは、デバイス側のマージロジックとサーバー側のマージロジックが別実装になり、同じ入力から非対称な結果が出ることだった。同じ delta-CRDT エンジンを両側で動かせば、この非対称性が原理的に発生しない。

CRDT(Conflict-free Replicated Data Type)が満たすべき3つの数学的性質——冪等性・結合性・可換性——を守る形で、変更履歴は4つのメタデータテーブルに記録される。

テーブル役割
crdt_data_rows挿入と可視性
crdt_data_fieldsフィールド単位の更新
crdt_data_foreign_key試行された外部キー値と実際に適用された値
crdt_data_tombstone削除と復元。単調な因果長により削除/復元の振動を防ぐ

重要なのは、これらが行データそのものをコピーしないことだ。履歴だけを持つ。

そして競合の扱い方に思想が出ている。このエンジンは競合を黙って解決しない。人間の判断が要る競合は、ユーザーから見える状態として浮上する。README が挙げる例が分かりやすい——並行する制約によって削除が取り消されたレコードは再び画面に現れ、ユーザーはもう一度削除すればいい。最終書き込み優先で静かにデータが消えるより、はるかに扱いやすい失敗の仕方だ。

オーバーヘッドの実測値も公開されている。SELECT が 0.38%、INSERT が 231μs/操作、UPDATE が 1.88ms/操作、DELETE が 408.75μs/操作。ストレージは元のサイズの 62〜300% 増。マイクロ秒〜ミリ秒のレイテンシはネットワーク遅延に埋もれる水準だが、ストレージが最大4倍になる点は容量見積もりに効いてくる。

代償はスキーマ設計に前払いで来る

ここが実務上いちばん重要なところだ。CRDT 同期を有効にすると、スキーマ側に無視できない制約が課される。

  • ユニークインデックスは spaceId を含めなければならない。グローバルなユニーク制約は(外部キー専用のものを除き)サポートされない。加えて、String / UuidValue / nullable のいずれかの列が最低1つ必要
  • onDelete: Restrict は使えない。NoAction への置き換えが必須
  • 非 nullable な外部キー関係は deferred 宣言が必須
  • 1:1 関係では外部キー列を nullable にしなければならない
  • 同期テーブルと非同期テーブルをまたぐ関係は、spaceId 経由を除いて基本的に未サポート

つまり「既存のスキーマにオフライン同期を後付けする」というシナリオは、素直には通らない。設計の最初から sync 前提でテーブルを切る必要がある。メールアドレスのグローバル一意制約のような、ごく普通の要件がそのままでは表現できない点は先に把握しておきたい。

加えてバージョンにも注意が要る。serverpod_offline_sync は Serverpod 4.0.0 を要求しつつ、自身のバージョンは 0.0.6。公式も本番投入前の段階であり破壊的変更があり得ると明記している。Serverpod 4 本体のリリース記事でも offline sync は experimental 扱いだ。

エンジニアへの影響 — 選択肢の中でどこに置くか

Dart バックエンドの選択肢は依然として3層に分かれる。Shelf は「信頼できる HTTP レイヤーが欲しい」、Dart Frog は「儀式の少ない REST API が欲しい」、そして Serverpod は「Flutter アプリと深く統合された全部入りが欲しい」に答える。4.0 はこの一番右の立ち位置をさらに強化した格好で、軽量さを求めるなら Dart Frog という判断は変わらない。

同期エンジンとしての比較軸も別にある。PowerSync や ElectricSQL はバックエンド DB に依存しない汎用の同期エンジンで、Postgres / MongoDB / MySQL を選べる代わりに、モデル定義・ORM・クライアント生成は自前で組む。Serverpod の CRDT 同期は Serverpod に強く縛られる代わりに、1つのモデル定義から ORM・型付きクライアント・同期設定が同時に出てくる。すでに Serverpod を使っているなら後者の統合度は明確な利点だが、実績で選ぶなら本番投入例の多い PowerSync のほうが安全側だ。

既存プロジェクトのアップグレードについても数字を見ておきたい。公式ガイドは 3.4.x からの移行を 約15分 と書いているが、serverpod_docs の issue #803 によれば 4.0 の changelog にある破壊的変更は16件で、アップグレードガイドがカバーしているのは4件(記事執筆時点)。実際に踏むものとしては、モデルファイルの .spy.yaml 拡張子必須化(.yaml / .yml は無視される)、ServerpodClientException の sealed 化(statusCode == -1 の廃止)、postMessage の既定が MessageScope.auto に変わった点あたりが影響範囲の広いところだ。SDK 要件も Dart 3.12.2 / Flutter 3.44.4 以上に上がっている。15分は「最短経路が通ったとき」の数字として読むのが安全だろう。

まとめ

  • Serverpod 4「Jetstream」が2026年9月14日に正式リリース。serverpod start でサーバー・DB・Flutter アプリが1環境として起動し、埋め込み Postgres により Docker が不要になった
  • 目玉のオフライン同期は delta-CRDT 実装。クライアントの SQLite とサーバーの Postgres で同一コードを走らせることで、マージ結果の非対称性を原理的に排除している
  • 競合は黙殺されずユーザーから見える状態として浮上する。実測オーバーヘッドは SELECT 0.38%・INSERT 231μs と小さい一方、ストレージは最大300%増
  • 代償はスキーマ制約。グローバルなユニーク制約が使えず、onDelete: Restrict も不可。後付けは難しく、最初から sync 前提の設計が要る
  • 同期パッケージは 0.0.6 で experimental。アップグレードの「15分」は16件の破壊的変更のうち4件しかガイド化されていない前提で見積もること

「バックエンドも Dart に寄せたい」Flutter プロジェクトにとって、Serverpod 4 は現実的な候補として一段階上がった。ただしオフライン同期を理由に選ぶなら、スキーマ制約を先に机上で当ててから決めるべきだ。制約が通るかどうかは、実装を始める前に分かる。

今日のその他のニュース

Flutter + iOS 26 Liquid Glass タブバーの実装知見(r/FlutterDev) — iOS 26+ では純正 UITabBar を PlatformView でホストし、Android / 旧 iOS は Flutter 側で自前描画。スクロール連動の縮小を両方に適用する際、UiKitView のサイズを直接アニメーションさせるとゴーストが出るため別アプローチが必要だった、という報告。ネイティブアプリの Flutter 化で必ず踏む地雷のひとつ。

Baseten の本番 GitHub org に25分で admin 到達(Strix) — コンテナレジストリ Harbor のイメージレイヤーから長期有効な GitHub PAT を読み出した攻撃経路のレポート。イメージにシークレットが焼き込まれる典型的なミスが AI インフラ企業の本番環境でも起きていた。

「ローカルLLMにレビューさせた話」の続報 — 実は動いていなかった(Zenn) — トークン節約でコードレビューをローカル LLM に移した構成が、実際にはレビューを実行していなかったという訂正記事。出力が出ている=処理が走っている、ではない。ログで呼び出しを確認するまで成功と判定しない原則の実例。

ソース