Supabase 架構與元件深入解析 | Supabase 完整教學
Supabase 的後端不是一個大黑箱,而是一群圍繞 PostgreSQL 運作的獨立開源服務。本篇拆開架構圖,帶你看清楚請求如何從 API Gateway 一路流經 PostgREST、GoTrue、Realtime、Storage 到達資料庫,再看懂為什麼「所有服務共用一個 Postgres」是這套設計的靈魂。
前言
一句話定義本篇主題:Supabase 的架構是一套「以 Postgres 為圓心、由多個開源微服務環繞」的系統,而 API Gateway 是它們對外的唯一大門。
上一篇《平台總覽》我們建立了「六大服務圍繞資料庫」的心智模型。這一篇要更進一步——把每一個方框打開,看裡面到底裝了什麼、彼此怎麼協作、一個請求從你的 App 送出到拿回資料,中間經過了哪些關卡。
如果要用現實世界的類比,你可以把 Supabase 想成一座大型辦公大樓。大樓只有一個「大廳警衛台」(API Gateway)——所有訪客都得先在這裡刷卡(驗證 API 金鑰)、說明要找哪個部門(路由)。通過之後,警衛會把你帶到對應的樓層:接待部門(Auth)、檔案室(Storage)、廣播室(Realtime)、臨時工作間(Edge Functions)、或是資料查詢櫃台(REST API)。而所有部門的檔案,最終都存放在地下室那間唯一的中央檔案庫(Postgres)。部門本身不各自藏一份資料,它們只是不同的「取用視窗」,背後指向的是同一個檔案庫。
這個「唯一的中央檔案庫」是理解 Supabase 架構的鑰匙。很多人第一次接觸會誤以為 Auth 有自己的資料庫、Storage 有自己的資料庫——其實沒有,它們全都寫進同一個 Postgres,只是各自佔用不同的 schema。
本篇你會學到:
- 請求的完整旅程:從 App 到 Gateway,再到各服務,最後抵達 Postgres 的每一步
- 每個元件的真面目:PostgREST、GoTrue、Realtime、Storage、Edge Functions 各自的職責與底層技術
- 受管服務背後的開源專案:每個雲端服務對應哪個 GitHub 倉庫,以及自架時它們如何組裝
- 「單一真相來源」如何落地:各服務怎麼靠不同 schema 共用同一個資料庫
核心概念
一張圖看懂請求的旅程
先把整體架構攤開。與其記憶零散的服務名稱,不如記住這張「請求流向圖」——資料是怎麼從你的應用程式一路流到 Postgres,再流回來的。
┌─────────────────────────────────────────────────────────┐
│ 你的應用程式 │
│ (Web / Mobile / Server / Edge) │
└───────────────────────────┬─────────────────────────────┘
│ HTTPS / WSS
│ apikey + Authorization: Bearer <JWT>
┌───────────────────────────▼─────────────────────────────┐
│ API Gateway(Kong / Envoy) │
│ 金鑰驗證 · 路由 · Rate Limiting · CORS · TLS 終止 │
└──┬──────────┬──────────┬───────────┬───────────┬─────────┘
│/rest/v1 │/auth/v1 │/storage/v1│/realtime/v1│/functions/v1
▼ ▼ ▼ ▼ ▼
┌────────┐┌────────┐┌──────────┐┌──────────┐┌────────────┐
│PostgREST││ GoTrue ││Storage API││ Realtime ││Edge Function│
│(Haskell)││ (Go) ││ (Node.js) ││ (Elixir) ││ (Deno) │
└───┬────┘└───┬────┘└─────┬─────┘└────┬─────┘└──────┬─────┘
│ │ │ │ │
│ auth │ storage │ 讀 WAL │ service_ │
│ schema │ schema │ │ role 連線 │
└─────────┴───────────┴───────────┴─────────────┘
│
┌───────────▼───────────┐
│ PostgreSQL │ ← 單一真相來源
│ public / auth / │ (含 RLS、Extensions)
│ storage schema、RLS │
└───────────┬───────────┘
│
Supavisor(連線池,取代 PgBouncer)
看這張圖時,請抓住三個重點:
- 只有一個入口:不論你要打哪個服務,第一站永遠是 API Gateway。它是安全與路由的統一關卡。
- 服務是平行的、獨立的:PostgREST、GoTrue、Realtime、Storage、Edge Functions 彼此不互相呼叫,它們是各自獨立的行程(甚至用不同語言寫成)。
- 終點都是 Postgres:無論走哪條路,資料最終都落在同一個資料庫。差別只在它們寫進不同的 schema、用不同的方式讀取(HTTP 查詢 vs 讀取 WAL 日誌)。
第一關:API Gateway(Kong / Envoy)
所有外部請求的統一入口就是 API Gateway。在 Supabase 的演進中,早期版本使用 Kong,較新版本改用 Envoy,兩者職責相同。它負責的工作包括:
- API 金鑰驗證:檢查請求 Header 裡的
apikey,區分這是公開的anon(匿名)金鑰,還是可繞過 RLS 的service_role(服務角色)金鑰。 - 路由:依照網址路徑,把請求分派到對應的後端服務(見下方路由表)。
- Rate Limiting(速率限制):防止濫用與 DDoS。
- CORS 處理:管理跨來源資源共享政策。
- TLS 終止:處理 HTTPS / WSS 的 SSL 解密。
Gateway 的路由規則非常直觀,就是「看路徑分流」:
/rest/v1/* → PostgREST(資料庫自動 REST API)
/auth/v1/* → GoTrue(身份驗證)
/storage/v1/* → Storage API(物件儲存)
/realtime/v1/* → Realtime(WebSocket 即時訂閱)
/functions/v1/* → Edge Functions(Deno 執行環境)
這裡有個關鍵觀念:API 金鑰不等於身份驗證。apikey Header 只是告訴 Gateway「你有權存取這個專案」,真正代表「你是誰」的是 Authorization: Bearer <JWT> 這個 Header 裡的 JWT。前者由 Gateway 驗證,後者則由後端服務(尤其是 PostgREST)拿去交給 Postgres 做 RLS 判斷。兩者常一起送出,但職責完全不同——這是後面實作與除錯時很常搞混的點。
各元件真面目與其開源專案
通過 Gateway 之後,請求會被送到對應的服務。Supabase 最迷人的地方在於:每一個雲端受管服務,背後都對應一個獨立、可自架的開源專案。下表把它們一次對齊:
| 服務(產品名) | 底層開源專案 | 語言/技術 | GitHub 倉庫 | 一句話職責 |
|---|---|---|---|---|
| Database | PostgreSQL | C | supabase/postgres | 資料庫核心,唯一真相來源 |
| REST API | PostgREST | Haskell | PostgREST/postgrest | 把 schema 反射為 RESTful API |
| Auth | GoTrue | Go | supabase/gotrue | JWT 發行、OAuth、MFA |
| Realtime | Realtime | Elixir/Phoenix | supabase/realtime | WebSocket、WAL 訂閱 |
| Storage | Storage API | Node.js | supabase/storage-api | S3 相容物件儲存 |
| Edge Functions | Deno runtime | TypeScript/Deno | supabase/edge-runtime | 全球邊緣執行函式 |
| API Gateway | Kong / Envoy | — | 對應各自專案 | 統一入口、路由、驗證 |
| 連線池 | Supavisor | Elixir | supabase/supavisor | 多租戶 Postgres 連線池 |
這張表回答了一個常見疑問:「Supabase 到底是不是黑箱?」不是。你在雲端點幾下就能用的功能,換到自架環境,其實就是把上面這幾個開源專案用 Docker Compose 拼起來(官方自架方案約 13 個服務)。理解這一點,你就不會把 Supabase 當成不透明的服務,而是「一組你隨時能看原始碼、能替換的積木」。
接著逐一認識這些積木:
PostgREST(自動 REST API,Haskell) 這是最能體現 Supabase 哲學的元件。PostgREST 讀取 Postgres 的 schema,自動把資料表、視圖、函式反射成一組 RESTful 端點,你完全不需要手寫任何後端。它的運作流程是:
- 讀取 Postgres schema,動態產生 HTTP 端點
- 收到請求後,把 HTTP 查詢翻譯成 SQL
- 從
AuthorizationHeader 取出 JWT,設定到 Postgres 的 session 變數 - Postgres 套用 RLS 政策,回傳過濾後的資料
它支援過濾(?status=eq.pending)、排序、分頁、透過外鍵自動 JOIN、以及呼叫預存函式(/rest/v1/rpc/函式名)。你之後用的 supabase-js 客戶端,底層打的就是 PostgREST 的端點。
GoTrue(身份驗證,Go)
GoTrue 是一台以 Go 撰寫的 JWT 身份驗證伺服器,負責註冊、登入、Token 發行與更新。它支援 Email/密碼、Magic Link 無密碼登入、OTP、近 20 種 OAuth 社交登入、SAML 企業 SSO 與 MFA。重點在於它把使用者資料存在 Postgres 的 auth schema(auth.users、auth.sessions 等表),而不是另外一個資料庫。這代表你的業務資料表可以用外鍵直接指向 auth.users——這是 NoSQL 世界做不到的。它發行的 JWT 裡含使用者 UUID、Email、角色與自訂聲明,正是 PostgREST 拿去做 RLS 判斷的依據。
Realtime(即時訂閱,Elixir/Phoenix) Realtime 是以 Elixir/Phoenix 打造的 WebSocket 伺服器。它與其他服務最不同的地方在於:它不是靠 HTTP 查詢資料庫,而是直接讀取 Postgres 的 WAL(Write-Ahead Log,預寫日誌)。當資料表發生 INSERT/UPDATE/DELETE,變更會寫入 WAL,Realtime 透過邏輯複製監聽這些變更,再透過 WebSocket 推送給訂閱的客戶端。它提供三種機制:監聽資料表變更的 Postgres Changes、低延遲 pub/sub 的 Broadcast、以及追蹤在線狀態的 Presence。選用 Elixir 是因為 BEAM 虛擬機天生擅長維持海量並發連線。
Storage API(物件儲存,Node.js)
Storage 提供 S3 相容的物件儲存,用來放圖片、影片、文件。它與純 S3 最大的差異在於存取控制走 Postgres 的 RLS:檔案的中繼資料(檔名、大小、MIME 類型、擁有者)存在資料庫的 storage schema,你可以用 SQL 查詢,也可以用 RLS 政策控制誰能上傳、下載。實際的檔案位元組存在物件儲存後端,但「誰能碰它」這件事由 Postgres 決定。
Edge Functions(邊緣函式,Deno)
Edge Functions 是在全球邊緣節點執行的 TypeScript/JavaScript 函式,執行環境是 Deno(不是 Node.js)。它用來處理自動 API 表達不了的自訂邏輯,例如串接第三方金流、寄 Email、Webhook。它通常持有 service_role 金鑰、在後端環境安全地繞過 RLS 存取資料庫。
Supavisor(連線池,Elixir) 最後補上一個容易被忽略、但生產環境很關鍵的元件。Postgres 每個連線都需要一個後端行程,在 Serverless 或 Edge 場景下,每次請求都新建連線會迅速耗盡連線上限。Supavisor 是 Supabase 自研的多租戶連線池(取代舊版 PgBouncer),透過重用連線解決這個問題。它有 Session 模式(埠 5432)與 Transaction 模式(埠 6543),後者適合 Serverless,但不支援 Prepared Statements,用 ORM 時要特別留意。
資料如何以 Postgres 為單一真相來源
把上面拼起來,就能回答本篇最核心的問題:這麼多服務,資料怎麼保持一致? 答案是它們共用同一個 Postgres,只是各自寫進不同的 schema:
| Schema | 由誰管理 | 存什麼 |
|---|---|---|
public | 你(開發者) | 你的業務資料表(todos、orders、profiles…) |
auth | GoTrue | 使用者、Session、身份提供者 |
storage | Storage API | Bucket 與檔案中繼資料 |
realtime | Realtime | 訂閱與發布設定 |
因為它們在同一個資料庫,你能做到跨 schema 的外鍵關聯與聯合 RLS 政策——例如業務表 todos.user_id 直接指向 auth.users.id。這就是「單一真相來源」的實際落地:使用者這個概念只存在一處,其他服務都以它為準,不需要在多個系統間同步。RLS 政策寫在資料庫層,因此無論請求走 REST、走 SDK、走 Realtime 還是 Edge Function,套用的都是同一套規則。
關鍵術語小結:
- schema(結構描述):Postgres 用來組織資料表的命名空間。Supabase 各服務用不同 schema 共用同一個資料庫。
- WAL(Write-Ahead Log,預寫日誌):Postgres 記錄所有資料變更的日誌,Realtime 靠讀它來推送即時事件。
- JWT(JSON Web Token):GoTrue 發行、代表使用者身份的權杖,PostgREST 拿它做 RLS 判斷。
- 連線池(Connection Pooling):Supavisor 重用資料庫連線,避免 Serverless 場景耗盡連線數。
實作範例
概念講完,我們用一個最能體現架構的實驗來收尾:建一張表,觀察同一份資料如何同時被「自動 REST API」與「supabase-js SDK」存取——兩條看似不同的路徑,其實都通過 Gateway,最終打到同一個 PostgREST、同一個 Postgres。
步驟一:用 SQL 建表,API 自動出現
在 Supabase Studio 的 SQL 編輯器貼上以下 SQL。建完表的那一刻,PostgREST 就已經自動幫它生成好 API 了,你不需要做任何額外設定。
-- 建立一張 notes 資料表
CREATE TABLE notes (
id bigserial PRIMARY KEY,
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
content text NOT NULL,
created_at timestamptz DEFAULT now()
);
-- 開啟 Row Level Security(務必第一步就開)
ALTER TABLE notes ENABLE ROW LEVEL SECURITY;
-- 政策:使用者只能讀取自己的筆記
CREATE POLICY "Users can read own notes."
ON notes FOR SELECT
USING ( user_id = auth.uid() );
注意 user_id uuid REFERENCES auth.users(id) 這一行:業務表直接用外鍵指向 GoTrue 管理的 auth.users。這正是「單一真相來源」在程式碼裡的樣子——因為它們在同一個 Postgres,才能這樣關聯。
步驟二:用原始 REST API 查詢(PostgREST 直球)
先用最底層的方式,直接 curl 打 PostgREST 的端點,感受 Gateway → PostgREST → Postgres 這條路徑。注意這裡送了兩個 Header:apikey(給 Gateway 驗證)與 Authorization(帶 JWT,給 Postgres 做 RLS)。
# 直接呼叫自動生成的 REST 端點(毋須任何後端程式碼)
curl 'https://<你的專案ref>.supabase.co/rest/v1/notes?select=id,content&order=created_at.desc' \
-H "apikey: <你的 anon key>" \
-H "Authorization: Bearer <使用者的 JWT>"
# 回傳:只包含當前使用者的筆記(RLS 已在資料庫層過濾)
# [{"id":2,"content":"架構筆記"},{"id":1,"content":"第一則筆記"}]
步驟三:用 supabase-js SDK 查詢同一份資料
現在換成官方 SDK。你會發現寫法完全不同,但底層打的是同一個 PostgREST 端點、同一個 Gateway、同一個 Postgres——SDK 只是把 curl 那串 URL 與 Header 包裝得更好用。
import { createClient } from '@supabase/supabase-js'
// anon key 可安全放前端,因為真正的防線是 RLS
const supabase = createClient(
'https://<你的專案ref>.supabase.co',
'<你的 anon key>'
)
async function getNotes() {
// 這行底層等同步驟二的 curl,走 /rest/v1/notes
const { data, error } = await supabase
.from('notes')
.select('id, content')
.order('created_at', { ascending: false })
if (error) {
console.error('查詢失敗:', error.message)
return
}
console.log('我的筆記:', data) // 輸出:只有自己的筆記,RLS 自動過濾
}
getNotes()
這個實驗的重點是:REST 與 SDK 是同一個服務的兩張臉。你沒有寫任何後端,資料庫一變(新增欄位、改政策),兩條路徑的行為就一起跟著變。這就是「以 Postgres 為單一真相來源、由 PostgREST 自動反射」的威力。
常見錯誤與最佳實踐
初學者理解 Supabase 架構時,最常踩的幾個坑:
誤解一:「各服務有各自獨立的資料庫,我要在它們之間同步資料。」
不對,而且這是最關鍵的誤解。Auth、Storage、Realtime 通通寫進同一個 Postgres,只是佔不同 schema。你不需要「把使用者從 Auth 同步到業務資料庫」——它們本來就在同一個資料庫,直接用外鍵關聯 auth.users 即可。把架構想成「一個資料庫、多個視窗」,而不是「多個資料庫要對帳」。
誤解二:「有了 apikey 就代表通過身份驗證了。」
apikey 只是 Gateway 層的專案存取憑證,它不代表使用者身份。真正代表「你是誰」的是 Authorization Header 裡的 JWT。常見的除錯陷阱是:只送了 apikey 沒送 JWT,結果 RLS 把 auth.uid() 當成 NULL,查不到任何資料還以為是 bug。正確做法是理解這兩個 Header 職責不同,登入後要帶上使用者的 JWT。
誤解三:「service_role key 只是另一把比較方便的鑰匙。」
不是。service_role 金鑰完全繞過 RLS,等於資料庫超級管理員。它只能存在後端(Edge Functions、你的伺服器環境變數),絕不可出現在前端或 Gateway 對外的請求裡。一旦洩漏,整個資料庫門戶大開。
誤解四:「忽略連線池,Serverless 直接連資料庫就好。」 在 Serverless 或 Edge 場景下,每次請求新建直連會迅速耗盡 Postgres 的連線上限。正確做法是走 Supavisor 的 Transaction 模式(埠 6543),並記得它不支援 Prepared Statements,用 ORM(如 Prisma)時要對應調整設定。
最佳實踐總結:
- 把架構理解成「一個 Postgres + 多個環繞它的獨立服務」,資料只有一份
- 分清楚
apikey(Gateway 驗證)與Authorization: Bearer JWT(RLS 身份)兩個 Header - 前端只用
anonkey,service_rolekey 僅限後端 - Serverless/Edge 走連線池(Supavisor Transaction 模式),別直連
- 需要客製邏輯時用 Edge Functions,而不是把
service_role塞進前端
小結
這篇是 Supabase 系列教學的第 002 篇,我們把上一篇《平台總覽》的架構圖徹底拆開:
- API Gateway 是唯一入口:Kong/Envoy 負責金鑰驗證、路由、速率限制與 CORS,依路徑分流到各服務
- 每個服務都是獨立的開源專案:PostgREST(Haskell)自動生成 REST API、GoTrue(Go)做身份驗證、Realtime(Elixir)讀 WAL 推送即時事件、Storage(Node.js)管物件儲存、Edge Functions(Deno)跑自訂邏輯
- 受管即開源:雲端服務背後就是可自架的 GitHub 倉庫,用 Docker Compose 就能組起來
- Postgres 是單一真相來源:各服務用不同 schema(public/auth/storage)共用同一個資料庫,靠外鍵與 RLS 保持一致
apikey不等於身份:金鑰給 Gateway,JWT 給 Postgres 做 RLS,兩者職責分明
下一篇《雲端 vs 自架與定價》,我們會接著談:既然這些元件都是開源可自架的,那什麼時候該用 Supabase 雲端、什麼時候該自己用 Docker Compose 拉起這 13 個服務?雲端各方案(Free、Pro、Team)的定價與額度又該怎麼估算?把架構看懂之後,這些取捨會清楚許多。
跟著系列一路走,你會從「知道有哪些元件」進展到「知道它們怎麼協作、怎麼部署」。下一篇見。