Skip to main content
The point of Powabase is that the BaaS substrate and the AI primitives compose. You can build an app that signs users in (Auth), stores their files (Storage), indexes those files into a knowledge base (Sources + KB), runs an agent that searches the KB (Agents), and streams the answer back to the user, all on one platform, with one auth model, with RLS gating the right things. This page is four worked patterns that string those primitives together. None of them is the only right way; treat them as starting points you’ll adapt to your shape. For deeper dives on each primitive, see the per-area pages linked from the Cards at the end. Two rules from the authorization model shape these recipes. First, a signed-in user’s JWT works only on the conversation routes (their own sessions, runs and approvals); knowledge bases, sources and search need the Service Role key, so those calls go through your backend. Second, an agent’s data tools act as whoever started the run: the user on a run started with their JWT, the agent’s own database login on a run started with the Service Role key.

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. The ai 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:
Step 2: on your backend, resolve the user’s sources, then search. Read 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:
The browser can’t call /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:
The data path is: user JWT → PostgREST (RLS on 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:
What the platform enforces for you:
  • History is per user. The run creates a session owned by the user. GET /api/agents/{id}/sessions with their token lists only their sessions, and another user’s session_id answers 404.
  • Data tools act as the user. The agent’s database_query and database_write tools query Postgres as the authenticated role 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_bases or runtime_knowledge_bases answers 403. 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.
Creating the agent, attaching its tools and knowledge bases, and uploading sources stay on your backend with the Service Role key.

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 or context_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.
The client never sees the Service Role key; only your backend does. Your backend decides who may call /api/chat and which session they may continue.
A run your backend starts with the Service Role key is a service-role run, even in a session owned by the user. The agent’s database tools use its own database login, which bypasses RLS on its configured tables, and its storage tools see every bucket except the internal sources bucket. Give an Option B agent data tools only over tables and buckets every user it serves may see in full, or use Option A for agents that carry data tools. If such a run pauses for approval, only the Service Role key can approve it, so relay the user’s decision through your backend.

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:
Then in your backend’s /internal/tools/search-articles handler:
The pattern: pass an opaque 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}/sources to trigger indexing.
For files that are user content displayed back to the user (avatars, gallery images, attachments):
  • Upload via POST /storage/v1/object/{bucket}/{path} (the typed Storage endpoint).
  • The file lands in storage.objects under your own bucket. Your application code reads/displays it.
The two flows use different buckets, different tables (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.
Pattern 4b: Use a database trigger. Insert each agent event into 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.
Pair this with the matching RLS on 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.