Skip to main content

Row-Level Security Policies

Row-Level Security (RLS) is the foundation of Artbase Studio's multi-tenant architecture. Every table has RLS enabled with policies that enforce organization-scoped access.

How RLS Works

-- Enable RLS on a table
ALTER TABLE products ENABLE ROW LEVEL SECURITY;

-- Create a policy
CREATE POLICY "policy_name"
ON table_name
FOR operation (SELECT, INSERT, UPDATE, DELETE, ALL)
USING (condition_for_existing_rows) -- For SELECT, UPDATE, DELETE
WITH CHECK (condition_for_new_rows); -- For INSERT, UPDATE

Policy Patterns

Organization Membership Check

Most policies check that the user belongs to the organization:

-- Helper function for checking org membership
CREATE OR REPLACE FUNCTION auth.user_org_ids()
RETURNS UUID[] AS $$
SELECT ARRAY_AGG(org_id)
FROM organization_members
WHERE user_id = auth.uid()
$$ LANGUAGE sql STABLE SECURITY DEFINER;

-- Using in policy
CREATE POLICY "Members access org data"
ON products FOR ALL
USING (org_id = ANY(auth.user_org_ids()));

Role-Based Access

Some operations require specific roles:

CREATE POLICY "Admins can delete"
ON products FOR DELETE
USING (
org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
AND role IN ('owner', 'admin')
)
);

Public Read Access

Storefront data is publicly readable:

CREATE POLICY "Public can view active products"
ON products FOR SELECT
USING (status = 'active');

Core Table Policies

profiles

-- Users can only see their own profile
CREATE POLICY "Users view own profile"
ON profiles FOR SELECT
USING (id = auth.uid());

-- Users can only update their own profile
CREATE POLICY "Users update own profile"
ON profiles FOR UPDATE
USING (id = auth.uid());

organizations

-- Members can view their organizations
CREATE POLICY "Members view organization"
ON organizations FOR SELECT
USING (id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
));

-- Only owners can update organization
CREATE POLICY "Owners update organization"
ON organizations FOR UPDATE
USING (id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
AND role = 'owner'
));

-- Public can view basic org info (for storefronts)
CREATE POLICY "Public view org storefronts"
ON organizations FOR SELECT
USING (true)
WITH CHECK (false); -- Read-only for public

organization_members

-- Members can view other members in their orgs
CREATE POLICY "Members view org members"
ON organization_members FOR SELECT
USING (org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
));

-- Only owners/admins can add members
CREATE POLICY "Admins add members"
ON organization_members FOR INSERT
WITH CHECK (org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
AND role IN ('owner', 'admin')
));

-- Only owners can remove members
CREATE POLICY "Owners remove members"
ON organization_members FOR DELETE
USING (org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
AND role = 'owner'
));

plans

-- Anyone can view active plans
CREATE POLICY "Public view plans"
ON plans FOR SELECT
USING (is_active = true);

Product Table Policies

products

-- Members view org products
CREATE POLICY "Members view org products"
ON products FOR SELECT
USING (org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
));

-- Public view active products (storefronts)
CREATE POLICY "Public view active products"
ON products FOR SELECT
USING (status = 'active');

-- Members create products
CREATE POLICY "Members create org products"
ON products FOR INSERT
WITH CHECK (org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
));

-- Members update products
CREATE POLICY "Members update org products"
ON products FOR UPDATE
USING (org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
));

-- Admins delete products
CREATE POLICY "Admins delete org products"
ON products FOR DELETE
USING (org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
AND role IN ('owner', 'admin')
));

product_variants

-- Access through product relationship
CREATE POLICY "Members view variants"
ON product_variants FOR SELECT
USING (product_id IN (
SELECT id FROM products
WHERE org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
)
));

-- Public view variants for active products
CREATE POLICY "Public view active variants"
ON product_variants FOR SELECT
USING (
is_active = true
AND product_id IN (
SELECT id FROM products WHERE status = 'active'
)
);

inventory_items

-- Members manage inventory
CREATE POLICY "Members view inventory"
ON inventory_items FOR SELECT
USING (org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
));

CREATE POLICY "Members update inventory"
ON inventory_items FOR UPDATE
USING (org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
));

Order Table Policies

orders

-- Members view org orders
CREATE POLICY "Members view org orders"
ON orders FOR SELECT
USING (org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
));

-- System creates orders (via service role)
-- No insert policy for regular users

-- Members update order status
CREATE POLICY "Members update orders"
ON orders FOR UPDATE
USING (org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
));

order_items

-- Access through order relationship
CREATE POLICY "Members view order items"
ON order_items FOR SELECT
USING (order_id IN (
SELECT id FROM orders
WHERE org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
)
));

customers

-- Members view org customers
CREATE POLICY "Members view org customers"
ON customers FOR SELECT
USING (org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
));

-- Members update customers
CREATE POLICY "Members update org customers"
ON customers FOR UPDATE
USING (org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
));

Email Table Policies

email_subscribers

-- Members view org subscribers
CREATE POLICY "Members view subscribers"
ON email_subscribers FOR SELECT
USING (org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
));

-- Public can subscribe (insert)
CREATE POLICY "Public can subscribe"
ON email_subscribers FOR INSERT
WITH CHECK (true);

email_campaigns

-- Members manage campaigns
CREATE POLICY "Members view campaigns"
ON email_campaigns FOR SELECT
USING (org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
));

CREATE POLICY "Members create campaigns"
ON email_campaigns FOR INSERT
WITH CHECK (org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
));

Channel Table Policies

channel_accounts

-- Members view connected channels
CREATE POLICY "Members view channel accounts"
ON channel_accounts FOR SELECT
USING (org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
));

-- Admins manage channels
CREATE POLICY "Admins manage channels"
ON channel_accounts FOR ALL
USING (org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
AND role IN ('owner', 'admin')
));

Service Role Bypass

Background services use the service role key to bypass RLS:

// services/etsy-sync/index.ts
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY! // Bypasses all RLS
);

// Now can access all data without policy restrictions
const { data: accounts } = await supabase
.from('channel_accounts')
.select('*');
warning

The service role key should never be exposed to clients. Only use it in server-side code and background services.


Debugging Policies

Check Current User

-- Get authenticated user ID
SELECT auth.uid();

-- Get user's organizations
SELECT org_id, role
FROM organization_members
WHERE user_id = auth.uid();

Test Policy Access

-- Test as specific user
SET request.jwt.claim.sub = 'user-uuid';

-- Run query
SELECT * FROM products WHERE org_id = 'org-uuid';

-- Reset
RESET request.jwt.claim.sub;

View Active Policies

SELECT
schemaname,
tablename,
policyname,
permissive,
roles,
cmd,
qual,
with_check
FROM pg_policies
WHERE schemaname = 'public'
ORDER BY tablename, policyname;

Best Practices

  1. Always enable RLS on new tables
  2. Use SECURITY DEFINER functions for complex permission checks
  3. Test policies with different user roles
  4. Avoid policy recursion - don't reference the same table in USING clause
  5. Index columns used in policy conditions for performance
  6. Use permissive policies (default) - they OR together
  7. Document policy intent with clear names and comments