-
-
Notifications
You must be signed in to change notification settings - Fork 34
feat: PostgREST best practices skill #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
| - [ ] RLS policies handle anon correctly | ||
| - [ ] Rate limiting for anon endpoints (API gateway) | ||
|
|
||
| **Disable anonymous access entirely:** |
There was a problem hiding this comment.
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
|
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 More info on this PR |
dc5dccf to
0ee4e36
Compare
|
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 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. |
| **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": [...]}' | ||
| ``` |
There was a problem hiding this comment.
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.
| **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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
| --- | ||
| 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) |
There was a problem hiding this comment.
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.
| --- | |
| 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 |
There was a problem hiding this comment.
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
| - 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 |
There was a problem hiding this comment.
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
| - `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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 5. Implement backup/audit trails | |
| 5. Implement backup/audit trails | |
| 6. Enable the safeupdate module |
| - Only works with **to-one** relationships (M2O, O2O) | ||
| - Cannot spread to-many relationships (would create multiple rows) |
There was a problem hiding this comment.
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>
0ee4e36 to
c5789d8
Compare
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).