From 53252e5621f2bb87572673735f98289d4f0a4019 Mon Sep 17 00:00:00 2001 From: hushamsaeed Date: Fri, 1 May 2026 20:24:51 +0500 Subject: [PATCH] docs: add /examples/access-requests walkthrough MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the placeholder examples list (counter / todo / approvals — none of which exist as repos) with a single shipped example: access-requests. The new /examples/access-requests page walks through the full flow with the architecture diagram, role-by-role steps, and what's deliberately out of scope (real auth, time-bound enforcement, notifications). Sidebar's autogenerated examples directory picks up both the rewritten index and the new page. --- src/content/docs/examples/access-requests.md | 144 +++++++++++++++++++ src/content/docs/examples/index.md | 20 ++- 2 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 src/content/docs/examples/access-requests.md diff --git a/src/content/docs/examples/access-requests.md b/src/content/docs/examples/access-requests.md new file mode 100644 index 0000000..8be9947 --- /dev/null +++ b/src/content/docs/examples/access-requests.md @@ -0,0 +1,144 @@ +--- +title: Access requests +description: Engineer requests temporary production access; an approver decides; every state change is audited. A worked example that exercises every Plinth SDK at once. +sidebar: + label: Access requests + order: 2 +--- + +A working internal-tool that demonstrates the full Plinth stack on the canonical "engineer requests temporary production access; an approver decides; audit log captures everything" flow. + +**Source:** [github.com/plinth-dev/example-access-requests](https://github.com/plinth-dev/example-access-requests). + +## What you'll see + +| Surface | Plinth SDK | +| --- | --- | +| Server-rendered list table, status filter, URL-driven pagination | `@plinth-dev/tables` + `sdk-go/paginate` | +| New-request form with Zod validation + RFC 7807 error mapping | `@plinth-dev/forms` + `sdk-go/errors` | +| Approve/Deny buttons gated on Cerbos permissions | `@plinth-dev/authz-react` + `@plinth-dev/authz` + `sdk-go/authz` | +| Every state transition produces an audit event (CloudEvents 1.0) | `sdk-go/audit` | +| Distributed tracing across web → API | `@plinth-dev/otel-web` + `sdk-go/otel` | +| Non-throwing API client with discriminated-union responses | `@plinth-dev/api-client` | +| Fail-fast env validation at module load | `@plinth-dev/env` + `sdk-go/vault` | + +## Domain + +``` + ┌─────────────────────┐ + POST / │ │ + ──────────▶ │ pending │ + │ │ + └────────┬────────────┘ + │ + ┌─────────────────┴─────────────────┐ + │ │ + POST /:id/approve POST /:id/deny + │ │ + ▼ ▼ + ┌────────────┐ ┌────────┐ + │ approved │ │ denied │ + │ + expires │ │+ reason│ + └────────────┘ └────────┘ +``` + +Approved and denied are terminal — once decided, no further mutation. The repo guards transitions atomically (`UPDATE … WHERE status = 'pending' RETURNING …`) so concurrent decisions don't both succeed. + +## Roles + +| Role | Permissions | +| --- | --- | +| `requester` | Create requests; read + list **own** requests | +| `approver` | Read + list **all** requests; approve/deny pending requests | +| `admin` | Same as approver (reserved for read-only auditors with broader future scope) | + +Cerbos enforces all of the above — the service layer calls `authz.CheckAction` before every operation; the web tier renders Approve/Deny buttons via ``. + +## Run it locally + +```bash +git clone https://github.com/plinth-dev/example-access-requests +cd example-access-requests + +# API tier (terminal one) +cd access-requests-api +docker compose up -d # Postgres + Cerbos +make migrate-up # apply schema +make run # serve on :8080 + +# Web tier (terminal two) +cd ../access-requests-web +pnpm install +pnpm dev # serve on :3000 +``` + +Then open `http://localhost:3000` and use the dev sign-in shortcuts: + +1. **Sign in as alice (requester)** — file a request: purpose `Investigate incident #1234`, scope `AWS prod read-only`, justification any sentence. +2. **Sign in as bob (approver)** — see all pending requests; open alice's; approve with `expiresAt = now + 24h`. +3. **Sign in as alice again** — your request is now `approved` with bob's signature. +4. **Inspect the audit log** — `docker compose logs api | grep audit` shows three CloudEvents (`access_request.created`, `access_request.approved`) with actor, resource, before/after, and trace ID populated. + +## Architecture + +The API and web tiers are independent runtimes that share only the JSON contract. Each is dropped onto the [`platform`](https://github.com/plinth-dev/platform) substrate (CloudNativePG + Cerbos + OpenTelemetry Collector) without modification — no adapter glue, just env vars. + +``` + ┌───── Web (Next.js + React 19) ─────────┐ + Browser ────▶ │ ServerTable, FormWrapper, │ + │ permissionMap → Cerbos via @authz │ + │ traceparent propagation via @otel-web │ + └────────────┬───────────────────────────┘ + │ Bearer : + ▼ + ┌───── API (Go + chi) ───────────────────┐ + │ chi → otel HTTP middleware │ + │ → auth (cookie shim or JWT) │ + │ → errors (RFC 7807) │ + │ → handlers → service │ + │ service: authz.CheckAction → Cerbos │ + │ repo (pgx) │ + │ audit.Publish (non-blocking) │ + └────────────┬────┬──────────────────────┘ + │ │ + ┌───────────────┘ └────────────┐ + ▼ ▼ + ┌──────────────┐ ┌────────────────────┐ + │ CloudNativePG│ │ Cerbos PDP │ + │ (Postgres) │ │ /policies mount │ + └──────────────┘ └────────────────────┘ +``` + +## Origin + +Both tiers were generated with the [`plinth` CLI](https://github.com/plinth-dev/cli): + +```bash +plinth new access-requests \ + --module-path github.com/plinth-dev/example-access-requests/access-requests-api +``` + +…then adapted from the starter's `Items` resource to `AccessRequest`. The same flow scaffolds your own modules — see the [60-minute walkthrough](/start/try-it/). + +## Replacing this with your own resource + +If you want to fork it as a starting point for a different internal tool: + +1. `db/migrations/` — replace the `access_requests` table with your schema. +2. `internal/repository/access_requests.go` — rename the type + the SQL. +3. `internal/service/access_requests.go` — change the methods, the audit action names, the Cerbos resource kind. +4. `cerbos/policies/access_request.yaml` — same kind rename; redefine actions and rules. +5. `internal/handlers/access_requests.go` — adjust routes + DTOs. +6. `cmd/server/main.go` — rename the variables wiring the repo/svc/handlers. + +Web tier follows the same pattern under `access-requests-web/src/app/access-requests/`. + +Or, easier — `plinth new your-thing` to start from a fresh starter and copy ideas selectively. + +## What it doesn't yet include + +The example deliberately stops at the boundary of the substrate. Things you'd add for a real deployment: + +- **Real auth.** The example ships a dev-only cookie shim (`plinth_dev_user=alice:requester`). Wire your IdP (OIDC, JWT, Clerk, Auth0, Stack) before anything that matters. +- **Time-bound access enforcement.** Approved requests carry an `expires_at`, but the example doesn't ship a worker that revokes the underlying access. Pair with whatever your environment uses (AWS IAM session policies, Teleport, Kubernetes RoleBindings with `expirationDate`). +- **Notifications.** No Slack / email when a request is filed or decided. Plinth's audit stream lands on NATS by default; subscribe and notify from there. diff --git a/src/content/docs/examples/index.md b/src/content/docs/examples/index.md index 0de08b5..2cc5640 100644 --- a/src/content/docs/examples/index.md +++ b/src/content/docs/examples/index.md @@ -1,17 +1,25 @@ --- title: Examples -description: Tiny modules that exercise different surface areas of the Plinth SDK and substrate. +description: Worked examples — full, running modules built on Plinth that you can clone, run, and read end-to-end. sidebar: label: Overview order: 1 --- -Three minimum-viable modules ship with the docs site to demonstrate the platform in use. Each is a real, running module; each is small enough to read end-to-end in 30 minutes. +Worked examples are full, running modules — not snippets. Each one is small enough to read end-to-end in an hour and exercises real Plinth surface area: SDK packages, the substrate, the CLI scaffolding flow. + +## Available | Example | What it shows | | --- | --- | -| `counter` | The smallest possible module — local state, no platform deps. Useful for "is the substrate up?" | -| `todo` | CRUD with auth + audit. The "hello world" of internal tooling. | -| `approvals` | Adds [Temporal](https://temporal.io) back into the stack. Demonstrates the optional-substrate-component pattern. | +| [Access requests](/examples/access-requests/) | Engineer requests temporary production access; an approver decides; every state change is audited. Exercises every Plinth SDK at once. | + +## Roadmap + +| Example | What it will show | +| --- | --- | +| Feature flags | Toggleable flags with audit log, role-gated mutation, and a public read-API. | +| Invoice approval | Multi-step approval workflow demonstrating the optional Temporal sub-chart. | +| On-call directory | Read-mostly catalog with team-scoped writes. | -The canonical end-to-end example currently lives in the [`starter-web`](https://github.com/plinth-dev/starter-web) and [`starter-api`](https://github.com/plinth-dev/starter-api) repos as the **Items** resource. The smaller examples above ship later as standalone modules. +These ship as separate `plinth-dev/example-*` repos, each scaffolded with `plinth new` and adapted from there. If you want one of these prioritised, file an issue at [github.com/plinth-dev/example-access-requests](https://github.com/plinth-dev/example-access-requests/issues) — happy to take requests.