Pixeltable vs Supabase

Same video-intelligence app, two implementations. Pick Supabase when you want Postgres, row-level security, realtime, and a managed database. Pick Pixeltable when the pipeline is the product: video, frames, transcripts, embeddings, and retrieval in one Python file. They are not mutually exclusive.

pip install 'pixeltable[serve]'
See how it works

The trade

SidePixeltableSupabase
At a glance
  • 129 lines, one file, one process: ffmpeg, Whisper, and CLIP run in the schema
  • A row inserted by anything at all gets processed
  • Adding a title-embedding index is one line; the table backfills in place
  • Per-cell errors, lineage, and revert live in the catalog
  • 294 app lines plus 252 in compute-service, because Deno cannot run ffmpeg
  • RLS enabled on every table; one config line authenticates the Edge Function
  • Realtime, PITR, branching, and a managed database your team already operates
  • Fastest ingest (11.64× realtime) and fastest evolve wall-clock on this corpus

What we measured

Same app. Pixeltable loses ingest, search, evolve wall-clock, RLS, and realtime. Pixeltable wins in-platform media and incremental schema.

FeaturePixeltableSupabase
Total code for this app
129 lines, 1 file
546 lines (294 + 252 compute-service), 7 files
ffmpeg, Whisper, CLIP
Computed columns, same process
compute-service; three of seven endpoints have no hosted-API substitute
Processing for any writer
Yes — the pipeline is the schema
No, unless you add database triggers
Add a derived column (lines)
1 line, 1 file, one command
24 lines, 2 files: ALTER TABLE plus a backfill script
Add a derived column (wall time)
3.88s (schema change and backfill are the same step)
1.6s — fastest of the three at two dozen rows
Ingest, 20 videos / 10 min
66.7s, 9.04× realtime
51.8s, 11.64× realtime
Frame search p50
39.0ms
26.1ms
Authenticated endpoints
Open in this repo
One line: withSupabase({ auth: 'secret' })
Row-level security
None here
Enabled and verified on all five tables
Realtime push
None here
Built in
Per-cell errors and lineage
errormsg / errortype; pxt dashboard draws what produced a column
A failed step leaves NULL; Studio does not record lineage
Vendor checker in CI
ruff — generic Python; no Pixeltable conformance checker
deno lint and supabase db advisors on a live database

All three beat realtime on this laptop

20 videos, 10 minutes of footage, CPU, local models — not Cloud. Bold is best on that row.

Ingest

FeaturePixeltableSupabase
Wall time66.7s51.8s
Faster than realtime9.04x11.64x
First video5.3s4.1s
Median video3.1s2.1s

Search

FeaturePixeltableSupabase
Frame search p5039.0ms26.1ms
Frame search p9542.9ms30.5ms
Transcript search p5018.5ms13.7ms
Transcript search p9521.6ms16.3ms

20 videos, 603.7 seconds of footage, 603 frames, 70 transcript chunks, one laptop, CPU, local models (Pixeltable 0.7.8). All three finished 20 of 20, all faster than realtime. Supabase led ingest (11.64× vs Pixeltable’s 9.04×); frame search is 26–39ms and most of that is embedding the query, not the index. 603 vectors is not a million-row benchmark, hosted Pixeltable Cloud was not in this run, and none of this is a cost comparison.

Adding a column to live data

One computed title-embedding index on a populated catalog. Lines compound; these seconds do not.

FeaturePixeltableSupabase
Schema change3.88s0.07s
Backfillsame step1.52s
Total3.88s1.6s
Lines written124
Files touched12
  • Supabase is the fastest in wall time. At two dozen rows the backfill is noise; anyone quoting these seconds as a scaling result is quoting noise.
  • After pxt schema update, an insert against the already-registered route answers 409 until pxt service update. Reads keep working. The other two resolve the table on every request.
  • Backfill time is the part that scales, and this corpus cannot show it. Pixeltable’s backfill is work proportional to the rows that changed; a backfill script is work proportional to the table.

Ingest a video

Insert a video. Frames, audio, transcripts, embeddings, and scenes have to exist after that. On Pixeltable they are the schema. On the other two they live in the ingest path and in a second service.

Pixeltable

