supabase-js 客戶端入門 | Supabase 完整教學

2026/10/05
supabase-js 客戶端入門 | Supabase 完整教學

前面十五篇,我們一路用 SQL、curl 與裸的 .from()/.rpc() 認識了 Supabase 的資料庫與 REST API。但真正在寫應用時,你不會手刻 HTTP 請求——你會用官方的 supabase-js 客戶端。這一篇帶你正式認識這個 SDK:怎麼 npm install @supabase/supabase-js、怎麼用 createClient(url, anonKey) 建立一個 client、這個 client 底下 from/auth/storage/realtime/functions 五大子模組各管什麼,以及最關鍵的兩個實務問題——瀏覽器該用哪把金鑰、伺服器該用哪把,還有為什麼整個 App 只該建立一個 client(單例模式)。

前言

一句話定義本篇主題:supabase-js 是 Supabase 官方的 JavaScript/TypeScript 客戶端函式庫,讓你用一個 createClient 建立出來的物件,就能以一致的語法操作 Database、Auth、Storage、Realtime、Edge Functions 全部服務。你不需要為驗證裝一套 SDK、為儲存裝另一套、再手刻 fetch 去打資料庫 API——一個 client 全包了。

前十五篇我們把 Supabase 的資料庫底層(表、關聯、RLS、函式)與存取層(PostgREST 的查詢、篩選、關聯查詢、RPC)都摸熟了。但你有沒有發現,我們一直在用兩種「臨時手段」跟 Supabase 溝通:一種是 curl 直接打 REST 端點,另一種是零星地寫 supabase.from(...)、supabase.rpc(...),卻從沒好好說清楚那個 supabase 物件到底是怎麼來的、它是什麼。這一篇就是要把這個貫穿全系列的主角——supabase-js 客戶端——正式介紹給你。

用一個類比:如果說 Supabase 後端是一棟大樓,裡面有資料庫部門、驗證部門、檔案倉庫、即時通訊室、函式運算中心,那 supabase-js 的 client 就是這棟大樓的「總機兼門禁卡」。 你不必分別去每個部門的窗口排隊、也不必記得每個部門的內線電話——你手上這張卡(client)刷一下,就能把電話轉接到任一部門(.from/.auth/.storage/.realtime/.functions)。而這張卡有兩種等級:一般訪客卡(anon key)只能做被允許的事、進得了公共區域;管理員萬能卡(service_role key)能開所有門、繞過所有門禁——所以萬能卡絕不能隨便發,尤其不能發給大樓外的陌生人(前端瀏覽器)。

本篇你會學到:

  • 安裝與初始化:npm install @supabase/supabase-js、createClient(url, anonKey) 的簽名與最小範例
  • client 五大子模組:from/auth/storage/realtime/functions 各管什麼、如何組成一個統一介面
  • 兩把金鑰的分野:瀏覽器用 anon key、伺服器才用 service_role,以及它們的安全邊界
  • 環境變數放置:金鑰該放哪、NEXT_PUBLIC_ 前綴的意義、為什麼絕不能寫死在程式碼裡
  • 單例模式:為什麼整個 App 只該建立一個 client,以及怎麼做

核心概念

先把整個 client 的心智模型刻進腦中。createClient 回傳的那一個物件,內部結構長這樣:

createClient(url, anonKey)  ──►  supabase(一個統一 client 物件)
                                       │
        ┌──────────────┬───────────────┼───────────────┬──────────────┐
        ▼              ▼               ▼               ▼              ▼
  supabase.from    supabase.auth   supabase.storage  supabase.       supabase.
  ('table')                                          realtime /      functions
                                                     .channel()      .invoke()
        │              │               │               │              │
     資料庫查詢/寫入   使用者驗證      物件儲存         即時訂閱        Edge Functions
   select/insert/    signUp/         upload/          監聽變更/       呼叫 Deno
   update/delete     signIn/         download/        broadcast/      無伺服器函式
                     getUser         getPublicUrl     presence

(另外還有 supabase.rpc() 呼叫資料庫函式,上一篇《RPC 呼叫資料庫函式》已詳談。)

這張圖背後有三個關鍵觀念要說清楚。

一、一個 client,統一操作全部服務

supabase-js 是所謂的同構(isomorphic)函式庫——同一套程式碼可以跑在瀏覽器、Node.js、Deno、React Native 等各種環境。它最大的設計價值是統一:你只用 createClient 建立一個 client,就能透過它底下的子模組操作 Supabase 的每一塊服務,而且全部服務都回傳一致的 { data, error } 結構。這代表你只要學會一種錯誤處理與資料取用的風格,就能套用到查詢、驗證、上傳檔案、即時訂閱、呼叫函式所有場景,不必為每塊服務各學一套 API 慣例。

二、createClient 的簽名——兩個必填、一個選填

建立 client 的函式簽名如下:

createClient(
  supabaseUrl: string,   // 專案 URL,格式 https://<project-id>.supabase.co
  supabaseKey: string,   // 金鑰:anon key(前端)或 service_role key(後端)
  options?: SupabaseClientOptions  // 選填設定(db schema、auth 行為、global headers…)
): SupabaseClient
  • supabaseUrl:你專案的唯一網址,在 Supabase Dashboard 的 Project Settings → API 找得到。
  • supabaseKey:兩把金鑰擇一——這正是本篇的重點,下一節詳談。
  • options:選填,可設定預設 schema、Auth 的 token 自動刷新/session 持久化行為、附加的全域 headers 等。多數情況用預設值即可。

三、兩把金鑰,決定這個 client「能做什麼、能放哪裡」

同一個 createClient,你放不同的金鑰,得到的 client 權限與適用環境完全不同:

金鑰權限RLS放哪裡
anon key(匿名公鑰)受限,一般存取受 RLS 限制瀏覽器/任何前端(可公開)
service_role key(服務金鑰)完整,可繞過權限繞過所有 RLS僅限你的伺服器(機密,絕不外流)

關鍵術語一次看懂:

  • anon key:匿名(anonymous)公鑰,設計上就是可以公開放進前端的。它本身不帶特權,真正保護資料的是資料庫上的 RLS(Row Level Security)政策。
  • service_role key:服務角色金鑰,擁有繞過所有 RLS 的完整資料庫權限,等同資料庫萬能鑰匙,只能放伺服器端。
  • RLS(Row Level Security):Postgres 的列級安全政策,決定「哪個角色能讀寫哪些列」——這是前端安全的真正防線(本系列 SB-1 已介紹)。
  • 單例(Singleton):整個應用程式只建立並共用一個 client 實例的設計模式。

實作範例

以下範例假設你有一個 Node.js/前端專案,並已在 Supabase Dashboard 建好專案、拿到了 URL 與兩把金鑰。所有程式碼皆可直接使用。

步驟一:安裝 supabase-js

用你慣用的套件管理器安裝官方 SDK:

# npm
npm install @supabase/supabase-js

# 或 yarn
yarn add @supabase/supabase-js

# 或 pnpm
pnpm add @supabase/supabase-js

步驟二:設定環境變數(金鑰絕不寫死在程式碼)

第一條鐵律:金鑰永遠放環境變數,不要硬編碼(hardcode)進原始碼。 原因很簡單——把金鑰寫死在 .ts/.js 裡,一旦程式碼被推上 Git(尤其是公開 repo),金鑰就外洩了。正確做法是放在專案根目錄的 .env 檔(並把 .env 加進 .gitignore):

# .env(記得把這個檔案加入 .gitignore,不要 commit)

# 專案 URL 與 anon key —— 這兩個給前端用,可公開
NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here

# service_role key —— 只給伺服器用,機密,注意沒有 NEXT_PUBLIC_ 前綴
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here

這裡有個容易被忽略的細節:在 Next.js(及類似框架)中,只有加了 NEXT_PUBLIC_ 前綴的環境變數才會被打包送到瀏覽器端。所以 URL 與 anon key 用 NEXT_PUBLIC_ 前綴(它們本來就要給前端);而 SUPABASE_SERVICE_ROLE_KEY 刻意不加這個前綴——這樣它就只存在於伺服器端,永遠不會被打包進前端 bundle。這個前綴的有無,本身就是一道安全機制。

步驟三:建立瀏覽器端 client(anon key)

前端(React、Vue、Svelte,或任何會送到使用者瀏覽器的程式碼)一律用 anon key。把建立邏輯抽成一個獨立模組,只執行一次:

// lib/supabase.ts —— 全 App 共用的單一 client 實例
import { createClient } from '@supabase/supabase-js'

// 從環境變數讀取 URL 與 anon key(結尾的 ! 是 TypeScript 的非空斷言)
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!

// 建立並匯出「唯一」的 client,其他檔案都 import 這一個
export const supabase = createClient(supabaseUrl, supabaseAnonKey)

之後在任何元件或模組,只要 import { supabase } from '@/lib/supabase',就能用同一個 client。

步驟四:你的第一個查詢——from().select()

client 建好後,最基本的操作就是查資料庫。透過 supabase.from('資料表').select() 進入資料庫子模組:

import { supabase } from '@/lib/supabase'

// 從 posts 表撈出 id、title、created_at 三個欄位
const { data, error } = await supabase
  .from('posts')
  .select('id, title, created_at')

if (error) {
  console.error('查詢失敗:', error.message)
} else {
  console.log('拿到的資料:', data)
  // data 例如:[{ id: 1, title: 'Hello', created_at: '2026-10-05...' }, ...]
}

注意這個 { data, error } 的回傳結構——supabase-js 所有操作都是這個風格,成功時 data 有值、error 為 null;失敗時反過來。永遠先檢查 error 再用 data,是使用這個 SDK 最基本的好習慣。

步驟五:認識五大子模組

同一個 supabase 物件,換一個子模組就切到另一塊服務。以下各示範一小段,讓你感受「統一介面」的樣子:

import { supabase } from '@/lib/supabase'

// 1) from —— 資料庫查詢與寫入
const { data: posts } = await supabase.from('posts').select('*')

// 2) auth —— 使用者驗證與 session
const { data: authData } = await supabase.auth.signInWithPassword({
  email: 'user@example.com',
  password: 'secret-password',
})

// 3) storage —— 物件儲存(上傳檔案到 avatars bucket)
const { data: upload } = await supabase.storage
  .from('avatars')
  .upload('user-123/avatar.png', fileObject)

// 4) realtime —— 即時訂閱(監聽 messages 表的新增)
const channel = supabase
  .channel('room-1')
  .on('postgres_changes',
    { event: 'INSERT', schema: 'public', table: 'messages' },
    (payload) => console.log('新訊息:', payload.new)
  )
  .subscribe()

// 5) functions —— 呼叫 Edge Function
const { data: fnResult } = await supabase.functions.invoke('hello-world', {
  body: { name: 'Ben' },
})

看出來了嗎?五塊服務,同一個 client、同樣的 { data, error } 回傳風格。你不用切換 SDK、不用重學 API,這就是 supabase-js「統一介面」的威力。這五塊之後的篇章都會各自深入,本篇你只要建立起「它們是同一個 client 的子模組」這個地圖概念即可。

步驟六:建立伺服器端 client(service_role key)

有些操作需要繞過 RLS——例如後台的批次資料清理、排程統計、Webhook 處理。這時才在伺服器端用 service_role key 建立一個具管理權限的 client,並與前端 client 分開放:

// lib/supabaseAdmin.ts —— 只在伺服器端 import,切勿在前端使用!
import { createClient } from '@supabase/supabase-js'

// 注意:service_role key 沒有 NEXT_PUBLIC_ 前綴,只存在於伺服器端
const supabaseAdmin = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!,
  {
    auth: {
      autoRefreshToken: false,  // 伺服器端不需要自動刷新 token
      persistSession: false,    // 也不需要持久化 session
    },
  }
)

// 這個 client 繞過所有 RLS,能讀寫任何資料 —— 威力強大,責任重大
export { supabaseAdmin }

兩個重點:其一,伺服器端 client 通常會把 autoRefreshToken 與 persistSession 設為 false,因為後端不像瀏覽器需要維護使用者 session;其二——也是最重要的——這個檔案永遠只能在伺服器端程式碼被 import(Node.js 後端、Next.js 的 Server Component/Route Handler、Edge Function),絕不能出現在會被送到瀏覽器的任何路徑上。

常見錯誤與最佳實踐

