/product
The platform, .
Everything a B2B product needs before its first feature, running in front of services you write in any language.
NNorthwind ▾Invite
| Member | Role | Status |
|---|---|---|
| ARAna Ruiz | owner | active |
| SOSam Okafor | admin | active |
| PNPriya Nair | member | active |
| ⌘deploy-botservice account | member | active |
| LCLee Chen | member | suspended |
# 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.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]deploy-bot · service account · role: member
New key, shown once
acme-k-2-hT9qWvB3nLx0Ke7JpRz4Ys1Uc8Fd5Ga2Ti6Mo9QCopyStarts with acme-k-, so secret scanners can spot it if it leaks.
- ci-deploy
acme-k-2-hT9q… - nightly-sync
acme-k-2-Mm3P… - old-importer
acme-k-2-Zr0c…
| Time | Member | Request | Result |
|---|---|---|---|
| 14:02 | Ana Ruiz | PATCH /org/members/{member_id} | 200 |
{ "role": { "from": "member", "to": "admin" } } | |||
| 13:58 | deploy-bot | POST /org/projects | 201 |
| 13:41 | Sam Okafor | POST /org/invites | 201 |
| 13:20 | Lee Chen | DELETE /org/projects/{project_id} | 403 |
| 13:12 | Priya Nair | PATCH /org/projects/{project_id} | 200 |
Written by Plat5 before each request reached the service.
Northwind
5 members · 1 service account · since Sep 2
- 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.
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
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: writesGET /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
}$ 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"HTTP/1.1 403 Forbidden
X-Request-ID: 01JD8X9M2P4Q
{
"error": {
"type": "invalid_request_error",
"code": "FORBIDDEN",
"message": "You don't have permission to do that.",
"request_id": "01JD8X9M2P4Q",
"details": {
"permission": "required_labels",
"resource": "route",
"resource_id": "/org/projects/{project_id}",
"required_labels": ["projects:write"]
}
}
}/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.