How to Build a True "Burn-After-Reading" App with Supabase
Many privacy-focused messaging apps claim to offer "burn-after-reading" functionality. The reality is often much darker: they utilize soft-deletes. The data disappears from your UI, but it remains fully intact in the backend database, simply flagged as is_deleted = true.
If you are building a secure payload delivery system, soft deletes are a massive security vulnerability. In this tutorial, we will explore how to architect a true zero-retention database using PostgreSQL Row Level Security (RLS) via Supabase, controlled by Vercel Serverless Functions.
The Danger of Public APIs
A common mistake developers make when using Backend-as-a-Service (BaaS) platforms is leaving the database accessible via the public anonymous key. To build a secure vault, we must completely lock down the database.
Step 1: PostgreSQL Row Level Security (RLS)
Supabase sits on top of PostgreSQL. We use RLS to ensure the public internet cannot interact with our secrets table under any circumstances.
-- 1. Create the table for encrypted payloads
CREATE TABLE public.secrets (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
encrypted_payload TEXT NOT NULL,
iv TEXT NOT NULL,
salt TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 2. Enable Row Level Security
ALTER TABLE public.secrets ENABLE ROW LEVEL SECURITY;
-- 3. Intentionally omit creating any public policies.
-- Result: The table is 100% locked.
Step 2: The Serverless "Executioner"
Since the browser cannot talk to the database directly, we use a Vercel Serverless Function as a secure middleman. When the frontend requests a secret, the function executes a strict two-step process: Fetch, then immediately Hard Delete.
// api/getAndDestroy.js (Vercel Serverless Function)
import { createClient } from '@supabase/supabase-js';
export default async function handler(req, res) {
// Initialize Supabase bypassing RLS with the Service Key
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY
);
const { id } = req.body;
// 1. Fetch the payload
const { data, error } = await supabase
.from('secrets')
.select('*')
.eq('id', id)
.single();
if (error || !data) return res.status(404).json({ error: 'Vault destroyed' });
// 2. HARD DELETE the row immediately
await supabase.from('secrets').delete().eq('id', id);
// 3. Return ciphertext to client for local decryption
return res.status(200).json(data);
}
See this architecture in action
We used this exact Supabase + Vercel architecture to build ZeroKey. Generate a payload, open the link, and watch the database row permanently self-destruct.
Summary
Trusting a frontend client to "hide" a message is not security. True burn-after-reading requires strict backend enforcement. By leveraging Supabase RLS and Serverless executioners, you guarantee absolute zero data retention.