TanStack Start + Supabase Auth: Cài đặt cho Môi trường Sản xuất (2026)
Jimmy Smith
Supabase Auth phiên bản production trên TanStack Start cần bốn thứ mà hầu hết các hướng dẫn nhanh đều bỏ qua: một client phía server của @supabase/ssr xử lý đúng cookie được kết nối với @tanstack/react-start/server, supabase.auth.getClaims() để có JWT đã được xác thực trên mọi yêu cầu, các bộ bảo vệ beforeLoad cộng với một ngăn xếp middleware RBAC thực sự cho các hàm phía server, và các RPC Postgres có phạm vi RLS để việc truy cập dữ liệu không bị rò rỉ vào các bộ lọc account_id tự tạo thủ công. Chúng tôi cung cấp tất cả những điều này ngay hôm nay trong biến thể Supabase của TanStack Start SaaS kit, và mọi mẫu mã nguồn dưới đây đều được lấy từ cơ sở mã nguồn đó chứ không phải phác thảo từ tài liệu.
Hướng dẫn nhanh chính thức của Supabase chỉ cho bạn cách CRUD chỉ đọc mà không có tính năng xác thực. Tích hợp Convex đẩy việc xác thực sang bên thứ ba. Một vài bài viết blog chất lượng đều bao phủ một nửa của thiết lập thực tế: một bài hoàn thiện client phía server xử lý đúng cookie nhưng bỏ qua việc bảo vệ tuyến đường, bài còn lại có tư duy bảo mật đúng đắn nhưng không bao giờ nhắc đến Supabase hoặc RLS. Không ai đưa toàn bộ luồng production, bao gồm MFA và quyền truy cập dựa trên vai trò, vào một nơi. Đó là nội dung của bài viết này, và nó không mang tính lý thuyết: đây là lớp xác thực của một sản phẩm đang được triển khai thực tế.
Bài viết này được viết cho TanStack Start trên Vite + Nitro với @supabase/ssr bản hiện tại và React 19, được kiểm thử vào tháng 7 năm 2026 với bản phát hành 1.0.0 của chính bộ công cụ chúng tôi. TanStack Start vẫn còn non trẻ và phát triển rất nhanh, vì vậy hãy lưu ý ngày tháng của bất kỳ mã nguồn nào bạn sao chép từ các hướng dẫn, bao gồm cả bài viết này.
Mới làm quen với framework này? Hãy bắt đầu với TanStack Start so với Next.js để hiểu về mô hình tư duy, sau đó quay lại đây.
Tại sao các hướng dẫn nhanh là chưa đủ
Khoảng trống ở đây không phải vì không có tài liệu hay, mà là vì không có nguồn duy nhất nào kết nối các mảnh ghép lại với nhau, và các mảnh ghép này lồng vào nhau:
- Hướng dẫn nhanh chính thức của Supabase: Chuẩn mực và chính xác trong phạm vi của nó, nhưng đó là CRUD chỉ đọc với RLS được bật mặc định và không có xác thực. Không có phiên
@supabase/ssr, không cócreateServerFn, không cóbeforeLoad. - Tích hợp Convex: Xếp hạng cao, nhưng hoàn toàn không có mô hình RLS và đẩy việc xác thực sang Clerk hoặc WorkOS. Không phải là tài liệu tham khảo về Supabase.
- Các bài viết blog chuyên sâu về Supabase hay nhất: Làm đúng phần
createServerClientxử lý cookie chính xác — phần khó nhất — nhưng lại dừng lại ở "bạn đã đăng nhập". Không bảo vệ tuyến đường, không ủy quyền hàm phía server, không có vai trò. - Các bài hướng dẫn xác thực hay nhất: Có mô hình tư duy bảo mật đúng đắn (hàm phía server là ranh giới, bảo vệ hai lần) nhưng không bao giờ chạm đến Supabase,
@supabase/ssr, hoặc RLS.
Kết hợp mô hình bảo mật với các chi tiết cụ thể của Supabase, thêm định tuyến được bảo vệ, RBAC, MFA và RLS, và bạn sẽ có một thiết lập có thể sống sót khi tiếp xúc với người dùng thực tế. Hãy cùng xây dựng nó.
Mô hình tư duy: Hàm phía server của bạn là ranh giới bảo mật
Hãy bắt đầu từ đây, bởi vì làm sai điều này chính là nguyên nhân khiến các ứng dụng "đã được xác thực" bị rò rỉ dữ liệu.
TanStack Start không có middleware.ts và không có proxy kiểu Next.js đứng trước mọi tuyến đường. Có một middleware yêu cầu toàn cục do bạn tự đăng ký, cộng với các bộ bảo vệ beforeLoad cấp độ tuyến đường, cộng với các hàm phía server. Điều quan trọng cần ghi nhớ: bộ bảo vệ beforeLoad bảo vệ việc điều hướng, chứ không phải dữ liệu. Nó quyết định xem người dùng có được phép xem một màn hình hay không. Nó không làm gì để bảo vệ các điểm cuối mà màn hình đó gọi đến.
Các hàm phía server (createServerFn) là các RPC kiểu POST có thể gọi trực tiếp. Bất kỳ ai có thể truy cập vào ứng dụng của bạn đều có thể gọi trực tiếp một hàm, có hoặc không cần tải tuyến đường thường gọi nó. Chúng chạy qua đường ống yêu cầu giống như bất kỳ yêu cầu POST nào khác, đó chính là lý do tại sao mỗi hàm phải tự xác thực quyền của chính nó. Không ai làm thay việc đó cho chúng.
Vì vậy, bạn bảo vệ hai lần, và hai bộ bảo vệ này làm hai nhiệm vụ khác nhau:
- Bộ bảo vệ
beforeLoad= Trải nghiệm người dùng (UX). Chuyển hướng người dùng ẩn danh đến trang đăng nhập trước khi họ nhìn thấy màn hình được bảo vệ. Nhanh chóng, trải nghiệm tốt, không phải là biện pháp kiểm soát bảo mật. - Xác thực hàm phía server = Bảo mật. Mọi hàm phía server đọc hoặc ghi dữ liệu riêng tư đều tự kiểm tra người dùng, hoặc chạy qua middleware làm nhiệm vụ đó. Đây mới là ranh giới thực sự.
Biến thể Supabase của bộ công cụ chúng tôi mã hóa chính xác sự phân chia này, và nó tiến xa hơn một hàm bảo vệ duy nhất: đó là một ngăn xếp middleware RBAC có thể kết hợp mà mọi hàm phía server đều tham gia theo cấu trúc. Đó là phần mà hầu hết các bài viết kiểu "cách thêm Supabase vào TanStack Start" hoàn toàn bỏ qua, bởi vì họ dừng lại ở việc "kiểm tra xem có người dùng hay không."
Thiết lập client phía server của @supabase/ssr
Client phía server là thành phần chịu lực chính. Nó phải đọc cookie xác thực từ yêu cầu đến và ghi các cookie đã được làm mới trở lại phản hồi, thông qua bề mặt server của TanStack Start chứ không phải các trình hỗ trợ của Next.js.
Cách thiết lập Supabase SSR server client trong TanStack Start:
- Tạo một dự án Supabase và lấy URL dự án cùng khóa công khai (anon key).
- Cài đặt
@supabase/ssrvà@supabase/supabase-js. - Xây dựng một client phía server với
createServerClient, truyền các hàm truy cập cookiegetAll/setAll. - Lấy các cookie đó từ
@tanstack/react-start/server(getRequest(),setCookie), được phân tích cú pháp bằngparseCookieHeader. - Đặt điều kiện
cookieOptions.securedựa trênNODE_ENV === 'production'để môi trường phát triển cục bộhttp://localhosttiếp tục hoạt động.
Dưới đây là client, không thay đổi từ packages/supabase/src/clients/server-client.server.ts trong bộ công cụ:
import { createServerClient, parseCookieHeader } from '@supabase/ssr';
import { getRequest, setCookie } from '@tanstack/react-start/server';
import { type Database } from '../database.types';
import { getSupabaseClientKeys } from '../get-supabase-client-keys';
export function getSupabaseServerClient<GenericSchema = Database>() {
const keys = getSupabaseClientKeys();
return createServerClient<GenericSchema>(keys.url, keys.publicKey, {
cookieOptions: {
secure: process.env.NODE_ENV === 'production',
},
cookies: {
getAll() {
const header = getRequest().headers.get('cookie') ?? '';
return parseCookieHeader(header).map(({ name, value }) => ({
name,
value: value ?? '',
}));
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
setCookie(name, value, options),
);
},
},
});
}