Thay Supabase bằng NestJS + PostgreSQL cho SaaS chuyên nghiệp

P P T P Chung

Thay thế Supabase bằng NestJS + PostgreSQL: kiến trúc backend chuẩn cho SaaS

Supabase rất hấp dẫn: DB PostgreSQL, auth, storage, realtime, API auto-gen. Dự án MVP → chạy nhanh. Nhưng SaaS tăng trưởng → vấn đề khác xuất hiện: logic nghiệp vụ phức tạp, phân quyền nhiều lớp, billing, audit, background jobs, multi-tenant, tích hợp bên thứ ba, observability, SLA nội bộ.

Lúc này, câu hỏi không còn là “Supabase có tốt không?” mà là: backend của bạn cần mức kiểm soát nào?

Với nhiều SaaS nghiêm túc, combo NestJS + PostgreSQL là lựa chọn bền vững: rõ kiến trúc, dễ test, dễ mở rộng, kiểm soát auth/permission/API/job/integration tốt hơn. Không thay PostgreSQL; thay lớp backend managed/auto-generated bằng backend tự thiết kế.


Vì sao Supabase phù hợp MVP nhưng dễ chạm trần với SaaS?

Supabase mạnh ở tốc độ. Bạn có thể có auth, bảng dữ liệu, REST API, realtime trong vài giờ. Nhưng SaaS thường không dừng ở CRUD.

Các điểm thường gây giới hạn

Business logic phân tán Logic nằm ở client, RLS policy, database function, trigger → khó đọc, khó test, khó onboard dev mới.

Phân quyền phức tạp SaaS B2B thường có org, workspace, team, role, permission, plan, feature flag. RLS xử lý được nhiều thứ, nhưng càng nhiều rule → càng khó debug.

API contract thiếu chủ động Auto API tiện lúc đầu. Về sau cần DTO rõ ràng, versioning, validation, backward compatibility.

Background jobs không phải first-class Email, invoice, sync, webhook retry, report export, usage aggregation → cần queue/job runner chuẩn.

Observability hạn chế theo nhu cầu riêng SaaS cần log có cấu trúc, tracing, metrics, audit trail, alerting theo domain.

Vendor coupling Auth, storage, realtime, edge function gắn chặt ecosystem → migration càng muộn càng tốn.

Kết luận: Supabase tuyệt cho MVP, internal tool, prototype, app ít logic. Nhưng với SaaS nhiều nghiệp vụ, backend riêng giúp giảm nợ kỹ thuật dài hạn.


Vì sao chọn NestJS + PostgreSQL?

NestJS là framework Node.js theo phong cách enterprise: module, service, controller, dependency injection, guard, interceptor, pipe. PostgreSQL là DB quan hệ mạnh, ổn định, phù hợp SaaS transactional.

Lợi ích chính

Kiến trúc rõ → module hóa theo domain. – Dễ test → unit test service, integration test API, e2e test flow. – Auth tùy biến → JWT, session, SSO, OAuth, magic link, MFA. – Permission kiểm soát được → RBAC/ABAC theo app logic. – API chuẩn → REST/GraphQL, DTO, validation, OpenAPI. – Job queue chuẩn → BullMQ, Redis, retry, backoff, dead-letter. – Mở rộng tốt → tách service sau này nếu cần. – Không mất PostgreSQL → vẫn dùng sức mạnh relational DB, transaction, index, JSONB, full-text search.


Kiến trúc backend SaaS đề xuất

Một backend SaaS chuẩn không chỉ có API + DB. Nên thiết kế theo lớp.

Tổng quan thành phần

NestJS API: xử lý req/res, validation, auth, business logic. – PostgreSQL: dữ liệu chính. – Redis: cache, rate limit, queue backend. – BullMQ Worker: job nền. – Object Storage: S3/R2/MinIO cho file. – Email Provider: SES, Resend, Postmark. – Payment: Stripe/Paddle. – Observability: logs, metrics, tracing. – CI/CD: migration, test, deploy tự động.

Luồng điển hình:

Client → NestJS API → Guard/Auth → DTO Validation → Service → Repository/ORM → PostgreSQL Event → Queue → Worker → External API → DB update → Notification


Cấu trúc module trong NestJS

Không nên tổ chức theo kiểu controllers/, services/, models/ chung toàn app. SaaS nên tổ chức theo domain.

Ví dụ:

src/
  modules/
    auth/
    users/
    organizations/
    memberships/
    billing/
    projects/
    files/
    notifications/
    audit-logs/
  common/
    guards/
    decorators/
    filters/
    interceptors/
    pipes/
  infrastructure/
    database/
    queue/
    mail/
    storage/
    config/

Mỗi module tự chứa:

projects/
  projects.controller.ts
  projects.service.ts
  projects.repository.ts
  dto/
  entities/
  policies/

