Supabase là gì? Hướng dẫn xây dựng Backend với PostgreSQL

Supabase là nền tảng Backend as a Service dựa trên PostgreSQL, tích hợp Database, Auth, Storage, Realtime và Edge Functions. Bài viết này hướng dẫn bạn xây dựng Backend đầu tiên với Supabase.

Supabase là gì? Hướng dẫn xây dựng Backend với PostgreSQL

Khi xây dựng một ứng dụng Web hoặc AI, phần giao diện thường chỉ là một nửa của bài toán. Chúng ta vẫn cần Database để lưu dữ liệu, Authentication để quản lý người dùng, Storage cho file và một lớp API để Frontend giao tiếp với Backend. Nếu tự xây dựng toàn bộ những phần này, thời gian để có một MVP có thể kéo dài khá nhiều.

Supabase giải quyết bài toán đó theo hướng Backend as a Service. Điểm mình thấy đáng chú ý là Supabase không tạo ra một Database riêng mà sử dụng PostgreSQL làm nền tảng. Vì vậy chúng ta có thể bắt đầu nhanh với Dashboard và SDK, nhưng vẫn giữ được khả năng viết SQL, tạo Index, Function, Trigger hay kết nối trực tiếp tới Database khi hệ thống lớn hơn.

Trong bài viết này mình sẽ đi qua Supabase là gì, các thành phần chính và xây dựng một Backend Todo đơn giản bằng PostgreSQL, Row Level Security và Python.

I. Supabase là gì?

Supabase là một nền tảng Backend as a Service, thường được nhắc tới như một lựa chọn thay thế Firebase theo hướng mã nguồn mở. Mỗi Project Supabase có một PostgreSQL Database riêng, sau đó các dịch vụ như Auth, Storage, Realtime và Edge Functions được tích hợp xung quanh Database đó.

Thay vì tạo REST API cho từng Table, Supabase có thể sinh Data API từ Database Schema. Frontend hoặc Backend sử dụng Client Library để thực hiện Query, còn quyền truy cập dữ liệu được kiểm soát bằng PostgreSQL Row Level Security, thường được viết tắt là RLS.

Một Project Supabase thường có các thành phần sau:

  • Database: PostgreSQL đầy đủ, hỗ trợ SQL, Relationship, Index, Function, Trigger và Extension.
  • Auth: đăng nhập bằng Email/Password, Passwordless, OAuth và nhiều Identity Provider.
  • Storage: lưu file theo Bucket, kết hợp với RLS để kiểm soát quyền truy cập.
  • Realtime: Broadcast, Presence và theo dõi thay đổi dữ liệu theo thời gian thực.
  • Edge Functions: chạy Server-side TypeScript trên Runtime tương thích Deno.
  • Data API: cung cấp REST API và GraphQL API từ PostgreSQL.

Điểm khác biệt quan trọng là PostgreSQL vẫn là trung tâm của hệ thống. Dữ liệu không bị giấu phía sau một lớp Database độc quyền. Nếu sau này cần dùng SQLAlchemy, kết nối bằng Driver PostgreSQL hoặc chuyển sang hạ tầng khác, chúng ta vẫn làm việc với một Database quen thuộc.

II. Khi nào nên sử dụng Supabase?

Supabase phù hợp khi mục tiêu là xây dựng MVP, Dashboard, SaaS, ứng dụng Mobile hoặc một sản phẩm AI cần Backend nhanh nhưng vẫn muốn sử dụng SQL. Với một Project nhỏ, việc có sẵn Auth, Storage và API giúp loại bỏ khá nhiều Code lặp lại.

Mình thường cân nhắc Supabase trong những trường hợp sau:

  • Cần một PostgreSQL Database có thể sử dụng ngay.
  • Frontend cần đọc dữ liệu trực tiếp nhưng vẫn phải giới hạn theo từng User.
  • Ứng dụng cần Authentication, Upload File hoặc Realtime mà không muốn tự triển khai từng Service.
  • Team nhỏ muốn tập trung vào Business Logic thay vì quản lý hạ tầng ở giai đoạn đầu.

Tuy nhiên Supabase không thay thế hoàn toàn Backend. Những Workflow phức tạp, Job chạy lâu, tác vụ AI nặng hoặc Logic cần kiểm soát chặt vẫn nên được đặt trong một Service riêng. Edge Functions phù hợp với API nhỏ, Webhook hoặc tác vụ ngắn; còn Training Model hay xử lý Batch lớn nên chạy ở hạ tầng chuyên dụng.

III. Khởi tạo Project Supabase

Sau khi tạo Project trên Supabase Dashboard, chúng ta sẽ có Project URL và API Key. Với ứng dụng Client, nên sử dụng Publishable Key cùng với RLS. Những Key có quyền cao như Secret Key hoặc service_role chỉ được sử dụng ở Server và tuyệt đối không đưa vào Frontend.

