> ## Documentation Index
> Fetch the complete documentation index at: https://docs.powabase.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Common pitfalls

> The footguns Powabase users hit most: silent failures, wrong field placements, default-permissive RLS on ai.*, the differences between agentic and database webhooks. With concrete fixes.

This page is a list of things people get wrong, with the fix for each. Most are documented elsewhere 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, 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](/concepts/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](/guides/auth-signup-signin).

### "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](/guides/rls-policies). 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, 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](/concepts/ai-schema-postgrest).

### `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](/guides/baas-ai-cookbook). 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`:

```json theme={null}
{"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](/concepts/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](/concepts/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](/api-reference/workflows).

### 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](/concepts/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](/guides/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](/guides/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](/concepts/realtime).

### 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](/guides/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](/guides/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](/api-reference/workflows).

### 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](/concepts/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](/concepts/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. 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

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` / `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

<CardGroup cols={2}>
  <Card title="Glossary" href="/concepts/glossary">
    The full terminology disambiguation list.
  </Card>

  <Card title="API conventions" href="/concepts/api-conventions">
    The shared patterns across all `/api/*` endpoints. Once you know them, fewer pitfalls.
  </Card>

  <Card title="Migrating from Supabase" href="/guides/migrating-from-supabase">
    A specific subset of pitfalls for users coming from Supabase.
  </Card>

  <Card title="BaaS + AI cookbook" href="/guides/baas-ai-cookbook">
    The recipes designed to keep you out of the worst of these.
  </Card>
</CardGroup>
