feat(stacks): Add PostgREST — auto-generated REST API for shared Postgres#645
feat(stacks): Add PostgREST — auto-generated REST API for shared Postgres#645stefanko-ch wants to merge 4 commits into
Conversation
…gres
PostgREST is a Go binary that introspects a Postgres schema and
exposes every table / view / RPC as a REST endpoint with built-in
filtering, ordering, pagination, embedded resources, and
content negotiation (JSON, CSV). Zero schema definitions on the
PostgREST side — the API surface is the database surface, and the
OpenAPI spec is auto-generated at the root for Hoppscotch / Swagger
UI consumption.
Pinned to v14.12 (latest stable, 2026-05-20). Multi-arch verified
(amd64 + arm64). Lightweight: ~10 MB image, 256 MB memory cap.
Architecture: PostgREST connects to the shared `postgres` stack on
`app-network` rather than bundling its own DB — the whole value
prop is exposing data that the operator stores in shared Postgres.
The connection role is `nexus-postgres` (the shared-DB superuser);
production hardening to a dedicated `web_anon` role with restricted
GRANTs is documented in docs/stacks/postgrest.md as the operator's
next step.
Secrets:
* POSTGREST_JWT_SECRET (64 chars, HS256-suitable entropy) —
generated by `random_password.postgrest_jwt_secret`, pushed to
Infisical at /postgrest/POSTGREST_JWT_SECRET. Operators mint
short-lived JWTs with this secret to elevate past the anon role.
* POSTGRES_PASSWORD — reused from the shared `postgres` stack, no
Infisical folder of its own.
Renderer fail-fast guard (matches HedgeDoc / Planka pattern): if
either secret is empty at deploy time, raises ServiceEnvError with a
single actionable message naming both keys + the Tofu+spin-up cycle
to fix them.
Files touched (mechanical Stack-Addition-Checklist work):
* stacks/postgrest/docker-compose.yml — single-service compose,
points at postgres on app-network, health-checks at /
* services.yaml — entry with subdomain "postgrest", port 3009→3000
* src/nexus_deploy/config.py — postgrest_jwt_secret field + secret-
map entry (now 100 entries, test_field_count bumped)
* src/nexus_deploy/infisical.py — /postgrest folder with the JWT secret
* src/nexus_deploy/service_env.py — _render_postgrest + EnvSpec
* tofu/stack/main.tf — random_password.postgrest_jwt_secret (64ch)
* tofu/stack/outputs.tf — postgrest_jwt_secret in the secrets output
* README.md — PostgREST badge, table row, stack count 74 → 75
* docs/stacks/postgrest.md — full operator-facing doc with workflow,
auth model, caveats
* docs/stacks/README.md — image-version table + stack-doc index
* tests/unit/test_service_env.py — 4 new tests for the renderer
(missing-JWT, missing-pg-password, both-missing, happy-path) +
full_config fixture extended with postgrest_jwt_secret
* tests/unit/test_config.py — bumped field-count assertion to 100
* tests/unit/__snapshots__/test_infisical.ambr — regenerated to
include the new /postgrest folder
All 12 (4 new + 8 existing planka) relevant unit tests pass; broader
test sweep stays green except for the same 3 macOS-only dump_shell
snapshot failures that have been pre-existing all session.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds PostgREST as a new service exposing the shared PostgreSQL database via an auto-generated REST API; includes Terraform secret generation, NexusConfig/Infisical wiring, env rendering with validation, docker-compose/service registration, tests, Marimo example notebook, and user documentation. ChangesPostgREST Stack Integration
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/stacks/postgrest.md`:
- Around line 25-28: Add a language identifier to the fenced code block
containing the flow diagram in docs/stacks/postgrest.md by changing the opening
triple backticks to include the identifier (e.g., ```text) so the diagram block
is fenced as a text code block; update the block that starts with ``` and
contains "HTTPS client → Cloudflare Access → Cloudflare Tunnel →
postgrest:3000 → postgres:5432" to use ```text.
In `@stacks/postgrest/docker-compose.yml`:
- Around line 46-49: The PGRST_DB_ANON_ROLE is currently set to the full-access
role nexus-postgres; change it to a dedicated least-privilege role (e.g.,
nexus-postgrest) so anonymous PostgREST requests cannot use the shared DB admin
privileges. Update the PGRST_DB_ANON_ROLE value in the docker-compose.yml to
point at the new least-privilege role and ensure that role exists in the DB with
only the minimal SELECT/EXECUTE privileges required for anon endpoints; keep
nexus-postgres reserved for elevated JWT-authorized roles.
- Around line 57-60: The compose service currently sets memory under
deploy.resources.limits.memory but must also include a top-level mem_limit and
restart policy; update the service definition to add mem_limit: 256m at the
service level and ensure restart: unless-stopped is present (preserving
deploy.resources.limits.memory if desired) so the container satisfies the
stacks/**/docker-compose.yml requirements.
In `@tests/unit/test_config.py`:
- Line 41: The snapshot fixtures are stale because the schema change added a new
POSTGREST_JWT_SECRET output line; run the snapshot regeneration for the failing
tests (test_dump_shell_full_snapshot, test_dump_shell_minimal_snapshot,
test_dump_shell_empty_snapshot), update the committed snapshot files
accordingly, and commit the updated snapshots so the assertion around _FIELDS
(assert len(_FIELDS) == 100) and the snapshot expectations stay in sync with the
new output.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 18deb3c0-90e9-452c-9a40-e010068c27b1
⛔ Files ignored due to path filters (1)
tests/unit/__snapshots__/test_infisical.ambris excluded by!tests/unit/__snapshots__/**
📒 Files selected for processing (12)
README.mddocs/stacks/README.mddocs/stacks/postgrest.mdservices.yamlsrc/nexus_deploy/config.pysrc/nexus_deploy/infisical.pysrc/nexus_deploy/service_env.pystacks/postgrest/docker-compose.ymltests/unit/test_config.pytests/unit/test_service_env.pytofu/stack/main.tftofu/stack/outputs.tf
…add SQL recipe CodeRabbit's review correctly flagged the default PGRST_DB_ANON_ROLE=nexus-postgres as a meaningful security concern. The previous doc buried this in a "Caveats" section at the bottom; moved it to a prominent warning callout right under the title so operators decide BEFORE enabling the stack. Also added a concrete "Production hardening" section with the copy-pasteable SQL for creating a least-privilege `web_anon` role — CREATE ROLE, GRANT USAGE, per-table SELECT — plus the env-edit line to switch PostgREST over to it. The previous text just said "tighten by creating a dedicated web_anon role" without showing how; operators now have a runnable recipe. Trivial: also added a `text` language identifier to the flow-diagram code fence (markdownlint).
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/stacks/postgrest.md`:
- Line 25: The relative link to service_env._render_postgrest in
docs/stacks/postgrest.md will 404; update the reference to either a stable
GitHub permalink pointing to the source function (service_env._render_postgrest)
in the repository or remove the hyperlink and render the symbol as inline code
(`service_env._render_postgrest`) so the docs site doesn’t contain a dead
external path—ensure the text still mentions the Tofu-generated secret names
(`random_password.postgrest_jwt_secret`) and Infisical keys for context.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 0c2ccaa4-2f56-44c0-948e-6f7083f2e906
📒 Files selected for processing (1)
docs/stacks/postgrest.md
Drops a Getting-Started Marimo notebook into
examples/workspace-seeds/marimo/ that walks through the full PostgREST
API surface against the shared `postgres` stack:
1. One-time setup SQL (run via CloudBeaver) creating a demo_books
table + sample rows
2. GET /demo_books — list rows
3. Filter (?in_stock=is.true&year=gte.2017), order, paginate (Range
header)
4. POST with `Prefer: return=representation` for server-defaults echo
5. PATCH with filter (PostgREST refuses unfiltered updates)
6. GET / — the auto-generated OpenAPI spec
7. Pointer to docs for the production-hardening anon-role swap
Uses stdlib only (`urllib.request` + `json`). No extra `pip install`
needed against the un-augmented Marimo image (which ships marimo[sql]
+ pyspark[connect] + ibis, but not httpx or requests). This keeps the
notebook portable AND avoids burdening the Marimo Dockerfile with a
runtime dep that only one seed actually needs.
Hits PostgREST at the internal `http://postgrest:3000` (both on
app-network), so no Cloudflare Access round-trip from inside the
compose network. External clients hitting `https://postgrest.<domain>`
still go through CF Access at the edge as before.
Docs alignment:
- docs/stacks/postgrest.md gains a "Try it out (Marimo notebook)"
section linking to the seed.
- docs/stacks/marimo.md's "Three seed notebooks" → "Four seed
notebooks" + bullet entry for the new file. The new bullet
matches the existing entries' shape (what it covers + which
stacks it requires).
The notebook's `__generated_with = "0.23.4"` matches the pinned
marimo image version per the existing convention.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/stacks/postgrest.md`:
- Line 36: The relative link
"../../examples/workspace-seeds/marimo/Getting_Started_PostgREST.py" in
docs/stacks/postgrest.md will 404 on the published site; replace it with a
stable GitHub permalink to that seed notebook (e.g. the raw or blob URL on the
repo with a commit SHA or main branch) so the reference remains valid; update
the markdown link target only (the visible text can stay the same) and verify
the new URL points to the notebook file in the repository.
In `@examples/workspace-seeds/marimo/Getting_Started_PostgREST.py`:
- Line 284: Replace the broken relative docs link in
Getting_Started_PostgREST.py: find the string
"../../docs/stacks/postgrest.md#production-hardening-replace-the-superuser-anon-role"
and update it to a stable published docs URL or a GitHub permalink (e.g., the
hosted docs site or the specific commit permalink for docs/stacks/postgrest.md
with the same anchor) so the link works when the notebook is seeded into the
workspace repo.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: caaa7031-09dc-4734-b065-51500846525d
📒 Files selected for processing (3)
docs/stacks/marimo.mddocs/stacks/postgrest.mdexamples/workspace-seeds/marimo/Getting_Started_PostgREST.py
Three relative links broke in their target rendering contexts:
1. docs/stacks/postgrest.md line 25 — `../../src/nexus_deploy/...`
resolves on GitHub but 404s on the published nexus-stack.ch site
(Starlight only serves the docs/ tree).
2. docs/stacks/postgrest.md line 36 — same pattern for
`../../examples/workspace-seeds/marimo/...`.
3. examples/workspace-seeds/marimo/Getting_Started_PostgREST.py
line 284 — `../../docs/stacks/postgrest.md#anchor` from a notebook
that gets seeded into the operator's Gitea workspace repo, where
the `docs/` folder doesn't exist at all.
Switched (1) and (2) to `https://github.com/stefanko-ch/Nexus-Stack/blob/main/...`
permalinks — matches the convention already in use in
docs/stacks/README.md's stack-doc table and in akhq.md / spark.md
etc. Switched (3) to the published-docs URL
https://nexus-stack.ch/docs/stacks/postgrest/#anchor — that's the
canonical user-facing path for a notebook reader who doesn't have
the source repo handy. Added a short inline note in the notebook
explaining the absolute-URL choice so a future maintainer doesn't
re-relativize it.
No-op for visible link text. Existing other-stack docs with the same
cross-folder anti-pattern (lakekeeper.md, chroma.md) are out of
scope here — they're tracked as a separate cleanup if it ever
becomes a recurring CodeRabbit theme.
Summary
Adds PostgREST as a new stack — a Go binary that introspects any Postgres schema and exposes every table / view / RPC as a REST endpoint with built-in filtering, ordering, pagination, embedded resources, and content negotiation. Zero schema definitions on the PostgREST side — the API surface is the database surface. OpenAPI spec auto-generated at
/for Hoppscotch / Swagger UI consumption.postgrest/postgrest:v14.12(multi-arch: amd64 + arm64)3009→ container3000postgrest→https://postgrest.<domain>postgresstack onapp-network(no own DB sidecar)Why this stack
Together with Cube.dev (next PR), PostgREST forms a data API tier on top of the existing shared Postgres:
Operators create tables in the shared
postgresstack (via CloudBeaver / pgAdmin / Adminer /psql) and immediately get a REST API on top — useful for data-engineering tutorials ("how to expose a Postgres view as REST") and for ad-hoc front-end integrations (Astro / Streamlit / Observable can hit the REST API without building a backend).Architecture
PostgREST connects to the shared
postgresstack onapp-networkrather than bundling its own DB. This is a deliberate departure from the project's "each stack owns its DB" convention because PostgREST's value prop is exposing the operator's data, not new data — bundling its own DB would always be empty.The connection role is
nexus-postgres(the shared-DB superuser). Production hardening to a dedicatedweb_anonrole with restricted GRANTs is documented indocs/stacks/postgrest.mdas the operator's next step but is out of scope for the lab/education default.Secrets
POSTGREST_JWT_SECRETrandom_password.postgrest_jwt_secret(64 chars) → Infisical/postgrest/POSTGREST_JWT_SECRETPOSTGRES_PASSWORDpostgresstack (no Infisical folder of its own)The
service_env._render_postgrestrenderer has the same fail-fast guard pattern as HedgeDoc / Planka: if either secret is empty at deploy time, raisesServiceEnvErrorwith a single actionable message naming both keys.Files
Mechanical Stack-Addition-Checklist work:
stacks/postgrest/docker-compose.yml(new)services.yaml(entry added betweenpostgresandplanka)src/nexus_deploy/config.py(field + secret-map entry;test_field_countbumped 99 → 100)src/nexus_deploy/infisical.py(new/postgrestfolder)src/nexus_deploy/service_env.py(_render_postgrest+ EnvSpec registration)tofu/stack/main.tf(random_password.postgrest_jwt_secret, length 64)tofu/stack/outputs.tf(added to the secrets output)README.md(badge, table row, stack count 74 → 75)docs/stacks/postgrest.md(new full operator doc with workflow / auth model / caveats)docs/stacks/README.md(image-version table + stack-doc index)tests/unit/test_service_env.py(4 new tests + fixture extension)tests/unit/test_config.py(field-count bump)tests/unit/__snapshots__/test_infisical.ambr(regenerated for the new folder)Test plan
dump_shellsnapshot failures (unrelated, present across the session).https://postgrest.<domain>/returns the OpenAPI JSON, a table created via CloudBeaver appears as/<table>after a SIGUSR1 schema reload.POSTGREST_JWT_SECRET(mint viajwt.ioor python-jose).Summary by CodeRabbit
New Features
Documentation
Chores