Database Architecture

How to Build a True "Burn-After-Reading" App with Supabase

7 Min Read

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.

Privacy First

We use essential cookies & analytics strictly to improve our free tools. No PII or cryptography secrets are ever tracked.

Policy