Auth and tokens
”You only need the Anon Key client-side; the Service Role Key is server-side only”
The Service Role Key bypasses RLS. Don’t embed it in browsers, mobile apps, or anything else that ships to user devices. If you’ve accidentally shipped it, rotate it from the Studio and treat any data accessed during the leak window as compromised. See Auth model.Authorization header with Bearer (trailing space) returns 401 silently on webhook routes
The webhook trigger route (POST /api/webhooks/{id}) checks auth_header.lower().startswith("bearer ") (with a trailing space) and pulls the token by slicing from index 7. If your client constructs Authorization: Bearer ${token ?? ""} and token is undefined, you send literally "Bearer " and get an empty string as the token. Returns 401 with no useful error. Fix: guard the construction so you don’t send a Bearer header when the token is missing, or pass the secret via the ?token=... query parameter instead.
The general /api/* auth path uses auth_header.split() and is whitespace-tolerant. A Bearer there returns 401 “Authorization header required” rather than a confusing empty-token failure.
”I’m sending the access token but getting unauthorized”
The access token expires after 1 hour. Most client SDKs refresh automatically; if you’re rolling your own, you need to call POST /auth/v1/token?grant_type=refresh_token before the 1-hour mark. See Signup, signin, magic link.
”The user is signed in but my queries return zero rows”
Probably an RLS policy issue. The signed-in user has theauthenticated role; check that your tables have policies granting SELECT to authenticated for the rows the user should see. See RLS Cookbook. If you’re hitting /rest/v1/* directly, confirm you’re sending the user’s access token in Authorization, not the Anon Key.
RLS and the ai schema
”My user’s token gets 403 on /api/*”
Since 0.12.0, a signed-in user’s JWT works only on the conversation routes: their own agent and orchestration sessions, runs on them, and approvals of their own paused runs. Every other /api/* route, including knowledge bases, knowledge-base search and sources, answers 403 with This endpoint requires the project's service role key. Call those routes from your backend with the Service Role key. The route list is in Which key for which route.
A 403 on /run or /run/stream that names knowledge_bases (or runtime_knowledge_bases, context_handler_id, context_items[].item_id) means an end user’s run tried to choose its own knowledge bases or stored context. Configure the knowledge bases on the agent, or run the agent from your backend. A 404 on a run with a session_id means the session doesn’t exist or isn’t that user’s; end users can’t name a new session id. See Rules for end-user runs.
Querying ai.* via PostgREST fails, even with Accept-Profile: ai
The ai schema isn’t exposed through PostgREST (it’s not in PGRST_DB_SCHEMAS), whichever key you send. Reach AI resources through the typed /api/* endpoints instead.
”I added an RLS policy but my old policy still applies”
RLS policies are additive (OR-combined) by default. Adding a permissive policy doesn’t replace existing permissive policies; they all apply. If you intended to replace,DROP POLICY first.
Agent runs
Agent data tools see more on backend runs than on user runs
An agent’s data tools act as whoever started the run. With a user’s own JWT,database_query and database_write run as the authenticated role with that user’s claims, so your RLS policies apply to the agent’s configured tables, and the storage tools use the user’s token. With the Service Role key, they use the agent’s own database login, which bypasses RLS on its configured tables, and the storage tools see every bucket except the internal sources bucket, even in a session owned by a user. If your backend runs agents for users, configure those agents only with tables and buckets every such user may see in full. See What agent data tools act as and BaaS+AI cookbook Recipe 2.
temperature at the top level of agent body silently dropped
Agent create/update bodies accept name, model, system_prompt, and settings. Top-level temperature is silently dropped. Nest it inside settings:
MCP transport: "sse" works but "http" is the default
The platform’s MCP integration accepts both sse and http, but the default at the database level is http. Newer MCP servers use streamable HTTP; SSE is the older variant. Use http unless you know your server only supports SSE.
Agent runs return provider_key_decrypt_failed
Your BYOK provider key can’t be decrypted. Re-upsert it via POST /api/ai-provider-keys. See Billing model.
Workflows
{"type": "input"} block type returns 400 Unknown block type
The block registry has 10 canonical types: starter, agent, code, condition, general_api, platform_api, response, split, webhook, orchestration. input, output, and llm are not real. See Workflows concept.
/execute body uses input but the new field is variables
POST /api/workflows/{id}/execute accepts both variables (canonical) and input (legacy alias); the platform reads data.get("variables", data.get("input", {})). Prefer variables in new code.
/arm returns {"ok": true, "armed_until": ...} not {"webhook_id", "secret"}
The arm endpoint doesn’t return webhook credentials. The webhook_id and webhook_secret live in the webhook block’s config; fetch the workflow and read them from there. See Workflows reference.
Workflow /execute returns 429
The endpoint is rate-limited at 20 requests per minute per user. Back off with jitter and re-try. See Rate limits.
Storage
Public-bucket URLs ignore RLS
GET /storage/v1/object/public/{bucket}/{path} returns files without auth from any bucket where public: true. No RLS check happens on the public URL. If you don’t want files publicly fetchable, don’t put them in a public bucket; use a private bucket with signed URLs instead. See Storage policies.
File over 50MB returns 413
The per-request file size limit is 50MB. For larger files, use TUS resumable uploads at /storage/v1/upload/resumable. See Storage uploads.
Storage MIME allowlist isn’t real security
The MIME check looks at theContent-Type header your client sends, not the actual file contents. A malicious client can upload an executable with Content-Type: image/png. Treat the allowlist as UX; validate file contents server-side for security.
Realtime
postgres_changes subscription joined but no events arrive
Thesupabase_realtime publication isn’t set up by default on Powabase projects. You have to CREATE PUBLICATION supabase_realtime FOR TABLE public.your_table (or FOR ALL TABLES IN SCHEMA public). See Realtime model.
Realtime returns 403 TenantNotFound
Only happens on self-hosted deployments where Kong isn’t preserving the Host header. Powabase managed cloud handles this; if you see it from managed cloud, file a support ticket.
WebSocket closes after 60 seconds
You’re not sending heartbeats. Send a{ topic: "phoenix", event: "heartbeat", payload: {}, ref: "..." } frame every 30 seconds. See Realtime subscriptions.
Postgres / pooler
prepared statement "..." does not exist
PgBouncer transaction mode breaks prepared statements. Disable them in your driver’s config; see Connection pooling for the per-driver table.
LISTEN/NOTIFY through the pooler doesn’t work
PgBouncer doesn’t maintain session-level state across statements. The LISTEN registers on one server connection; the next statement lands on a different one. Use Realtime instead.SET statement_timeout = '5s' outside a transaction doesn’t apply
Same reason as LISTEN/NOTIFY: the SET sticks to one server connection. Use SET LOCAL inside a transaction, or pass settings via the connection string.
Username is <ref> not postgres
Powabase’s PgBouncer routes by database name, where the database is <ref> and the user is also <ref>. Coming from Supabase or a standalone Postgres, the muscle memory of postgres:postgres@host/postgres is wrong. Copy the URL from the Connect modal verbatim.
Webhooks (agentic, inbound)
Webhook returns 401 even with the right secret
Common cause: extra whitespace in the secret. The platform useshmac.compare_digest which is byte-exact. Also check that you’re not sending Bearer with no token (see auth section).
Webhook returns 403 after firing once
Armed (single-use) webhooks fire exactly once per arm. Re-arm withPOST /api/workflows/{id}/arm. For unlimited fires, deploy the workflow instead. See Workflows reference.
Webhook secret rotates on arm/deploy?
No. The webhook secret is fixed in the webhook block’s config when the block was created. Arming and deploying don’t rotate it. To rotate, update the block config viaPUT /api/workflows/{id}/graph.
Billing
Agent run returns 402 insufficient_credits
Free-tier hard cap. The response includes balance, estimated_cost, and renews_at. Surface the renewal date to the user; don’t retry. See Billing model.
Agent run returns 503 billing service unreachable
Transient. Back off and retry. Don’t treat as a permanent error.
Confusingly named
”Webhook”: agentic vs database
- Agentic webhooks: external systems triggering workflows.
POST /api/webhooks/{id}. - Database webhooks: Postgres rows changing and Postgres calling out via pg_net.
”Session”: agent vs auth
- Agent session: a multi-turn conversation. In
ai.agent_sessions. - Auth session: a signed-in user’s authenticated state. GoTrue-internal.
”Hook”: agent vs database trigger vs GoTrue
- Agent hook: lifecycle callback (PreToolUse, approval, etc.).
- Database trigger: Postgres function fired on row changes.
- GoTrue hook: auth-layer extension point (not exposed on Powabase per-project today).
Debugging a failed run
When an agent run, workflow execution, or orchestration finishes withstatus: failed, the pieces you need are spread across several endpoints. Check them in this order.
Step 1: get the run record
GET /api/agents/runs/{run_id} returns the full run: status, error, usage, events (every SSE event that was persisted), tool_calls, reasoning_steps, retrieved_context, input_messages, output_messages. The error field is the highest-signal place to start.
Step 2: scan the events array for the first non-success event
Theevents array preserves the order of execution. The first event with a failure shape (type: "error", type: "tool_error", etc.) is usually the actual cause; everything after it is symptom.
Common shapes:
Step 3: for retrieval issues, get the retrieved context
GET /api/sessions/{session_id}/runs/{run_id}/retrieved-context returns what context was injected into the LLM call. If the agent’s answer is “I don’t know,” compare this to what you expected. Empty or wrong-source context here is the root cause.
Step 4: for workflow runs, get the per-block logs
GET /api/workflows/{id}/executions/{execution_id}/logs returns the per-block logs for a workflow execution. A failed general_api block’s response body lives here, as do code-block stderr lines.
Step 5: rate limit?
If the failure is on the 22nd+ workflow execution within a minute, you’ve hit the in-memory rate limit (20/min per user). Wait 60 seconds or pace your calls. The error is"Rate limit exceeded".
Step 6: still stuck?
The Studio’s run-detail view renders all the above on one page and is usually faster than stitching the API calls yourself. For platform issues (suspected bug, weird state), file a support ticket with the run_id; the platform team can read the same data plus the internal logs.Next steps
Glossary
The full terminology disambiguation list.
API conventions
The shared patterns across all
/api/* endpoints. Once you know them, fewer pitfalls.Migrating from Supabase
A specific subset of pitfalls for users coming from Supabase.
BaaS + AI cookbook
The recipes designed to keep you out of the worst of these.