Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions src/content/docs/examples/access-requests.md
Original file line number Diff line number Diff line change
@@ -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 `<Can action="decide">`.

## 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, <Can/> β”‚
β”‚ permissionMap β†’ Cerbos via @authz β”‚
β”‚ traceparent propagation via @otel-web β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ Bearer <userid>:<roles>
β–Ό
β”Œβ”€β”€β”€β”€β”€ 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.
20 changes: 14 additions & 6 deletions src/content/docs/examples/index.md
Original file line number Diff line number Diff line change
@@ -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.