Trong Python, ta có thể khai báo biến môi trường:

bash
SUPABASE_URL="https://YOUR_PROJECT.supabase.co"
SUPABASE_KEY="YOUR_PUBLISHABLE_KEY"


Sau đó cài Client Library:

bash
python -m pip install supabase


Khởi tạo Client:

python
import os

from supabase import Client, create_client

supabase: Client = create_client(
    os.environ["SUPABASE_URL"],
    os.environ["SUPABASE_KEY"],
)


Trong ứng dụng thực tế, mình không ghi Key trực tiếp vào Source Code. Có thể sử dụng file .env trong môi trường Local và Secret Manager khi Deploy, đồng thời đảm bảo file chứa Secret không được Commit lên Git.

IV. Tạo Table Todo bằng PostgreSQL

Ta sẽ xây dựng một Table todos, trong đó mỗi Todo thuộc về một User đã đăng nhập. Có thể chạy đoạn SQL sau trong SQL Editor:

sql
create table public.todos (
    id uuid primary key default gen_random_uuid(),
    user_id uuid not null
        references auth.users(id)
        on delete cascade
        default auth.uid(),
    title text not null,
    is_completed boolean not null default false,
    created_at timestamptz not null default now()
);

create index todos_user_id_created_at_idx
on public.todos (user_id, created_at desc);


user_id tham chiếu tới auth.users và mặc định nhận giá trị từ auth.uid(). Index được tạo trên user_id và created_at vì đây là Pattern Query phổ biến: lấy Todo của một User và sắp xếp theo thời gian.

Đến đây Table đã tồn tại, nhưng chưa nên cho Client truy cập trực tiếp. Phần quan trọng tiếp theo là Row Level Security.

V. Bảo vệ dữ liệu bằng Row Level Security

RLS cho phép PostgreSQL quyết định Row nào mà một Request được phép đọc hoặc thay đổi. Với Table nằm trong Schema được Data API expose, chúng ta nên bật RLS và viết Policy rõ ràng cho từng Operation.

sql
alter table public.todos enable row level security;

revoke all on table public.todos from anon;
grant select, insert, update, delete
on table public.todos to authenticated;


Sau đó tạo Policy để User chỉ thao tác với dữ liệu của chính họ:

sql
create policy "Users can read their own todos"
on public.todos
for select
to authenticated
using ((select auth.uid()) = user_id);

create policy "Users can create their own todos"
on public.todos
for insert
to authenticated
with check ((select auth.uid()) = user_id);

create policy "Users can update their own todos"
on public.todos
for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);

create policy "Users can delete their own todos"
on public.todos
for delete
to authenticated
using ((select auth.uid()) = user_id);


Ở đây, Grant quyết định Role có được thực hiện một Operation hay không, còn Policy quyết định Operation đó áp dụng lên những Row nào. Hai lớp này bổ sung cho nhau. Chỉ bật RLS nhưng viết Policy quá rộng vẫn có thể làm lộ dữ liệu; ngược lại, thiếu Grant sẽ khiến Request bị từ chối trước khi Policy được đánh giá.

Một lỗi khá phổ biến là dùng service_role để Test rồi kết luận Policy hoạt động. Role này có thể Bypass RLS, vì vậy cần Test cả trường hợp User A không thể đọc, sửa hoặc xoá Todo của User B.

VI. Authentication và CRUD bằng Python

Đầu tiên ta đăng nhập bằng Email và Password:

python
auth_response = supabase.auth.sign_in_with_password(
    {
        "email": "user@example.com",
        "password": "YOUR_PASSWORD",
    }
)

user = auth_response.user
print(user.id)


Sau khi đăng nhập, Client giữ Session và gửi Access Token trong các Request tiếp theo. PostgreSQL sử dụng thông tin trong JWT để xác định auth.uid().

Tạo Todo

python
response = (
    supabase.table("todos")
    .insert({"title": "Tìm hiểu Supabase RLS"})
    .execute()
)

todo = response.data[0]
print(todo)

Ta không cần truyền user_id vì Column này đã có giá trị mặc định là auth.uid(). Policy Insert vẫn kiểm tra Row mới có thực sự thuộc về User hiện tại hay không.

Lấy danh sách Todo

python
response = (
    supabase.table("todos")
    .select("id, title, is_completed, created_at")
    .order("created_at", desc=True)
    .execute()
)

todos = response.data

Query không có điều kiện user_id, nhưng RLS tự động giới hạn kết quả về những Row mà User được phép đọc. Mình vẫn có thể thêm Filter để tối ưu Query, nhưng Filter ở Application không thay thế cho Authorization ở Database.

Cập nhật Todo

python
response = (
    supabase.table("todos")
    .update({"is_completed": True})
    .eq("id", todo["id"])
    .execute()
)

