Canonical Supabase Clients in DeskcommCRM: Admin, Server, and Browser Patterns
DeskcommCRM defines three canonical Supabase client wrappers—Admin, Server, and Browser—that enforce strict security boundaries by separating service-role operations, RLS-compliant server requests, and realtime browser interactions.
The melgarafael/DeskcommCRM repository implements a defense-in-depth strategy for PostgreSQL access through specialized client factories. These canonical Supabase clients isolate privileged webhook handlers from tenant-scoped API routes and client-side realtime subscriptions, ensuring each execution context uses the appropriate credentials and respects Row-Level Security (RLS) policies defined in the codebase architecture.
The Three Canonical Supabase Client Types
DeskcommCRM exposes distinct client constructors in lib/supabase/ to prevent credential leakage between execution contexts.
Admin Client (createAdminClient)
Located in lib/supabase/admin.ts, the Admin client instantiates a service-role Supabase client using SUPABASE_SERVICE_ROLE_KEY. This client bypasses RLS entirely, requiring developers to manually filter by organization_id from trusted sources such as cookies, JWTs, or webhook secrets.
Use this client exclusively for server-only code including WAHA/Nuvemshop webhook processors, background cron workers, tenant provisioning scripts, and health-check endpoints that require unrestricted database access.
Server Client (createClient from lib/supabase/server.ts)
Exported from lib/supabase/server.ts, the Server client uses the NEXT_PUBLIC_SUPABASE_ANON_KEY to enforce RLS automatically. When used within Next.js Server Components or API route handlers (app/api/**), this client automatically scopes queries to the current user's organization_id through PostgreSQL RLS policies.
This is the default choice for standard REST API endpoints and server-side data fetching where tenant isolation must be maintained.
Browser Client (createClient from lib/supabase/browser.ts)
Defined in lib/supabase/browser.ts, the Browser client wraps @supabase/ssr's createBrowserClient for use in Client Components ("use client"). It stores sessions in a SameSite-Strict cookie named sb-deskcomm-auth and handles Realtime authentication through a dedicated token endpoint at /api/v1/auth/realtime-token.
Use this for live UI updates, client-side CRUD operations that respect RLS, and browser-based authentication flows.
Security Implementation and Execution Contexts
Each canonical client enforces specific security rules aligned with DeskcommCRM's multi-tenant architecture.
Privileged Operations and Manual Filtering
The Admin client's service-role privileges require explicit safeguards. Because RLS is bypassed, every query must manually specify organization_id derived from trusted authentication contexts. Failure to filter results in cross-tenant data exposure.
Automatic Tenant Isolation
The Server client relies on the anon key and PostgreSQL RLS policies to enforce tenant boundaries without manual intervention. When a request reaches an API handler using createClient() from lib/supabase/server.ts, the database automatically filters rows based on the authenticated user's organization membership.
Realtime Authentication Flow
Browser-based Supabase Realtime connections require special handling. The prepareRealtimeAuthentication() function fetches short-lived tokens from /api/v1/auth/realtime-token, allowing websockets to authenticate without exposing long-lived credentials in browser memory.
Practical Code Examples
Processing Webhooks with the Admin Client
Use the Admin client when handling external webhooks that require bypassing RLS to write data across tenant boundaries:
import { createAdminClient } from '@/lib/supabase/admin';
export async function POST(req: Request) {
// Trusted source – no RLS, must filter org manually
const supabase = createAdminClient();
const { data, error } = await supabase
.from('leads')
.insert({ organization_id: 42, name: 'Novo Lead' });
if (error) return fail(500, error.message);
return ok(data);
}
API Routes with Automatic RLS
Standard endpoints use the Server client to respect tenant isolation automatically:
import { createClient } from '@/lib/supabase/server';
export async function GET() {
// Uses anon key, RLS automatically scopes to the user’s org
const supabase = createClient();
const { data, error } = await supabase
.from('contacts')
.select('*');
if (error) return fail(400, error.message);
return ok(data);
}
Realtime UI with Browser Client
Client Components leverage the Browser client for live data subscriptions:
'use client';
import { createClient, prepareRealtimeAuthentication } from '@/lib/supabase/browser';
import { useEffect, useState } from 'react';
export default function LeadFeed() {
const [leads, setLeads] = useState<any[]>([]);
useEffect(() => {
(async () => {
await prepareRealtimeAuthentication(); // ensure socket has a valid token
const supabase = createClient();
const channel = supabase
.channel('public:leads')
.on('postgres_changes', { event: '*', schema: 'public', table: 'leads' }, payload => {
setLeads(prev => [...prev, payload.new]);
})
.subscribe();
return () => supabase.removeChannel(channel);
})();
}, []);
return <ul>{leads.map(l => <li key={l.id}>{l.name}</li>)}</ul>;
}
Environment Configuration
All three clients rely on centralized environment variables defined in lib/env.ts. This file exports NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, and SUPABASE_SERVICE_ROLE_KEY, ensuring consistent configuration across the Admin, Server, and Browser client factories.
Summary
- Three specialized clients cover all execution contexts: Admin for privileged operations, Server for RLS-compliant API routes, and Browser for realtime UI interactions.
- Admin client requires explicit manual filtering of
organization_idwhen bypassing RLS inlib/supabase/admin.ts, preventing accidental cross-tenant data leaks. - Server client automatically enforces tenant isolation via Row-Level Security policies when using
lib/supabase/server.ts. - Browser client manages secure SameSite-Strict cookies (
sb-deskcomm-auth) and dedicated realtime tokens fetched from/api/v1/auth/realtime-tokenfor websocket connections.
Frequently Asked Questions
When should I use the Admin client versus the Server client?
Use the Admin client only for server-only code requiring RLS bypass, such as webhook handlers, background workers, or tenant provisioning scripts. Use the Server client for standard API routes and Server Components where data access must respect tenant isolation policies enforced by PostgreSQL RLS.
How does the Browser client handle realtime authentication?
The Browser client calls prepareRealtimeAuthentication() to fetch a short-lived token from /api/v1/auth/realtime-token, enabling secure websocket subscriptions while maintaining the primary session in a SameSite-Strict cookie named sb-deskcomm-auth as implemented in lib/supabase/browser.ts.
Why does DeskcommCRM separate these clients into different files?
According to the source code in lib/supabase/, separating concerns into admin.ts, server.ts, and browser.ts prevents credential leakage between execution contexts and enforces the security architecture described in CLAUDE.md and AGENTS.md. This separation ensures service-role keys never leak to browser bundles or Server Component contexts where they aren't required.
What security risks exist if I forget to filter by organization_id in the Admin client?
Since the Admin client uses the service role key and bypasses RLS, omitting manual organization_id filters exposes data across all tenants in the database. This violates the multi-tenant security model and can lead to unauthorized cross-organization data access.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →