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 — 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 ?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
”Anyone signed in can read every other user’s agents”
This is the defaultai.* RLS posture. The authenticated role has blanket SELECT on most ai.* tables; only session-shaped tables (agent_sessions, agent_runs, orchestration_sessions, orchestration_runs) filter by auth.uid(). For multi-user projects, you need to either tighten the policies or only let end users hit the typed /api/* (which the platform’s backend enforces ownership against). See Querying the ai schema.
query.from(...).select() is empty even though rows exist
If you’re querying ai.* from PostgREST, you need Accept-Profile: ai. Without it, PostgREST looks in public and returns nothing (or 404).
”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 runs with end-user JWTs leak data
The platform does NOT forward end-user JWTs to agent tools.database_query and database_write builtins run as superuser regardless of who invoked the run. If you expose /api/agents/{id}/run to clients with their own access tokens, the agent has full project-wide DB access — not the caller’s RLS-filtered view. See BaaS+AI cookbook Recipe 2. Always run agents from a trusted backend.
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. Here’s the order to check them in:
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:
Symptom in events / error | Probable cause |
|---|---|
"insufficient_credits" with balance: 0 | Free-tier cap hit. The 402 from check_balance_or_503. Top up or grant the project credits. |
"billing service unreachable" | 503 from check_balance_or_503 (fail-closed). The billing service is down — retry. |
"Missing API Key" or "provider_key_decrypt_failed" | The agent’s model has neither a BYOK key nor a platform env key. Set a key in Settings → LLM Provider Keys. |
"Exa API key not configured" | web_search tool called without EXA_API_KEY setting. Set in Studio → Settings → Tools. |
"Sandbox is not configured" | code_execute called but CODE_SANDBOX_URL env not set on the platform. Operator issue. |
"Doom loop detected" | The agent called the same tool with the same args 3 times in a row. Usually means the LLM is misreading the tool’s response. Inspect the tool’s result in tool_calls. |
"Output truncated" after 3 retries | LLM hit max_tokens repeatedly and continuation prompts didn’t help. Increase max_tokens or shorten the system prompt. |
Empty output_messages but status: completed | Often means a multimodal context was sent to a non-multimodal model. Check the model’s capabilities. |
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.