Skip to content

Conversation

@gregnr
Copy link
Member

@gregnr gregnr commented Jan 22, 2026

Adds an agent skill for PostgREST / supabase-js best practices. Particularly helps LLMs navigate PostgREST syntax which is very powerful but can be complex. Demonstrates how to use common patterns directly via cURL but also using supabase-js (which can be translated pretty easily to other sdk languages).

- [ ] RLS policies handle anon correctly
- [ ] Rate limiting for anon endpoints (API gateway)

**Disable anonymous access entirely:**

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe worth adding information on disabling anonymous access by unsetting db_anon_role property altogether? See https://docs.postgrest.org/en/v14/references/configuration.html#db-anon-role

@Rodriguespn
Copy link
Collaborator

Hey @gregnr, thank you for putting this up. Just letting you know that I'm working on a new repo structure that has a common build package called skills-build that validates all skills structure (since they all follow the agent skills open format) and builds each skill AGENTS.md file. It's also possible to only validate and build only a specific skill if we pass the skill name to the script.
This way, we don't need to create new build packages every time we want to add a new skill.

More info on this PR

@Rodriguespn Rodriguespn force-pushed the feat/postgrest-best-practices branch 2 times, most recently from dc5dccf to 0ee4e36 Compare January 23, 2026 16:05
@Rodriguespn
Copy link
Collaborator

Hey @gregnr

Just a heads up — I rebased your branch to align with the new generic build system we merged in. The main change is that I removed the postgrest-build package from the PR since that's now handled by the shared skills-build package.

This should make the PR much easier to review since it now focuses purely on the PostgREST skill/rules you're introducing, without the build tooling changes mixed in.

Sorry for the force push! I know that can be disruptive. Let me know if you have any questions or if something looks off.

Comment on lines +100 to +108
**Important:** If PostgREST doesn't auto-detect the single-param pattern, use `Prefer: params=single-object`:

```bash
curl "http://localhost:3000/rpc/process_order" \
-X POST \
-H "Content-Type: application/json" \
-H "Prefer: params=single-object" \
-d '{"customer_id": 123, "items": [...]}'
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is outdated and should be deleted, this prefer value has been dropped from latest versions.

Suggested change
**Important:** If PostgREST doesn't auto-detect the single-param pattern, use `Prefer: params=single-object`:
```bash
curl "http://localhost:3000/rpc/process_order" \
-X POST \
-H "Content-Type: application/json" \
-H "Prefer: params=single-object" \
-d '{"customer_id": 123, "items": [...]}'
```


## Test Mutations with Transaction Rollback

Use `Prefer: tx=rollback` to execute a mutation and see the result without persisting the changes. Perfect for validation and testing.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This requires setting the config db-tx-end = "commit-allow-override". https://docs.postgrest.org/en/v12/references/configuration.html#db-tx-end

Since this is not exposed on Supabase UI, the way users can enable on Supabase is by:

ALTER ROLE authenticator SET pgrst.db_tx_end TO 'commit-allow-override'`.
NOTIFY pgrst, 'reload config';

-H "Accept: application/vnd.pgrst.plan+json; options=analyze|verbose"
```

**Note:** Requires proper configuration to allow plan output. In Supabase, this is available in the dashboard or via direct database access.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Supabase, users can configure this with:

ALTER ROLE authenticator SET pgrst.db_plan_enabled to 'true';

This should only be enabled for debugging purposes.

Comment on lines +1 to +98
---
title: Use Range Headers for HTTP-Standard Pagination
impact: MEDIUM
impactDescription: RFC 7233 compliant pagination with Content-Range response
tags: pagination, range, headers, rfc7233
---

## Use Range Headers for HTTP-Standard Pagination

Use the `Range` header instead of query parameters for RFC 7233 compliant pagination. Response includes `Content-Range` with total count.

**Incorrect (mixing pagination approaches):**

```bash
# Query params don't give you total count in headers
curl "http://localhost:3000/products?limit=10&offset=0"
# No Content-Range header in response
```

**Correct (Range header pagination):**

```bash
# Request items 0-9 (first 10)
curl "http://localhost:3000/products" \
-H "Range-Unit: items" \
-H "Range: 0-9"

# Response includes:
# HTTP/1.1 206 Partial Content
# Content-Range: 0-9/1000

# Next page: items 10-19
curl "http://localhost:3000/products" \
-H "Range-Unit: items" \
-H "Range: 10-19"

# Open-ended range (from 50 to end)
curl "http://localhost:3000/products" \
-H "Range-Unit: items" \
-H "Range: 50-"
```

**supabase-js:**

```typescript
// supabase-js uses range() which translates to limit/offset
const { data, count } = await supabase
.from('products')
.select('*', { count: 'exact' }) // Request count
.range(0, 9)

