Three implementations of one application: a video intelligence pipeline that extracts frames, transcribes speech, embeds both, detects scenes, and answers questions about what it saw and heard.
Most stacks glue a blob store, a warehouse, a vector database, an orchestrator and custom endpoints together, and you pay for the joints. Pixeltable is the database, the orchestration and the serving: tables, computed columns, indexes and endpoints in one Python file. Insert a row. Transforms run.
Pixeltable sponsors this repo, so the methodology is written to be attacked: same
contract, same fixtures, same models, every implementation held to its vendor's own
checker in CI, and every number generated by harness/run_comparison.py from the source.
n/a means a metric does not apply, never that it scored zero.
| Pixeltable | Supabase | Convex | |
|---|---|---|---|
| App code you maintain | 123 | 306 | 426 |
| Plus the shared compute service | 0 | 278 | 278 |
| Total | 123 | 584 | 704 |
| Files you open to read the backend | 1 | 7 | 7 |
| Schema objects | 2 tables, 2 views | 5 tables, 1 view, 3 FKs | 5 tables |
| Vector indexes | 2 | 2 | 2 |
| Orchestration hops | 2 | 13 | 9 |
| HTTP routes with a hand-written handler | 1 | 5 | 5 |
Speed, on one laptop (docs/SCALE.md): Pixeltable is last on ingest and on search, and degrades most under eight concurrent clients; Convex is fastest on reads; on the agent query, with the chat model on the same GPU everywhere, Convex answers first and Pixeltable last. An earlier version of this repo reported Pixeltable far ahead on the agent. That was our compute service leaving the model on CPU (docs/device.json), not a platform difference.
Read docs/TRADEOFFS.md before the rest. This app is media-heavy, which suits Pixeltable; if you need realtime, row-level security or a managed database, the answer changes.
- Media processing lives in a second service. Edge Functions'
documented memory and CPU-time limits
sit below what these models need, and Convex's
default runtime exposes no subprocess;
ffmpeg inside Convex's Node runtime was not attempted. So both use
compute-service/, and three of its seven endpoints have no hosted-API substitute. - Adding a column to live data is 1 line against 24 and 41
(docs/EVOLVE.md); the backfill is fused into
pxt schema update. - Processing fires for any writer, because the pipeline is the schema. On the other two it lives in the ingest path, so a row written by another client is not processed.
- Retrieval knows its own model. Elsewhere you embed the query yourself and the dimension is the only guard.
- Errors are per cell (
errormsg,errortype); elsewhere a failed step marks the whole videostatus: 'error'. - Lineage is in the catalog (
pxt columns,pxt dashboard); the other two record nothing to draw. - Request validation is derived: a bad body is a 422 on Pixeltable, against 28 and 42 hand-written lines.
- A hosted-model swap is a 6-line schema change against 29 lines of retry/backoff (docs/hosted.json). On the paid endpoint all three answer 12/12; the free pool returns malformed 200 bodies (docs/hosted_probe.json), which a computed column evaluates to a null answer - 7/12 against 12/12 in the earlier run there.
| Checker | What it gates | |
|---|---|---|
| Supabase | deno lint, supabase db advisors --local |
Edge Function style; no security or performance errors on a live database |
| Convex | @convex-dev/eslint-plugin, tsc --noEmit |
Their own best-practice rules against real generated code |
| Pixeltable | ruff |
Generic Python. Pixeltable ships no conformance checker, the weakest automated proof of the three. |
ci.yml is the fast gate on every push; live.yml stands up all three plus the compute
service and runs every suite in docs/METHODOLOGY.md against them.
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)]The whole thing is pixeltable/app.py: 129 lines, HTTP included.
Everything runs locally on CPU, no API key.
pip install gTTS && python fixtures/videos/generate.pycd pixeltable && pip install -e . && pxt init
pxt schema update app.py media # creates the tables
pxt service update app.py media # starts HTTP
URL=$(pxt service list | awk '/^media/{print $2}')Supabase and Convex need compute-service/ first
(cd compute-service && pip install -e . && uvicorn app:app --host 0.0.0.0 --port 9000 --timeout-keep-alive 120;
Supabase reaches it from inside a container, and app.py says why keep-alive is set), then
supabase-app/README.md or
convex-app/README.md. Convex picks its ports at startup;
npx convex dev writes CONVEX_SITE_URL into convex-app/.env.local.
SUPA=http://127.0.0.1:54321 # supabase status
CONVEX=http://127.0.0.1:3211 # CONVEX_SITE_URL in convex-app/.env.local
SECRET=... # supabase status, the Edge Function needs it
python harness/run_comparison.py # regenerates docs/metrics.json; CI fails on drift
python harness/check_docs.py # fails if the docs stop quoting the artifacts
python harness/seed.py --impl pixeltable
python harness/seed.py --impl supabase --base-url $SUPA --auth-token $SECRET
python harness/seed.py --impl convex --base-url $CONVEX
pytest harness/test_equivalence.py --impl pixeltable
pytest harness/test_differential.py harness/test_resilience.py \
--compare pixeltable --compare supabase=$SUPA --compare convex=$CONVEX \
--auth-token $SECRET
pytest harness/test_metrics.py # tests the measuring code itself
python fixtures/videos/generate.py --tier large
python harness/benchmark.py --impl pixeltable --tier large # docs/SCALE.md
python harness/bench_evolve.py --supabase-token $SECRET # docs/EVOLVE.md
# run compute-service on :9100 first; this script's proxy takes its usual :9000
python harness/bench_roundtrip.py --impl supabase --base-url $SUPA --auth-token $SECRET
python harness/bench_roundtrip.py --impl convex --base-url $CONVEX --add-latency-ms 80 # docs/roundtrip.json
OPENROUTER_API_KEY=sk-or-... python harness/bench_hosted.py --supabase-token $SECRETtest_recovery.py writes and needs --destructive; re-seed afterwards.
- docs/TRADEOFFS.md: which stack wins when, priced in even swaps.
- docs/SCALE.md: latency and throughput at two corpus sizes.
- docs/EVOLVE.md: adding a column to live data, measured.
- docs/METHODOLOGY.md: what is measured and where this favours Pixeltable.
See CONTRIBUTING.md. Corrections that shrink the gap get published.
- Docs and quickstart
- Starter kit:
uvx pixeltable-new myapp - Migrating from another stack
- Discord
Apache 2.0