Skip to content

plinth-dev/example-access-requests

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Plinth example — Access requests

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. Intended as proof that the bank-grade-foundation pitch works for a real, not-toy use case — clone, run, click around, read the audit log.

Every Plinth SDK is exercised:

What you see 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

Layout

example-access-requests/
├── access-requests-api/    # Go + chi + pgx — the API tier
└── access-requests-web/    # Next.js 16 + React 19 — the web tier

Both tiers were generated with plinth new access-requests and adapted to the AccessRequest domain. The same flow (plinth new <name>) is what you use to scaffold your own modules — see github.com/plinth-dev/cli.

Domain

                ┌─────────────────────┐
   POST /       │                     │
  ──────────▶   │      pending        │
                │                     │
                └────────┬────────────┘
                         │
       ┌─────────────────┴─────────────────┐
       │                                   │
 POST /:id/approve                  POST /:id/deny
       │                                   │
       ▼                                   ▼
 ┌────────────┐                       ┌────────┐
 │  approved  │                       │ denied │
 │ + expires  │                       │+ reason│
 └────────────┘                       └────────┘

Approved and denied are terminal states — 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

One-shot

git clone https://github.com/plinth-dev/example-access-requests
cd example-access-requests

# API tier
cd access-requests-api
docker compose up -d        # Postgres + Cerbos
make migrate-up             # apply schema
make run                    # serve on :8080
cd ..

# Web tier (separate terminal)
cd access-requests-web
pnpm install
pnpm dev                    # serve on :3000

Then open http://localhost:3000 and use one of the dev sign-in shortcuts.

Try the full flow

  1. Sign in as alice (requester) — file a request: purpose Investigate incident #1234, scope AWS prod read-only, justification Customer reported timeouts; need to read CloudWatch.
  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 logdocker compose logs api | grep audit. Three CloudEvents: access_request.created, access_request.approved. The actor, resource, before/after, and trace ID are all populated.

Architecture

The API and web tiers are independent runtimes that share only the JSON contract. Each is dropped onto the 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   │
    └──────────────┘                 └────────────────────┘

The Auth shim is intentionally trivial — Bearer <userid>:<roles>, dev only. Replace with whatever your identity provider hands you (OIDC, JWT, Auth0, Clerk, Stack, etc.) before running anything that matters.

Replacing this with your own resource

If you want to fork this as a starting point for a different internal tool:

  1. db/migrations/ — replace 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: 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.

Related

  • plinth.run — full architecture reference, ADRs, tutorials.
  • Manifesto — the six commitments.
  • sdk-go / sdk-ts — the SDKs this example exercises.
  • starter-web / starter-api — the templates this example was derived from.
  • cliplinth new for scaffolding new modules.
  • platform — Helm chart that brings up the substrate this example targets.

License

MIT — see the per-tier LICENSE files.

About

Worked example: engineer requests temporary production access, an approver decides, every state change is audited. Demonstrates every Plinth SDK at once.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors