Pixeltable vs Supabase vs Convex

Same video-intelligence app, three implementations: ingest, frames, transcripts, embeddings, scenes, an agent. Pixeltable is the database, the orchestration, and the serving in one Python file. Insert a row. Transforms run.

pip install 'pixeltable[serve]'
Compare 11 free tiersSee how it worksRun the benchmark

Measured from the source

The app is media-heavy, which suits Pixeltable. Convex is the easiest local install and the fastest read path. Supabase ingests fastest at both measured tiers; the evolve wall-clock winner flips with corpus (Pixeltable at 23 videos, Supabase at 123). Pixeltable is last on serial search by single-digit milliseconds, 3.4x faster than Supabase on the agent query, and slowest under concurrent load: same cause, models in the request path. None of this is a Cloud run. Every number is produced by reading the source or running the harness; n/a means the metric does not apply, never that it scored zero.

FeaturePixeltableSupabaseConvex
App code you maintain
129
one Python file
302
SQL + TypeScript
425
TypeScript
Plus the shared compute service
0
ffmpeg, Whisper, CLIP run in-process
246
Deno cannot run ffmpeg
246
Convex runtime cannot run ffmpeg
Total
129
548
671
Files you open to read the backend
1
7
7
Services you operate
1
2
2
Orchestration hops
3
one hand-written route
13
from / storage / rpc
9
runMutation / runAction / scheduler
HTTP routes with a hand-written handler
1
the agent; declared routes carry no handler code
5
five path === branches, five handlers, one fetch export
5
five http.route blocks; http.ts is 90 lines

Non-blank, non-comment lines. Lock files counted nowhere. Regenerated by harness/run_comparison.py.

The benchmark, plotted

Same cells as the tables below, drawn to scale. Local CPU on one machine; not a Cloud measurement.

p50 latency by operation - lower is betterPixeltableSupabaseConvex1ms10ms100ms1s10sSearch - frames22ms17ms15msSearch - transcripts17ms17ms11msRead - list5.1ms7.8ms2.4msRead - frame0.7ms2.1ms0.4msLoad p50, 8 clients120ms67ms65msAgent - local 1.5B206ms700ms808msAgent - hosted8.4s2.3s3.9sLand a schema change - 123-video corpus0153045600s3s6s9slines writtenPixeltable (1 line)Supabase (24 lines)Convex (53 lines)Convex via searchIndex (1 line)Hosted agent swap - same model, all 12/12bars are lines of retry/backoff written0122436487 linesPixeltable - p50 8.4s36 linesSupabase - p50 2.3s37 linesConvex - p50 3.9ssources: benchmarks.json 2026-09-18, evolve.json 2026-09-20, hosted.json 2026-09-22
Rendered from the same committed artifacts as the tables: benchmarks.json (xl tier, small tier for the local agent), evolve.json (123-video corpus), hosted.json (paid endpoint). The benchmark repo renders the equivalent as docs/summary.svg.

What the difference actually is

Not the line count. Lines are the least durable thing in the table.

  • Media processing has to live somewhere else

    compute-service/

    Neither Deno nor the Convex runtime can execute ffmpeg, so both need compute-service/. Three of its seven endpoints are ffmpeg and have no hosted-API substitute. The extra service, most of the orchestration hops, and base64 on the wire all follow from that one fact.

  • Processing fires for any writer

    A row inserted into a Pixeltable table by anything at all gets processed, because the pipeline is the schema. On Supabase or Convex the processing lives in the ingest path, so a row written by another client, a backfill, or a psql session is not processed unless you add triggers or a scheduled action.

  • Retrieval knows its own model

    similarity(string=q)

    similarity(string=q) asks the index. The other two embed the query themselves and nothing checks it came from the model that filled the column; the dimension is the only guard, and 384 equals 384.

  • Errors are per cell

    A Pixeltable cell holds a value or its own errormsg and errortype, selectable like any other column. Elsewhere a failed step leaves a NULL and finding the affected rows is a query you write.

  • Lineage is in the catalog

    pxt dashboard

    Every computed column carries the expression that produced it. pxt dashboard draws column and table lineage locally, with no deploy and no account. Supabase Studio and the Convex dashboard both ship in this benchmark; neither knows what produced a column, because nothing recorded it.

Consequences

Even swaps from the benchmark. Check is built in, dash is partial or a script, x is not here.

built in·partial / extra work· not in this implementation·— n/a

FeaturePixeltableSupabaseConvex
Media and the pipeline
ffmpeg, Whisper, CLIP run in-platform
computed columns
compute-service
compute-service
Processing fires for any writer
the pipeline is the schema
unless you add triggers
unless you add a scheduler
Add a derived column to live data
backfills in place
migration + backfill script
schema + migration action
Per-cell error state
errormsg, errortype
NULL plus a query you write
Retrieval knows its own model
similarity(string=q)
you embed; dimension is the guard
you embed; 256-hit ceiling
A derived value stays fresh when a row updates
recomputed on write
serves the old vector until a re-run
silent staleness
A failed ingest is rejected, not stored
typed error on the request
error row, filtered out of list
error row, filtered out of list
Ingest call returns a synchronous response
async job, polled
Column lineage
expression per column, in the catalog
nothing recorded to draw
nothing recorded
Serving
Agent query completes without a second service
the model runs where the data is
three round trips to compute-service
three round trips to compute-service
Request validation before handlers
derived from the signature; malformed is a 422
~28 lines by hand
~46 lines by hand
Hosted-model pacing and retries
provider scheduler; 7-line swap
36-line retry loop
37-line retry loop
Install and operations
Local install
one package; large Python deps
Docker, 12 containers, or hosted
npx convex dev, no account
Cloud account required to run locally
not for local Docker
anonymous local backend
Operations
you run the process; Cloud is separate
managed
managed
Local UI
dashboard, with lineage graphs
Studio
dashboard
Vendor ships a conformance checker
ruff only; weakest automated proof
deno lint + db advisors
ESLint plugin + tsc
Free tier
self-hosted in this benchmark
Auth, realtime, versioning
Endpoints authenticated by default
open in this repo
withSupabase({ auth: 'secret' })
open in this repo
Row-level security
enabled and verified on all five tables
Realtime push to clients
the core idea
Data versioning
per-table history and revert
PITR, branching, migrations
snapshot export/import

The same task, from the repo

Snippets are shortened from the three implementations. None are invented.

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.

Pixeltableapp.py
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'}])
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);

75 lines in one function, plus compute-service. A row that arrives any other way is not processed.

The same ten steps, three ways

Judgments, not measurements. Convex wins install. Pixeltable wins the media steps.

FeaturePixeltableSupabaseConvex
1. Install
Adequate
One package; pulls torch, whisper, sentence-transformers
Weak
Docker, 12 containers, or hosted, plus compute-service
Strong
npx convex dev, no account, no Docker
2. Schema
Strong
2 tables, 2 views, same file as everything else
Adequate
5 tables, 3 FKs, 2 HNSW indexes, 1 view
Adequate
5 tables, 2 vector indexes; ingest writes complete rows
3. Ingest
Strong
One insert
Adequate
75-line function
Adequate
61 lines plus 105 lines of mutations
4. Process
Strong
It is the schema
Weak
Lives in the ingest path
Weak
Lives in the ingest path
5. Embed
Strong
One line; the index knows its model
Weak
Second service
Weak
Second service
6. Search
Strong
An expression; the view supplies the title
Adequate
SQL function plus join
Adequate
vectorSearch plus a batched lookup
7. Agent
Strong
Retrieval is a column; evidence is stored with the answer
Adequate
Assembled and written by the handler
Adequate
Assembled and written by the handler
8. Serve
Strong
Declared routes next to the queries
Strong
One function, or PostgREST
Weak
Weak for REST, strong for reactive
9. Evolve
Strong
Incremental backfill; one command
Weak
Migration plus backfill script
Weak
Migration action; revert needs a second migration
10. Inspect
Strong
Per-cell errors, lineage, revert, local dashboard
Adequate
PITR, branching; no per-cell errors
Adequate
Snapshot export; function logs

All three beat realtime on this laptop

Two corpus tiers: 20 videos ingested, then 100 more for 203 total. CPU, local models, not Cloud. Bold is best on that row.

Ingest

FeaturePixeltableSupabaseConvex
Wall time · 20 videos62.3s45.8s47.6s
Wall time · 100 videos361.6s223.7s231.5s
Faster than realtime · 20 videos9.7x13.18x12.68x
Faster than realtime · 100 videos10.45x16.89x16.32x
Median video · 20 videos2.9s2.0s1.9s
Median video · 100 videos3.5s2.2s2.3s

Search

FeaturePixeltableSupabaseConvex
Frame search p50 · 23 videos20.9ms20.4ms13.9ms
Frame search p50 · 203 videos22.2ms17.3ms15.3ms
Frame search p95 · 23 videos22.4ms26.5ms18.1ms
Frame search p95 · 203 videos24.2ms21.2ms21.5ms
Transcript search p50 · 23 videos17.9ms17.0ms11.2ms
Transcript search p50 · 203 videos17.0ms16.6ms11.4ms
Transcript search p95 · 23 videos21.1ms21.9ms14.6ms
Transcript search p95 · 203 videos18.5ms22.2ms16.5ms

Agent

Local Qwen2.5-1.5B on the 3-video baseline; retrieval is a fixed top-4 per index, so generation dominates.

FeaturePixeltableSupabaseConvex
Agent query p50206.1ms700.3ms807.8ms
Agent query p95235.0ms851.1ms1104.3ms

Reads

FeaturePixeltableSupabaseConvex
GET /videos p50 · 23 videos6.1ms6.4ms1.9ms
GET /videos p50 · 203 videos5.1ms7.8ms2.4ms
Frame fetch p50 · 23 videos1.7ms2.8ms0.4ms
Frame fetch p50 · 203 videos0.7ms2.1ms0.4ms

Under load, 8 clients

The same ten searches with eight clients in flight; measures degradation, not speed.

FeaturePixeltableSupabaseConvex
Concurrent p50 · 23 videos173.6ms64.9ms63.1ms
Concurrent p50 · 203 videos119.5ms67.0ms65.2ms
Concurrent p95 · 23 videos264.6ms197.3ms72.9ms
Concurrent p95 · 203 videos173.7ms123.9ms72.8ms

Large tier ingests 20 videos (603.7s of footage) into a 3-video table; xl ingests 100 more (3,778s) for a 203-video, 7,689-frame corpus. All three finished every video at both tiers; Pixeltable logged one retried attempt at xl. Supabase led ingest at both tiers and the gap widened rather than shrank: 1.36x at large, 1.62x at xl. Search separates by single-digit milliseconds, because most of every number is embedding the query, not the index. The agent runs on the 3-video baseline, where Pixeltable is 3-4x faster because one question costs the other two three compute-service round trips and costs it none. Under eight concurrent search clients that same property inverts: the in-process embedding serializes at 120-174ms while the other two hold ~65ms. One machine, one afternoon: read the gaps, not the milliseconds.

One hosted model, three ways to call it

The agent again, with local generation swapped for nvidia/nemotron-3-super-120b-a12b on OpenRouter’s paid endpoint, identical for all three. 12 questions, 6 workers, then every patch reverts.

FeaturePixeltableSupabaseConvex
Answered of 12121212
Wall time36.6s9.4s18.8s
p508.4s2.3s3.9s
p9534.1s5.2s18.8s
Retriesscheduler-internal00
Lines written for the swap73637

The swap is the measurement: on Pixeltable it is a 7-line schema change and the request-rate scheduler paces and retries; on the other two it is a 36-37 line helper, because pacing, Retry-After and backoff are application code. On a healthy pool that code is never exercised: all three answer every question, both loops record zero retries, so the remaining differences are who wrote the retry code and latency, where provider response time dominates and the longest tail stays on Pixeltable, whose scheduler serializes pacing. The failure that code exists to catch is real, and now measured rather than inferred: probing the same model’s free pool returned HTTP 200 carrying an upstream error and no content on 3 of 36 calls. A hand-written loop inspects the body and retries that shape; a computed column evaluates the response it is given, so a malformed 200 lands as a null answer, since the scheduler retries raised errors, not well-formed wrong ones. An earlier free-pool run recorded exactly that: five empty cells, 7/12 answers where both loops held 12/12.

