Recipe 1: User-scoped agent context
The pattern: you have an agent that needs to retrieve from a knowledge base, but each end user should see only their own sources within that KB. The naive approach (agent has unrestricted KB access; you filter results in your app) leaks data via the response. Theai schema is backend-only, so you can’t put RLS policies on ai.chunks and query it under the user’s JWT. (Earlier versions of this recipe did that; since the ai schema lockdown, PostgREST rejects Accept-Profile: ai.) Instead, keep ownership in your own public table, where RLS does apply, and have your backend pass the user’s source ids to the typed search endpoint.
Step 1: an ownership table in public with RLS:
public.documents via PostgREST with the user’s access token, so RLS returns only their rows (and an invalid token gets a 401). Then call the typed search endpoint with the Service Role key, restricted to those sources with source_ids:
/api/knowledge-bases/{id}/search itself: the route requires the Service Role key and answers a user’s JWT with 403. Keep it that way in your own API too. Never forward source_ids the browser chose, because the search endpoint doesn’t check who owns the sources.
Step 3: pass the retrieved chunks as agent context_items. The agent run endpoint accepts a context_items array that replaces the agent’s own retrieval:
public.documents) → the user’s source ids → typed search restricted to those sources → context_items → agent. /run makes a single LLM call with no tools, so the agent never sees chunks from sources the user doesn’t own. If you switch to /run/stream for tool use, remember this is a service-role run: the knowledge bases configured on the agent are searched without your ownership filter, and its database tools bypass RLS on their configured tables. Keep those off this agent (see Recipe 2).
Trade-offs. You keep the platform’s hybrid scoring and reranking, because the typed search endpoint still does the retrieval. The cost is a backend in the middle holding the Service Role key. If users never share sources, one knowledge base per user (or per tenant) is simpler still: your backend picks the KB, and no source_ids list is needed.
Recipe 2: Build-your-own authenticated chatbot
A chat app where each user talks to an agent and the response streams back to them. User A’s chat history must not be visible to user B. There are two working shapes; pick by what the agent needs.Option A: the browser calls the conversation routes
Agent runs, sessions and approvals are end-user routes, so the browser can stream a run with the signed-in user’s own access token. No Service Role key is involved:- History is per user. The run creates a session owned by the user.
GET /api/agents/{id}/sessionswith their token lists only their sessions, and another user’ssession_idanswers404. - Data tools act as the user. The agent’s
database_queryanddatabase_writetools query Postgres as theauthenticatedrole with the user’s claims, so your RLS policies on the agent’s configured tables decide what it can read and write. Its storage tools use the user’s token, so your Storage policies apply. - The run can’t choose knowledge bases. It searches the knowledge bases configured on the agent; sending
knowledge_basesorruntime_knowledge_basesanswers403. Every user of the agent searches the same knowledge bases, so don’t attach a knowledge base holding one user’s private documents to a shared agent. Use Recipe 1 or Option B for per-user retrieval.
Option B: your backend runs the agent
When a run needs something an end user can’t send, such as knowledge bases chosen per request orcontext_items your backend filtered (Recipe 1), run it from your backend with the Service Role key. Create the session with the user’s user_id, so the conversation still belongs to them: they can list and read it with their own token.
/api/chat and which session they may continue.
Adding a Custom Tool that’s user-aware
On Option A runs, the builtin database tools already act as the user. For user-specific data that lives outside this project’s Postgres, or for agents run from your backend (Option B), register a Custom HTTP Tool that points at your own backend:/internal/tools/search-articles handler:
session_token through the tool’s arguments at call time. Your backend resolves it to the user identity and gates the query. The agent doesn’t know who the user is; your backend does.
The system prompt for the agent includes “always call search_user_articles with session_token = <token>.” You inject the current user’s token into the system prompt at agent-run-time, server-side. From the agent’s perspective, the token is opaque data it passes through.
Custom HTTP tools send only the headers configured on the tool, never the caller’s token, which is why the identity travels as an argument your backend resolves.
Recipe 3: Storage vs ai.sources
You have a multi-format app: users upload PDFs that need to be indexed for RAG, plus they upload images for display. Two distinct flows. For files that need to enter the RAG pipeline (PDFs, docs, anything you want chunked + embedded):- Upload via
POST /api/sources/upload(the typed Sources endpoint). - The platform creates a row in
ai.sources, stores the file in a platform-managed Storage bucket, dispatches the extraction Celery task, and returns the source id. - Add the source to a KB via
POST /api/knowledge-bases/{id}/sourcesto trigger indexing.
- Upload via
POST /storage/v1/object/{bucket}/{path}(the typed Storage endpoint). - The file lands in
storage.objectsunder your own bucket. Your application code reads/displays it.
ai.sources vs storage.objects), and have different RLS postures. Don’t try to share buckets between them. The platform assumes ownership of its Sources bucket layout and will overwrite paths.
For files that need to be both (e.g., a PDF the user uploaded that they should be able to download AND that’s indexed for RAG): upload twice, once through each path. They’re independent: one row in storage.objects (for download) and one row in ai.sources (for indexing). Or pick one path, upload to ai.sources, and use the platform’s pre-signed download URL for the user-facing download.
A worked end-to-end flow for an “upload a PDF, then chat with it” pattern:
public.documents is your own table for “who owns which source.” ai.sources is the platform’s table; public.documents.source_id references it for the ownership graph.
Recipe 4: Realtime + agent runs
For showing live progress during agent runs: the agent SSE stream is one event source, but if you want UI updates to reach other devices too (the user’s phone watching while their laptop runs the chat), Realtime is the right second channel. Two patterns: Pattern 4a: Mirror the SSE stream to a Realtime channel. Your backend receives the SSE stream from/api/agents/{id}/run/stream, persists each event to a row in a public.chat_messages table, AND re-broadcasts each event to a per-user Realtime channel. Clients subscribe to the channel for live updates.
public.chat_messages; a trigger function calls realtime.send() to broadcast it. The application code doesn’t need to call Realtime explicitly; the database side-effect drives the broadcast.
realtime.messages (so only the user themselves can subscribe to their channel) and you have device-syncing chat, gated by the same auth model as the rest of the app. See Realtime model for the function reference.
Next steps
AI resource recipes
Typed /api/* patterns for multi-KB search, usage summaries and bulk source updates.
RLS Cookbook
The lower-level RLS patterns these recipes build on.
Billing model
What every agent run and workflow execution costs in credits.
Realtime subscriptions
Detailed Realtime patterns the recipe-4 broadcast is one application of.