---
title: "What Is Pixeltable? One app.py, Local to Cloud to Day 2"
date: "2026-10-20"
author: "Pierre Brunelle"
tags:
  - Pixeltable Cloud
  - TableModel
  - Computed Columns
  - Multimodal AI
  - Deployment
  - AI Agents
description: "Pixeltable puts the multimodal database, orchestration, vector index, and HTTP serving in one app.py. Two commands apply it on your laptop, three put the same file on Pixeltable Cloud, and after that a change costs an upload, a rebuild, or a restart."
url: "https://www.pixeltable.com/blog/what-is-pixeltable-one-app-py-local-to-cloud"
---

# What Is Pixeltable? One app.py, Local to Cloud to Day 2

**Summary:** Pixeltable puts four systems in one Python file. Tables hold images, video, audio, and documents. Computed columns are the orchestration. An embedding index is a declaration on the table, and an HTTP route is a declaration over its columns. `pxt schema update` creates the tables, `pxt service update` serves the routes, and the same `app.py` runs on Pixeltable Cloud after `pxt db update`. This post follows one file through three stages: the file, the first deploy, and every change after it.

 
## The stack the file replaces

 
A multimodal feature usually takes four products: an object store for the bytes, a vector database for the embeddings, an orchestrator to run the models, and a FastAPI service to put a URL on the result. Keeping them in agreement is a second job. The orchestrator re-runs rows that did not change. Nobody deletes the embedding when its source file goes. A handler copies a column into JSON by hand.

 
Pixeltable keeps all four in the table. The longer argument is [The Unified Multimodal Backend Agents Build With](/blog/unified-multimodal-backend-agents-build-with). This post is the path.

 
 [Image: Six-panel comic. The old AI stack: a character tangled in cables between an object store, a vector database, an orchestrator, and FastAPI glue. Collapse it into one file: one machine labeled app.py for database, orchestration, and serving. Multimodal TableModel: a cabinet holding pxt.Image, pxt.Video, pxt.Audio, pxt.Document, __indexes__, and a uuid7() primary key. Computed columns: raw inputs go in, frames, chunks, and embeddings come out. Declarative HTTP routes: FastAPIRouter ingest with POST /docs, /docs/update, and /titles. Built for coding agents: an agent writes app.py and the reviewer sees one diff.]
 *The stack, the file that replaces it, and the four things the file declares.*
 

 
## Stage 1: one file

 
```bash
pip install -U 'pixeltable[serve]'
pxt init
pxt service example --out app.py
```

 
Python 3.11 or later. `pxt init` marks the project root and writes `pixeltable.toml`. `pxt service example` writes this file, shown here without its docstring and comments:

 
```python
import pixeltable as pxt
import pixeltable.functions as pxtf
from pixeltable.serving import FastAPIRouter

TableModel = pxt.model_base()

@pxt.udf
def excerpt(text: str, n: int = 12) -> str:
 return text if len(text) <= n else f'{text[:n]}...'

class Docs(TableModel, name='docs'):
 id = pxt.Column(value=pxtf.uuid.uuid7(), primary_key=True)
 title: pxt.String
 body: pxt.String | None
 title_upper = pxtf.string.upper(title)
 summary = excerpt(title)

ingest = FastAPIRouter(name='ingest')

ingest.add_insert_route(
 Docs, path='/docs', inputs=[Docs.title, Docs.body], outputs=[Docs.id, Docs.title_upper, Docs.summary]
)
ingest.add_update_route(Docs, path='/docs/update', inputs=[Docs.title], outputs=[Docs.id, Docs.title_upper])
ingest.add_compute_route(Docs, path='/titles', inputs=[Docs.title], outputs=[Docs.title_upper])
```

 
Three kinds of line do the work.

 

 - An annotation, `title: pxt.String`, is a column you insert.

 - An assignment, `summary = excerpt(title)`, is a computed column. It runs on every insert and update. `excerpt` is a `@pxt.udf` in the same file, and no loop calls it. The `id` column is a `uuid7()` generated on insert.

 - A route is HTTP over those columns. `POST /docs` inserts a row and returns its `id` and computed columns. `POST /docs/update` takes that `id` and a new `title`. `POST /titles` computes without storing a row.

 

 
The example uses strings so the first response is easy to read. The same class takes `pxt.Image`, `pxt.Video`, `pxt.Audio`, and `pxt.Document` columns. An iterator turns one row into many, a document into chunks or a video into frames, and an embedding index is a list on the class. This is the retrieval file from the [multimodal backend](https://docs.pixeltable.com/use-cases/multimodal-backend) docs:

 
```python
import pixeltable as pxt
import pixeltable.functions as pxtf
from pixeltable.serving import FastAPIRouter

TableModel = pxt.model_base()
embed = pxtf.huggingface.sentence_transformer.using(
 model_id='sentence-transformers/all-MiniLM-L6-v2'
)

class Docs(TableModel, name='docs'):
 document: pxt.Document

class Chunks(
 TableModel,
 name='chunks',
 base=Docs,
 iterator=pxtf.document.document_splitter(
 Docs.document, separators='token_limit', limit=300
 ),
):
 __indexes__ = [
 pxt.EmbeddingIndex(text, embedding=embed, name='chunks_embed')
 ]

@pxt.query
def search_docs(query_text: str):
 sim = Chunks.text.similarity(string=query_text)
 return Chunks.order_by(sim, asc=False).limit(10).select(Chunks.text, sim)

api = FastAPIRouter(name='rag')
api.add_insert_route(
 Docs,
 path='/docs',
 uploadfile_inputs=[Docs.document],
 outputs=[Docs.document],
)
api.add_query_route(path='/search', query=search_docs, method='post')
```

 
Inserting a PDF adds its chunks, their embeddings, and the index entries. There is no vector database to keep in sync and no chunking framework to call.

 
### The file is the unit an agent edits

 
The whole backend is one file, so a coding agent holds all of it in context and you review one diff. The [Pixeltable skill](https://docs.pixeltable.com/overview/building-pixeltable-with-llms) teaches the agent to write a `TableModel` and computed columns rather than per-row model loops and a separate vector database:

 
```bash
npx skills add pixeltable/pixeltable-skill
```

 
`pxt skills install` copies the same skill into the repository you start the agent in.

 
## Two switches: tables, then HTTP

 
```bash
pxt schema update app.py my_app -f
pxt service update app.py my_app -f
pxt service list
```

 
`my_app` is the catalog directory the tables go in. `pxt schema update` creates and migrates the tables. It starts no HTTP. `pxt service update` starts the routes. It creates no tables. `-f` skips the confirmation prompt. `pxt service list` prints each route with its inputs and outputs:

 
```text
my_app/ingest http://127.0.0.1:51158 pid 69084 app.py
 POST /docs insert in: title, body out: id, title_upper, summary
 POST /docs/update update in: id, title out: id, title_upper
 POST /titles compute in: title out: title_upper
```

 
The port is assigned, so read it back rather than hardcoding it:

 
```bash
URL=$(pxt service list --json | jq -r '.[0].endpoint')
curl -X POST "$URL/docs" \
 -H 'Content-Type: application/json' \
 -d '{"title": "Hello from HTTP", "body": "world"}'
# {"id":"01a15b42-0e95-7025-a6c2-4aed3a03ed5a","title_upper":"HELLO FROM HTTP","summary":"Hello from H..."}
```

 
The insert computed both columns before it answered. Calling a model works the same way: the column is an assignment to a provider function, and the route returns its output.

 
## Stage 2: the same file on Pixeltable Cloud

 
Pixeltable Cloud is in Limited Beta. With no account, `pxt new` creates a trial organization and a database named `main`, and prints the `pxt://` URI, the `pixeltable.toml` entry, and the commands below with your names filled in. Claim the trial at the URL it prints, or it is deleted, normally 48 hours later. The `pxt` commands need no API key. Calling a service over HTTP does, and a trial gets one only after the claim (see [below](#keys-and-secrets)).

 
With an account, sign in and list your organizations. Create one only when `pxt org list` prints `No orgs.`: a person can own one free organization, and `pxt org create` provisions its `main` database.

 
```bash
pxt login
pxt org list
pxt org create NAME
```

 
Add a hosted entry to `pixeltable.toml`, beside the unnamed local entry `pxt init` wrote. Replace `org` with your organization name:

 
```toml
[[pixeltable.database]]
name = 'pxt://org:main'
```

 
A new database accepts `pxt db update` only after `pxt db status pxt://org:main` shows `AVAILABLE`. Then run three commands, in this order, from the project directory, and a fourth that prints the URL. `pxt db update` and `pxt service update` ask for confirmation. With no terminal to answer, as in an agent or CI, they refuse and exit 3 unless you pass `-f`. `pxt schema update` asks only before a destructive change.

 
```bash
pxt db update pxt://org:main -f
pxt schema update app.py pxt://org:main -f
pxt service update app.py pxt://org:main -f
pxt service list pxt://org:main
```

 

 - `pxt db update` uploads the project and builds the image. The first one takes several minutes and returns when the database is ready. It inserts no rows and starts no routes.

 - `pxt schema update` creates the hosted tables.

 - `pxt service update` starts the routes. `pxt service list` prints the URL, which looks like `https://{org}-{db}.svc.pxt.run/<service>`. Copy it from the command.

 

 
The local and hosted tables now come from the same file. A Cloud table path is the URI plus the table name:

 
```python
import pixeltable as pxt

docs = pxt.get_table('pxt://org:main/docs')
docs.insert(title='Hello', body='world')
print(docs.select(docs.title, docs.title_upper).collect())
```

 
### Media goes to the home bucket

 
Every Cloud database has a private home bucket at `pxt://org:main/buckets/home`, and hosted tables write media there with no `destination=` and no bucket setup. Inserting a local file path on a Cloud table uploads the file. A service route returns a media column as a signed URL that lasts an hour. An app backend signs at most 100 files per call with `POST https://storage.pixeltable.com/org/main/sign`, for 1 second to 7 days, 3,600 seconds when `expires_in` is omitted.

 
### Two credentials, kept apart

 
Provider keys, such as `OPENAI_API_KEY`, are secrets that the database and its services read at startup. The key your backend sends in `X-api-key` is a Pixeltable API key. Keep it on your server. The browser calls your server.

 
```bash
pxt secret set pxt://org:main OPENAI_API_KEY=your-provider-key
pxt db restart pxt://org:main
pxt service restart pxt://org:main/ingest
pxt key create my-app
```

 
A key created without `--grant` acts as you. A trial's services answer only the trial's own key, which `pxt new` never prints: claim the trial, then run `pxt key create`.

 
Organization and database scopes, sizing, and keys limited to one service are in [Configure Pixeltable Cloud](/blog/configure-pixeltable-cloud).

 
## Stage 3: day 2, three layers

 
A hosted database runs three things from your project, and each one moves on its own.

 
| Layer | What it holds | A change costs |
| --- | --- | --- |
| Image | Dependencies from uv.lock or requirements.txt, the Python version, system packages | A rebuild, the slow step |
| Project files | app.py, its UDFs and routes, and whatever else the entry selects | An upload, no rebuild |
| Settings | The hosted entry's vars and db_* settings, and sections such as [openai] | A restart, no upload or rebuild |

 
All three go through `pxt db update`, which works out which one changed. Editing a UDF costs an upload, not a container build. Without `uv.lock` or `requirements.txt`, the image has Pixeltable only. That is enough for the generated file. List anything else `app.py` imports in one of them. In an agent or CI, pass `-f` to each `pxt db update` and `pxt service update` below.

 
| You changed | Run |
| --- | --- |
| A UDF body or a route | pxt db update, then pxt service update |
| A column or a table | pxt db update, then pxt schema update, then pxt service update |
| A dependency, the Python version, a system package | pxt db update, then pxt service update. The image rebuilds. Until pxt service update, running services keep the old image, or, when the rebuild raises Pixeltable's metadata version, stay at zero pods. |
| A setting in pixeltable.toml | pxt db update, then pxt service update |
| cpu, memory_mb, disk_gb, or workers on the entry | pxt db update. One resize: the pods restart once. |
| A secret | pxt secret set, then pxt db restart and pxt service restart |
| Nothing, but the last update failed | pxt db retry retries it. pxt db build-image forces a rebuild. |

 
### Diff before you ship

 
```bash
pxt db diff pxt://org:main
pxt schema diff app.py pxt://org:main
pxt service diff app.py pxt://org:main
```

 
Each prints what its update would change and applies nothing. Exit 0 means nothing is pending. Exit 2 means a change is. Add a column to `Docs` and `pxt schema diff app.py my_app` names it before anything runs:

 
```text
~ my_app/docs update
 + column 'title_lower' will be added safe

Plan: 0 create, 1 update, 0 unchanged, 0 extra | 0 destructive
```

 
A script or an agent can gate the deploy on that exit code.

 
### Operate and sleep

 
```bash
pxt db status pxt://org:main
pxt db logs pxt://org:main
pxt service logs pxt://org:main/ingest
pxt db stop pxt://org:main
pxt db start pxt://org:main
```

 
`pxt db stop` stops the pods and keeps the data. `pxt db start` wakes them.

 
### Deploy from a cloud coding agent

 
A cloud agent's sandbox usually limits outbound traffic. Allow these hosts, all on port 443:

 
| Host | Used by |
| --- | --- |
| internal-api.pixeltable.com | pxt login, pxt db, pxt secret, pxt key, pxt org, pxt service |
| api.workos.com | pxt login and renewing its session |
| www.pixeltable.com, signin.pixeltable.com | pxt new |
| codeload.github.com | pxt skills install |
| *.s3.amazonaws.com | pxt db update, to upload the project |
| *.r2.cloudflarestorage.com | Media in the home bucket |
| *.svc.pxt.run | Requests to a hosted service |
| *.pxt.run | Everything that uses tables: pxt schema update, pxt service update, pxt.get_table() |

 
The `*.pxt.run` connection is TLS on TCP port 443. From Pixeltable 0.7.19, `pxt` opens it with an HTTP `CONNECT` through the `http://` proxy that `HTTPS_PROXY` names, unless `NO_PROXY` lists the host, and connects directly when there is no such proxy. Claude Code on the web sets `HTTPS_PROXY` and answers a direct connection itself, so there only the `CONNECT` reaches your database: use 0.7.19 or later, and leave `*.pxt.run` out of `NO_PROXY`. Codex cloud has not been tested. The settings for each agent are under [Network access](https://docs.pixeltable.com/cloud#network-access).

 
## Local and Cloud, side by side

 
| Step | Local | Pixeltable Cloud |
| --- | --- | --- |
| Start the project | pxt init, pxt service example --out app.py | The same, plus name = 'pxt://org:main', or pxt new |
| Ship the files | Runs from local disk | pxt db update pxt://org:main |
| Tables | pxt schema update app.py my_app | pxt schema update app.py pxt://org:main |
| HTTP | pxt service update app.py my_app | pxt service update app.py pxt://org:main |
| Preview | pxt schema diff app.py my_app | pxt db diff pxt://org:main, pxt schema diff app.py pxt://org:main |
| Media | Local disk | pxt://org:main/buckets/home, signed URLs |

 
 [Image: Poster: the entire AI backend in one app.py file. A machine labeled app.py holds a multimodal database of tables, an orchestration conveyor, and a server for HTTP routes, and outputs embeddings and predictions.]
 *To serve HTTP routes, install `pixeltable[serve]`.*
 

 
Email [contact@pixeltable.com](mailto:contact@pixeltable.com) for a Cloud account, or run `pxt new`. The reference is [Deploy to Pixeltable Cloud](https://docs.pixeltable.com/cloud). Calling a hosted route from TypeScript is [How to serve a typed AI endpoint](/blog/serve-typed-ai-endpoint-pixeltable).
## Frequently asked questions

### What is Pixeltable?

An open-source Python backend for multimodal AI apps. One app.py declares tables with image, video, audio, and document columns, computed columns that run functions and models on insert, embedding indexes, and HTTP routes. pxt schema update creates the tables and pxt service update serves the routes. The same file deploys to Pixeltable Cloud.

### What is the difference between pxt schema update and pxt service update?

pxt schema update creates and migrates the tables app.py declares. It starts no HTTP. pxt service update starts or restarts the routes the FastAPIRouter in app.py declares. It creates no tables. Run schema update first.

### How do I deploy the same app.py to Pixeltable Cloud?

Add a [[pixeltable.database]] entry with name = 'pxt://org:main' to pixeltable.toml. Then run pxt db update pxt://org:main, pxt schema update app.py pxt://org:main, and pxt service update app.py pxt://org:main, in that order, once pxt db status shows AVAILABLE. With no terminal, as in an agent or CI, add -f to pxt db update and pxt service update, or they refuse and exit 3. pxt service list prints the HTTPS URL. With no account, pxt new creates a trial organization and prints these commands with its names filled in.

### What does a change cost after the first Cloud deploy?

Every change to app.py, its dependencies, or pixeltable.toml goes through pxt db update, which works out what changed. A UDF or route edit uploads the project files and does not rebuild the image. A new dependency, Python version, or system package rebuilds the image. A setting in pixeltable.toml, such as vars, a db_* setting, or an [openai] section, restarts the database with no upload or rebuild. Follow each with pxt service update: running services keep their release until it moves them onto the change.

### Can a cloud coding agent deploy to Pixeltable Cloud?

Yes, when its sandbox allows the Pixeltable hosts on port 443, including *.pxt.run and *.svc.pxt.run. Commands that use tables open a TLS connection to {org}-{db}.pxt.run on TCP port 443. From Pixeltable 0.7.19, that connection goes through the http:// proxy that HTTPS_PROXY names. Claude Code on the web sets HTTPS_PROXY to its proxy, so pxt needs no proxy settings there. Codex cloud has not been tested.

