AI 能寫前端了,但後端呢?
這是許多開發者用 AI 助手做全棧應用時會遇到的瓶頸。前端 UI、組件頁面,Copilot 和 Claude 這些工具已經能夠不錯地完成。但後端涉及數據庫、認證、實時同步、離線支援,這些基礎架構並不容易用幾行提示詞就能生成。
Instant 1.0 想解決這個問題。這是一個專為 AI 編碼應用打造的後端,宣稱能讓你的 coding agent 直接變成全棧應用建構者。
三個核心承諾
Instant 的主張可以歸結為三個核心優勢:無限應用、同步引擎、內建服務。
先從無限應用說起。傳統部署模式下,你要麼付費購買 VM,要麼受服務商限制。很多免費服務會限制你建立的應用數量,閒置時凍結,解凍可能要花上 30 秒甚至幾分鐘。
Instant 的解決方案是多租戶架構。當你建立新專案時,他們不會啟動新的 VM,而是在一個多租戶實例中插入幾行資料庫記錄。如果應用沒有活動,完全不產生運算或記憶體成本。活躍時,只需要幾 KB 的額外 RAM 開銷,對比 VM 的幾百 MB,差距巨大。
這意味著你可以真正建立無限數量的應用。他們在文章中示範了一個隔離後端的建立過程,整個流程包括往返時間,只需要幾百毫秒。你會得到一個用於識別後端的公開 App ID,以及用於特權操作的私密 Admin Token。
這給了你一個關聯式資料庫、同步引擎,以及其他內建服務,如認證和儲存。把無限應用和 agents 結合起來,你的開發方式會開始改變。現在你可以用 agents 生成很多應用,有了 Instant,你再也不會因為部署問題而被阻擋在生產環境之外。
同步引擎:讓應用現代化
應用建好了,但怎麼讓它變得好用?
建傳統 CRUD 應用很容易。讓 agent 接上資料庫遷移、後端端點、客戶端 store,就能跑起來。但讓應用令人喜愛,又是另一回事。
對比傳統 CRUD 應用和現代應用:Linear、Notion、Figma。現代應用是多人協作的,支援離線,而且感覺很快。在 Linear 裡改了一個 todo,其他地方也會同步更新。在 Notion 裡離線,你還能標記文件。在 Figma 裡為形狀上色,不需要等伺服器,你就能立刻看到結果。
這些應用需要自訂基礎架構。實時功能需要狀態化的 websocket 伺服器。離線模式需要在 IndexedDB 存快取。樂觀更新則需要在客戶端處理如何套用和取消變更。
Linear、Notion、Figma 都建了自己的基礎架構來處理這些。業界稱這類架構為同步引擎。開發者撰寫 UI 和查詢資料時,就像資料在本地可用一樣。同步引擎在底下處理所有資料管理。
如果現代應用需要同步引擎,那你不應該每次都從頭建立。
Instant 在內部建了一個通用的同步引擎。每個應用預設就有多人協作、離線模式和樂觀更新。這對建構者有好處:程式碼更易懂、更容易維護。對使用者有好處:他們得到一個令人喜愛的應用。對你的 agents 也有好處:同步引擎是緊密的抽象,agents 可以用它寫出更簡潔的程式碼,使用更少的 token,犯更少的錯誤。
內建服務
資料同步只是開始。應用通常需要的不僅僅是資料同步。
舉例來說,現在每個打開示範應用的人看到的都是同一組 todos。如果我們想加入認證或權限呢?我們可能還想支援檔案上傳,或「誰在線上」區塊。或者我們加入了一個 AI 助手,就需要用來向客戶端串流 token 的基礎架構。
這些是大多數應用都需要的常見功能。但通常我們需要把不同服務串接在一起才能得到它們。這不僅麻煩,還會引進一層複雜度。當你管理多個服務時,你就管理了多個資料來源。
為了讓增強應用更容易,Instant 把一堆常見服務烘焙在內部。每個服務都被設計成一個單一整合系統,一起運作。
讓我們再看一次 todo 應用,這次加入檔案上傳支援。
傳統做法會是什麼?我們會先在交易資料庫中建立一個 files 表,連結到 todos。但然後我們需要儲存實際檔案 blobs,可能會加入 S3。
一旦加入 S3,我們就要處理多個資料來源。舉例來說,如果我們刪除一個 todo,就需要執行一個背景 worker 來清理 S3 中對應的 blob。
有了 Instant,這一切都不是問題。
你預設就得到 File Storage,檔案物件只是資料庫中的資料列。它們就像任何其他實體:你可以建立它們,把它們連結到其他資料,對它們執行實時查詢。
這意味著你甚至可以建立 CASCADE 刪除規則,你可以說「刪除 todos 時也刪除檔案」。不需要背景 worker。取而代之的是一個整合的資料庫。共享基礎架構在底下處理所有邊緣情況。
而且這只是 Instant Storage。你還得到 Auth。你可以用 Magic Codes、OAuth 和 Guest Auth,開箱即用。而且當使用者註冊時,他們也只是資料庫中的資料列。
如果你想分享游標、打字指示器或「誰在線上」標記,你可以用 Instant Presence。
如果你需要分享持久的串流,你得到 Instant Streams。
他們在 recipes 頁面有很多實際範例可以玩。你會注意到大多數這些服務需要很少設定和很少程式碼。你和你的 agents 都能更快動作,讓應用功能豐富。你不需要尋找不同供應商,處理雙向資料同步。
代理能做的事,你也能做
這篇文章中,你可能會好奇,這些示範是怎麼運作的?
Instant 完全可程式化。你可以透過 API 或 CLI 建立應用、推送 schema、更新權限。這篇文章使用 API,但你的 agents 可能會用 CLI。
大多數時候,你不需要點擊任何儀表板。你的 agents 可以代表你執行動作。
現在,希望你夠興奮到願意註冊。
技術上你甚至不需要註冊就能玩,但他們注意到如果你註冊了,你更有可能留下來。所以他們真的鼓勵你註冊。
而且如果你想讓你的 agents 馬上開始玩 Instant,你可以做幾件事:
# 這會為你腳手架一個新的啟動專案
# 在 NextJS, Tanstack, Bun, Vite, 或 Expo 中
# 你的 agent 會有建構所需的一切
npx create-instant-app
# 如果你有一個現有應用,也可以加入我們方便的技能
# 告訴你的 agent 做一些新功能
npx skills add instantdb/skills
架構解析
Instant 有三個獨特的地方:Client SDK、Clojure 後端、多租戶資料庫。
你的應用直接向 Client SDK 發送查詢和交易。它負責離線解析查詢,並在你製作交易時立即套用。
Client SDK 然後跟 Clojure 後端通訊。Clojure 後端保持查詢實時。它接收交易,找出哪些客戶端需要知道。它也實作所有額外服務:權限、認證、presence、儲存、串流。
最後,Clojure 後端向單一 Postgres 實例發送查詢和交易。他們把 Postgres 當作多租戶 Triple store,用 App ID 邏輯隔離每個資料庫。
這是系統的草圖。讓我們更深入一點。
Client SDK 的設計
Client SDK 背後的設計受到兩個限制驅動:我們需要一個支援離線的系統,而且需要支援樂觀更新。
讓我們從最明顯的盒子開始。如果我們想在離線時顯示應用,我們需要一個地方讓資料在重新整理後繼續存在。
對網頁來說你沒有太多選擇。IndexedDB 是最好的候選。你可以儲存很多 MB 的資料,甚至有一些有限的查詢能力。
所以選擇了 IndexedDB。下一個問題是,那裡儲存什麼類型的資料?
考慮一個像「顯示所有開放的 todos 和它們的附件」這樣的查詢。這是在 Instant 中寫它的方式:
{
todos: {
$: { where: { done: false } },
attachments: {},
},
}
如果只是想要一個唯讀快取,可以儲存伺服器返回給我們的任何東西。但不是只想要唯讀快取。
需要客戶端在伺服器確認之前回應動作。舉例來說,如果使用者新增一個 todo,查詢應該就立即更新。
這意味著客戶端需要理解查詢。所以客戶端真正需要的是一個資料庫本身。一個能夠處理 where 子句(即「done 是 false」)和關聯(「todos 和它們的附件」)的資料庫。
一個選擇是用 SQLite。可以在那裡儲存正規化表,像 todos 和 files,然後對它們跑 SQL。但這太重了。SQLite 大約是 300 KB gzipped。對大多數應用來說,加這麼重的依賴不合理。
經過一些調查後,他們發現了 Triple stores 和 Datalog。
Triple stores 讓你以 [entity, attribute, value] 元組儲存資料。這是 todos 在 Triple store 中的樣子:
Team Tasks / #42
Ship!
這個統一結構可以同時建模屬性和關聯。一旦資料以這種方式儲存,就可以用 Datalog 對它進行查詢。
Datalog 是一個基於邏輯的查詢引擎。它的樣子:
InstaQL
{ todos: { $: { where: { done: false } } } }
Datalog
[?todo "done" ?val]
[?todo ?attr ?val]
語法看起來奇怪,但 Datalog 很強大。它可以像 SQL 一樣支援 where 子句和關聯。而且簡單實作。事實上,你可以在少於一百行程式碼內寫一個基本的 Datalog 引擎。
所以建立了一個 Triple store 和 Datalog 引擎。這讓可以在客戶端完全評估查詢,不必等待伺服器。
如果使用者建立一個新的 todo,有需要重新執行查詢並立即觀察到變更的所有東西。嗯,幾乎。還需要一個方法把變更套用到查詢。
不能直接變更結果。還要顧慮到伺服器。
舉例來說,如果伺服器拒絕交易會怎樣?如果變更了查詢結果,就沒辦法取消變更。
這就是 Pending Queue 登場的地方。當使用者做變更時,不直接套用到 Triple store。而是在一個獨立的佇列中追蹤變更。
為了滿足任何查詢,可以把待處理變更套用到 triple store,看到結果:
Triple store + Pending Queue = 結果
這個選擇推動把 Triple store 設計成不可變的。這樣可以套用變更並產生一個新的 Triple store,而不是變更已提交的那個。為了讓這個工作,把 transact API 包裝在 mutative 裡,一個在 JavaScript 中處理不可變變更的程式庫。
有了這個,就有 undo。如果伺服器返回失敗,就從待處理佇列中移除變更,undo 自動運作。
你可能注意到 Instant 查詢看起來不像 Datalog。它們是用一種稱為 InstaQL 的語言寫的:
{
todos: {
$: { where: { done: false } },
attachments: {},
},
}
做這個是因為認為應用查詢資料最符合人體工學的方式,是描述它們想要的回應形狀。
這個想法受到 GraphQL 啟發。主要差異是語法糖。不是引入特定文法,InstaQL 建構在普通 javascript 物件之上。這個選擇讓使用者可以跳過建置步驟,還可以程式化生成查詢。
Reactor:客戶端協調器
現在有了一個 Client SDK 的完整視圖!
使用者寫 InstaQL 查詢,被轉成 Datalog。查詢由 Triple stores 滿足,結合來自待處理佇列的變更。資料被快取到 IndexedDB。
這僅僅是從兩個限制產生的有趣選擇!
客戶端的最後一個問題是:所有這些盒子怎麼串在一起?
這就是 Reactor 登場的地方。它是協調所有不同程序的主要狀態機。當應用想要一個查詢,Reactor 負責看 IndexedDB,跟伺服器通訊。它處理網路離線或待處理變更失敗的時候。
Reactor 透過 websockets 跟伺服器通訊。它發送查詢和交易的請求,伺服器發送結果和資料庫的新資料。
這帶我們到伺服器。
Clojure 後端的設計
後端背後的設計受到兩個限制驅動:需要讓查詢反應式,而且需要對多租戶資源公平。
系統看起來大概像這樣:
Query Store
先想想當使用者要求一個查詢時會發生什麼。
伺服器可以先去問資料庫。在一個無狀態系統中,這差不多就是故事的結尾。返回回應,收工。
但記得,查詢必須是反應式的。為了那個需要一個地方儲存哪個使用者做了哪個查詢。這就是 Query Store 的用途:
如果只是追蹤查詢和提出它們的 socket 連線,原則上就有讓應用反應式所需的一切。舉例來說可以 tail 每個交易並重新整理每個查詢。這會運作,但資料庫會被大量 spam 濫炸。
理想情況下,應該只改變需要改變的查詢。
Topics
四處尋找想法,發現 Asana 的 Luna 和 Figma 的 LiveGraph 背後的架構很有前景。Asana 寫到他們如何把查詢轉換成「topics」的集合。大致上,一個 topic 描述查詢關心的索引部分。
對於像「給我所有 todos」這樣的東西,可以想像一個說「追蹤 TodosIndex 的所有更新」的 topic。
把這個想法調整到他們系統中。當跑查詢時,也生成一組它關心的 topics:
這裡是「Watch all todos」的 topic:
現在有一個資料結構可以用來描述查詢的依賴。下一步是追蹤交易並找出受影響的查詢。
Invalidation
這就是 invalidator 登場的地方。invalidator 追蹤 Postgres 的 WAL(Write-Ahead Log)。
可以取 WAL 條目並從它們生成 topics。舉例來說,如果有一個像「Set todo.done = false for id = 42」這樣的更新,可以轉換它:
這得到跟查詢製作的完全相同種類的 topic 結構。現在可以配對它們,發現什麼過期了:
這個演算法的版本零非常沒有效率。會有效地從每個交易 topic 到每個查詢 topic 做 N^2 比較。但可以直觀感覺這些 topic vectors 適合索引。把它們保持在樹狀結構中。只比較子集,並提早修剪。
有了那個,可以取一個 WAL 條目並基於它們重新整理查詢。下一步是平行化。
Grouped Queues
因為資料庫是多租戶,WAL 包含來自多個應用的更新。
為了讓無效演算法運作,單一應用內的交易必須串行且有序處理。但可以在不同應用間平行化無效操作。
需要某種方式保證單一應用內的順序並跨應用平行化。還需要確保一個高流量應用不會霸佔所有資源。
這就是 Grouped Queue 抽象登場的地方:
每個應用得到自己的子佇列。這保證特定應用的所有項目都被串行處理。
Workers 可以從多個不同子佇列取。這讓可以跨應用平行化無效操作。
當把一個 WAL 條目推到 grouped queue 時,它被加到應用的子佇列,但子佇列的全局順序不改變。這讓即使一個應用每秒加幾千項,其他應用仍然有相等機會被 invalidator 撿起。
這個資料結構對他們非常有用,滲透到整個程式碼庫,包括 Session Manager。
Session Manager,以及對 Clojure 和 JVM 的讚美
這帶我們到系統內的主要協調器。當 Client SDK 開啟一個 websocket 連線,是 session manager 撿起訊息:
Session Manager 的工作是把所有東西黏在一起。它做反應式查詢,跑權限,傳遞請求到其他服務。
注意 Grouped Queue 抽象也在這裡出現。如果不同客戶端開始轟炸後端,Grouped Queue 確保盡可能平行化,並防止一個壞 socket 霸佔所有資源。
在這裡暫停並讚美 Clojure 和 JVM 可能是對的。它們在建立這個基礎架構上對他們是巨大的勝利。
首先,Clojure 有很棒的並發原語,有真正的 threads。這讓可以用更大的機器擴展更遠,幫助避免過早分割系統。抽象也非常簡單易於組合。grouped queue 舉例來說只有 215 行程式碼。
其次,JVM 有繁榮的生態系統,真的享受這些程式庫。舉例來說,需要一個方法讓使用者在 Instant 內定義權限。想要一個快速且容易 sandbox 的語言。經過一些搜尋,發現了 Google 的 CEL。幸好有 CEL Java 可用,可以直接選用。
第三,Clojure 很適合 DSL 和實驗性程式設計。開始建立 Instant 時需要發現很多這些抽象,在 REPL 裡玩對它們很重要。
很多人嘲笑 DSL 但認為沒有它們沒辦法建立 Instant。多租戶查詢就是個好例子。需要讓資料庫多租戶。為了那個需要寫一些相當複雜的 SQL。不是手動做,做了一個 DSL,既容易推理,又保證可以傳入 App ID。
這帶我們到多租戶資料庫。
多租戶資料庫
資料庫也受到兩個限制驅動:需要一種方法便宜地啟動新資料庫,而且需要它是關聯式的。
最後到達的地方:
Triples Table
從這個問題開始:如何讓使用者建立很多不同資料庫?
最直接的路徑會是啟動 Postgres VMs。但如提到,VMs 有很多 RAM 開銷。如果啟動 VMs 沒有可持續方式支援無限應用。
另一個選擇會是用 Postgres schemas。可以為不同應用建立不同表,然後保持誰可以看什麼的映射。這會運作,但 Postgres 不是被設計來跟 tables 很好地擴展。從研究看到大約 6000 tables 後,Postgres 開始有問題:磁碟上建立的檔案數量有問題,pg_dump 和 autovacuum 開始失敗。
這說得通。平均 Postgres 應用有一些大表,不是很多小表,這意味著大表被優化。嗯,如果大表行,如果把問題重構成一個巨大表?
這帶我們回到…Triple stores!
它們在客戶端運作好是因為它們是一個支援關聯查詢的簡單 DB。認為這在 Postgres 中也可以運作好。所以在 Postgres 中加入了一個 triples 表:
所有資料都在一個 triples 表中,用 app_id 邏輯隔離。
如果想從 blog 應用取得 post_1,可以生成一個大概像這樣的 SQL 查詢:
select *
from triples
where app_id = 'blog' and entity_id = 'post_1' and attr_id in (posts/id, posts/title)
有了那個,建立新資料庫實際上是免費的。正如示範中提到,只是資料庫中的幾列。
令人驚喜的好處
這個選擇帶來了一些令人驚喜的好處。
因為自己管理 columns,能夠優化開發者體驗。
舉例來說,Postgres 建立列時鎖定表。因為自己實作列,可以讓它們無鎖定。
在 Postgres 刪除列時,資料就沒了。但在 agents 世界認為這太危險。所以在列層級實作了軟刪除。即使一個流氓 agent 刪除你的列,你可以 undo 並在幾毫秒內拿回所有資料。
這些是好處,當然也有代價。
Partial Indexes
考慮一個說「我想要我的 posts 有唯一的 slug」的使用者。在 Postgres 建立唯一列很容易。但既然實作自己的列,需要自己來做。
這就是 partial indexes 救場的地方。可以在 triples 表加入布林標記:
table_name: triples
app_id | entity_id | attr_id | value | column_unique | ...
有了那個,可以為整個表建立一個 partial index,被標記開啟:
create unique index unique_columns
on triples(app_id, column, value) where column_unique
現在如果使用者試著插入兩個有相同 slug 的 posts,unique_columns index 觸發並阻止!
而且這個同樣技巧讓查詢更有效率。如果想找 slug ‘hello’ 的 posts,可以生成這個查詢:
select entity_id
from triples
where app_id = 'blog' and attr_id = 'slug' and value = 'hello' and column_unique;
可以把這個模式延伸到整個範圍的查詢:唯一列、索引、日期、引用等等。
剛開始只用 partial indexes 並依賴 Postgres 做對的查詢對他們運作良好。但達到幾億 tuples 規模後,Postgres 開始有麻煩。
Count-Min Sketches
如果你是讀這個的 Postgres 專家,可能看那個 triples 表時停了一下。在 Postgres 圈子這稱為 EAV 模式,通常不鼓勵。
不鼓勵是因為 Postgres 依賴表和列做統計。
那些統計讓查詢規劃器決定哪些索引最有效、哪些 joins 用什麼順序。
一旦把所有資料存在一個表,Postgres 失去關於資料集中潛在頻率的資訊。它不能分辨有 10 個不同值的列和有 10 百萬個的。
為了解決,開始追蹤自己的統計。使用一個稱為 count-min sketches 的資料結構,幫助估計列頻率。如果好奇怎麼運作,寫了一篇文章。
可以把那些統計給查詢引擎,讓查詢再次有效率。
Query Engine
這帶我們到查詢引擎。
到目前為止展示的 SQL 查詢都很簡單易懂。但想像翻譯更複雜的 InstaQL 查詢。甚至只有一個 where 子句的查詢就會有 CTEs。然後會想用那些統計決定開啟哪些索引。
這就是查詢引擎做的。它接收 InstaQL 查詢以及 count-min sketches,生成 SQL 查詢計畫:
InstaQL
{
todos: {
$: {
where: {
done: false
}
}
}
}
Postgres
WITH done_triples AS (
SELECT entity_id
FROM triples
WHERE app_id = 'instalinear'
AND attr_id = 'todo-done'
AND value = 'false'
),
todo_data AS (
SELECT t.entity_id, t.attr_id, t.value
FROM triples t
JOIN done_triples d
ON t.entity_id = d.entity_id
WHERE t.app_id = 'instalinear'
)
SELECT * FROM todo_data
這個引擎用 Clojure 後端寫。從 Postgres 自己的查詢引擎取了很多靈感。有時這些查詢看起來嚇人地長,但對 Postgres 能多好處理它們真的驚訝。傳入一些提示用 pg_hint_plan,Postgres 就運作並產生結果。
四年的心血
這涵蓋了資料庫,這涵蓋了整個系統!
希望你覺得有趣!這是愛的勞動。建立 Instant 因為想要驅動下一代建構者。任何產品,都用 Instant 建立,數千開發者信賴他們跑核心基礎架構。
如果你用 agents 建構,認為你會喜歡用他們。
希望給他們一個嘗試,加入他們的 Discord。
這是一個專為 AI 時代設計的後端,把多租戶資料庫、同步引擎、認證、儲存等基礎架構整合成一個統一系統。對於想用 coding agents 快速迭代、大量創造應用的開發者來說,這是一個值得關注的選擇。