Adding a column to live data

Make the title semantically searchable on a populated catalog. Lines compound; these seconds do not.

FeaturePixeltableSupabaseConvex
Total · 23-video catalog0.96s2.71s9.10s
Total · 123-video catalog5.22s3.51s7.66s
Schema change · 23 videos0.96s0.08s7.01s
Schema change · 123 videos5.22s0.09s6.19s
Model-free control · 23 videos0.53s0.23s1.52s
Model-free control · 123 videos0.81s0.12s1.52s
Backfill · 23 videossame step2.63s2.09s
Backfill · 123 videossame step3.42s1.48s
Lines written12453
Files touched122
  • The wall-clock winner flips with corpus: Pixeltable is cheapest at 23 videos (0.96s), Supabase at 123 (3.51s). The fused step scales with rows while the script’s fixed cost amortises; a model-free control per platform separates mechanism cost from backfill work.
  • The controls expose the slope behind that flip: Pixeltable’s schema-minus-control residual grew 0.4s to 4.4s between the two corpus sizes while Convex’s stayed flat.
  • Reverting is not symmetric. Convex needs a second migration (17 of its 53 lines) because pushing a schema that no longer declares the field is rejected while documents still carry it.
  • Convex also has a lexical searchIndex path: 1 line, 2.75s at 23 videos and 1.67s at 123, no backfill, because it indexes a field that already exists rather than computing an embedding. It answers a different query.
  • 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.

When to use what

These are not mutually exclusive. Pixeltable as the media layer behind Supabase or Convex is a coherent architecture.

  • The workload is media-heavy and the pipeline is the productPixeltable
  • You want Postgres, RLS, realtime, PITR, branching, or a managed database your team already operatesSupabase
  • Reactivity is the point: the UI re-renders on writeConvex
  • CRUD app with auth and live subscriptionsSupabase or Convex
  • You already run Supabase or Convex and need a media and retrieval layerPixeltable alongside; they are not mutually exclusive

Costs the other two pay for media

Not stack folklore. These are properties of the three implementations.

  • A second Python process because the runtime cannot run ffmpeg

    compute-service/

    Deno Edge Functions and the Convex runtime have no subprocess, so ffmpeg, Whisper, and CLIP live in compute-service/. Three of its seven endpoints have no hosted-API substitute. Swap embeddings to OpenAI and the service shrinks; it does not vanish. You then operate two runtimes, most of the orchestration hops, and base64 on the wire.

  • Processing that lives only in the ingest path

    A 75-line (Supabase) or 61-line (Convex) ingest function is the idiomatic shape and it is fast. A row that arrives from a backfill, a second client, or a psql session is not processed. Getting that behaviour back means database triggers or a scheduled action, and a webhook or a job per row. On Pixeltable the pipeline belongs to the table, so it fires for every writer.

  • Embedding the query with a model you hope matches the index

    similarity(string=q)

    similarity(string=q) asks the index. The other two embed the query themselves. Nothing checks it came from the model that filled the column; the dimension is the only guard. Convex’s vectorSearch also clamps to 256 hits.

Where this is favourable to Pixeltable

