Part 2: Securing and Extending — RLS, File Storage Policies, and Edge Functions

In this post, we go deep into how we implemented fine‑grained access control, secure media handling, and lightweight business logic using Supabase’s RLS policies, storage schema, and Edge Functions.

Enforcing Row‑Level Security Across Data

Supabase’s RLS (Row‑Level Security) allows us to restrict database access at the row level. For example, in our orders table, we used policies that only permit users to see or modify their own orders:

CREATE POLICY "Select own orders"
ON public.orders
FOR SELECT
TO authenticated
USING (auth.uid() = user_id);

CREATE POLICY "Insert own orders"
ON public.orders
FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);

Here:

  • USING filters rows a user can view.
  • WITH CHECK enforces that new or updated rows must match conditions—for example, confirm a row’s user_id equals the logged-in user.

We defined similar policies across order_items, menus, and user tables to ensure strict ownership-based access. While implementing the above we discovered that real-time subscriptions respect RLS policies. If your table uses complex RLS logic and many active subscribers, performance may degrade—so test your filters and indexing.

Secure File Storage: Buckets and Access Control

We created buckets (e.g. menu-images) as private by default, forcing all file operations through RLS policies. This ensures only users with valid access can upload or retrieve assets 

To allow only authenticated users to upload image files under a folder named after their user ID:

CREATE POLICY "Authenticated uploads"
ON storage.objects FOR INSERT
TO authenticated
WITH CHECK (
  bucket_id = 'menu-images' AND
  (storage.foldername(name))[1] = auth.uid()::text
);

CREATE POLICY "User selects own files"
ON storage.objects FOR SELECT
TO authenticated
USING (
  bucket_id = 'menu-images' AND
  (storage.foldername(name))[1] = auth.uid()::text
);

This ensures both secure upload (INSERT) and download (SELECT) based on user ownership through metadata in the file path.

Adding Business Logic via Edge Functions

For business workflows that extend beyond simple CRUD—such as sending order confirmation emails or triggering POS integrations—we used Edge Functions, deployed globally on Supabase’s Deno-based runtime.

Edge Function Use Cases:
  • Order confirmation email: trigger after order insertion.
  • Webhook to POS: send external systems updates in near real time.
  • Signed URL generation: secure temporary access to Storage files.
import { serve } from 'https://deno.land/std/http/server.ts';
import { createClient } from '@supabase/supabase-js';

serve(async req => {
  const supabase = createClient(SUPA_URL, DENO_ENV.SUPA_SERVICE_ROLE_KEY);
  const { order_id } = await req.json();

  const { data: order } = await supabase
    .from('orders')
    .select('*')
    .eq('id', order_id)
    .single();

  await sendConfirmationEmail(order.user_email, order);
  return new Response(JSON.stringify({ status: 'sent' }));
});

While Edge Functions are convenient for light workloads, they may struggle under high concurrency and intense processing. For heavy integration or high‑frequency tasks, a dedicated queue (e.g. AWS Lambda with queues) may be more suitable.

Policy Validation & Testing Workflow

To ensure security and correctness, we follow these steps:

  1. Simulate user roles via SQL clients using auth.login_as_user().
  2. Test SELECT, INSERT, UPDATE, and DELETE across roles.
  3. Validate file uploads/downloads across buckets for correct paths and ownership.
  4. Monitor real-time subscription updates and policy enforcement.
  5. Audit execution plans and indexing to optimize filter performance. 

In Part 3, we’ll showcase our front-end integration with Supabase—covering React/Next.js UI, authentication flows, real‑time subscriptions for menus and orders, media handling, pagination, and UX-level performance optimizations.

Leave a Reply

Your email address will not be published. Required fields are marked *