Chat history is sensitive: securing an AI backend on Supabase
An AI chatbot’s database holds its most honest transcripts. Users type medical worries, financial anxieties and confidential plans into a box that feels private, and every message lands in a conversations table. Whoever can read that table reads your users’ inner monologues — which makes conversation scoping the first security decision of such a product, not a cleanup task for later.
Supabase backends suit chat products neatly: Postgres for messages, storage for attachments, functions calling model providers. The recurring weakness is architectural haste — a service key doing writes from the client, a messages table relying on application-layer filtering, embeddings tables nobody considers user data. All fixable, none self-healing.
This remains a manual craft page: RowShield does not automate the checklist, and prompt-layer judgment stays with your team. What follows is the backend posture that lets the product promise privacy plausibly — conversations owned, writes confined to the server, and every derived table inheriting the scoping of its source.
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.
Scope conversations to their humans
Two tables carry the sensitivity: conversations and their messages. Give each conversation an owner id and let messages derive access through the parent — one traversal, no duplicated ownership columns to disagree. Policies then read plainly: users see their own conversations and the messages inside them, and nothing else by any path.
Decide early whether staff may read transcripts for support. If yes, route it through a scoped role with logging rather than a blanket clause; if no, say so truthfully in your privacy text. Both choices are respectable; discovering the de facto choice during an incident is not.
ALTER TABLE public.conversations ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.conversations FORCE ROW LEVEL SECURITY;
CREATE POLICY "owners_manage_own_conversations"
ON public.conversations
FOR ALL
TO authenticated
USING (owner_id = (SELECT auth.uid()))
WITH CHECK (owner_id = (SELECT auth.uid()));
CREATE POLICY "owners_read_messages_via_conversation"
ON public.messages
FOR SELECT
TO authenticated
USING (conversation_id IN (
SELECT c.id FROM public.conversations c
WHERE c.owner_id = (SELECT auth.uid())
));Writes belong server-side
Let completions flow through your server: the client sends the user’s turn, your function attaches credentials, calls the provider, and writes the assistant’s reply using the service key kept in function secrets. Clients holding only the anon key then ever read their own history through policies.
The payoff is a smaller blast radius. A compromised frontend leaks one user’s view of their own transcript, not a service credential reading every conversation ever written. Rotate the service key on any suspicion, and keep it out of repositories, build logs and browser network tabs alike.
Embeddings and the forgotten copies
Retrieval pipelines duplicate content: chunked text, embedding vectors, cached summaries. Teams routinely fence the messages table and leave the embeddings table world-readable — same words, different geometry. Apply the same ownership traversal to every derived table, and index the owner path so vector searches stay scoped without scanning tenants.
Deletion promises travel these pipes too. When a user erases their history, the erasure must reach chunks, vectors and caches, or the privacy claim quietly fails. Map every derived store on day one; retrofitting lineage after launch is archaeology, not engineering, and archaeology rarely finishes before the next release.
Watching a fast-moving backend
AI products iterate quickly by design, and guardrails must survive that pace. New tables appear weekly — feedback, ratings, traces — and each arrival is another chance at a forgotten policy pair. A weekly manual sweep loses to a scheduled scan almost immediately, and the loss compounds with every launch.
RowShield supplies the scheduled half: scans snapshot RLS posture and anonymous reachability, alert destinations learn of transitions, and remediation SQL arrives generated from your actual columns. Reads touch catalogue and bucket metadata only — never transcripts — and come from an independent product, unaffiliated with Supabase.
Frequently asked
- Is storing transcripts on Supabase risky by default?
- Not inherently; risk comes from access shape. With per-user RLS on conversations and messages, server-side writes under a guarded service key, and derived tables inheriting the same scoping, transcripts are about as well-held as application data gets. Without those measures, the box users confide in is effectively public.
- Should the model provider see user text?
- That is a product and policy question before a technical one — providers process prompts under their own terms. Technically, send the minimum context needed, honour deletion end to end, and write the privacy text to match reality, because users increasingly ask exactly this before typing.
- How do we test isolation without real transcripts?
- Seed synthetic conversations for two fabricated users, then attempt cross-reads: one user requesting the other’s conversation id, the anon key requesting anything, a message query filtered by a foreign owner. Expect refusals or emptiness throughout, and keep the script beside the migrations that created the tables.
Check your project in about ten seconds
Paste a URL. No signup, no writes, nothing stored.
Run the free audit