Summary: Convex gives full-stack developers a reactive TypeScript backend: queries read, mutations write in transactions, actions call the outside world. Pixeltable gives AI developers a declarative data plane: tables, computed columns, embedding indexes, and @pxt.query functions. This post maps Convex primitives onto Pixeltable using the help-desk app from Database School's Convex course and the Convex docs overview. It is a translation guide, not a "replace Convex" pitch, structured so we can later extract sections into official docs.
Two Stacks, Same Shape, Different Center of Gravity#
Convex's architecture is three layers: a document-relational database, server functions (query / mutation / action), and client libraries that keep React in sync via WebSockets. Pixeltable's architecture is also three layers, but the guarantees differ:
- Convex keeps your frontend and backend consistently reactive: when data changes, subscribed queries rerun and the UI updates.
- Pixeltable keeps your data and AI transformations consistently derived: when source data changes, computed columns and embedding indexes update incrementally.
Convex is an excellent application coordination layer for TypeScript full-stack apps. Pixeltable is an excellent AI data layer for multimodal storage, model orchestration, and retrieval. Many production systems need both; AI-heavy backends often reimplement Convex's query/mutation/action split in Python glue. Pixeltable collapses the data + AI side into the schema.
For a feature matrix against Supabase and Convex, see our Compare AI backends page. For the broader "schema IS infrastructure" thesis, see Schema-Driven Infrastructure.
1. Database Layer#
Convex is a document-relational database: JSON-like documents in tables, relations via v.id() references, validators at the boundary, and ACID transactions with serializable isolation. Pixeltable is a multimodal table store: typed columns, computed expressions, and native media types.
| Convex | Pixeltable | Notes |
|---|---|---|
defineSchema + validators | pxt.create_table({ ... }) | Types enforced at insert boundary |
Documents + v.id() relations | Tables + ID columns (workspace_id, ticket_id) | Relational model; unbounded collections (messages) get their own table |
v.union status enum | pxt.String + validation in write path | State machines enforced in mutation-equivalent code |
| Reactive query invalidation | Incremental recomputation | Derived data stays fresh; not live WebSocket push |
| File storage IDs | pxt.Image, pxt.Video, pxt.Document | Media is first-class in the schema |
Parallel walkthrough: help-desk data model#
Database School's help desk models workspaces (tenants), customers, tickets, and messages as separate tables, not embedded arrays. That lesson applies directly in Pixeltable:
Model multi-tenancy early via workspace_id on every tenant-scoped table. That is the same advice Aaron gives in the Convex course, applied here with explicit columns instead of Convex validators.
2. Server Functions: Queries, Mutations, Actions#
This is the core mapping. Convex splits backend logic into three function types with strict rules. Pixeltable splits logic into reads, writes, and derived computation, with a different default for external side effects.
Queries (read-only)#
Convex queries are pure TypeScript: they read the database, cannot call the network, and their results can be subscribed to via useQuery. Pixeltable reads are table expressions or @pxt.query functions exposed over HTTP.
| Convex | Pixeltable |
|---|---|
query({ handler: (ctx) => ctx.db.query("tickets").withIndex(...).collect() }) | tickets.select(...).where(...).order_by(...).collect() |
| Cached + live-subscribable | Read-only by convention; serve via FastAPIRouter or pxt serve query |
Screen-specific BFF query (getThread) | @pxt.query for single-table reads; plain Python for multi-table assembly |
Example: list open tickets by workspace (index-backed read)
Convex explicitly chooses an index with withIndex. Pixeltable filters and sorts on columns; for vector paths you add add_embedding_index and use .similarity() instead.
Mutations (transactional writes)#
Convex mutations are transactions: the entire handler commits or rolls back, with serializability and optimistic concurrency control. Pixeltable writes are insert, update, and delete on tables, with versioning and incremental recompute for downstream computed columns.
| Convex | Pixeltable |
|---|---|
mutation({ handler: (ctx) => ctx.db.patch(...) }) | t.insert([...]), t.update(...), t.delete(...) |
| Automatic OCC retries on conflict | Server-side guards + versioning |
| State machine in mutation | Validate transition in Python before update; insert system message in same logical batch |
| Denormalized search row in same mutation | Computed search_text column updates automatically on insert |
Example: assign ticket with concurrent-write guard
Convex encodes the product decision inside the mutation and relies on OCC so two simultaneous assigns serialize cleanly. In Pixeltable you enforce the same rule in application code (or a @pxt.udf wrapper) before writing. The lesson from Database School still applies: do not trust the client with business rules.
Actions (external side effects)#
Convex actions call the network (OpenAI, email, webhooks) and must call a mutation to persist results. Pixeltable often collapses this split: external model calls live in computed columns, and results persist automatically when rows change.
| Convex | Pixeltable |
|---|---|
action → OpenAI, Resend, webhooks | @pxt.udf or computed column calling openai.* / huggingface.* |
| Must call mutation to persist | Computed column persists on insert/update automatically |
scheduler.runAfter(0, internalAction) | Incremental engine runs after commit; no manual scheduler for derived columns |
internalAction | Non-exposed UDF / query; FastAPIRouter controls public HTTP surface |
Key insight for Convex developers: many Convex "actions" become Pixeltable computed columns. The durable write and the side effect are declared together, not split across action → mutation.
Example: generate embeddings on ticket create
See Incremental Embedding Indexes for why this matters at scale. Our compare hub walks through the same RAG pipeline in ~50 lines on Pixeltable vs hundreds on Convex + external compute. For a runnable side-by-side multimodal pipeline (Convex action + scheduler + external compute vs Pixeltable computed columns), see the platform-comparison repo.
BFF query: getThread#
Database School's ticket detail screen uses a single getThread query that assembles workspace, ticket, customer, and messages server-side: backend-for-frontend without a separate BFF layer. In Pixeltable, use @pxt.query for single-table reads and a plain Python function for multi-table assembly (the same split Convex uses with getThread plus internalQuery helpers):
The frontend gets one round trip with the exact shape it needs. Single-table slices stay reusable via @pxt.query; multi-table screens compose them in plain Python at the serving layer.
3. Indexes and Access Patterns#
| Convex | Pixeltable |
|---|---|
.index("by_workspace", ["workspaceId", "updatedAt"]) | Filter/sort columns; compound access via .where().order_by() |
withSearchIndex on denormalized field | Computed search_text + string or embedding index |
vectorIndex + manual embed action | add_embedding_index(): embed + index incrementally |
Convex compound indexes follow the left-prefix rule: an index on [A, B, C] serves queries on A, A+B, and A+B+C. In Pixeltable, express the same access path as filters and sorts on those columns. For semantic search over tickets, use search_text.similarity(string=query) after adding an embedding index.
4. Client Layer: The Honest Gap#
Convex's useQuery is a major developer-experience win: live subscriptions, consistent multi-query snapshots, and no manual WebSocket wiring. Pixeltable does not ship a React hook. You serve data over HTTP and let the frontend poll, use SSE, or wrap endpoints in a Next.js API route.
| Convex | Pixeltable |
|---|---|
useQuery WebSocket subscription | HTTP via pxt serve / FastAPIRouter / Pixeltable Cloud |
| Optimistic updates built into client | Frontend concern; not in the data layer |
| All subscribed queries update at same logical time | Consistent per query; app composes multi-fetch |
Bridge pattern: keep Convex (or Next.js) for reactive UI and auth; call Pixeltable for AI retrieval and multimodal pipelines. See our PixelAssist retrospective for a real Next.js → data-layer split.
5. Production Patterns (Database School Module 5)#
| Course lesson | Convex | Pixeltable |
|---|---|---|
| Cron jobs | crons.interval | External cron (e.g. Vercel) hitting API; or insert-triggered incremental compute |
| Durable workflows | Convex Workflows | Computed column DAG; see dependency graph |
| Rate limits | Rate limiter component | App-side rate limiting / serve middleware |
| Vector search | vectorSearch action | column.similarity(string=query) |
| AI suggest reply | Action + rate limit + OpenAI | @pxt.query RAG over ticket + similar tickets via embedding index |
6. Cheat Sheet (docs-ready)#
| Convex primitive | Pixeltable equivalent | When to use which |
|---|---|---|
query | @pxt.query / select().collect() | Convex for live UI reads; Pixeltable for AI retrieval reads |
mutation | insert / update / delete | Both for durable writes; Convex adds OCC + live invalidation |
action | Computed column or @pxt.udf | Pixeltable when side effect is derived data (embed, transcribe, classify) |
scheduler | Incremental engine (derived) or external cron (app) | Pixeltable for pipeline steps; cron for calendar-time jobs |
| Schema / validators | create_table + add_computed_column | Pixeltable schema also declares AI compute |
| Index | Column filters + add_embedding_index | Pixeltable for semantic / multimodal search |
| BFF query | @pxt.query or plain Python for multi-table BFF | Same pattern: compose reads server-side |
| Auth / tenancy | workspace_id + serve-layer auth | Convex stronger for app auth; Pixeltable for data-plane isolation |
Live UI (useQuery) | HTTP serving | Convex wins for reactive TypeScript apps |
7. When to Use Convex, Pixeltable, or Both#
Choose Convex when#
- You are building a TypeScript full-stack app with live-updating UI
- Auth, roles, invitations, and optimistic updates are core product features
- Your backend is mostly CRUD + coordination, not heavy ML pipelines
Choose Pixeltable when#
- Your backend is an AI data plane: embeddings, transcription, classification, multimodal RAG
- You need incremental recomputation when source data changes, not manual backfills
- You want one schema to declare storage, compute, indexes, and serving (transformations in the schema)
Use both when#
- Convex handles reactive ticket UI, auth, and real-time presence
- Pixeltable handles ticket embeddings, similar-ticket search, attachment transcription, and agent-suggested replies
- The Convex action calls Pixeltable's HTTP query endpoint for retrieval; each system does what it is best at
Learn More#
- Compare AI backends: Pixeltable vs Supabase vs Convex feature matrix
- Schema-Driven Infrastructure: the Vercel analogy for AI backends
- Incremental Embedding Indexes: why action→mutation embed chains become one declaration
- Pixeltable Core Concepts: computed columns and incremental execution
- platform-comparison repo: runnable Convex vs Pixeltable multimodal pipeline
- Database School: Convex series: the help-desk course that inspired this mapping
- Convex docs overview: queries, mutations, actions, and reactivity




