Skip to main content
This page is a list of things people get wrong, with the fix for each. Most are documented somewhere else in the docs too — this page exists so you can grep for the specific error message or symptom and jump to the right fix without reading three concept pages. If you’re hitting something here, the right move is to read the linked source-of-truth page after applying the fix. The pitfalls list is a shortcut; the concept pages are the explanations.

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 the authenticated 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 default ai.* 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:
{"name": "...", "model": "gpt-4o", "settings": {"temperature": 0.7}}

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 the Content-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

The supabase_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 uses hmac.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 with POST /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 via PUT /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.
See Glossary.

”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 with status: 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

The events 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 / errorProbable cause
"insufficient_credits" with balance: 0Free-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 retriesLLM hit max_tokens repeatedly and continuation prompts didn’t help. Increase max_tokens or shorten the system prompt.
Empty output_messages but status: completedOften 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.