← वापस मंद लकड़ी की डेस्क पर चमकता पुराना टर्मिनल मॉनिटर, बिखरे हुए कागज़ और नरम बादलों की रोशनी

Supabase RLS: हर प्रोजेक्ट में जो पॉलिसीज़ मैं लिखता हूँ

2021 में एक SaaS क्लायंट लॉन्च के छः हफ़्ते बाद मेरे पास आया। उनका प्रोडक्ट एक प्रोजेक्ट मैनेजमेंट टूल था। शानदार UI, सॉलिड ऑनबोर्डिंग, बढ़िया रिटेंशन। फिर एक बीटा यूज़र को कुछ नज़र आया: GET रिक्वेस्ट में project_id को बदलकर, वो किसी और यूज़र का डेटा पढ़ सकते थे। कोई auth बाईपास नहीं। कोई SQL injection नहीं। बस एक मिसिंग पॉलिसी। टेबल में RLS चालू था, पर SELECT पॉलिसी पूरी तरह खुली थी – USING (true)। किसी ने किसी टूटोरियल से कॉपी-पेस्ट कर दिया था और फिर कभी पीछे मुड़ के देखा ही नहीं।

वो घटना मेरे साथ रह गई। तब से, मैं RLS को architecture की तरह मानता हूँ, afterthought की तरह नहीं। और Seahawk में 12,000 से ज़्यादा साइट्स और ऐप्स बनाने के बाद, मेरे पास कुछ पॉलिसीज़ हैं जो मैं लगभग आटोमैटिक लिख देता हूँ – फ्रंटएंड का एक लाइन भी लिखने से पहले।

ये वही लिस्ट है।

RLS क्यों इतना महत्वपूर्ण है

Supabase PostgreSQL पर चलता है, मतलब Row Level Security पहले दर्जे की database फीचर है, कोई bolt-on नहीं। जब आप किसी टेबल पर RLS चालू करते हो और कोई यूज़र Supabase क्लायंट से क्वेरी करता है, तो हर row आपकी पॉलिसीज़ से ऑटोमैटिक फ़िल्टर हो जाता है। चाहे आपकी API लेयर क्या करे या न करे।

बस यही बात अहम है। मैंने ऐसी टीमों के साथ काम किया है जो REST, GraphQL, edge functions, और background jobs – सब एक ही database को हिट कर रहे हैं। एक्सेस कंट्रोल को सभी के लिए application layer पर enforce करना एक nightmare है। Database layer पर enforce करना... बस हो जाता है।

ईमानदारी से, पॉलिसीज़ लिखने की friction का भुगतान उसी दिन हो जाता है जब कोई junior dev WHERE clause लगाना भूल जाता है।

एक चीज़ है जिसे लोग गलत समझते हैं। RLS चालू करना बिना किसी पॉलिसी के मतलब "खुली एक्सेस" नहीं है। मतलब normal roles के लिए कोई एक्सेस ही नहीं। हर row डिफॉल्ट से डिनाई है। इसलिए अगर आप RLS चालू करते हो और आपका ऐप तुरंत टूट जाता है, तो बस यही वजह है।

चार टेबल्स जिन्हें मैं सबसे पहले RLS से सुरक्षित करता हूँ

सबको एक जैसी जांच की ज़रूरत नहीं है। लेकिन ये चार टेबल टाइप्स को मैं कुछ भी और छूने से पहले लॉक कर देता हूँ।

  • यूज़र प्रोफाइल टेबल्स (profiles, users, accounts): ये तो ज़ाहिर है। यूज़र्स अपनी ही row पढ़ सकते हैं। शायद admins सब पढ़ सकते हैं। कोई किसी और की प्रोफाइल में लिखना नहीं चाहिए।
  • रिसोर्स/कंटेंट टेबल्स (projects, documents, posts): जो भी आपके ऐप की कोर "चीज़" है। Ownership आमतौर पर यहाँ सीधा-सादा होता है।
  • बिलिंग और सब्सक्रिप्शन टेबल्स: अगर आप Stripe यूज़ कर रहे हो और Supabase में plan डेटा, subscription स्टेटस, या invoice history स्टोर कर रहे हो, तो ये कसकर लॉक होना चाहिए। मैंने ऐसे ऐप्स देखे हैं जो trial end dates दूसरे यूज़र्स को एक्सीडेंटली expose कर गए।
  • ऑडिट लॉग्स: ये यूज़र्स के लिए read-only हैं (अगर हैं तो)। सिर्फ service role को इनमें लिखना चाहिए।

