وبلاگ / راهنمای مسیر یادگیری و فناوری
راهنمای مسیر یادگیری و فناوری

Supabase چیست؟ ساخت اپ یادداشت امن با Next.js و Row Level Security

در این آموزش یک اپ یادداشت با Next.js و Supabase می‌سازید، Auth و Client را راه می‌اندازید، جدول Postgres ایجاد می‌کنید و با RLS تضمین می‌کنید هر کاربر فقط داده خودش را ببیند.

· ۴۰ بازدید

ساخت یک اپلیکیشن فقط به طراحی رابط کاربری ختم نمی‌شود. کاربران باید وارد شوند، داده ذخیره شود، API پاسخ بدهد و هر شخص فقط اطلاعات مجاز را ببیند. این اجزا را روی ، سیستم ، API، Storage و قابلیت‌های Realtime کنار هم قرار می‌دهد.

در این آموزش یک اپ یادداشت کوچک با می‌سازیم. هر کاربر پس از ورود می‌تواند یادداشت‌های خودش را ایجاد و مشاهده کند. بخش مهم پروژه یا RLS است؛ حتی اگر درخواست مستقیماً از Browser به برسد، Database اجازه مشاهده سطرهای کاربر دیگر را نمی‌دهد.

چیست؟

یک پلتفرم توسعه مبتنی بر است که Database، Auth، API خودکار، Storage، Realtime و Edge Function ارائه می‌کند. برخلاف Backend ساختگی، هسته داده یک واقعی است و می‌توانید Schema، Constraint، Index، Function و Policyهای خود Database را طراحی کنید.

جای طراحی Backend و Security را نمی‌گیرد. Dashboard ساخت منابع را سریع می‌کند، اما تصمیم درباره مدل داده، سطح دسترسی، Validation، Migration، Backup و Observability همچنان بر عهده تیم است.

معماری پروژه Notes

بخشوظیفهمرز امنیتی
رابط، Server Component و Routeورودی را Validate می‌کند
Authورود و Session کاربرهویت را در JWT بیان می‌کند
Postgresذخیره یادداشت‌هاConstraint و Transaction
RLSفیلتر دسترسی هر سطرتطبیق auth.uid با user_id
Publishable Keyشناسایی پروژه در Clientقدرت آن با Role و RLS محدود می‌شود
Secret Keyعملیات مدیریتی ServerRLS را دور می‌زند و هرگز عمومی نیست

در این نمونه Browser از Publishable Key استفاده می‌کند. این کلید برای حضور در Client طراحی شده، اما عمومی‌بودن آن به معنی عمومی‌بودن داده نیست. مجوز نهایی را Grant و RLS تعیین می‌کنند. Secret Key یا Service Role نباید با پیشوند NEXT_PUBLIC، داخل Bundle یا Repository قرار بگیرد.

پیش‌نیازها

  • Node.js و npm سازگار با نسخه فعلی
  • یک حساب و Project آزمایشی
  • آشنایی پایه با React، TypeScript و SQL
  • Git و یک Branch جدا برای تمرین
  • دو حساب آزمایشی برای تست جداسازی داده

از Project آزمایشی استفاده کنید. اجرای SQL مقاله روی Database واقعی بدون Migration، Backup و Review مناسب نیست. Region و Plan را براساس محل کاربران، نیاز قانونی و هزینه انتخاب کنید.

ساخت پروژه

یک پروژه App Router بسازید و Packageهای رسمی را نصب کنید. بسته @/ssr برای Frameworkهایی مناسب است که Session را در Cookie نگه می‌دارند. API آن در مستندات فعلی Beta معرفی شده و ممکن است تغییر کند؛ نسخه Package را ثبت و راهنمای همان نسخه را دنبال کنید.

terminal.shBash
npx create-next-app@latest secure-notes --ts --app --eslintcd secure-notesnpm install @supabase/supabase-js @supabase/ssrnpm run dev
UTF-8Bash

قبل از Commit، Lockfile را بررسی کنید. Package را از Registry معتبر بگیرید و نام Scope را دقیق بنویسید. نصب نسخه latest برای شروع آموزشی ساده است، اما در CI باید Lockfile و ارتقای کنترل‌شده داشته باشید.

ساخت Project در

  1. یک Project جدید با Region مناسب بسازید.
  2. رمز Database را در Password Manager نگه دارید.
  3. از بخش Connect یا API، Project URL و Publishable Key را بردارید.
  4. Secret Key را فقط برای Backend مدیریتی جدا نگه دارید.
  5. Auth Redirect URLهای Local و Production را صریح ثبت کنید.

کلید Publishable برای درخواست‌های معمول Client است. Secret Key دارای قدرت مدیریتی و عبور از RLS است. اگر Secret داخل Browser Bundle منتشر شود، Policyهای سطح سطر عملاً قابل دورزدن می‌شوند. پسوند یا نام متغیر امنیت ایجاد نمی‌کند؛ محل اجرا و نوع Key مهم است.

تنظیم Environment Variables

در ریشه پروژه فایل .env.local بسازید و فقط مقادیر Client-safe را با پیشوند عمومی قرار دهید. فایل واقعی را Commit نکنید؛ یک .env.example بدون مقدار حساس برای تیم نگه دارید.

.env.localdotenv
NEXT_PUBLIC_SUPABASE_URL=https://YOUR_PROJECT.supabase.coNEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=YOUR_PUBLISHABLE_KEY
UTF-8dotenv

این مقادیر به Browser می‌رسند و نباید Secret تلقی شوند. امنیت داده باید با Auth، Role، Grant و RLS برقرار باشد. Secret Key، Database Password و Token مدیریتی را با این پیشوند تعریف نکنید.

ساخت Client مرورگر

برای Client Component از createBrowserClient استفاده می‌کنیم. URL و Key را در Startup کنترل کنید تا خطای پیکربندی به‌صورت واضح دیده شود.

lib//client.tsTypeScript
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)}
UTF-8TypeScript

برای 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 متصل می‌شود.

001_create_notes.sqlSQL
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);
UTF-8SQL

این تغییر را در پروژه تیمی به‌صورت Migration ثبت کنید، نه Paste تکرارنشدنی در Dashboard. Constraint سمت Database جای Validation رابط را نمی‌گیرد؛ هر دو لازم‌اند. رابط پیام سریع می‌دهد و Database آخرین مرز یکپارچگی است.

چرا RLS ضروری است؟

API می‌تواند مستقیماً از Browser فراخوانی شود. اگر جدول در Schema قابل دسترس بدون RLS و Grant درست منتشر شود، مهاجم می‌تواند UI شما را کنار بگذارد و مستقیم API را صدا بزند. مخفی‌کردن Endpoint یا Obfuscate کردن JavaScript دفاع نیست.

RLS شبیه افزودن یک شرط WHERE اجباری به هر Query است. Policy زیر تضمین می‌کند user_id سطر با شناسه User فعلی یکسان باشد. Policy را برای SELECT، INSERT، UPDATE و DELETE جدا می‌نویسیم تا قصد هر عملیات روشن باشد.

فعال‌کردن RLS و تنظیم Grantها

002_secure_notes.sqlSQL
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);
UTF-8SQL

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 را از Auth فعال کنید. Redirect URL باید دقیقاً با محیط Local و Production هماهنگ باشد. هر URL دلخواه را Allow نکنید؛ Open Redirect می‌تواند Token یا جریان ورود را به مقصد نامطمئن ببرد.

در SSR، Session در Cookie قرار می‌گیرد و @/ssr برای ساخت Client مرورگر و Server طراحی شده است. Cookie را در Client دستی جعل نکنید و نتیجه getSession را برای Authorization حساس تنها مرجع قرار ندهید؛ راهنمای رسمی همان نسخه را برای Validation هویت Server دنبال کنید.

خواندن یادداشت‌های کاربر

پس از ورود، Query ساده notes فقط سطرهایی را برمی‌گرداند که Policy اجازه می‌دهد. فیلتر user_id در Client می‌تواند برای وضوح یا Performance اضافه شود، اما جای RLS نیست؛ مهاجم می‌تواند Query را تغییر دهد.

