An open contact table reads like a customer list, because it is one
A CRM’s contacts table is a customer list in the plainest sense: names, companies, email addresses, phone numbers, deal notes. Exposed through a Supabase project’s anonymous path, it reads less like a misconfiguration and more like a handover — your prospecting targets, delivered as JSON. The exposure is common precisely because CRMs get built fast and demoed faster.
Discovery often arrives sideways: a researcher’s responsible disclosure, a competitor’s suspiciously well-targeted outreach, a customer asking how their details appeared elsewhere. None of these is the notification channel you would choose, which argues for finding it first with the same one-line probe an outsider would use.
RowShield does not detect this yet. This guide gives you the catalog queries to check it yourself. The nine rules that do ship are listed on the rules index.
Confirm the exposure in one request
Confirmation needs no tooling beyond the project URL and anon key: a GET against the contacts endpoint. Rows returning means exposed; a permission refusal means policies engaged; an empty array means ambiguous — seed a known contact and retry before concluding. Whatever the outcome, record it, because the before-and-after is your evidence of repair.
Then widen the lens. Contacts rarely leak alone: companies, deals, activities and notes tables surround them, and any sibling built by copying the contacts migration shares its omissions. The catalogue query below lists every reachable table with its RLS state, turning one anecdote into a complete map:
SELECT c.relname AS table_name,
c.relrowsecurity AS rls_enabled
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public'
AND c.relkind IN ('r', 'p')
ORDER BY c.relrowsecurity DESC, c.relname;Scope contacts to the workspace
CRMs are multi-tenant by nature — workspaces, seats, pipelines. The durable fix mirrors that structure: a workspace_members table, and policies letting a contact surface only to members of its workspace. Seats and roles refine the grant later; membership comes first, because it answers who works here, which is the very question exposure violated.
ALTER TABLE public.contacts ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.contacts FORCE ROW LEVEL SECURITY;
CREATE POLICY "members_read_workspace_contacts"
ON public.contacts
FOR SELECT
TO authenticated
USING (workspace_id IN (
SELECT wm.workspace_id FROM public.workspace_members wm
WHERE wm.user_id = (SELECT auth.uid())
));
CREATE POLICY "members_write_workspace_contacts"
ON public.contacts
FOR INSERT
TO authenticated
WITH CHECK (workspace_id IN (
SELECT wm.workspace_id FROM public.workspace_members wm
WHERE wm.user_id = (SELECT auth.uid())
));Assume copying, act proportionately
Expect the app to complain briefly: screens that fetched everything and filtered in the browser now receive only legitimate rows, which is the point. Fix the queries to trust the database, resist reinstating breadth for one dashboard, and give that dashboard its own narrow policy if it genuinely needs more.
Exposure duration matters more than comfort. Anyone could have held the anon key for months, so treat the window as unknown and the data as potentially copied. Practical responses follow: notify affected customers according to your obligations, rotate API credentials that touched the dataset, and watch for phishing that weaponises accurate contact details.
Resist both extremes of theatre. The event does not mandate heroics, and it does not permit silence. A dated incident note — what was exposed, for roughly how long, what changed — serves customers, advisers and your future self better than either panic or a shrug.
Never again, mechanically
The repair that matters is procedural: exposure happened because nothing watched the boundary between demo posture and production reality. RowShield does not automate this page’s judgment calls — notification wording, customer conversations — but it automates the watching: scheduled scans for disabled RLS, anonymously readable tables and public buckets, alerting on every transition.
Scan cadence follows plan — daily on Free, hourly on Indie, every fifteen minutes on Team. Reads touch catalogue and bucket metadata only, never your contact rows, and RowShield operates as an independent product unaffiliated with Supabase. The next forgotten migration meets an alert, not a researcher’s email.
Frequently asked
- Is the anon key really enough to read the table?
- When RLS is disabled or policies grant broadly, yes — the anon key plus the project URL is the entire attack, reproducible from a laptop in seconds. That simplicity cuts both ways: the same request confirms your fix the moment the corrected policy lands.
- Must we tell affected customers?
- Jurisdiction and contract decide the obligation, so treat notification as a legal question rather than a polling exercise. Practically, early honest notice costs less than discovery by others, and the incident note you wrote becomes the backbone of that communication.
- We found it via a researcher. Now what?
- Thank them, fix first, then correspond. Researchers reward responsiveness, and a clear timeline — report acknowledged, exposure closed, verification shared — usually concludes the exchange constructively. Arguing scope before repairing prolongs everyone’s worst afternoon for no benefit at all.
Check your project in about ten seconds
Paste a URL. No signup, no writes, nothing stored.
Run the free audit