जो पॉलिसीज़ मैं असल में लिखता हूँ

1. ओनर-ओनली पॉलिसी (मेरा सबसे ज़्यादा इस्तेमाल होने वाला पैटर्न)

ये वही है जो मैं किसी और चीज़ से ज़्यादा लिखता हूँ। सीधी बात: यूज़र्स सिर्फ वही rows देख सकते हैं जो उनकी हैं।

`` create policy "Users can view own rows" on profiles for select using (auth.uid() = user_id); ``

auth.uid() एक Supabase helper है जो currently authenticated यूज़र का UUID देता है। साफ़, तेज़, indexed अगर user_id indexed है। मैं इसको एक insert policy के साथ पेयर करता हूँ जो user_id को auth.uid() पर सेट करता है डिफॉल्ट से, ताकि यूज़र्स किसी और होने का नाटक करके rows insert न कर सकें।

`` create policy "Users can insert own rows" on profiles for insert with check (auth.uid() = user_id); ``

with check क्लॉज़ write operations के लिए है। using reads के लिए है। बहुत सारे लोग इन्हें आपस में गड़बड़ा देते हैं और ऐसी policies के साथ खत्म होते हैं जो सही दिखती हैं लेकिन असल में bad inserts को रोकती नहीं हैं।

2. Org/Team Policy (Multi-Tenant Apps)

यहीं चीजें दिलचस्प हो जाती हैं। किसी भी multi-tenant SaaS के लिए, मुझे users को सिर्फ अपनी rows नहीं बल्कि अपने organisation के rows दिखाने होते हैं।

जिस pattern पर मैं आता हूँ: एक memberships join table जो users को organisations से जोड़ती है।

`` create policy "Org members can view org resources" on projects for select using ( exists ( select 1 from memberships where memberships.org_id = projects.org_id and memberships.user_id = auth.uid() ) ); ``

Seahawk के पास एक fintech project था जहाँ org के पास दर्जनों users थे, कुछ read-only roles वाले, कुछ write access वाले। हमने इस pattern को एक role column के साथ extend किया memberships पर और इसे directly policy में इस्तेमाल किया। तो एक viewer role UPDATE या DELETE run नहीं कर सकता database level पर, बस। API से enforce नहीं होता। Database से enforce होता है।

3. Public Read / Owner Write Policy

ऐसे content के लिए जो publicly visible है लेकिन सिर्फ owner edit कर सकता है। Blog posts, public profiles, product listings।

``` create policy "Anyone can read published posts" on posts for select using (published = true);

create policy "Authors can update own posts" on posts for update using (auth.uid() = author_id) with check (auth.uid() = author_id); ```

दो अलग policies। मैं लोगों को इन्हें एक में combine करते देखता हूँ और logic खत्म हो जाती है जिसे समझ पाना मुश्किल है। इन्हें अलग रखो। Postgres same operation के लिए इन्हें automatically OR कर देगा जब ज़रूरत हो।

4. Service Role Escape Hatch

कुछ operations को legitimately RLS को bypass करने की ज़रूरत होती है। Background jobs, webhooks, admin scripts। इन्हीं के लिए मैं service_role key use करता हूँ, जो RLS को पूरी तरह bypass कर देता है।

लेकिन बात यह है: मैं service role key को frontend code में expose नहीं करता। कभी नहीं। यह environment variables में server side पर ही रहता है। मैंने codebases देखे हैं जहाँ यह Next.js pages/ directory में hardcoded था। वह तुम्हारा पूरा database है, बिलकुल खुला।

अगर तुम Supabase edge functions use कर रहे हो, तो तुम service role client को उनके अंदर safely use कर सकते हो, क्योंकि edge functions server-side चलते हैं। Supabase के auth और service roles पर अपने docs पूरे cover to cover पढ़ने लायक हैं अगर तुमने अभी नहीं किए।

5. Admin Override Policy

Apps जिनके पास admin panel है, मैं एक ऐसी policy add करता हूँ जो admins को full access देती है, user के JWT metadata में या एक अलग user_roles table में stored role के against check होती है।

