All Stories
2026-10-17•13 min read

Authentication · Multi-Tenancy · Pixeltable Cloud · FastAPI · RAG · TableModel

Authentication and Multi-Tenancy for Pixeltable Apps

Keep your auth provider. Verify sessions on your server, scope Pixeltable queries by tenant, and authorize private media and background jobs before returning them.

Pierre Brunelle

Pierre Brunelle

Pixeltable Team

View as Markdown

Pixeltable apps can keep the authentication provider they already use. Verify the user's session on your application server, call Pixeltable Cloud with a server-only runtime key, and scope every data operation to the verified user or organization. Private media and background jobs need the same ownership check before their results reach the browser.

Consider a document search app. Alice uploads a PDF; Pixeltable splits it into chunks and builds an embedding index. Bob uploads another PDF. Both users can search, but each should see only their own documents. The backend needs to preserve that rule through uploads, vector search, deletion, file delivery, and job polling.

Who is calling, and whose data can they use?#

A Pixeltable Cloud credential answers whether a caller may invoke your service. Your app's session answers which person is using it. A runtime key shared by your application server carries no knowledge of Alice or Bob.

BoundaryWhat establishes accessWhat your app must do
User to application serverA verified session or access tokenResolve identity and check the requested action.
Application server to Pixeltable CloudA runtime key scoped to the serviceKeep the key server-side and call only intended routes.
Service route to tenant dataYour query predicate or mutation ownership checkApply the verified tenant id to every operation.
Browser to private mediaA short-lived signed URLAuthorize the owning row before requesting the URL.

For a web app with an existing Next.js, Remix, SvelteKit, or Python backend, that server is a natural place to verify identity. The browser sends its session and the requested action. The server supplies the tenant id when it calls Pixeltable.

Declare ownership alongside the document pipeline#

This app.py declares a document table, a chunk view, and routes for upload, listing, search, and deletion. The user_id column is the tenant boundary for a personal document library. The chunk view inherits that column from its base table, so search can apply the same predicate.

python

The search filters the chunk relation by user_id before selecting the ten highest-scoring matches. Fetching ten matches across all tenants and discarding unauthorized rows afterward would expose data to application code and could leave the user with fewer relevant results.

The delete route requires both the document UUID and user_id to match. If Alice supplies Bob's document id, a request scoped to Alice matches no row. Your application server must inject the verified user id for deletion just as it does for search.

A required parameter does not enforce a filter. Removing Docs.where(Docs.user_id == user_id) would break isolation even if the function still accepted user_id. Pixeltable does not turn an ownership column into an automatic row-level security policy. The predicates and ownership checks are application code that you must review and test.

The hosted image needs the same dependencies as your local app. For a requirements-based project, include these packages in requirements.txt, using your project's pinned versions. Pixeltable includes pypdfium2 for PDF parsing; the example does not need PyMuPDF.

text

If the project uses uv, declare those dependencies in pyproject.toml and update uv.lock instead. Install them locally too. Keep the OpenAI provider key in Cloud managed secrets for the hosted embedding calls.

Run pxt init in the directory containing app.py if it is not already a Pixeltable project. Keep its local database entry and add a hosted entry to pixeltable.toml. Replace myorg and main with your existing organization and database:

toml

If pxt init wrote the project settings to pyproject.toml, use [[tool.pixeltable.database]] there. Wait until pxt db status pxt://myorg:main reports AVAILABLE, then run the updates from the project directory in this order:

bash

pxt db update uploads the project and builds its dependency environment. The schema update creates the tables, and the service update starts HTTP. Add -f to the update commands when running without an interactive terminal. Copy the service URL from pxt service list; its host depends on your environment. See Configure Pixeltable Cloud for project file selection, managed secrets, and deployment details.

For team accounts, make the organization the tenant#

A shared workspace usually needs org_id for isolation and user_id for attribution. Store both on the base table, use the verified active organization in read and mutation predicates, and check the user's membership and permissions before calling the service. An organization id supplied in JSON proves no membership.

Your SaaS customer's organization and the Pixeltable Cloud organization hosting the database are different scopes. Many application tenants can share one Pixeltable database. A Cloud service grant does not create a policy for those tenants.

Give the application access to its service#

Use your developer login or a user API key for deployment. Create a separate runtime key for the application server, restricted to the api service:

bash

The secret prints once. Store it as PIXELTABLE_API_KEY on your server. This grant permits calls to every route in api; the application must still enforce ownership. It does not grant storage-signing access or permission to reconfigure the service. The CLI key reference lists the supported access: and manage: grant forms.

Call Pixeltable from an authenticated Next.js route#

Set PIXELTABLE_SERVICE_URL to the api service URL copied from pxt service list pxt://myorg:main, with /v1 appended for this router's prefix and no trailing slash. Keep this URL and PIXELTABLE_API_KEY in server environment variables.

This route assumes WorkOS AuthKit is configured in the Next.js app. Its withAuth() helper reads the session. With Clerk, Better Auth, Auth.js, or another provider, use that provider's verified server session instead.

typescript

The browser supplies only the search text. Even if it sends an extra user_id, the server constructs a new upstream body using user.id. Upload routes should follow the same rule: build the multipart form on the server with the file, validated metadata, and verified tenant id.

Add per-user rate limits, request-size limits, and your framework's appropriate CSRF or origin protection for cookie-authenticated mutations. An authorized upload can still consume storage and model credits. Never put a shared runtime key in NEXT_PUBLIC_*, browser JavaScript, or a mobile binary.

For a client checked against the HTTP contract, follow the OpenAPI and TypeScript client walkthrough. Generated types check request and response shapes; they cannot prove that a tenant predicate is present or that the server chose the right identity.

When the service needs the end-user token#

Some trusted backend services already receive an end-user JWT and need the Pixeltable-hosted application to verify it too. This is a separate deployment: a custom FastAPI application named app with a /my-notes route. Set PIXELTABLE_SERVICE_URL to that service's URL from pxt service list, and use a PIXELTABLE_API_KEY granted access:pxt://myorg:main/services/app.

Send the Cloud credential in X-api-key and the distinct end-user token in Authorization:

bash

The Cloud gateway is designed to validate and strip X-api-key, then forward a distinct Authorization value to your service. It removes Authorization when that header supplied the gateway credential or repeats the key. This describes the gateway implementation; verify the header flow in your deployed environment before relying on passthrough. Have a test route report only whether the end-user header arrived, without logging either credential, and confirm missing or invalid user tokens are rejected.

Your custom route must validate the forwarded token with the provider's supported verifier: check the signature with trusted keys, restrict accepted algorithms, validate the issuer and expiry, and validate the intended audience when applicable. Derive identity from verified claims and apply the tenant predicate inside the route. Decoding a JWT or forwarding it through the gateway does not authenticate the user.

Use synchronous def handlers for custom FastAPI routes that call Pixeltable's synchronous catalog APIs, so FastAPI runs that work in its threadpool. For browser and mobile clients, keep a trusted application server in front of this two-header flow; distributing the shared runtime key would let clients call the service outside your application's checks.

Authorize the row before returning its media URL#

A filtered JSON response is only part of a multimodal app's access model. A PDF, image, or video also needs a delivery path that preserves the owning row's authorization.

For this app's uploaded documents, a hosted media output already contains a signed URL valid for one hour. The serving implementation uploads local response media to the home bucket and signs it; object-store media is also presigned. Authorize the row before the route returns that URL. Forwarding the service JSON gives its recipient access to the bytes until the URL expires.

Add a document retrieval query to the same app.py. It matches both the verified owner and document id, then returns the media column. The application server supplies user_id from the session, just as it does for search:

python

After adding the route, rerun pxt db update pxt://myorg:main and pxt service update app.py pxt://myorg:main to upload and serve the changed code. The table schema is unchanged.

Alice's request for Bob's document matches no row and returns 404. The upload, listing, and search routes above omit media outputs; this retrieval route returns one only after the ownership check. Store document ids, then query again when a fresh URL is needed. Anyone who receives a valid URL can use it until expiry, even after the user's session ends.

If your backend separately manages object keys#

The document example needs no separate signing request or extraction of keys from signed URLs. If another part of your backend already manages home-bucket object keys, it can request shorter-lived URLs from POST https://www.pixeltable.com/media/{org}/{db}/sign. Keep a separate signing credential, created with:

bash

This database grant is broader than signing: it also permits invoking every service in the database and obtaining read-write bucket credentials. Keep it in the trusted component that needs that access. Do not replace the application server's service-scoped key with it.

json

Use a short lifetime appropriate to the viewing session; the example requests five minutes. Batch a gallery's authorized keys in one request. Store durable object references, then mint URLs when the app needs to display them.

The signing service does not know your app's row ownership. It checks the Cloud credential and its storage permission. Never turn a browser-supplied list of object keys directly into a signing request. Sign only keys resolved from rows the current user may access.

Supported media keys receive signed URLs on usercontent.pxt.run. The storage edge checks the signature and expiry. It serves uploads from a separate site from pixeltable.com, with sandbox and nosniff response headers. These controls restrict how uploaded content executes; possession of the URL is still what grants access to the bytes. Anyone given a valid URL can use it until expiry, and a session logout does not revoke it.

Local services use their own /media route for local response files. That local path is not the hosted delivery contract described above. Neither flow needs a runtime key in a media tag.

Keep job polling inside the same ownership boundary#

The upload route above uses background=True because document splitting and embedding can outlast a comfortable browser request. It returns a job handle; polling eventually reports pending, done with a result, or error. The serving documentation describes the response contract.

When your application starts an upload, record the returned job id and polling URL with the verified owner in server-side storage. Your /api/jobs/[id] handler should authenticate the caller, check that record, and poll the stored URL using the server's Cloud credential. Accept a job id from the browser, then resolve its URL server-side; an arbitrary client-supplied polling URL must never receive your API key.

A job result can contain the same document metadata or private media as a normal query. Knowing a job id must not let Bob inspect Alice's upload. Polling authorization deserves a test alongside document search and deletion.

Test the boundary with two users#

Before exposing the application, create Alice's and Bob's documents with distinct content and exercise the public application routes:

  • A request without a valid session fails before making a Pixeltable call.
  • Alice's list and search return only Alice's rows, including when Bob's text is a better semantic match.
  • Supplying Bob's user_id in Alice's browser request does not change the identity sent upstream.
  • Alice cannot delete Bob's document by its UUID.
  • Alice cannot obtain a signed URL for Bob's object key or poll Bob's job.
  • For shared workspaces, a user without membership cannot select that organization by changing a request field.

Keep your existing login flow and put its verified identity into the data operations that need it. Pixeltable's TableModel, chunk views, embedding indexes, and HTTP routes can live in one app.py; your application's ownership rule must follow the document through every route that returns or changes it.

Frequently asked questions

Declarative. Multimodal. Incremental.

Focus on innovation, not infrastructure.

10-minute tutorial · Join community