Ưu điểm: domain rõ, dễ refactor, dễ giao task, dễ test.


Database: vẫn là PostgreSQL, nhưng thiết kế chủ động hơn

PostgreSQL vẫn là lõi. Khác biệt nằm ở cách backend kiểm soát truy cập và logic.

Schema cơ bản cho SaaS multi-tenant

Các bảng thường gặp:

usersorganizationsmembershipsrolespermissionsprojectssubscriptionsinvoicesaudit_logsapi_keyswebhook_events

Ví dụ quan hệ:

users 1--n memberships n--1 organizations
organizations 1--n projects
organizations 1--1 subscriptions
users 1--n audit_logs

Multi-tenant: chọn chiến lược nào?

Phổ biến nhất:

Shared database, shared schema, tenant_id/org_id trên bảng Dễ vận hành, phù hợp đa số SaaS.

Cần bắt buộc:

– Mọi bảng domain có organization_id. – Mọi query phải filter theo organization_id. – Index theo tenant:

CREATE INDEX idx_projects_org_id ON projects(organization_id);
CREATE INDEX idx_projects_org_status ON projects(organization_id, status);

Nếu SaaS enterprise lớn hơn:

– Shared DB, separate schema per tenant. – Separate DB per tenant.

Nhưng nên bắt đầu với shared schema nếu chưa có lý do mạnh.


Auth: từ Supabase Auth sang auth tự kiểm soát

Auth là phần dễ đánh giá thấp. Với SaaS, auth không chỉ login.

Nên hỗ trợ

– Email/password hoặc magic link. – OAuth Google/GitHub/Microsoft. – JWT access token + refresh token. – Session/device management. – Password reset. – Email verification. – Optional MFA. – SSO/SAML cho enterprise sau này.

Trong NestJS:

AuthGuard xác thực user. – RolesGuard kiểm tra role. – PoliciesGuard kiểm tra permission theo resource. – Decorator @CurrentUser() lấy user từ req. – Decorator @Org() lấy organization context.

Ví dụ permission:

user → membership → role → permissions

Không nên hard-code kiểu if user.role === 'admin' khắp code. Nên gom vào policy/service.


Authorization: RBAC + ABAC

Supabase RLS mạnh, nhưng trong SaaS nhiều logic cần ngữ cảnh app.

RBAC

Role-based:

– Owner – Admin – Manager – Member – Viewer

Phù hợp permission tổng quát.

ABAC

Attribute-based:

– User có thuộc org không? – Plan có bật feature này không? – Resource thuộc org nào? – Project đang archived không? – User có phải creator không?

Ví dụ rule:

Can update project → user thuộc org + permission project:update + project chưa archived

Cách tốt: tạo policy layer riêng.

canUpdateProject(user, membership, project) {
  return membership.permissions.includes('project:update')
    && project.organizationId === membership.organizationId
    && !project.archivedAt;
}

Rõ hơn RLS phức tạp, dễ unit test hơn.


API design: đừng expose DB trực tiếp

Supabase auto API tiện nhưng dễ làm client phụ thuộc schema DB. Backend riêng nên expose API theo use case.

Thay vì:

PATCH /projects/:id

có thể tách:

POST /projects/:id/archive
POST /projects/:id/restore
POST /projects/:id/transfer-owner

Lợi ích:

– API thể hiện nghiệp vụ. – Validation rõ. – Audit log chính xác. – Permission riêng từng action. – Không leak cấu trúc DB.

DTO + validation

NestJS mạnh ở DTO:

export class CreateProjectDto {
  name: string;
  description?: string;
}

Kết hợp class-validator:

@IsString()
@MinLength(3)
name: string;

Req sai → reject sớm. Service sạch hơn.


Background jobs: thứ SaaS nào cũng cần

Đừng xử lý tác vụ nặng trong HTTP request.

Các job phổ biến:

– Gửi email welcome. – Retry webhook. – Generate report. – Sync dữ liệu CRM. – Tính usage hằng ngày. – Gửi invoice reminder. – Xóa file hết hạn. – Export CSV.

Dùng:

BullMQ + Redis cho queue. – Worker NestJS riêng process job. – Retry/backoff. – Idempotency key. – Dead-letter queue.

Luồng:

API nhận req → lưu DB → enqueue job → res nhanh
Worker → xử lý async → update DB/log

Kết quả: UX nhanh hơn, hệ thống ổn định hơn.


Billing: thiết kế từ đầu, đừng vá sau

SaaS thường chết vì billing logic lộn xộn.

Cần model rõ:

planssubscriptionssubscription_itemsinvoicesusage_recordspayment_events

Stripe/Paddle gửi webhook. Backend phải:

– Verify signature. – Lưu raw event. – Xử lý idempotent. – Update subscription. – Mở/khóa feature theo plan.

Không nên tin client báo “đã thanh toán”. Source of truth phải là payment provider + backend verified webhook.


Audit log và compliance

SaaS B2B cần trả lời:

– Ai đã làm gì? – Lúc nào? – Trên resource nào? – IP/device nào? – Trước/sau thay đổi ra sao?

Audit log nên là first-class module.

Ví dụ:

actor_id
organization_id
action
resource_type
resource_id
metadata
ip_address
user_agent
created_at

Ghi audit tại service layer hoặc interceptor tùy action. Với hành động nhạy cảm: invite member, change role, delete project, export data, billing update → bắt buộc log.


Migration từ Supabase sang NestJS + PostgreSQL

Không nên “big bang rewrite”. Nên đi từng bước.

Lộ trình thực tế

1. Giữ PostgreSQL hiện tại Supabase đang dùng PostgreSQL → tận dụng.

2. Dựng NestJS đọc DB trước Implement API read-only cho vài màn hình.

3. Tách auth dần Có thể ban đầu verify Supabase JWT trong NestJS. Sau đó thay auth riêng.

4. Chuyển write path theo module Ví dụ chuyển projects, rồi billing, rồi memberships.

5. Đưa business logic khỏi client/RLS/function Gom về service layer.

6. Thêm queue/job/audit Các luồng quan trọng chạy qua backend.

7. Đóng auto API không cần thiết Giảm attack surface, tránh client bypass logic.

8. Chuẩn hóa migration DB Dùng Prisma, TypeORM, MikroORM hoặc Knex migration.


ORM: Prisma, TypeORM hay query builder?

Không có đáp án tuyệt đối.

Prisma

– DX tốt. – Type-safe mạnh. – Phù hợp team cần tốc độ. – Migration ổn. – Hạn chế với query SQL rất phức tạp.

TypeORM

– Hợp NestJS ecosystem. – Entity/repository quen thuộc. – Linh hoạt. – Cần discipline để tránh entity quá phình.

Knex/Drizzle

– Gần SQL hơn. – Kiểm soát tốt. – Phù hợp team thích explicit query.

Gợi ý thực dụng: Prisma + raw SQL cho query phức tạp là lựa chọn cân bằng cho nhiều SaaS.


Bảo mật: backend riêng không tự động an toàn hơn

Tự quản backend → tự chịu trách nhiệm nhiều hơn.

Checklist tối thiểu:

– Hash password bằng Argon2/bcrypt. – Rotate refresh token. – Rate limit login/reset password. – Validate input mọi endpoint. – CORS chặt. – Secrets qua env/secret manager. – SQL injection prevention qua ORM/parameterized query. – Webhook signature verification. – Audit log action nhạy cảm. – Backup PostgreSQL định kỳ. – Migration có rollback plan. – Least privilege DB user. – Không log token/password/PII nhạy cảm.

Backend riêng mạnh hơn khi team vận hành đúng. Nếu không, managed platform vẫn an toàn hơn.


Khi nào không nên thay Supabase?

Không phải dự án nào cũng cần NestJS.

Giữ Supabase nếu:

– App chủ yếu CRUD. – Team nhỏ, thiếu backend dev. – Chưa có product-market fit. – Logic auth đơn giản. – Không có billing phức tạp. – Không có yêu cầu audit/compliance. – Tốc độ ra tính năng quan trọng hơn kiến trúc.

Đổi sang NestJS nếu:

– SaaS đã có khách trả tiền. – Logic nghiệp vụ tăng nhanh. – Permission nhiều tầng. – Cần billing/usage chuẩn. – Cần job nền nhiều. – Cần API public ổn định. – Cần kiểm soát dữ liệu, logs, compliance.


Kết luận: không phải “Supabase vs NestJS”, mà là giai đoạn sản phẩm

Supabase giúp bạn đi nhanh từ 0 đến MVP. NestJS + PostgreSQL giúp bạn đi xa khi SaaS bước vào giai đoạn vận hành nghiêm túc.

Kiến trúc tốt không phải kiến trúc phức tạp nhất. Kiến trúc tốt là kiến trúc khiến team:

– hiểu logic nhanh, – test được, – debug được, – mở rộng được, – bảo mật được, – phục vụ khách hàng ổn định.

Cách thực tế nhất: bắt đầu đơn giản, migrate từng module, giữ PostgreSQL làm lõi, đưa logic quan trọng về NestJS service layer, bổ sung queue/audit/billing/observability đúng thời điểm.

Nếu SaaS của bạn đã vượt khỏi CRUD và bắt đầu có permission, billing, workflow, integration, audit, thì NestJS + PostgreSQL không chỉ là thay thế Supabase. Nó là nền móng backend dài hạn.

Tác giả

P T P

Chia sẻ

Bài viết liên quan

Bình luận (0)

Email của bạn sẽ không được hiển thị công khai.

Chưa có bình luận. Hãy là người đầu tiên!