Xây Supabase riêng với Next.js, Prisma, PostgreSQL và S3

18/05/2026 · P T P · Chung

Xây stack thay thế Supabase với Next.js, Prisma, PostgreSQL và S3

Supabase rất mạnh: database PostgreSQL, Auth, Storage, Realtime, Edge Functions, dashboard đẹp. Nhưng khi sản phẩm lớn hơn, nhiều team muốn kiểm soát sâu hơn: schema migration rõ hơn, auth tùy biến hơn, storage riêng hơn, chi phí dự đoán hơn, ít phụ thuộc vendor hơn.

Stack Next.js + Prisma + PostgreSQL + S3 là lựa chọn thực tế. Không phải “clone Supabase” hoàn toàn, mà là dựng nền tảng backend đủ mạnh cho SaaS, marketplace, CMS, app nội bộ, sản phẩm AI, hoặc dashboard doanh nghiệp.

Ý tưởng: Next.js xử lý frontend + API, Prisma quản lý data layer, PostgreSQL làm database chính, S3 lưu file/object. Thêm auth, queue, realtime nếu cần. Stack này ít màu mè hơn Supabase, nhưng linh hoạt, dễ scale, dễ debug.


Vì sao muốn thay Supabase?

Supabase giúp launch nhanh. Nhưng có vài điểm khiến team cân nhắc tự dựng stack:

Kiểm soát kiến trúc tốt hơn

Supabase gom nhiều thứ vào một nền tảng. Tiện, nhưng đôi khi khó tùy biến sâu. Khi cần luồng business phức tạp, quyền truy cập nhiều tầng, audit log, workflow doanh nghiệp, hoặc tích hợp legacy system, tự quản backend dễ hơn.

Migration và schema rõ hơn

Prisma có migration file, schema typed, code review dễ. Dev thấy thay đổi database ngay trong repo. CI/CD chạy migration có kiểm soát. Với team nhiều người, điều này giảm lỗi production.

Chi phí dễ tính hơn

Supabase tính theo compute, storage, bandwidth, project plan. Với app nhiều file, nhiều query, hoặc workload bất thường, tự dùng PostgreSQL managed + S3-compatible storage có thể dễ tối ưu hơn.

Tránh lock-in nhẹ

PostgreSQL và S3 là chuẩn phổ biến. Prisma chạy với nhiều database. Next.js deploy nhiều nơi. Nếu đổi nhà cung cấp, app ít bị kẹt.


Bức tranh kiến trúc tổng thể

Stack đề xuất:

Next.js App Router: UI, Server Components, API Routes, Server Actions.
Prisma: ORM, migration, type-safe query.
PostgreSQL: dữ liệu chính, transaction, index, relation.
S3 hoặc S3-compatible: file upload, avatar, document, media.
Auth.js / Clerk / Lucia / custom JWT: xác thực.
Redis + queue nếu cần job nền.
Pusher / Ably / Socket.IO / Postgres listen-notify nếu cần realtime.

Luồng cơ bản:

1. User đăng nhập.
2. Next.js nhận request.
3. Server code kiểm tra session.
4. Prisma đọc/ghi PostgreSQL.
5. File upload đi qua presigned URL tới S3.
6. Database lưu metadata file: key, bucket, size, MIME type, ownerId.


Thiết kế project Next.js

Cấu trúc gọn:

src/
  app/
    api/
    dashboard/
    login/
  lib/
    auth.ts
    prisma.ts
    s3.ts
    permissions.ts
  server/
    users.ts
    files.ts
    organizations.ts
prisma/
  schema.prisma
  migrations/

Prisma client singleton

Trong dev mode, Next.js hot reload có thể tạo nhiều Prisma client. Dùng singleton:

// src/lib/prisma.ts
import { PrismaClient } from "@prisma/client";

const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient; };

export const prisma = globalForPrisma.prisma ?? new PrismaClient({ log: ["query", "error", "warn"], });

if (process.env.NODE_ENV !== "production") { globalForPrisma.prisma = prisma; }

Điểm cần nhớ: Prisma hoạt động tốt nhất ở server runtime. Tránh gọi Prisma trong client component.


PostgreSQL: lõi của hệ thống

PostgreSQL không chỉ là nơi lưu row. Nó là engine mạnh cho transaction, index, constraint, JSONB, full-text search, row lock, view, materialized view.

Schema mẫu cho SaaS nhiều organization:

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

memberships Membership[] files File[] }

model Organization { id String @id @default(cuid()) name String slug String @unique createdAt DateTime @default(now())

memberships Membership[] files File[] }

model Membership { id String @id @default(cuid()) userId String organizationId String role Role @default(MEMBER)

user User @relation(fields: [userId], references: [id], onDelete: Cascade) organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)

@@unique([userId, organizationId]) @@index([organizationId]) }

model File { id String @id @default(cuid()) key String @unique bucket String filename String contentType String size Int userId String organizationId String? createdAt DateTime @default(now())

user User @relation(fields: [userId], references: [id]) organization Organization? @relation(fields: [organizationId], references: [id])

@@index([userId]) @@index([organizationId]) }

enum Role { OWNER ADMIN MEMBER }

Nguyên tắc schema

– Dùng foreign key để giữ dữ liệu sạch.
– Thêm unique constraint cho email, slug, key.
– Thêm index cho field hay filter: userId, organizationId, createdAt.
– Dùng transaction cho thao tác nhiều bảng.
– Không lưu file binary trong PostgreSQL. Lưu metadata, file thật để S3.


Prisma: type-safe data layer

Prisma giúp query dễ đọc, type tự sinh từ schema. Ví dụ lấy organization kèm quyền user:

const membership = await prisma.membership.findUnique({
  where: {
    userId_organizationId: {
      userId,
      organizationId,
    },
  },
});

Tạo file metadata sau khi upload:

await prisma.file.create({
  data: {
    key,
    bucket: process.env.S3_BUCKET!,
    filename,
    contentType,
    size,
    userId,
    organizationId,
  },
});

Khi nào không nên lạm dụng Prisma?

Prisma tốt cho 90% CRUD. Nhưng với query cực phức tạp, report nặng, analytics, recursive CTE, hoặc bulk operation lớn, dùng raw SQL hợp lý:

await prisma.$queryRaw`
  SELECT date_trunc('day', "createdAt") AS day, count(*) 
  FROM "File"
  GROUP BY day
  ORDER BY day DESC
`;

Quy tắc: Prisma cho business query thường ngày. SQL cho query tối ưu sâu.


S3: storage thay Supabase Storage

S3 lưu object theo key. Key nên có cấu trúc:

organizations/{organizationId}/users/{userId}/{fileId}-{filename}

Không để user tự quyết key hoàn toàn. Tránh ghi đè, tránh path lạ, tránh lộ cấu trúc nhạy cảm.

Upload qua presigned URL

Không nên upload file lớn qua Next.js server nếu không cần. Tạo presigned URL, client upload thẳng lên S3.

// src/lib/s3.ts
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

export const s3 = new S3Client({ region: process.env.S3_REGION!, endpoint: process.env.S3_ENDPOINT || undefined, credentials: { accessKeyId: process.env.S3_ACCESS_KEY_ID!, secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!, }, });

export async function createUploadUrl(params: { key: string; contentType: string; }) { const command = new PutObjectCommand({ Bucket: process.env.S3_BUCKET!, Key: params.key, ContentType: params.contentType, });

return getSignedUrl(s3, command, { expiresIn: 60 }); }

API route tạo URL:

export async function POST(req: Request) {
  const session = await requireSession();
  const body = await req.json();

const key = users/${session.user.id}/${crypto.randomUUID()}-${body.filename};

const url = await createUploadUrl({ key, contentType: body.contentType, });

return Response.json({ url, key }); }

Sau khi client upload thành công, gọi API khác để lưu metadata vào PostgreSQL.

Bảo mật storage

– Presigned URL phải hết hạn ngắn, ví dụ 60 giây.
– Validate contentType, size, extension.
– Scan virus nếu file nhạy cảm.
– Bucket private mặc định.
– Download cũng nên dùng presigned URL.
– Không public toàn bộ bucket trừ khi là asset công khai.


Auth và phân quyền

Supabase Auth là phần khó thay nhất nếu tự làm. Có 3 hướng:

Auth.js

Phù hợp nếu muốn open-source, tự host, OAuth provider, session trong database. Kết hợp Prisma adapter khá ổn.

Clerk

Nhanh, UI đẹp, nhiều tính năng enterprise. Nhưng lại phụ thuộc vendor khác. Tốt nếu team muốn tiết kiệm thời gian auth.

Custom auth

Chỉ nên làm khi có yêu cầu đặc biệt. Password login cần hash chuẩn, reset token, email verification, rate limit, session rotation. Sai auth là rủi ro lớn.

Dù chọn gì, cần tách permission rõ:

export async function requireOrgRole(
  userId: string,
  organizationId: string,
  roles: Role[]
) {
  const membership = await prisma.membership.findUnique({
    where: {
      userId_organizationId: { userId, organizationId },
    },
  });

if (!membership || !roles.includes(membership.role)) { throw new Error("Forbidden"); }

return membership; }

Không tin dữ liệu từ client. Mọi API ghi dữ liệu phải kiểm tra quyền ở server.


Realtime: có cần thay ngay không?

Supabase Realtime tiện. Nhưng không phải app nào cũng cần realtime thật. Nhiều dashboard chỉ cần refresh, polling, hoặc optimistic UI.

Nếu cần realtime:

Pusher / Ably: nhanh, ít vận hành.
Socket.IO: tự host, linh hoạt.
PostgreSQL LISTEN/NOTIFY: nhẹ cho event nội bộ.
Redis Pub/Sub: tốt khi có nhiều instance.
WebSocket server riêng: cần khi traffic lớn, logic phức tạp.

Đừng dựng realtime chỉ vì “cho giống Supabase”. Hãy bắt đầu bằng polling nếu đủ.


Deploy và vận hành

Stack này chạy tốt trên:

Vercel cho Next.js.
Neon / Supabase Postgres / RDS / Railway / Render cho PostgreSQL.
AWS S3 / Cloudflare R2 / MinIO / DigitalOcean Spaces cho object storage.

Checklist production

– Bật connection pooling cho PostgreSQL.
– Dùng Prisma migration trong CI/CD.
– Backup database hằng ngày.
– Bật point-in-time recovery nếu có.
– Log lỗi với Sentry.
– Rate limit endpoint nhạy cảm.
– Tách biến môi trường dev/staging/prod.
– Theo dõi query chậm.
– Dọn file orphan: file đã upload S3 nhưng không có metadata DB.
– Lifecycle rule cho S3: xóa temp object sau vài ngày.

Vấn đề hay gặp

Serverless + Prisma + PostgreSQL có thể gây nhiều connection. Dùng pooler như PgBouncer, Neon pooling, hoặc Prisma Accelerate nếu phù hợp.

Upload thành công nhưng lưu DB lỗi tạo orphan object. Cần job cleanup theo prefix temp/.

DB có record nhưng S3 thiếu file xảy ra khi client báo sai. Có thể dùng flow: upload vào temp/, server verify bằng HeadObject, rồi chuyển trạng thái READY.


So sánh thực tế với Supabase

Stack tự dựng mạnh ở:

– Kiểm soát code và schema.
– Tích hợp business logic phức tạp.
– Dễ đổi vendor storage/database.
– Type safety tốt với Prisma.
– Phù hợp team backend mạnh.

Supabase mạnh ở:

– Launch cực nhanh.
– Dashboard quản trị tiện.
– Auth và Storage có sẵn.
– Realtime tích hợp.
– Ít code hạ tầng ban đầu.

Nói ngắn: nếu cần MVP nhanh, Supabase thắng. Nếu cần nền tảng dài hạn, quy trình kỹ thuật chặt, quyền phức tạp, hoặc chi phí riêng, stack Next.js + Prisma + PostgreSQL + S3 đáng chọn.


Kết luận thực tế

Không cần ghét Supabase để xây stack thay thế. Supabase giải bài toán tốc độ. Stack tự dựng giải bài toán kiểm soát.

Cách tốt nhất: bắt đầu nhỏ. Dựng Next.js, Prisma, PostgreSQL trước. Thêm auth ổn định. Sau đó đưa file sang S3 bằng presigned URL. Chỉ thêm realtime, queue, cache khi có nhu cầu thật.

Kiến trúc tốt không phải nhiều service. Kiến trúc tốt là ít phần, rõ trách nhiệm, dễ debug, dễ thay. Với Next.js, Prisma, PostgreSQL và S3, bạn có nền móng đủ bền để đi từ MVP đến production nghiêm túc mà không phụ thuộc quá nhiều vào một nền tảng duy nhất.

#next #postgresql #prisma #rieng #supabase
Chia sẻ:
← Trước
Thay Supabase Realtime bằng Socket.IO/Pusher: Hướng dẫn thực chiến

Bài viết tương tự

Bình luận

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