Stated so you do not have to find it yourself. The sponsor’s implementation has the weakest automated proof.

  • The response envelope is { rows: [...] }

    That is what Pixeltable’s FastAPIRouter.add_query_route emits natively. Supabase and Convex build their JSON by hand either way, so it costs them nothing, but it was not chosen neutrally.

  • Local models suit Pixeltable

    Running everything on CPU with no API key makes the benchmark reproducible, and it also removes a hosted-API dependency Pixeltable would otherwise share. With OpenAI, all three would gain one external host; Pixeltable would still gain no orchestration.

  • The app is media-heavy

    Video ingest, frame extraction, and transcription are exactly what computed columns are for. A CRUD application with real-time subscriptions would read very differently, and Convex in particular would look much better.

  • One author wrote all three

    The Pixeltable version had the benefit of knowing what the contract needed. The other two follow their vendors’ documented guidance and pass their vendors’ own checkers, but they were still not written by their platforms’ experts.

  • The contract is REST-shaped

    That is Pixeltable’s native serving surface, Supabase’s third-best (behind PostgREST and Realtime), and Convex’s worst. Convex’s http.ts is 90 lines that exist only because we asked for REST instead of using its reactive client.

  • compute-service is charged in full to both competitors

    It is 45% of Supabase’s total and 37% of Convex’s. Roughly half of it would disappear behind a hosted embedding API; the ffmpeg half would not.

  • Auth, RLS, realtime, and cost are out of frame

    This repo runs on a service-role key and writes no per-tenant policy. For a multi-tenant product those are decisive, and Supabase and Convex both have answers where Pixeltable, here, does not. Dollars, p99, cold starts, and team familiarity are unmeasured.

Outside the contract

  • Hosted-model scheduling is exercised by a separate tier, not the contract corpus: harness/bench_hosted.py swaps all three to one OpenRouter model on the paid endpoint and fires the agent questions concurrently, and harness/probe_hosted.py measures the malformed-200 failure shape on the free pool. Results in docs/hosted.json and docs/hosted_probe.json.
  • Only FrameIterator and AudioSplitter run. Five other iterators ship and go unmeasured.
  • pxt dashboard lineage has no counterpart on Studio or the Convex dashboard, because the other two record nothing to draw. Those UIs are also unmeasured.

Reproduce every number: the commands in pixeltable/pixeltable-vs-supabase-vs-convex

Frequently asked questions

Is Pixeltable a replacement for Supabase or Convex?

Not for user auth, row-level security, or realtime chat. Pixeltable replaces the services you bolt on when media and retrieval enter the picture: the second compute process, the orchestrator, the embedding glue, and the backfill script. Many teams use Supabase or Convex for app data and Pixeltable for the AI pipeline. The benchmark says pick more than one when you already run one of those.

Why do Supabase and Convex need a second service?

Their function runtimes cannot execute ffmpeg. Every video, audio, or image pipeline therefore requires an external compute service. Three of that service’s seven endpoints have no hosted-API substitute. Pixeltable runs ffmpeg, Whisper, and CLIP as computed columns in the same process.

Is Pixeltable faster?

Depends which operation. It is last on ingest at both measured tiers (10.45x realtime against Supabase’s 16.89x at 100 videos) and last on serial search by single-digit milliseconds. It is 3-4x faster on the agent query, because the model runs where the data is while the other two pay three compute-service round trips per question. Convex wins the read path outright, and under eight concurrent search clients Pixeltable’s in-process embedding serializes while the other two hold ~65ms. All local, CPU, one laptop; the hosted-model tier above is measured separately. The durable claims are structural: line counts, hops, where media processing runs, and what happens when you add a column to live data.

Why does Convex look worse than it is?

A REST-shaped contract is Convex’s worst event. videos.ts (105 lines) exists because an action cannot write to the database directly, and http.ts (90 lines) exists because we asked for REST and then had to validate request bodies there by hand. Build the same app with Convex’s reactive client and both taxes disappear. Discount that column accordingly.

What was actually executed?

All three, end to end, locally, with no API key. 11 contract tests each, 20 differential tests comparing them against each other, 32 resilience tests, 6 recovery tests, and 30 tests of the measuring code itself. A separate hosted-model tier needed a real OpenRouter key and reverted its own patches when done. CI runs each vendor’s own checker: deno lint and supabase db advisors, Convex’s ESLint plugin and tsc, and ruff for Pixeltable. Pixeltable has the weakest automated proof of the three, because it ships no conformance checker.

What does this benchmark not measure?

Cost in dollars. Auth and multi-tenancy. Realtime and reactivity. Managed operations, uptime, and on-call. Cold starts and p99 latency. Team familiarity. Migration cost from whatever you run today. Any of these can outweigh everything in the tables.

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 works