/multi-tenancy

Customers get organizations. You get isolation for free.

  • Organizations, members and invites: single-use, limited, or an open join link.
  • The organization comes from the caller's credential, so every route is isolated the moment it's published.
  • Suspend or remove members. Former members keep their name on what they wrote.
Read the docs →
GET/org/members
NNorthwind ▾Invite
MemberRoleStatus
ARAna Ruizowneractive
SOSam Okaforadminactive
PNPriya Nairmemberactive
⌘deploy-botservice accountmemberactive
LCLee Chenmembersuspended

/identity

Bring any identity provider. Or start with ours.

  • Any OIDC provider: Auth0, Okta, Keycloak, Clerk, and the rest.
  • Plat5 Auth gives you email sign-in on day one, and runs anywhere you run Plat5.
  • People are created on first sign-in. Your services hold Plat5 ids, never the provider's.
Read the docs →
gateway environment
# Start with Plat5 Auth: email sign-in, runs anywhere
AUTH_ISSUER=https://auth.acme.com
AUTH_JWKS_URI=https://auth.acme.com/.well-known/jwks.json
AUTH_USER_ID_CLAIM=properties.user_id

# Later, move to Okta. Your services don't change.
AUTH_ISSUER=https://acme.okta.com/oauth2/default
AUTH_JWKS_URI=https://acme.okta.com/oauth2/default/v1/keys
AUTH_USER_ID_CLAIM=sub
  • Plat5 Auth
  • Auth0
  • Okta
  • Keycloak
  • Clerk
  • Any OIDC provider

/roles

Your roles, in one file. Checked before your code runs.

  • Name your roles and the permissions each one grants. Plat5 names none.
  • Each route says which permission it needs. The gateway checks it.
  • A refusal says which permission was missing, so your app can explain it.
Read the docs →
roles.yml · routes.yml
# roles.yml: your roles and the permissions they grant
roles:
  owner: ["*"]
  admin: [org:write, org:members:write, org:audit:read, projects:write]
  member: [projects:write]
creator_role: owner
default_role: member

# routes.yml: the permission a route needs
- path: /org/projects/{project_id}
  methods:
    GET:
    DELETE:
      required_labels: [projects:write]

/api-keys

An API for your customers, on day one.

  • Personal keys, member keys, and service accounts for customers' integrations.
  • One-hour sessions for your web app, opened in whichever organization the person picks.
  • Your brand on every key, so secret scanners can catch leaks.
Read the docs →
POST/org/service-accounts/{service_account_id}/api-keys

deploy-bot · service account · role: member

New key, shown once

acme-k-2-hT9qWvB3nLx0Ke7JpRz4Ys1Uc8Fd5Ga2Ti6Mo9QCopy

Starts with acme-k-, so secret scanners can spot it if it leaks.

  • ci-deployacme-k-2-hT9q…created Oct 2
  • nightly-syncacme-k-2-Mm3P…created Sep 18
  • old-importeracme-k-2-Zr0c…revoked

/audit-logs

The audit log enterprise customers ask for. Already written.

  • Recorded by the gateway before your code runs. Nothing to instrument.
  • A log for every organization and every person, read through the API.
  • Your service adds what changed with one response header.
Read the docs →
GET/org/audit-events
TimeMemberRequestResult
14:02Ana RuizPATCH /org/members/{member_id}200
{ "role": { "from": "member", "to": "admin" } }
13:58deploy-botPOST /org/projects201
13:41Sam OkaforPOST /org/invites201
13:20Lee ChenDELETE /org/projects/{project_id}403
13:12Priya NairPATCH /org/projects/{project_id}200

Written by Plat5 before each request reached the service.

/admin

A staff door into every customer, with a record of every step.

  • Staff sign in with your company's identity provider. No accounts to manage in Plat5.
  • They call the same service paths your customers do, on any organization.
  • Every staff request goes in a staff audit log, kept apart from customers' logs. Fork the console and deploy it.
Operator on GitHub →
GET/organizations/01JD8X3K7Q

Northwind

5 members · 1 service account · since Sep 2

staff view
  • Ana RuizownerSuspend
  • Lee Chenmember · suspendedReactivate
  • deploy-bot · ci-deployacme-k-2-hT9q…Revoke key