class Videos(TableModel, name='videos'):
video: pxt.Video
title: pxt.String
audio = extract_audio(video, format='mp3')
duration_sec = pxtf.video.get_duration(video)
scenes = video.scene_detect_content(threshold=8.0)
class Frames(TableModel, name='frames', base=Videos,
iterator=frame_iterator(Videos.video, fps=1.0)):
still = pxtf.image.resize(frame, (320, 180))
__indexes__ = [pxt.EmbeddingIndex(frame, embedding=VISUAL)]
class Chunks(TableModel, name='chunks', base=Videos,
iterator=audio_splitter(Videos.audio, duration=10.0)):
transcript = transcribe(audio_segment, model='base.en').text.astype(pxt.String)
__indexes__ = [pxt.EmbeddingIndex(transcript, embedding=SEMANTIC)]
Videos.insert([{'video': 'lecture.mp4', 'title': 'CS101'}])

Supabase

const { frames } = await compute("/extract-frames", { video_url, fps: FRAME_FPS });
const { embeddings } = await compute("/embed-clip", { images_b64: frames });
const frameRows = await Promise.all(frames.map(async (b64, i) => {
const path = `videos/${videoId}/frame_${i}.jpg`;
await supabase.storage.from("frames").upload(path, decodeBase64(b64), {
contentType: "image/jpeg", upsert: true,
});
return { video_id: videoId, frame_idx: i, embedding: embeddings[i] };
}));
await supabase.from("frames").insert(frameRows);

Search frames

Find frames of a whiteboard. Pixeltable asks the index. The other two embed the query themselves, then join or fetch rows in a second step.

Pixeltable

sim = Frames.frame.similarity(string=query)
return (
Frames.order_by(sim, asc=False)
.limit(limit)
.select(
frame_url=Frames.still,
frame_idx=Frames.pos,
video_title=Frames.title,
similarity=sim,
)
)

Supabase

CREATE FUNCTION search_frames(query_embedding vector(512), match_count INT)
RETURNS TABLE(frame_url TEXT, frame_idx INT, video_title TEXT, similarity FLOAT) AS $$
SELECT f.frame_url, f.frame_idx, v.title,
1 - (f.embedding OPERATOR(public.<=>) query_embedding)
FROM public.frames f
JOIN public.videos v ON f.video_id = v.id
ORDER BY f.embedding OPERATOR(public.<=>) query_embedding
LIMIT match_count;
$$ LANGUAGE sql STABLE;

Add a column to live data

Make the video title semantically searchable. The rows already exist. An embedding is not derivable in SQL, so every existing row has to be read, sent to a model, and written back.

Pixeltable

class Videos(TableModel, name='videos'):
...
__indexes__ = [pxt.EmbeddingIndex(title, embedding=SEMANTIC)]
# pxt schema update app.py media
# updated media/videos
# unchanged media/frames, media/chunks, media/conversations

Supabase

ALTER TABLE videos ADD COLUMN IF NOT EXISTS title_embedding vector(384);
CREATE INDEX IF NOT EXISTS videos_title_embedding_idx ON videos
USING hnsw (title_embedding vector_cosine_ops);
// then a script, because Postgres cannot call a model:
const { data: rows } = await db.from("videos")
.select("id,title").is("title_embedding", null);
for (let i = 0; i < rows.length; i += BATCH) {
/* embed the batch, update each row */
}

When to choose which platform

Choose Pixeltable when

  • The pipeline is the product

    Video, audio, images, documents, embeddings, and a retrieval step over them. The whole backend is one file, and nothing extra has to exist to run ffmpeg.

  • Schema changes have to stay cheap

    Adding a column backfills only that column. A row inserted by anything at all gets processed. That compounds; a line-count difference does not.

  • You already have an app backend

    Pixeltable as the media and retrieval layer behind a Supabase application is a coherent architecture, and for a team that already runs Postgres it is likely cheaper than moving.

Choose Supabase when

  • You want Postgres and the things around it

    Realtime subscriptions, row-level security for multi-tenancy, an auto-generated REST API, PITR, database branching, or a managed database your team already knows how to operate. Media work will live in a second service. That is the trade.

  • Throughput at this scale is the binding constraint

    On 20 videos and 10 minutes of footage, Supabase ingests fastest and answers frame search fastest. If that is the constraint, follow it rather than the sponsor.

Making the right choice

  • What this page does not measure

    • Cost in dollars, multi-tenant authorization against auth.uid(), p99 latency, and on-call.
    • Reproduce the numbers: https://github.com/pixeltable/pixeltable-vs-supabase-vs-convex

Frequently asked questions

One file. The whole pipeline.

Declare the tables. Apply the schema. Insert a row. Serve the same file.

pip install 'pixeltable[serve]'
See how it worksGet expert guidance