Xoá Todo

python
response = (
    supabase.table("todos")
    .delete()
    .eq("id", todo["id"])
    .execute()
)

Nếu cố gắng cập nhật hoặc xoá Todo của User khác, Policy sẽ không cho phép Row đó tham gia vào Operation. Đây là lý do RLS đặc biệt hữu ích khi Frontend Query trực tiếp tới Supabase.

VII. Storage, Realtime và Edge Functions

Storage

Supabase Storage tổ chức File theo Bucket. Public Bucket phù hợp với Asset mà bất kỳ ai cũng có thể xem, còn Private Bucket cần Authorization hoặc Signed URL. Quyền truy cập được kết nối với Policy trên storage.objects, vì vậy không nên chỉ kiểm tra tên Folder ở Frontend rồi xem đó là cơ chế bảo mật.

Với ảnh người dùng Upload, một Pattern thường gặp là lưu File trong Path có chứa User ID, sau đó viết Policy chỉ cho phép User thao tác trong Folder của họ.

Realtime

Realtime có thể dùng cho Chat, Notification, trạng thái Online hoặc cập nhật Dashboard. Supabase hiện có Broadcast, Presence và Postgres Changes. Broadcast phù hợp với phần lớn Use Case Realtime và các Event tần suất cao, trong khi Postgres Changes thuận tiện cho thử nghiệm hoặc hệ thống có lượng kết nối nhỏ.

Không phải Table nào cũng cần Realtime. Mỗi Subscription tạo thêm chi phí xử lý và kết nối, vì vậy mình chỉ bật cho những phần giao diện thực sự cần cập nhật ngay.

Edge Functions

Edge Functions là Server-side Function viết bằng TypeScript trên Runtime tương thích Deno. Đây là nơi phù hợp để nhận Webhook, gọi API bên thứ ba, gửi Email hoặc giữ Secret không được phép xuất hiện ở Client.

Ví dụ, Frontend có thể gửi yêu cầu thanh toán tới Edge Function. Function kiểm tra JWT, gọi Payment Provider bằng Secret Key rồi ghi kết quả về Database. Secret vẫn nằm ở Server thay vì bị Bundle vào JavaScript phía Client.

VIII. Một số lưu ý khi đưa Supabase lên Production

Supabase giúp khởi đầu nhanh, nhưng Production vẫn cần những quyết định kiến trúc rõ ràng:

  • Thiết kế RLS trước khi mở Data API: viết Policy cho từng Operation và Test cả trường hợp Allow lẫn Deny.
  • Không đưa Key đặc quyền ra Client: Publishable Key có thể xuất hiện ở Frontend khi RLS đúng; Secret Key và service_role chỉ ở Server.
  • Quản lý Migration: thay đổi Schema bằng Migration thay vì chỉ sửa trực tiếp trên Dashboard, đặc biệt khi có nhiều môi trường.
  • Tạo Index theo Query: PostgreSQL không tự động tạo mọi Index cần thiết. Hãy kiểm tra Query Plan khi dữ liệu tăng.
  • Phân biệt Database Backup và Storage: Backup Database không đồng nghĩa toàn bộ Object trong Storage cũng được sao lưu.
  • Giữ Business Logic ở đúng nơi: Constraint và Authorization gần dữ liệu; Workflow dài hoặc tác vụ nặng ở Worker/Backend riêng.

Theo mình, lợi thế lớn nhất của Supabase không phải là không cần Backend, mà là chúng ta có thể trì hoãn việc xây dựng những phần Backend lặp lại. Khi sản phẩm còn nhỏ, SDK và Dashboard giúp đi nhanh. Khi yêu cầu phức tạp hơn, PostgreSQL vẫn cho phép mở rộng bằng SQL, Function, Trigger và các công cụ quen thuộc.

IX. Kết luận

Supabase là một lựa chọn thực tế khi cần xây dựng Backend nhanh trên nền PostgreSQL. Chỉ với một Project, chúng ta có Database, Auth, Storage, Realtime, Edge Functions và Data API. Tuy nhiên khả năng Query trực tiếp từ Client chỉ an toàn khi Grant và Row Level Security được thiết kế đúng.

Trong ví dụ Todo, phần CRUD bằng Python khá ngắn. Phần đáng quan tâm hơn nằm ở Schema, Relationship và Policy. Đây cũng là cách mình nghĩ nên tiếp cận Supabase: xem nó như một PostgreSQL Backend có sẵn nhiều Service hỗ trợ, thay vì một công cụ khiến chúng ta không cần hiểu Database.

Nguồn tham khảo

Được chọn theo chủ đề và các khái niệm xuất hiện trong bài này.

Xem tất cả bài viết

Thảo luận bài viết

Câu hỏi, ghi chú và góc nhìn của bạn về nội dung này.

0 bình luận