坑一:把 service_role key 放進前端。 這是最致命的錯誤,沒有之一。service_role key 繞過所有 RLS,等於資料庫萬能鑰匙。一旦它出現在前端程式碼裡,打包後任何人都能在瀏覽器 DevTools 看到,進而讀寫刪你所有資料。正確做法:前端一律只用 anon key;service_role key 只放伺服器端環境變數(不加 NEXT_PUBLIC_ 前綴),且相關檔案只在後端 import。判準很簡單:這行程式碼打包後使用者看得到嗎?看得到就只能用 anon key。

坑二:以為 anon key 是「祕密」而不敢用在前端。 反過來的誤解也常見。anon key 本來就是設計成可公開的公鑰,放進前端 bundle 完全正常、也是官方推薦做法。它不帶特權,真正保護資料的是資料庫上的 RLS 政策。正確做法:放心把 anon key 放前端,但務必為每張表設好 RLS——安全靠的是 RLS,不是把 anon key 藏起來。

坑三:每次要用就 createClient 一個新 client。 在元件裡、在每支函式裡各自 createClient,會製造多個實例,各自維護獨立的 Auth session 與 token 刷新計時器、各開一條 Realtime WebSocket,導致登入狀態不同步、資源浪費。正確做法:用單例模式——把 createClient 抽到一個獨立模組只執行一次、export 出實例,全 App 都 import 同一個。

坑四:金鑰硬編碼寫死在原始碼裡。 把 URL、金鑰直接寫進 .ts 字串裡,方便一時,卻極易隨 Git 外洩。正確做法:一律走環境變數(.env + process.env),並把 .env 加進 .gitignore;部署時在平台(Vercel、Netlify、伺服器)的環境變數設定中填入。

坑五:環境變數沒設,client 拿到 undefined 就報奇怪的錯。 process.env.NEXT_PUBLIC_SUPABASE_URL 忘了設或前綴打錯,值會是 undefined,createClient 收到後拋出如 supabaseUrl is required 之類的錯,讓人一頭霧水。正確做法:確認 .env 有正確的鍵名與前綴、開發伺服器重啟過(環境變數改動通常要重啟才生效),必要時加一段啟動檢查,缺變數就明確拋錯提醒。

最佳實踐總結:

  • 一個環境一個 client(單例):createClient 抽成模組只跑一次,全 App 共用
  • 金鑰分前後端:前端 anon key,後端才 service_role,且各自獨立模組
  • 金鑰走環境變數:.env + .gitignore,絕不硬編碼
  • 安全靠 RLS:anon key 可公開,真正的防線是資料庫的 RLS 政策
  • 統一的錯誤處理:所有操作都是 { data, error },永遠先檢查 error

小結

這是 Supabase 系列教學的第 016 篇,也是第三大主題 SB-3「客戶端 SDK」的開篇。上一篇《RPC 呼叫資料庫函式》帶你跨出宣告式查詢、用一支 API 觸發資料庫端的程序邏輯;但我們一直沒好好認識那個貫穿全系列的主角——這一篇補上了:supabase-js 客戶端。

核心觀念濃縮成一句話:用 npm install @supabase/supabase-js 安裝、createClient(url, key) 建立出一個統一的 client,透過它底下的 from/auth/storage/realtime/functions 五大子模組以一致的 { data, error } 風格操作 Supabase 全部服務;瀏覽器用可公開的 anon key(安全靠 RLS),伺服器才用機密的 service_role key(繞過 RLS);金鑰一律走環境變數、絕不硬編碼;整個 App 只建立一個 client(單例模式)。

  • 一個 client 全包:五大子模組統一介面,回傳都是 { data, error }
  • 兩把金鑰要分清:anon key 給前端(可公開)、service_role 給後端(機密)
  • 安全靠 RLS 而非藏金鑰:anon key 本就可公開,RLS 才是資料防線
  • 金鑰走環境變數:.env + .gitignore,不硬編碼、按前綴區分前後端
  • 單例模式:createClient 只執行一次、全 App 共用同一個實例

認識了 client 這個總機之後,接下來就要把最常用的那一塊——資料庫子模組——徹底玩熟。下一篇《CRUD 查詢與寫入》,我們回到 supabase.from(),系統性地學會用 SDK 進行 select(查)、insert(增)、update(改)、delete(刪)四大操作,以及分頁、排序、單筆查詢等實戰技巧。下一篇見。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →