Thay Supabase bằng Neon + Auth.js: Stack PostgreSQL tối giản

P P T P Chung

Thay thế Supabase bằng Neon + Auth.js: stack serverless PostgreSQL tối giản

Supabase rất mạnh: PostgreSQL, Auth, Storage, Realtime, Edge Functions, dashboard đẹp. Nhưng không phải dự án nào cũng cần “all-in-one”. Nhiều app chỉ cần database PostgreSQL serverless + đăng nhập ổn định + ORM nhẹ + deploy nhanh. Khi đó, Neon + Auth.js là lựa chọn đáng cân nhắc: ít thành phần hơn, kiểm soát tốt hơn, chi phí linh hoạt hơn.

Stack tối giản:

Neon → PostgreSQL serverless. – Auth.js → xác thực người dùng. – Prisma / Drizzle → ORM/query builder. – Next.js / SvelteKit / Remix → app framework. – Vercel / Cloudflare / Netlify → deploy.

Ý tưởng chính: Supabase cung cấp nhiều thứ trong một nền tảng. Neon + Auth.js tách nhỏ trách nhiệm: DB riêng, auth riêng, app tự quyết định logic.


Vì sao muốn thay Supabase?

Supabase tốt, nhưng có vài điểm khiến team muốn đổi:

1. Chỉ cần PostgreSQL, không cần cả hệ sinh thái

Nhiều sản phẩm không dùng:

– Realtime. – Storage. – Edge Functions. – Supabase Auth rules phức tạp. – Dashboard policy.

Nếu app chỉ cần CRUD + login, dùng toàn bộ Supabase có thể hơi “nặng”.

2. Muốn auth linh hoạt hơn

Supabase Auth tiện, nhưng Auth.js lại gần với code app hơn. Bạn kiểm soát:

– Provider đăng nhập: Google, GitHub, Credentials, Email magic link. – Session strategy: JWT hoặc database. – Callback logic. – Role, permission, tenant mapping. – Custom user schema.

Auth trở thành một phần của app, không phụ thuộc chặt vào backend platform.

3. Muốn PostgreSQL serverless chuyên biệt

Neon tập trung vào PostgreSQL serverless:

– Branch database như Git. – Autosuspend. – Scale compute/storage tách biệt. – Connection string chuẩn PostgreSQL. – Tương thích tốt với Prisma, Drizzle, Kysely.

Nếu bạn muốn DB “thuần Postgres” hơn, Neon rất hợp.

4. Muốn giảm lock-in

Supabase vẫn là PostgreSQL, nhưng nhiều app dùng sâu:

– Supabase Auth. – RLS policy. – Supabase client. – Storage bucket. – RPC. – Realtime channel.

Càng dùng nhiều, càng khó đổi. Neon + Auth.js giúp kiến trúc rõ hơn: DB là DB, auth là lib, app giữ business logic.


Kiến trúc đề xuất

Một kiến trúc tối giản:

Client
  ↓
Next.js App / API Routes / Server Actions
  ↓
Auth.js
  ↓
Neon PostgreSQL

Nếu dùng ORM:

App
  ↓
Auth.js + Prisma/Drizzle Adapter
  ↓
Prisma/Drizzle
  ↓
Neon

Các bảng auth thường gồm:

usersaccountssessionsverification_tokens

Bảng nghiệp vụ riêng:

projectstaskssubscriptionsorganizationsmemberships

Điểm quan trọng: không trộn logic auth với logic business quá sớm. Auth xác định user. App quyết định user được làm gì.


Neon: PostgreSQL serverless gọn, mạnh

Neon không cố thay toàn bộ backend. Neon làm một việc: PostgreSQL cloud-native.

Tính năng đáng chú ý

Serverless compute DB có thể autosuspend khi không dùng. Phù hợp MVP, SaaS nhỏ, side project. Branching Bạn có thể tạo branch DB cho preview deployment:
main
 ├─ preview/pr-123
 └─ staging

Rất hữu ích khi dùng Vercel preview. Mỗi pull request có DB riêng → test migration an toàn hơn.

Connection pooling Serverless thường gặp vấn đề quá nhiều connection. Neon có pooled connection string. Với Prisma, bạn thường dùng:
DATABASE_URL="postgresql://..."
DIRECT_URL="postgresql://..."
DATABASE_URL dùng pooling. DIRECT_URL dùng migration. PostgreSQL chuẩn Dùng SQL, extension, index, transaction, constraint như Postgres thật. Dễ chuyển sang provider khác nếu cần.

Auth.js: xác thực nằm trong app

Auth.js, trước đây là NextAuth.js, hỗ trợ nhiều framework. Với Next.js, cấu hình phổ biến:

// auth.ts
import NextAuth from "next-auth"
import GitHub from "next-auth/providers/github"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"

export const { handlers, auth, signIn, signOut } = NextAuth({ adapter: PrismaAdapter(prisma), providers: [GitHub], session: { strategy: "database", }, })

Route handler:

// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth"

export const { GET, POST } = handlers

Dùng session:

import { auth } from "@/auth"

export default async function Page() { const session = await auth()

if (!session?.user) { return <div>Chưa đăng nhập</div> }

return <div>Xin chào {session.user.email}</div> }

Ưu điểm: session lấy ở server component, API route, server action đều tiện.


Prisma hay Drizzle?

Cả hai đều ổn.

Prisma phù hợp nếu bạn muốn

– Schema rõ ràng. – Migration dễ dùng. – Adapter Auth.js phổ biến. – DX tốt. – Team quen ORM truyền thống.

Ví dụ schema Auth.js rút gọn:

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?

accounts Account[] sessions Session[] }

Drizzle phù hợp nếu bạn muốn

– Type-safe SQL gần database. – Nhẹ hơn Prisma. – Kiểm soát query tốt. – Cold start thấp hơn trong vài môi trường. – Migration linh hoạt.

Nếu app nhỏ, Prisma nhanh hơn để bắt đầu. Nếu app cần tối ưu query nhiều, Drizzle đáng chọn.


So sánh Supabase vs Neon + Auth.js

Supabase mạnh hơn khi

– Cần dashboard backend đầy đủ. – Cần Realtime nhanh. – Cần Storage tích hợp. – Muốn Auth có sẵn ngoài app. – Muốn RLS làm lớp security chính. – Team frontend muốn thao tác DB trực tiếp qua client SDK.

Neon + Auth.js mạnh hơn khi

– App server-rendered. – Business logic nằm ở server. – Chỉ cần PostgreSQL + auth. – Muốn schema auth tùy biến. – Muốn DB branch cho preview. – Muốn giảm phụ thuộc vào SDK platform. – Muốn kiến trúc module hóa.

Nói ngắn: Supabase = backend platform. Neon + Auth.js = database + auth library. Chọn theo nhu cầu, không theo hype.


Vấn đề bảo mật: khác biệt quan trọng

Supabase thường khuyến khích dùng Row Level Security vì client có thể gọi DB thông qua Supabase API. RLS khi đó là hàng rào chính.

Với Neon + Auth.js, app thường không cho client truy cập DB trực tiếp. Client gọi server. Server kiểm tra session, role, ownership rồi query DB.

Mẫu kiểm tra:

const session = await auth()

if (!session?.user?.id) { throw new Error("Unauthorized") }

const project = await db.project.findFirst({ where: { id: projectId, ownerId: session.user.id, }, })

Điểm cần nhớ:

– Không bao giờ expose DATABASE_URL ra client. – Kiểm tra quyền ở mọi mutation. – Không chỉ kiểm tra login; phải kiểm tra ownership. – Dùng constraint DB để bảo vệ dữ liệu. – Dùng transaction cho thao tác nhiều bước. – Log sự kiện nhạy cảm: login, đổi email, xóa dữ liệu.

RLS vẫn dùng được trên Neon, nhưng không bắt buộc theo cùng cách Supabase.


Migration từ Supabase sang Neon

Quy trình thực tế:

1. Kiểm kê phụ thuộc

Liệt kê app đang dùng gì:

– Supabase Auth? – Supabase Storage? – Realtime? – Edge Functions? – RLS? – RPC? – Trigger? – Extension?