lib/notes.tsTypeScript
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}
UTF-8TypeScript

فقط ستون‌های لازم را Select کنید. نمایش Error خام Database به کاربر می‌تواند Schema را افشا کند؛ در UI پیام عمومی و در Log امن Correlation ID نگه دارید.

ایجاد یادداشت جدید

ابتدا User جاری را بگیرید، ورودی را Trim و محدود کنید و سپس user_id را از Session معتبر قرار دهید. user_id ارسالی از Form را قبول نکنید.

lib/create-note.tsTypeScript
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}
UTF-8TypeScript

RLS دوباره بررسی می‌کند که auth.uid با user_id برابر باشد. این دفاع دوم ضروری است؛ Validation و Session در Client قابل تغییرند، اما Policy داخل Database برای هر درخواست اعمال می‌شود.

تست واقعی RLS با دو کاربر

  1. با User A وارد شوید و یک Note بسازید.
  2. شناسه Note را ثبت کنید و Sign out کنید.
  3. با User B وارد شوید.
  4. فهرست Notes را باز کنید؛ Note کاربر A نباید دیده شود.
  5. با Query مستقیم تلاش کنید همان ID را Select یا Update کنید.
  6. عملیات باید صفر سطر برگرداند یا با خطای مجوز رد شود.
  7. با User A دوباره وارد و دسترسی صحیح را تأیید کنید.

فقط تست مسیر مجاز کافی نیست. برای هر Policy تست Allow و Deny بنویسید. مستندات رسمی اجرای 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 DefinerView را مطابق راهنمای امن Expose کنید
Log شامل Token استثبت Header یا Cookie کاملRedaction و Log حداقلی اعمال کنید

SSR در چه تغییری ایجاد می‌کند؟

در SSR، Session باید میان Browser و Server از طریق Cookie امن جابه‌جا و Refresh شود. بسته @/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 و دسترسی فایل

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 سریع
PreviewSeed محدودبررسی Pull Request
Stagingغیرحساس و شبیه ProductionSmoke 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 می‌کنند.

یا Backend اختصاصی؟

برای MVP، ابزار داخلی، پروژه رزومه‌ای و محصولی که و Auth استاندارد می‌خواهد، می‌تواند زمان راه‌اندازی را کم کند. اگر Domain پیچیده، Workflow تراکنشی سنگین، شبکه خصوصی خاص یا Compliance ویژه دارید، Backend اختصاصی یا ترکیب کنترل‌شده ممکن است مناسب‌تر باشد.

این انتخاب صفر و یک نیست. می‌توانید را برای Auth و Database به‌کار ببرید و Business Logic حساس را در Server خود نگه دارید. مهم این است که مرز مسئولیت، Credential و Authorization روشن باشد.

مسیرهای مکمل یادگیری

برای مقایسه با API اختصاصی Python، آموزش FastAPI و SQLAlchemy را ببینید. اگر Frontend مستقل از CMS می‌سازید، راهنمای Headless WordPress و Next.js مفید است. برای Automation بعد از ثبت داده نیز آموزش n8n مسیر مکملی ارائه می‌دهد.

جمع‌بندی

سرعت ساخت Backend را با قدرت ترکیب می‌کند، اما امنیت آن به طراحی درست Role، Grant و وابسته است. Publishable Key می‌تواند در Client باشد؛ Secret Key هرگز نباید عمومی شود. مهم‌ترین آزمون این پروژه این است که User دوم نتواند Note User اول را ببیند یا تغییر دهد.

با Project آزمایشی شروع کنید، جدول Notes را در Migration بسازید، RLS را فعال و Policy هر Operation را جدا تعریف کنید. سپس Auth، Query و Form را اضافه و تست Allow و Deny را با دو حساب مستقل اجرا کنید.

امروز یک Project آزمایشی بساز، فقط جدول Notes را ایجاد کن و قبل از طراحی UI، جداسازی داده دو کاربر را با RLS ثابت کن.

منابع رسمی