// count contains total number of rows
console.log(`Showing ${data.length} of ${count} items`)
```

**Response headers:**

```
HTTP/1.1 206 Partial Content
Content-Range: 0-9/1000
Content-Type: application/json
```

| Header | Meaning |
|--------|---------|
| `206 Partial Content` | Partial result returned |
| `Content-Range: 0-9/1000` | Items 0-9 of 1000 total |
| `Content-Range: 0-9/*` | Total unknown (no count) |

**Combine with Prefer: count:**

```bash
# Get exact count
curl "http://localhost:3000/products" \
-H "Range-Unit: items" \
-H "Range: 0-9" \
-H "Prefer: count=exact"
# Content-Range: 0-9/1000 (exact count)

# Get estimated count (faster for large tables)
curl "http://localhost:3000/products" \
-H "Range-Unit: items" \
-H "Range: 0-9" \
-H "Prefer: count=estimated"
```

**Benefits over query params:**
- HTTP standard compliance
- Total count in response headers
- Clear partial content semantics (206 vs 200)
- Client libraries often support Range natively

**Notes:**
- `Range-Unit: items` is required (PostgREST specific)
- Range is 0-indexed and inclusive
- Without Range header, all rows returned (up to max)

Reference: [PostgREST Range Headers](https://postgrest.org/en/stable/references/api/pagination_count.html#limits-and-pagination)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Range header currently is wrong in PostgREST and will need a breaking change.

This hasn't been a priority since limit/offset do the same and it's more natural on the supabase-js side.

I'd suggest removing this file altogether to not confuse the agents.

Suggested change
---
title: Use Range Headers for HTTP-Standard Pagination
impact: MEDIUM
impactDescription: RFC 7233 compliant pagination with Content-Range response
tags: pagination, range, headers, rfc7233
---
## Use Range Headers for HTTP-Standard Pagination
Use the `Range` header instead of query parameters for RFC 7233 compliant pagination. Response includes `Content-Range` with total count.
**Incorrect (mixing pagination approaches):**
```bash
# Query params don't give you total count in headers
curl "http://localhost:3000/products?limit=10&offset=0"
# No Content-Range header in response
```
**Correct (Range header pagination):**
```bash
# Request items 0-9 (first 10)
curl "http://localhost:3000/products" \
-H "Range-Unit: items" \
-H "Range: 0-9"
# Response includes:
# HTTP/1.1 206 Partial Content
# Content-Range: 0-9/1000
# Next page: items 10-19
curl "http://localhost:3000/products" \
-H "Range-Unit: items" \
-H "Range: 10-19"
# Open-ended range (from 50 to end)
curl "http://localhost:3000/products" \
-H "Range-Unit: items" \
-H "Range: 50-"
```
**supabase-js:**
```typescript
// supabase-js uses range() which translates to limit/offset
const { data, count } = await supabase
.from('products')
.select('*', { count: 'exact' }) // Request count
.range(0, 9)
// count contains total number of rows
console.log(`Showing ${data.length} of ${count} items`)
```
**Response headers:**
```
HTTP/1.1 206 Partial Content
Content-Range: 0-9/1000
Content-Type: application/json
```
| Header | Meaning |
|--------|---------|
| `206 Partial Content` | Partial result returned |
| `Content-Range: 0-9/1000` | Items 0-9 of 1000 total |
| `Content-Range: 0-9/*` | Total unknown (no count) |
**Combine with Prefer: count:**
```bash
# Get exact count
curl "http://localhost:3000/products" \
-H "Range-Unit: items" \
-H "Range: 0-9" \
-H "Prefer: count=exact"
# Content-Range: 0-9/1000 (exact count)
# Get estimated count (faster for large tables)
curl "http://localhost:3000/products" \
-H "Range-Unit: items" \
-H "Range: 0-9" \
-H "Prefer: count=estimated"
```
**Benefits over query params:**
- HTTP standard compliance
- Total count in response headers
- Clear partial content semantics (206 vs 200)
- Client libraries often support Range natively
**Notes:**
- `Range-Unit: items` is required (PostgREST specific)
- Range is 0-indexed and inclusive
- Without Range header, all rows returned (up to max)
Reference: [PostgREST Range Headers](https://postgrest.org/en/stable/references/api/pagination_count.html#limits-and-pagination)

```

**Safety configuration:**
- PostgREST `db-max-rows` limits affected rows
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is false, should be deleted

Suggested change
- PostgREST `db-max-rows` limits affected rows

**Safety configuration:**
- PostgREST `db-max-rows` limits affected rows
- RLS policies can restrict updates
- `max-affected` header provides request-level limit
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a work in progress but should be noted supabase/postgres#1308

Suggested change
- `max-affected` header provides request-level limit
- `max-affected` header provides request-level limit
- Enable the `safeupdate` extension to prevent unqualified UPDATEs

2. Use RLS policies to restrict deletions
3. Use `max-affected` header for safety limits
4. Consider soft deletes for critical data
5. Implement backup/audit trails
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
5. Implement backup/audit trails
5. Implement backup/audit trails
6. Enable the safeupdate module

Comment on lines +59 to +60
- Only works with **to-one** relationships (M2O, O2O)
- Cannot spread to-many relationships (would create multiple rows)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is outdated. @gregnr Perhaps you can ask the LLM to redo this considering the v13 version? Specifically this feature: https://docs.postgrest.org/en/v13/references/api/resource_embedding.html#spread-to-many-relationships

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@Rodriguespn Rodriguespn force-pushed the feat/postgrest-best-practices branch from 0ee4e36 to c5789d8 Compare January 25, 2026 17:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants