Supabase چیست؟ ساخت اپ یادداشت امن با Next.js و Row Level Security
در این آموزش یک اپ یادداشت با Next.js و Supabase میسازید، Auth و Client را راه میاندازید، جدول Postgres ایجاد میکنید و با RLS تضمین میکنید هر کاربر فقط داده خودش را ببیند.
ساخت یک اپلیکیشن فقط به طراحی رابط کاربری ختم نمیشود. کاربران باید وارد شوند، داده ذخیره شود، API پاسخ بدهد و هر شخص فقط اطلاعات مجاز را ببیند. Supabase این اجزا را روی PostgreSQL، سیستم احراز هویت، API، Storage و قابلیتهای Realtime کنار هم قرار میدهد.
در این آموزش یک اپ یادداشت کوچک با Next.js میسازیم. هر کاربر پس از ورود میتواند یادداشتهای خودش را ایجاد و مشاهده کند. بخش مهم پروژه Row Level Security یا RLS است؛ حتی اگر درخواست مستقیماً از Browser به Supabase برسد، Database اجازه مشاهده سطرهای کاربر دیگر را نمیدهد.
Supabase چیست؟
Supabase یک پلتفرم توسعه مبتنی بر PostgreSQL است که Database، Auth، API خودکار، Storage، Realtime و Edge Function ارائه میکند. برخلاف Backend ساختگی، هسته داده یک PostgreSQL واقعی است و میتوانید Schema، Constraint، Index، Function و Policyهای خود Database را طراحی کنید.
Supabase جای طراحی Backend و Security را نمیگیرد. Dashboard ساخت منابع را سریع میکند، اما تصمیم درباره مدل داده، سطح دسترسی، Validation، Migration، Backup و Observability همچنان بر عهده تیم است.
معماری پروژه Notes
| بخش | وظیفه | مرز امنیتی |
|---|---|---|
| Next.js | رابط، Server Component و Route | ورودی را Validate میکند |
| Supabase Auth | ورود و Session کاربر | هویت را در JWT بیان میکند |
| Postgres | ذخیره یادداشتها | Constraint و Transaction |
| RLS | فیلتر دسترسی هر سطر | تطبیق auth.uid با user_id |
| Publishable Key | شناسایی پروژه در Client | قدرت آن با Role و RLS محدود میشود |
| Secret Key | عملیات مدیریتی Server | RLS را دور میزند و هرگز عمومی نیست |
در این نمونه Browser از Publishable Key استفاده میکند. این کلید برای حضور در Client طراحی شده، اما عمومیبودن آن به معنی عمومیبودن داده نیست. مجوز نهایی را Grant و RLS تعیین میکنند. Secret Key یا Service Role نباید با پیشوند NEXT_PUBLIC، داخل Bundle یا Repository قرار بگیرد.
پیشنیازها
- Node.js و npm سازگار با نسخه فعلی Next.js
- یک حساب و Project آزمایشی Supabase
- آشنایی پایه با React، TypeScript و SQL
- Git و یک Branch جدا برای تمرین
- دو حساب آزمایشی برای تست جداسازی داده
از Project آزمایشی استفاده کنید. اجرای SQL مقاله روی Database واقعی بدون Migration، Backup و Review مناسب نیست. Region و Plan را براساس محل کاربران، نیاز قانونی و هزینه انتخاب کنید.
ساخت پروژه Next.js
یک پروژه App Router بسازید و Packageهای رسمی Supabase را نصب کنید. بسته @supabase/ssr برای Frameworkهایی مناسب است که Session را در Cookie نگه میدارند. API آن در مستندات فعلی Beta معرفی شده و ممکن است تغییر کند؛ نسخه Package را ثبت و راهنمای همان نسخه را دنبال کنید.
npx create-next-app@latest secure-notes --ts --app --eslintcd secure-notesnpm install @supabase/supabase-js @supabase/ssrnpm run devقبل از Commit، Lockfile را بررسی کنید. Package را از Registry معتبر بگیرید و نام Scope را دقیق بنویسید. نصب نسخه latest برای شروع آموزشی ساده است، اما در CI باید Lockfile و ارتقای کنترلشده داشته باشید.
ساخت Project در Supabase
- یک Project جدید با Region مناسب بسازید.
- رمز Database را در Password Manager نگه دارید.
- از بخش Connect یا API، Project URL و Publishable Key را بردارید.
- Secret Key را فقط برای Backend مدیریتی جدا نگه دارید.
- Auth Redirect URLهای Local و Production را صریح ثبت کنید.
کلید Publishable برای درخواستهای معمول Client است. Secret Key دارای قدرت مدیریتی و عبور از RLS است. اگر Secret داخل Browser Bundle منتشر شود، Policyهای سطح سطر عملاً قابل دورزدن میشوند. پسوند یا نام متغیر امنیت ایجاد نمیکند؛ محل اجرا و نوع Key مهم است.
تنظیم Environment Variables
در ریشه پروژه فایل .env.local بسازید و فقط مقادیر Client-safe را با پیشوند عمومی قرار دهید. فایل واقعی را Commit نکنید؛ یک .env.example بدون مقدار حساس برای تیم نگه دارید.
NEXT_PUBLIC_SUPABASE_URL=https://YOUR_PROJECT.supabase.coNEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=YOUR_PUBLISHABLE_KEYاین مقادیر به Browser میرسند و نباید Secret تلقی شوند. امنیت داده باید با Auth، Role، Grant و RLS برقرار باشد. Secret Key، Database Password و Token مدیریتی را با این پیشوند تعریف نکنید.
ساخت Client مرورگر
برای Client Component از createBrowserClient استفاده میکنیم. URL و Key را در Startup کنترل کنید تا خطای پیکربندی بهصورت واضح دیده شود.
import { createBrowserClient } from '@supabase/ssr' export function createClient() { const url = process.env.NEXT_PUBLIC_SUPABASE_URL const key = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY if (!url || !key) { throw new Error('Supabase public configuration is missing') } return createBrowserClient(url, key)}برای Server Component و Server Action باید Client جداگانه Cookie-aware مطابق راهنمای SSR بسازید. Client Browser را روی Server و Client مدیریتی را در Browser استفاده نکنید. تفکیک Utilityها جلوی واردشدن اشتباه Secret به Bundle را میگیرد.
طراحی جدول Notes
جدول باید مالک هر سطر را صریح ذخیره کند. Constraint طول عنوان و متن از داده بینهایت یا نامعتبر جلوگیری میکند. created_at را Server تولید میکند و user_id به auth.users متصل میشود.
create table public.notes ( id uuid primary key default gen_random_uuid(), user_id uuid not null references auth.users(id) on delete cascade, title text not null check (char_length(title) between 1 and 120), body text not null default '' check (char_length(body) <= 5000), created_at timestamptz not null default now(), updated_at timestamptz not null default now()); create index notes_user_created_idx on public.notes (user_id, created_at desc);این تغییر را در پروژه تیمی بهصورت Migration ثبت کنید، نه Paste تکرارنشدنی در Dashboard. Constraint سمت Database جای Validation رابط را نمیگیرد؛ هر دو لازماند. رابط پیام سریع میدهد و Database آخرین مرز یکپارچگی است.
چرا RLS ضروری است؟
Supabase API میتواند مستقیماً از Browser فراخوانی شود. اگر جدول در Schema قابل دسترس بدون RLS و Grant درست منتشر شود، مهاجم میتواند UI شما را کنار بگذارد و مستقیم API را صدا بزند. مخفیکردن Endpoint یا Obfuscate کردن JavaScript دفاع نیست.
RLS شبیه افزودن یک شرط WHERE اجباری به هر Query است. Policy زیر تضمین میکند user_id سطر با شناسه User فعلی یکسان باشد. Policy را برای SELECT، INSERT، UPDATE و DELETE جدا مینویسیم تا قصد هر عملیات روشن باشد.
فعالکردن RLS و تنظیم Grantها
alter table public.notes enable row level security; revoke all on table public.notes from anon;revoke all on table public.notes from authenticated;grant select, insert, update, delete on table public.notes to authenticated; create policy "read own notes" on public.notes for select to authenticated using ((select auth.uid()) = user_id); create policy "insert own notes" on public.notes for insert to authenticated with check ((select auth.uid()) = user_id); create policy "update own notes" on public.notes for update to authenticated using ((select auth.uid()) = user_id) with check ((select auth.uid()) = user_id); create policy "delete own notes" on public.notes for delete to authenticated using ((select auth.uid()) = user_id);Grant مشخص میکند Role اصولاً چه Operationهایی دارد و Policy تعیین میکند آن Operation روی کدام سطرها مجاز است. اضافهکردن Policy بهتنهایی Grantهای گسترده موجود را پس نمیگیرد. هر دو را در یک Migration و Test نگه دارید.
تفاوت USING و WITH CHECK
| عبارت | کنترل | نمونه |
|---|---|---|
| USING | کدام سطر موجود قابل مشاهده یا هدف عملیات است | فقط Note متعلق به User |
| WITH CHECK | سطر جدید یا وضعیت نهایی Update مجاز است یا نه | کاربر نمیتواند user_id را به شخص دیگر تغییر دهد |
در UPDATE استفاده همزمان از USING و WITH CHECK اهمیت دارد. اولی انتخاب سطر اولیه و دومی وضعیت نهایی را کنترل میکند.
راهاندازی ورود کاربر
برای نمونه ساده میتوانید ورود ایمیلی یا Magic Link را از Supabase Auth فعال کنید. Redirect URL باید دقیقاً با محیط Local و Production هماهنگ باشد. هر URL دلخواه را Allow نکنید؛ Open Redirect میتواند Token یا جریان ورود را به مقصد نامطمئن ببرد.
در Next.js SSR، Session در Cookie قرار میگیرد و @supabase/ssr برای ساخت Client مرورگر و Server طراحی شده است. Cookie را در Client دستی جعل نکنید و نتیجه getSession را برای Authorization حساس تنها مرجع قرار ندهید؛ راهنمای رسمی همان نسخه را برای Validation هویت Server دنبال کنید.
خواندن یادداشتهای کاربر
پس از ورود، Query ساده notes فقط سطرهایی را برمیگرداند که Policy اجازه میدهد. فیلتر user_id در Client میتواند برای وضوح یا Performance اضافه شود، اما جای RLS نیست؛ مهاجم میتواند Query را تغییر دهد.
import { createClient } from '@/lib/supabase/client' export async function listNotes() { const supabase = createClient() const { data, error } = await supabase .from('notes') .select('id,title,body,created_at,updated_at') .order('created_at', { ascending: false }) if (error) throw error return data}فقط ستونهای لازم را Select کنید. نمایش Error خام Database به کاربر میتواند Schema را افشا کند؛ در UI پیام عمومی و در Log امن Correlation ID نگه دارید.
ایجاد یادداشت جدید
ابتدا User جاری را بگیرید، ورودی را Trim و محدود کنید و سپس user_id را از Session معتبر قرار دهید. user_id ارسالی از Form را قبول نکنید.
import { createClient } from '@/lib/supabase/client' export async function createNote(titleInput: string, bodyInput: string) { const title = titleInput.trim() const body = bodyInput.trim() if (!title || title.length > 120 || body.length > 5000) { throw new Error('Invalid note') } const supabase = createClient() const { data: authData, error: authError } = await supabase.auth.getUser() if (authError || !authData.user) throw new Error('Sign in required') const { data, error } = await supabase .from('notes') .insert({ user_id: authData.user.id, title, body }) .select('id,title,body,created_at') .single() if (error) throw error return data}RLS دوباره بررسی میکند که auth.uid با user_id برابر باشد. این دفاع دوم ضروری است؛ Validation و Session در Client قابل تغییرند، اما Policy داخل Database برای هر درخواست اعمال میشود.
تست واقعی RLS با دو کاربر
- با User A وارد شوید و یک Note بسازید.
- شناسه Note را ثبت کنید و Sign out کنید.
- با User B وارد شوید.
- فهرست Notes را باز کنید؛ Note کاربر A نباید دیده شود.
- با Query مستقیم تلاش کنید همان ID را Select یا Update کنید.
- عملیات باید صفر سطر برگرداند یا با خطای مجوز رد شود.
- با User A دوباره وارد و دسترسی صحیح را تأیید کنید.
فقط تست مسیر مجاز کافی نیست. برای هر Policy تست Allow و Deny بنویسید. مستندات رسمی اجرای supabase test db را برای آزمون Policyها پیشنهاد میکنند. تست باید Roleهای anon و authenticated و عملیات SELECT، INSERT، UPDATE و DELETE را پوشش دهد.
خطاهای رایج RLS
| نشانه | علت محتمل | راه اصلاح |
|---|---|---|
| همه کاربران همه Notes را میبینند | RLS خاموش یا Secret Key در Client | کلید را Rotate و Policy را تست کنید |
| هیچ Queryای کار نمیکند | Grant یا Policy لازم وجود ندارد | هر دو لایه را بررسی کنید |
| Insert رد میشود | user_id با auth.uid برابر نیست | شناسه را از User معتبر بگیرید |
| Update مالکیت را تغییر میدهد | WITH CHECK جا افتاده | وضعیت نهایی را نیز محدود کنید |
| View داده را افشا میکند | رفتار Security Definer | View را مطابق راهنمای امن Expose کنید |
| Log شامل Token است | ثبت Header یا Cookie کامل | Redaction و Log حداقلی اعمال کنید |
SSR در Next.js چه تغییری ایجاد میکند؟
در SSR، Session باید میان Browser و Server از طریق Cookie امن جابهجا و Refresh شود. بسته @supabase/ssr Utilityهای Browser و Server را جدا میکند. Server Component، Server Action و Route Handler باید Client Server-side مخصوص خود را دریافت کنند.
SSR امنیت RLS را جایگزین نمیکند. حتی اگر Page فقط روی Server Render شود، درخواست Database باید تحت Role User اجرا شود. استفاده بیدلیل از Secret Key در Server باعث میشود RLS دور زده شود و یک Bug ساده همه دادهها را برگرداند.
Secret Key چه زمانی لازم است؟
عملیات مدیریتی مانند Job داخلی قابل اعتماد، Migration یا پردازش Backend ممکن است به Secret Key نیاز داشته باشد. این Client را در Module server-only، محیط امن و با Scope عملیاتی محدود نگه دارید. هیچ User Access Token را همراه Client مدیریتی اشتباه ترکیب نکنید.
- Secret را در NEXT_PUBLIC قرار ندهید.
- آن را در Git، Docker Image یا Log ذخیره نکنید.
- برای Local، Staging و Production کلید جدا داشته باشید.
- مصرف و آخرین استفاده را Monitor کنید.
- در صورت افشا فوراً Rotate کنید.
- عملیات مدیریتی را Audit و Rate Limit کنید.
Storage و دسترسی فایل
Supabase Storage نیز از RLS روی storage.objects استفاده میکند. Bucket عمومی برای Avatar عمومی مناسب است، اما سند خصوصی، فاکتور و فایل کاربر نباید فقط به دلیل داشتن URL تصادفی Public شوند. برای Upload، Read، Update و Delete Policy جدا بسازید.
Service Key کنترل Storage را دور میزند و نباید به Browser برسد. Ownership بهتنهایی مجوز ایجاد نمیکند؛ Policy باید owner_id یا مسیر Object را با User تطبیق دهد. عملیات فایل را از API Storage انجام دهید و جدولهای Schema storage را مستقیم دستکاری نکنید.
Realtime و RLS
Realtime تجربه همزمان میدهد، اما Subscription نیز باید از مرز Authorization عبور کند. Channel را پس از Login بسازید، Subscription را هنگام Sign out پاک کنید و Payload را بهعنوان ورودی غیرقابل اعتماد Validate کنید.
اگر هر User فقط Notes خودش را میبیند، Query اولیه و رویدادهای Realtime باید همان Policy را رعایت کنند. تست کنید User B با تغییر دستی Filter نتواند Payload User A را دریافت کند.
Migration و محیطهای مختلف
تغییر Schema و Policy را به Migration تبدیل کنید و روی Local یا Branch Database آزمایش کنید. ویرایش مستقیم Production در Dashboard تاریخچه قابل Review ندارد و Rollback را دشوار میکند.
| محیط | داده | هدف |
|---|---|---|
| Local | ساختگی | Migration و Test سریع |
| Preview | Seed محدود | بررسی Pull Request |
| Staging | غیرحساس و شبیه Production | Smoke Test و Performance |
| Production | واقعی | فقط Migration تأییدشده |
چکلیست امنیتی پیش از انتشار
- RLS روی همه جدولهای Exposed فعال است.
- Grantهای anon و authenticated حداقلی هستند.
- برای هر Operation Policy جدا و تست Allow و Deny وجود دارد.
- Secret Key در Browser Bundle یا Repository نیست.
- Redirect URLهای Auth محدود و دقیق هستند.
- ورودی در UI و Database Validate میشود.
- دو User واقعی جداسازی سطرها را آزمودهاند.
- View، Function و Storage نیز از نظر مجوز بررسی شدهاند.
- Migration، Backup و Rollback مستند هستند.
- Logها Token، Cookie و اطلاعات شخصی را Redact میکنند.
Supabase یا Backend اختصاصی؟
برای MVP، ابزار داخلی، پروژه رزومهای و محصولی که PostgreSQL و Auth استاندارد میخواهد، Supabase میتواند زمان راهاندازی را کم کند. اگر Domain پیچیده، Workflow تراکنشی سنگین، شبکه خصوصی خاص یا Compliance ویژه دارید، Backend اختصاصی یا ترکیب کنترلشده ممکن است مناسبتر باشد.
این انتخاب صفر و یک نیست. میتوانید Supabase را برای Auth و Database بهکار ببرید و Business Logic حساس را در Server خود نگه دارید. مهم این است که مرز مسئولیت، Credential و Authorization روشن باشد.
مسیرهای مکمل یادگیری
برای مقایسه با API اختصاصی Python، آموزش FastAPI و SQLAlchemy را ببینید. اگر Frontend مستقل از CMS میسازید، راهنمای Headless WordPress و Next.js مفید است. برای Automation بعد از ثبت داده نیز آموزش n8n مسیر مکملی ارائه میدهد.
جمعبندی
Supabase سرعت ساخت Backend را با قدرت PostgreSQL ترکیب میکند، اما امنیت آن به طراحی درست Role، Grant و Row Level Security وابسته است. Publishable Key میتواند در Client باشد؛ Secret Key هرگز نباید عمومی شود. مهمترین آزمون این پروژه این است که User دوم نتواند Note User اول را ببیند یا تغییر دهد.
با Project آزمایشی شروع کنید، جدول Notes را در Migration بسازید، RLS را فعال و Policy هر Operation را جدا تعریف کنید. سپس Auth، Query و Form را اضافه و تست Allow و Deny را با دو حساب مستقل اجرا کنید.
امروز یک Project آزمایشی بساز، فقط جدول Notes را ایجاد کن و قبل از طراحی UI، جداسازی داده دو کاربر را با RLS ثابت کن.