`` create policy "Admins can do everything" on projects for all using ( exists ( select 1 from user_roles where user_roles.user_id = auth.uid() and user_roles.role = 'admin' ) ); ``

मैं पहले roles को JWT custom claims में store करता था, जो faster होता है (कोई subquery नहीं), लेकिन इसका मतलब है कि तुम्हें JWT को re-issue करना पड़ता है जब भी role change हो। ज़्यादातर apps के लिए, subquery ठीक है। अगर तुम scale पर performance issues देख रहे हो, तो Supabase Auth hooks के through JWT custom claims सही move है।

Common Mistakes I've Made (And Seen)

मुझे सीधे कहने दो उन चीजों के बारे में जो मुझे या मेरे clients को असल में काटी हैं।

  1. UPDATE और DELETE policies भूल जाना। SELECT policy लिखना आसान है और लगता है कि तुम हो गए। तुम नहीं हो। सभी चार operations test करो: SELECT, INSERT, UPDATE, DELETE। मैं अब Supabase dashboard का built-in policy tester use करता हूँ, लेकिन साल भर के लिए मैं raw SQL psql में लिख रहा था और manually test कर रहा था।
  2. USING बनाम WITH CHECK confusion। USING यह filter करता है कि एक query कौन सी rows को देख सकती है। WITH CHECK यह validate करता है कि एक write operation allowed है या नहीं। UPDATE के लिए, तुम्हें दोनों की ज़रूरत है: USING यह control करने के लिए कि कौन सी rows को target किया जा सकता है, WITH CHECK यह control करने के लिए कि update के बाद row कैसी दिखे।
  3. Recursive policy loops। अगर table A पर तुम्हारी policy table B को query करती है, और table B की एक ऐसी policy है जो table A को query करती है, तो तुम्हें infinite recursion मिलेगा। मुझे यह एक बार teams और team_members table के साथ हुआ जो एक दूसरे को reference करते थे। Fix: security definer functions use करो cycle को break करने के लिए।
  4. Anonymous user के रूप में test न करना। Supabase तुम्हें anonymous role (anon) use करने देता है। हमेशा अपनी policies को authenticated और anon दोनों के रूप में test करो। मैं Postman use करता हूँ अलग-अलग auth tokens के साथ इसे simulate करने के लिए, कोई token नहीं, एक valid user token, और एक अलग user का token के बीच switch करते हुए।
  5. Storage buckets पर RLS को बंद रखना। RLS storage.objects table पर भी लागू होता है। अगर आप Supabase Storage bucket बनाते हैं और उस table को unprotected छोड़ देते हैं, तो कोई भी path का अनुमान लगाकर आपकी "private" files को पढ़ सकता है। मैंने यह एक client project पर सीखा जहाँ user-uploaded documents store थे।

मैं अपनी Policies को Ship करने से पहले कैसे Test करता हूँ

यह मेरी वास्तविक प्रक्रिया है, न कि कोई सैद्धांतिक checklist।

  1. Supabase SQL editor में policy लिखें।
  2. एक दूसरा browser tab खोलें, एक अलग test user के रूप में sign in करें।
  3. ऐसे data को access करने का प्रयास करें जिसे block होना चाहिए। पुष्टि करें कि यह block है।
  4. ऐसे data को access करने का प्रयास करें जिसे visible होना चाहिए। पुष्टि करें कि यह काम करता है।
  5. एक UPDATE और DELETE को एक ऐसी row के विरुद्ध चलाएँ जिसके मालिक आप नहीं हैं। यह fail होना चाहिए।
  6. Supabase logs को row-level security policy violated errors के लिए check करें।

कुछ भी जटिल चीजों के लिए, विशेष रूप से multi-tenant org policies के लिए, मैं supabase-js client का उपयोग करके एक छोटा test script लिखता हूँ जिसमें दो अलग-अलग user sessions हों और अपेक्षित परिणामों को assert करूँ। लिखने में शायद 20 मिनट लगते हैं, production में debugging के घंटों बचाता है।

RLS का उपयोग कब न करें

RLS हमेशा सही tool नहीं होता।

अगर आप एक internal admin tool बना रहे हैं जहाँ सभी users trusted employees हैं, तो RLS complexity जोड़ता है बिना ज्यादा return के। एक simple server-side auth check ठीक है। अगर आपका data model इतना complex है कि policies को 5-level deep subqueries की जरूरत है, तो आप शायद एक API layer में access control को enforce करने में बेहतर हैं जिसमें proper service decomposition हो।

साथ ही: अगर आप Supabase को purely एक backend के रूप में उपयोग कर रहे हैं अपने सामने अपने API के साथ (कभी Supabase URL या anon key को clients को expose नहीं करते), तो RLS optional है। API आपकी security layer बन जाता है। यह कहा जाए, मैं अब भी इन cases में basic policies जोड़ता हूँ क्योंकि defence in depth इसके लायक है।

देखें, RLS एक tool है। कोई धर्म नहीं। इसे वहाँ उपयोग करें जहाँ यह आपकी system को simpler और safer बनाता है। इसे cargo-cult न करें क्योंकि एक tutorial ने आपको ऐसा करने के लिए कहा।

FAQ

क्या मुझे RLS की जरूरत है अगर मैं Supabase को केवल एक server-side API के साथ उपयोग कर रहा हूँ?

strictly नहीं। अगर आपका frontend कभी Supabase को directly touch नहीं करता है और सब कुछ आपके अपने server के through जाता है, तो आपका API security layer है। लेकिन मैं अभी भी कम से कम owner-based policies को एक दूसरी line of defence के रूप में जोड़ने की सिफारिश करता हूँ। अगर कोई आपके API में एक bug ढूंढता है, तो RLS वह पकड़ता है जो through गिर जाता है।

क्या RLS performance को प्रभावित करता है?

यह कर सकता है, अगर आपकी policies में large tables पर expensive subqueries शामिल हैं। fix लगभग हमेशा indexing है। सुनिश्चित करें कि आपकी policy conditions में उपयोग किए गए columns (user_id, org_id, आदि) के पास indexes हैं। पिछले साल एक project पर, org_id पर एक index जोड़ने से policy evaluation time को ~40ms से 800k rows वाली एक table पर 2ms से नीचे गिरा दिया।

क्या मैं Supabase Realtime के साथ RLS का उपयोग कर सकता हूँ?

हाँ। Realtime subscriptions RLS policies को respect करते हैं। अगर एक user एक table पर changes के लिए subscribe करता है, तो वह केवल उन rows के लिए events प्राप्त करेगा जिन्हें उसकी policies देखने की अनुमति देती हैं। यह Supabase की architecture में एक genuinely अच्छा design decision है।

`for all` और separate policies लिखने के बीच अंतर क्या है?

for all एक single policy बनाता है जो SELECT, INSERT, UPDATE, और DELETE को cover करता है। यह admin override patterns के लिए convenient है। बाकी सब कुछ के लिए, मैं operation per अलग-अलग policies लिखता हूँ क्योंकि conditions आमतौर पर अलग होते हैं। SELECT public reads को allow कर सकता है जबकि INSERT को ownership की जरूरत होती है। Separate policies को reason करना आसान होता है जब 11pm पर कुछ गलत हो जाता है।

मैं एक policy को debug कैसे करूँ जो requests को block कर रहा है जिन्हें नहीं करना चाहिए?

पहले: check करें कि auth.uid() actually एक value return कर रहा है। अगर user authenticated नहीं है, तो यह null return करता है और ज्यादातर policies fail होंगी। दूसरा: temporarily policy को USING (true) पर set करें यह confirm करने के लिए कि query itself काम करती है। तीसरा: एक test policy जोड़ें जो आप check कर रहे हैं values को logs करता है (एक security definer function का उपयोग करके जो एक notice raise करता है)। Supabase dashboard logs भी RLS violations को surface करते हैं जो इसे पहले की तुलना में बहुत कम painful बनाता है।

---

पंक्ति स्तर पर सुरक्षा आकर्षक काम नहीं है। कोई भी उस उल्लंघन के बारे में ब्लॉग पोस्ट नहीं लिख रहा है जो हुआ ही नहीं। लेकिन 2021 में उजागर प्रोजेक्ट डेटा वाली उस घटना ने मुझे सिखाया कि "RLS सक्षम" और "RLS सही तरीके से किया गया" के बीच का अंतर अधिकांश लोग जितना सोचते हैं उससे कहीं ज्यादा बड़ा है। ये नीतियाँ उस अंतर को बंद करती हैं। कम से कम मेरे लिए तो करती हैं।

← वापस