Signed in as [email protected]. Every request is in the staff audit log.

/observability

Follow a request from the edge into your code.

  • A request id and a W3C trace on every request, passed into your service.
  • Prometheus metrics and JSON logs from every component. OpenTelemetry when you want it.
  • plat5 start --observability runs Grafana with logs, traces, metrics and dashboards locally.
Read the docs →
trace 4bf92f3577b34da6 · request 7d6f1c2e…
gatewayGET /org/projects38.2ms
gatewaysign in (cached)0.4ms
gatewayrate limit1.1ms
auditrecord intent3.6ms
apiGET /organizations/{organization_id}/projects28.3ms
apiSELECT projects9.1ms
Plat5Your service

/rate-limits

Fair use per customer, set per route.

  • Counted per organization, member, user or IP, depending on who owns the route.
  • Named policies per service, shared across services when you choose.
  • Every gateway replica shares one budget.
Read the docs →
routes.yml
services:
  projects:
    rate_limits:
      writes: { requests: 30, window_seconds: 60 }
    organization:
      routes:
        - path: /org/projects
          methods:
            GET:
              rate_limit: { requests: 600, window_seconds: 60 }
            POST:
              rate_limit: writes

/lifecycle

Know when customers and members arrive and leave.

  • organization.created, organization.deleted, member.created and member.removed.
  • Written in the same transaction as the change, so a committed change always has its event.
  • Read the feed from where you left off, to set up or clean up your own data.
Read the docs →
identity · internal
GET /internal/lifecycle-events?starting_after=1040

{
  "lifecycle_events": [
    { "id": "1041", "type": "organization.created", "organization_id": "01JD8X…" },
    { "id": "1042", "type": "member.created", "organization_id": "01JD8X…",
      "member_id": "01JD8Y…", "principal": "user" },
    { "id": "1043", "type": "member.removed", "organization_id": "01JD8X…",
      "member_id": "01JD8Z…", "principal": "service_account" }
  ],
  "has_more": false
}

/routing

Publish routes like code.

  • Routes are YAML next to each service.
  • Every change is a revision. Restore an old one in one call.
  • The gateway picks up changes without a restart. Conflicts between services are caught when you apply.
Read the docs →
terminal
$ plat5 routes apply
$ plat5 routes list

# In production, against the route registry
$ curl -X POST http://route-registry:5002/apply \
    -H "Authorization: Bearer $ADMIN_TOKEN" \
    -H "Content-Type: application/yaml" \
    --data-binary @routes.yml

$ curl -X POST http://route-registry:5002/services/projects/revisions/12/restore \
    -H "Authorization: Bearer $ADMIN_TOKEN"

/standards

Decided once, so every service answers the same way.

  • One error format: a code to branch on, a message safe to show, and the request id.
  • A request id in every response, error, log line, trace and audit event.
  • Every list pages the same way, with limit, starting_after and has_more.
  • Rate-limit headers on limited routes, and Retry-After on a 429.
  • Health checks, metrics and JSON logs in the same place on every service.
  • Settled names for paths, fields, metrics and error codes.
Read the docs →
POST /org/projects
HTTP/1.1 422 Unprocessable Entity
X-Request-ID: 7d6f1c2e-4b0a-4e8e-9a51-3f2b8c1d9e04

{
  "error": {
    "type": "invalid_request_error",
    "code": "VALIDATION_ERROR",
    "message": "Name is required.",
    "request_id": "7d6f1c2e-4b0a-4e8e-9a51-3f2b8c1d9e04",
    "details": {
      "fields": [{ "path": "name", "message": "Name is required." }]
    }
  }
}

The same id in

Gateway log
{"request_id":"7d6f1c2e…","route":"/org/projects","status":422}
Your service
{"request_id":"7d6f1c2e…","status":422,"duration_ms":2.4}
Trace
POST /org/projects request_id=7d6f1c2e…
Audit log
GET /org/audit-events?request_id=7d6f1c2e…

/next

Coming next.

  • Per-resource permissions

    Private projects and per-project access, through an optional policy engine.

  • Custom roles per organization

    Roles a customer defines for their own organization.

  • Plat5 Cloud

    Plat5, run for you. Each project gets its own isolated stack.

Try all of it on your laptop.