The supabase-js Client
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”createClient สร้าง object เดียวที่แอปของคุณใช้คุยกับ Postgres, Auth, Storage และ Realtime — และควร initialize ด้วย public anon key เสมอ ไม่ใช่ service_role key เพราะสิ่งที่ปกป้องข้อมูลจริง ๆ คือ row level security policy ใน schema ไม่ใช่ความลับของ key ตัวนี้
Initialize client
หัวข้อที่มีชื่อว่า “Initialize client”ทุกแอป supabase-js เริ่มแบบเดียวกันหมด import createClient ใส่ project URL กับ key เข้าไป แล้วได้ object กลับมาที่มี .from(), .auth, .storage และ .channel() พร้อมใช้
import { createClient } from '@supabase/supabase-js';
// Safe to ship in browser bundles: this is the anon key, not the service_role keyconst supabase = createClient( process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!);ข้อนี้ควรพูดซ้ำให้ชัดเพราะสลับกันผิดง่ายมาก client ฝั่ง browser หรือ mobile ต้อง initialize ด้วย anon key เสมอ anon key ถูกออกแบบมาให้เป็น public อยู่แล้ว key นี้ติดไปกับ JavaScript bundle ที่ใครก็อ่านได้ และนั่นไม่ใช่ปัญหา สิ่งที่กันไม่ให้คนแปลกหน้าอ่านหรือเขียน row ที่ไม่ควรแตะได้คือ row level security (RLS) บน table ของคุณ ไม่ใช่ความลับของ key ตัวนี้ ส่วน service_role key เป็นคนละเรื่องเลย key ตัวนี้ bypass RLS ทั้งหมด ซึ่งทรงพลังมากและเป็นเหตุผลว่าทำไมห้ามให้ไปโผล่ในโค้ดฝั่ง client เด็ดขาด ควรอยู่แค่ใน server environment ที่เชื่อถือได้ (เช่น Edge Function หรือ backend process) ที่ไม่มีทางไปถึง browser
Generated type เปลี่ยน .from() ให้กลายเป็น typed API
หัวข้อที่มีชื่อว่า “Generated type เปลี่ยน .from() ให้กลายเป็น typed API”Supabase CLI อ่าน schema จริงของคุณแล้ว generate TypeScript Database type ที่ตรงกันให้
supabase gen types typescript --project-id abcdefghijklmnopqrst > database.types.tsคำสั่งนี้ inspect ทุก table, column และ constraint ในโปรเจกต์ แล้ว generate type ที่มี shape Row, Insert และ Update ให้แต่ละ table Database['public']['Tables']['movies']['Row'] คือ column ที่ select จะคืนกลับมาเป๊ะ ๆ Insert คือหน้าตาของ row ใหม่ที่ valid และ Update คือหน้าตาของ partial patch เอา type นี้ไปเป็น generic ให้ createClient ก็จะผูก query builder ทั้งหมดเข้ากับ type นั้นทันที
import { createClient } from '@supabase/supabase-js';import type { Database } from './database.types';
const supabase = createClient<Database>( process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!);
// Autocomplete on table names, column names, and the shape of dataconst { data, error } = await supabase .from('movies') .select('id, title, year');จากจุดนี้ไป พิมพ์ชื่อ column ผิด, select table ที่ไม่มีอยู่จริง หรือส่ง payload ของ insert ที่ขาด field ที่จำเป็น จะกลายเป็น TypeScript error ตอน compile แทนที่จะเป็นเรื่องเซอร์ไพรส์ตอน runtime พอ schema เปลี่ยน ก็แค่รัน CLI command เดิมซ้ำเพื่อ regenerate database.types.ts แล้ว type ของทั้งแอปก็จะ sync กับ database ทันที
Client เดียว สร้างครั้งเดียว
หัวข้อที่มีชื่อว่า “Client เดียว สร้างครั้งเดียว”supabase-js client มักจะถูกสร้างแค่ครั้งเดียวแล้ว reuse ทั่วทั้งแอป — แบบ singleton — แทนที่จะสร้างใหม่ทุก request หรือทุก component render
// lib/supabase.ts — created once, imported wherever it is neededimport { createClient } from '@supabase/supabase-js';import type { Database } from './database.types';
export const supabase = createClient<Database>( process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!);มีเหตุผลจริงสองข้อ ข้อแรก client เก็บ session state ไว้ใน memory (และใน local storage) การสร้างใหม่จะทิ้งหรือทำ state นั้นซ้ำซ้อน แล้วอาจทำให้ auth listener ทำงานไม่สอดคล้องกัน ข้อสอง client จัดการ network resource ของตัวเอง รวมถึง WebSocket connection ที่ Realtime ใช้ — สร้าง client ใหม่ทุก component แปลว่าได้ connection ซ้ำซ้อนแทนที่จะเป็น connection เดียวที่แชร์กันและ reuse ได้ การ import instance เดียวกันไปใช้ทุกที่แก้ปัญหาทั้งสองข้อนี้พร้อมกัน
flowchart LR
schema[("Your Postgres schema")] --> gen["supabase gen types typescript"]
gen --> types["Generated Database type (database.types.ts)"]
types --> client["createClient<Database>()"]
client --> query["Typed .from() queries with autocomplete"]