Nếu chỉ dùng Postgres + Auth, migration khá nhẹ. Nếu dùng Storage/Reatime/RLS sâu, cần kế hoạch riêng.

2. Xuất schema/data

Có thể dùng pg_dump:

pg_dump "$SUPABASE_DATABASE_URL" > backup.sql

Import sang Neon:

psql "$NEON_DATABASE_URL" < backup.sql

Với DB lớn, nên dump theo schema/data riêng, kiểm tra extension, index, sequence.

3. Chuyển auth

Đây là phần khó nhất. Supabase Auth lưu user trong schema riêng. Auth.js có schema khác. Bạn có vài lựa chọn:

– Bắt user đăng nhập lại bằng OAuth. – Map user cũ sang bảng users mới. – Giữ bảng profile cũ, thêm authUserId. – Gửi email reset/magic link để xác nhận lại.

Không nên copy password hash nếu không hiểu rõ format, policy, risk. Với OAuth, migration thường dễ hơn.

4. Thay Supabase client bằng server query

Trước:

const { data } = await supabase
  .from("projects")
  .select("*")

Sau:

const projects = await db.project.findMany({
  where: { ownerId: session.user.id },
})

Logic chuyển từ client SDK sang server function/API.

5. Kiểm tra quyền

Nếu trước đây phụ thuộc RLS, khi chuyển phải viết lại authorization ở server. Đây là điểm dễ lỗi nhất.

Checklist:

– User A không đọc được dữ liệu User B. – User không sửa object không sở hữu. – Admin route có role check. – API route không tin input client. – Mutation có validation.


Chi phí và vận hành

Neon + Auth.js thường tiết kiệm cho app nhỏ vì:

– DB autosuspend. – Không trả tiền cho tính năng không dùng. – Auth.js miễn phí, chạy trong app. – Deploy serverless scale theo traffic.

Nhưng đổi lại bạn tự chịu trách nhiệm nhiều hơn:

– Email provider nếu dùng magic link. – Session table cleanup. – Rate limit login. – Audit log. – User management UI. – Admin dashboard. – Storage nếu cần upload file.

Supabase cho nhiều thứ sẵn. Neon + Auth.js cho sự tối giản, nhưng không phải “miễn công”.


Stack mẫu khuyến nghị

Cho SaaS nhỏ dùng Next.js:

Next.js App RouterNeon PostgreSQLPrismaAuth.jsZodReact Hook FormVercelResend nếu cần email – S3/R2 nếu cần file storage

Cấu trúc thư mục:

app/
  api/
  dashboard/
auth.ts
lib/
  prisma.ts
  authz.ts
  validators.ts
prisma/
  schema.prisma

Nguyên tắc:

– Query DB chỉ ở server. – Validation bằng Zod. – Auth bằng auth(). – Permission tách vào authz.ts. – Không để logic quyền rải rác.


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

Đừng đổi nếu:

– App đang dùng nhiều Realtime. – RLS đã ổn, đã test kỹ. – Supabase Storage đang phục vụ file tốt. – Team quen Supabase dashboard. – Migration auth quá rủi ro. – Không có thời gian viết lại permission server-side.

Đổi stack chỉ vì “tối giản” nhưng làm chậm sản phẩm là không đáng.


Kết luận thực tế

Neon + Auth.js là stack rất hợp cho app serverless hiện đại: PostgreSQL chuẩn, auth linh hoạt, ít lock-in, dễ deploy, dễ kiểm soát logic. Nó đặc biệt tốt với các app Next.js/SaaS nhỏ đến vừa, nơi server là lớp trung tâm xử lý dữ liệu.

Nhưng đây không phải bản “Supabase rẻ hơn”. Đây là cách kiến trúc khác: ít nền tảng hơn, nhiều trách nhiệm trong code hơn. Nếu bạn cần Realtime, Storage, dashboard backend hoàn chỉnh, Supabase vẫn rất mạnh. Nếu bạn chỉ cần Postgres tốt + auth linh hoạt + server-side logic rõ ràng, Neon + Auth.js là lựa chọn gọn, sạch, đáng dùng.

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!