PermDock
Adapters

Cloud

permdock/cloud is the thin, optional client for PermDock Cloud; it implements ApprovalStore, DecisionSink and SnapshotSource over a documented HTTP API, never sits in the decision path, and is provisioned from the Vercel Marketplace.

permdock/cloud is one implementation of the interfaces the rest of the package defines. cloud({ url, key }) returns a DecisionSink with a queryable decision log sold as compliance evidence and agent governance, an ApprovalStore with an inbox UI behind it, and a SnapshotSource that distributes signed snapshots and invalidates them on CAEP events. Nothing in this entry is consulted by can, decide, filter, where or simulate: every decision still runs in-process on the policy in your bundle. Remove the entry and the application keeps working on the in-memory defaults.

PermDock Cloud itself, the hosted service, lives in the separate PermDock-Cloud repository. This page defines what the client speaks; that repository implements it. Production hosts:

HostService
app.permdock.comDashboard and inbox
api.permdock.comMachine API (PERMDOCK_CLOUD_URL)
mcp.permdock.comRead-only MCP server

Identity gateway

The Cloud is also an identity gateway: each environment is an OAuth issuer whose issuer URL is the environment URL <PERMDOCK_CLOUD_URL>/v1/environments/<env> (cloudEndpoints().issuer), with /.well-known/openid-configuration next to its /.well-known/jwks.json. It federates upstream IdPs through RFC 8693 token exchange (below); authorization code plus PKCE and the device flow for agents follow later. It maps Agent Auth capability grants onto permission references. The only OSS integration is subjectFromJwt({ discovery: cloudEndpoints().issuer }). There is no cloud().identity helper, and the Cloud still never sits on the decision path.

The gateway exists because the Cloud already signs with a per-environment JWKS, and an OIDC Discovery document next to it is the other half of an issuer, not a new product. Humans and agents that use the inbox, the evidence export, the read-only MCP server and the hosted ADS need a first-party way to get those tokens. It is deliberately not a login SDK:

  • Federation, not a session layer. An upstream IdP (Clerk, Auth0, WorkOS, Google, enterprise SSO, any Discovery issuer) authenticates; the Cloud issues the access token the application and the ADS consume. The application keeps its own sessions.
  • One capability vocabulary. Agent Auth capability grants on a device-flow token name permission key or scope values, never a second set of names.
  • The library still does not authenticate. Core never verifies a token; permdock/jwt verifies the Cloud's token exactly as it verifies any other issuer's, and there is no permdock/cloud-auth entry. A Cloud outage leaves the embedded engine deciding from the application's own subjectFrom* and tokens already issued.

Directory modes

Each environment picks one directory mode:

ModeDirectory of recordHow facts reach decideCloud outage
Cloud-nativeThe Cloud: users, groups, memberships, held declared roles and plansClaims on the Cloud-issued access token (roles, groups, entitlements, the tenant claim per JWT authorization claims), verified by subjectFromJwt in your appNew logins fail; issued tokens keep deciding until they expire
Bring your ownYour tables, Clerk, Okta, Entra ID or Better AuthThe provider's token through its subjectFrom*, and your DirectoryStore / MembershipSource, fed by the SCIM relay when you use itNothing changes; memberships stay at their last synced state

In both modes permdock/cloud implements neither MembershipSource nor RoleSource. Switching modes changes a connector and a claim mapping, not your policy.

In Cloud-native mode there is no new entry: the application verifies the Cloud's access token with permdock/jwt, like any other issuer.

import { cloudEndpoints } from "permdock/cloud";
import { subjectFromJwt } from "permdock/jwt";

const subject = await subjectFromJwt(request, {
  discovery: cloudEndpoints().issuer, // the environment URL; issuer, JWKS and algorithms from OIDC Discovery
  audience: "https://app.example.com",
  claims: { tenant: "org_id", memberships: "memberships" }, // the environment's tenant claim, `org_id` by default
});
Claim on a Cloud-native access tokenCarriesRead as
subThe Cloud user idprincipal.id
rolesDeclared global roles (no on) the principal holds in the active tenant, from assignments plus group memberships at issue timeprincipal.roles
memberships[{ tenant, roles, team? }] for declared assignable roles with on: 'tenant' or 'team', normalised to the named scopes form on read (the canonical { scope, id, within? } form is planned for the Cloud)principal.memberships
The tenant claim (org_id unless the environment configures another name)The active tenant of the exchangeThe active tenant (claims.tenant), kept only when a membership matches it
groupsDirectory group idsTeam memberships through groupRoles or a MembershipSource
entitlementsPlans heldprincipal.plans

The token endpoint is POST <env URL>/oauth/token (RFC 6749 form body, client authentication with the environment's OAuth client, client_secret_basic). It supports RFC 8693 token exchange and refresh_token:

ParameterValue
grant_typeurn:ietf:params:oauth:grant-type:token-exchange
subject_tokenAn ID token from a trusted-issuer connector; the Cloud verifies it with permdock/jwt and maps its sub to a directory principal
subject_token_typeurn:ietf:params:oauth:token-type:id_token
audienceYour application; becomes the access token's aud
tenantOptional: the active tenant; defaults to the principal's only tenant

The response is RFC 8693 { access_token, issued_token_type: 'urn:ietf:params:oauth:token-type:access_token', token_type: 'Bearer', expires_in }. The access token is an RFC 9068 JWT (typ: at+jwt) signed with the environment key: iss is the environment URL, aud the requested audience, sub the directory principal id, plus the claims above. Lifetimes are short (five minutes by default); there is no proprietary token-minting route and no endpoint that returns a subject for a request-time lookup.

The Cloud mints only names the pushed catalog declares (permdock cloud push), and the application still drops a role its policy does not declare and ignores a tenant with no matching membership. The package's scenario tests cover the contract with signed fixture tokens: a Cloud-native token decides locally, an undeclared role grants nothing, a tenant with no membership is no tenant, and a token for another audience is anonymous.

Two modes exist because two kinds of team ask for opposite things. Teams without an identity provider want one place to manage people, groups and held roles without building an admin UI; for them an issuer that knows nothing about roles is useless. Teams on Clerk, Okta, Entra ID, Better Auth or their own tables will not move their directory of record and want the Cloud only for federation, the provisioning relay and evidence. Both modes keep the same line: a directory that reaches the engine only as signed claims on a token verified locally sits on the login path, not the decision path, which is the pattern Clerk already uses with org_role, pla and fea. Letting permdock/cloud implement MembershipSource behind a cache was rejected because a remote table on the decision path is the failure mode of hosted control planes, and a cache does not change who is authoritative during an outage. Providers that want to act as a Cloud directory or issuer speak OIDC Discovery with JWKS, SCIM 2.0 and the PermDock claim table; there is no PermDock-specific provider interface and no per-vendor entry. Self-hosters get the same shape with their own issuer, subjectFromJwt, and their own DirectoryStore behind an admin screen.

  • Assignment UI. The Cloud edits users, groups, memberships and which declared roles and plans a principal holds. It reads roles, plans and permissions from the catalog permdock collect produces, never accepts a free-text role, and only hands out roles the policy declares assignable, narrowed by the tenant's assignable set and by what the acting admin holds there.
  • Freshness. A removal takes effect at the next token issue or refresh. Cloud-native access tokens default to a lifetime of minutes; for faster revocation the Cloud's CAEP transmitter sends session-revoked or credential-change for the subject (SSF adapter) and the snapshot edge invalidates signed snapshots. Your app never looks the directory up live.
  • Evidence. Every assignment or removal writes a membership event with source: 'cloud' and by set to the Cloud admin, so "who gave this person that role" is a decision-log query.
  • Hosted grants. Editing grants themselves, not only assignments, on permissions the code marks hostable (below).

Hosted grants

A Cloud admin can write grants such as "the pro plan may read audit logs" without a deploy, bounded by the code policy:

  • The Cloud publishes portable grants as a PolicyDocument (v: 1, wire formats) under the policy claim of a compact JWS with typ: permdock-policy+jwt, signed by the environment key. cloud().policies.refresh() fetches it, verifies it with the verifier you pass to cloud() (checking typ, exp, and iss and aud against the environment URL <PERMDOCK_CLOUD_URL>/v1/environments/<env>), and keeps it. The Cloud sets exp to 24 hours after issue, so an instance that stops refreshing falls back to its code policy. An unsigned, wrongly typed or unverifiable document is absent, never partially applied, and the last verified document stays; without a verifier the source never holds a document.
  • cloud().policies is a PolicySource. createPermDock reads its current() document once per instance; the application calls refresh() on its own schedule (an interval, a cron, or the catalog webhook), so no check waits on the Cloud.
  • Hosted grants apply only to permissions the policy marks hostable, target only declared roles, plans and relations, never override a code deny and never remove an approval a code grant requires. The merge rules are on policies and the interface on extension interfaces.
  • A decision matched by a hosted grant records the document fingerprint and grant id in matched.hosted, so the decision log can answer which hosted grant allowed it.
import { cloud, cloudEndpoints } from "permdock/cloud";
import { joseTokenVerifier } from "permdock/jwt";

const permdockCloud = cloud({
  verifier: joseTokenVerifier({
    jwks: cloudEndpoints().jwks,
    algorithms: ["Ed25519", "ES256"],
  }),
});
await permdockCloud.policies.refresh();
setInterval(() => void permdockCloud.policies.refresh(), 60_000);

const permdock = await createPermDock(policy, user, {
  policies: permdockCloud.policies,
});

A snapshot source never delivers hosted grants, because a snapshot is a per-subject result the server computed, not a policy; a snapshot the application builds from an instance already reflects that instance's merged grants. Fetching the policy per request would put the Cloud on the decision path.

Purpose

Four things are worth paying for around an embedded decision engine, in the order PermDock Cloud leads with them: a place where decisions accumulate as evidence, so an access review, an agent-activity review or "which agent did what for whom" is a query with a signed export rather than a log grep; a way for services that do not run TypeScript, and for API gateways, to enforce the same policy; a place where pending approvals wait for a person with a UI and notifications; and a hosted SCIM endpoint that relays directory provisioning into a store your application owns. PermDock Cloud provides all four as an Authorization Decision Service plus control plane, and permdock/cloud is the client for the first three (the fourth targets permdock/scim in your app). The Supabase model applies: every capability has an interface in the open-source package with an in-process default, the Cloud is the managed implementation, and the interfaces are documented well enough to implement yourself (approvals adapter, audit).

Design rules

  • An embedded PDP, an optional ADS. PermDock is a policy decision point that runs inside your process, and it never needs a network call to decide. Hosted authorization products put every check behind a request and take strings in and booleans out; that adds latency to every check and makes the service a failure mode for the application. The pdp provider is the one opt-in exception, for permissions the application explicitly delegates to a remote PDP. The docs call the embedded engine the PDP and the hosted endpoint the Authorization Decision Service (ADS), never the other way round.
  • No forced subscription. Everything the Cloud does, an application can do on its own with the interface and its in-process default, the way Supabase users can run Postgres themselves. That is why approvals and the decision log are interfaces rather than Cloud features, and why a gateway or MCP proxy in the decision path was rejected.
  • One package, one server-only entry. The client is a subpath of permdock, not a separate @permdock/cloud package, so consumers and the permdock-wire skill have one install path and one version line; an app that never imports it pays nothing. The hosted service, its persistence, UI and billing live in the separate PermDock-Cloud repository and follow this repository's wire formats.
  • Evidence leads. Buyers with budget pay for proof of who could do what and a way to stop it: procurement for access reviews and audit evidence, security teams for agent governance. The decision event (principal, actor, delegation, tenant, outcome) and the bound approval token are that proof, so the decision log is the lead capability and the inbox is a feature on top. What makes an approval trustworthy is not the inbox but the resume rule: re-run decide and recompute the token before trusting a stored approval.
  • Every event is evidence. Sinks receive every granted, denied and approval-required event; sampling is opt-in and never applies to the evidence log, because access reviews are about what was allowed. Ingest cost is handled by retention tiers.
  • Governance actions never decide. The Cloud can reject pending approvals and invalidate snapshots; it cannot deny a future check. A "freeze this agent" action is those two operations plus a kill-switch table the application's own RoleSource or context reads.
  • A relay, not a membership source. The hosted SCIM endpoint replays provisioning into a DirectoryStore your application owns, so self-hosters get the same feature from permdock/scim and the Cloud has one documented target instead of writing into arbitrary customer databases.
  • Approvals are resolved by people. No approval is resolvable through the Cloud's MCP server; approver identity comes only from the Cloud's own authentication, because an agent approving an agent's action is a denial the model can talk its way past.
  • No authentication product and no gateway in the decision path. The identity gateway above federates and issues tokens; it does not replace the application's login SDK, and it never decides.

API

import { cloud } from "permdock/cloud";

const permdockCloud = cloud({
  url: process.env.PERMDOCK_CLOUD_URL, // set by the Marketplace integration or by hand
  key: process.env.PERMDOCK_CLOUD_KEY, // server-only; never in NEXT_PUBLIC_* or a client bundle
  environment: "production", // defaults to VERCEL_ENV when present
});

// Plug into any adapter that accepts store / sink / policies
export const { getPermDock, getPermission, PermDockProvider, permdockHandler } =
  createPermDock(policy, {
    subject,
    store: permdockCloud.approvals, // ApprovalStore: persistent, inbox UI, delivery
    sink: permdockCloud.sink, // DecisionSink: batched, retried, queryable
    policies: permdockCloud.policies, // PolicySource: hosted grants on hostable permissions (optional)
  });
  • cloud() returns a frozen object; approvals, sink, snapshots and policies are the interfaces, issuer is the environment URL <PERMDOCK_CLOUD_URL>/v1/environments/<env> and jwks is the environment's JWK Set URL. cloudEndpoints() returns the same issuer and jwks without a key, for building a verifier first. There is no decide on it.
  • verifyWebhook(request, { jwks, audience, replay }) and parseCloudEvent verify the Cloud's signed webhook deliveries (Cloud integrations); there is no unsigned mode.
  • policies holds the last verified policy document (hosted grants). It needs verifier (a TokenVerifier over jwks); the expected iss and aud are the environment URL, so there is no audience to configure. Without a verifier current() stays null.
  • sink batches events in memory and flushes on size (flushAt) and at the end of a request when the runtime exposes waitUntil. It posts the raw SinkEvent objects; the Cloud wraps them in CloudEvents envelopes (wire formats). A failed batch is re-queued, bounded by capacity (default 10 000, the same as memorySink), and the oldest events are dropped first, so a Cloud outage never grows memory without limit. Every event is written; sampling is opt-in and never applies to the evidence log. Delivery failure never affects a decision and is reported through on('error'). The Cloud's export endpoint serves the same events back as signed batches (permdock-decisions+jwt), OCSF or CSV. The sink does not accept permdock/otel spans: traces go to your collector (the Cloud can export a copy of the log to it as OTLP) and evidence goes to the sink (audit and observability).
  • approvals speaks the same ApprovalRequest wire shape as the in-memory store (v: 1, with the optional approvers and subject.session); a Cloud inbox rejects an unknown major rather than approve without checking eligibility. The inbox UI, Slack, Teams and email delivery, and approver groups are Cloud features layered on the same records. Slack and Teams delivery is the Vercel Chat SDK recipe from the approvals adapter "Delivery" section, run by the Cloud instead of by your application; a self-hoster who copies that recipe gets identical cards and the same signature-verified responder.
  • snapshots lets clients fetch a scoped snapshot from the Cloud edge instead of from the application server, and honours session-revoked and credential-change CAEP events received by the Cloud's SSF receiver (Shared Signals). Everything the Cloud serves is signed: a compact JWS with typ: permdock-snapshot+jwt and the unchanged Snapshot object under the snapshot claim (wire formats).

Key kinds

Each environment issues three kinds of API key. None of them can sign anything or stand in for a subject:

KindVariableReaches
clientPERMDOCK_CLOUD_KEYWhat the running application and CI call: decisions, approvals, snapshot, policy and permdock cloud push (POST /catalog)
adminPERMDOCK_CLOUD_ADMIN_KEYAuthoring: hosted grants, directory principals, groups and assignments, connectors. Never deployed with the application
exportSet per export destinationGET /export only

A leaked client key cannot widen access: it can push a catalog, but the application merges hosted grants only on permissions its own code marks hostable, and authoring a grant needs the admin key or the dashboard.

Keys

The Cloud signs with a per-environment key through the TokenSigner interface (extension interfaces) and publishes the public half as a JWK Set at <PERMDOCK_CLOUD_URL>/v1/environments/<env>/.well-known/jwks.json (cloud().jwks). cloud().snapshots.get() asks for application/jwt and returns only a compact permdock-snapshot+jwt; an unsigned body is an error, never a snapshot. The client PermDockProvider that receives it verifies it with joseTokenVerifier({ jwks: cloud().jwks, typ: 'permdock-snapshot+jwt' }); a consumer in another language points its JOSE library at the same JWKS. Algorithms are Ed25519 by default, ES256 on request for consumers whose libraries lack Ed25519; never RS256 in a FAPI 2.0 environment. Rotation follows the JWKS rule: the new kid is published, the old key stays until the last snapshot signed with it has passed exp, and the dashboard shows both. Signed decision-log exports (typ: permdock-decisions+jwt) use the same key and endpoint (audit and observability). The private key never leaves the Cloud; the PERMDOCK_CLOUD_KEY API key is unrelated to it and cannot sign anything.

  • Zero required dependencies: the client uses fetch and the wire formats from core. Each request is aborted after 10 seconds, combined with any signal you pass, and then fails like an unreachable Cloud. It is server-only and tests/bundle asserts no client entry reaches it.

HTTP API

The client speaks these paths under PERMDOCK_CLOUD_URL. In production that URL is https://api.permdock.com; a self-hosted deployment of the Cloud repository works by pointing PERMDOCK_CLOUD_URL at it, with no Marketplace involvement and no discovery step. :env is environment, then PERMDOCK_CLOUD_ENV, then VERCEL_ENV, then production.

MethodPathInterface
POST/v1/environments/:env/decisionssink.write / sink.flush body { events }
POST/v1/environments/:env/approvalsapprovals.create
GET/v1/environments/:env/approvals/:tokenapprovals.get (unreachable or unknown → null)
POST/v1/environments/:env/approvals/:token/resolveapprovals.resolve (409 not pending, 410 expired, 404 missing, 403 approver refused). A body code that names an ApprovalError code, with its detail, becomes the thrown error
POST/v1/environments/:env/approvals/:token/consumeapprovals.consume: the approved record with consumedAt set, or 409 when it is not approved, already consumed or expired (any failure → null)
GET/v1/environments/:env/approvalsapprovals.list: ?status=, principalId, actorId, tenant, session, limit, cursor; returns { items, next? } (unreachable → { items: [] })
POST/v1/environments/:env/approvals/expireapprovals.expire
POST/v1/environments/:env/approvals/cancelapprovals.cancel (what cancelApprovals calls on session revocation): body { filter, by, note? } with an ApprovalListFilter; rejects the matching pending requests with resolvedBy: 'system:<by>' like the in-memory store and returns { cancelled }; a non-2xx status throws
GET/v1/environments/:env/snapshotsnapshots.get with accept: application/jwt: a compact permdock-snapshot+jwt as the body; an unsigned JSON body throws
POST/v1/environments/:env/catalogpermdock cloud push: body { fingerprint, catalog, policy? } (cloud); 200 { version, unchanged: true } when the fingerprint is already published; not called by cloud()
GET/v1/environments/:env/policypolicies.refresh: a permdock-policy+jwt compact JWS as the body; 404 means no hosted grants and clears the document, any other failure keeps the last verified one

Authoring and export routes, used by the dashboard, scripts and the contract tests, never by cloud(). Validation failures are Problem Details (wire formats):

MethodPathKeyPurpose
GET/v1/environments/:env/catalog, /catalog/versionsclientThe latest catalog and its versions
GET, POST/v1/environments/:env/hosted-grantsadminList ({ items }) or author a hosted grant: body a HostedGrant without id, 201 with the stored grant; a grant mergeHostedGrants would drop is 422 with the drop reason as reason
DELETE/v1/environments/:env/hosted-grants/:idadminRemove a hosted grant; 204. Every change issues a new policy document
GET, POST/v1/environments/:env/directory/principals, /directory/groupsadminCloud-native directory: list ({ items }) or create (201 { id }); a principal is created with the trusted-issuer subject it federates from
GET, POST, DELETE/v1/environments/:env/directory/assignments[/:id]adminRole assignments { principal | group, tenant, team?, role }: only catalog roles marked assignable, narrowed to roles the acting admin holds there; each change writes a membership event with source: 'cloud'
GET, POST/v1/environments/:env/connectorsadminIntegrations: { kind, name, config }, for example { kind: 'webhook', name, config: { url, types } } (Cloud integrations)
GET/v1/environments/:env/export?format=jws|ocsf|csv|otlpexportThe decision log as a permdock-decisions+jwt, OCSF (toOcsf), CSV (toCsvRow) or OTLP logs
POST/v1/environments/:env/oauth/tokenOAuth clientRFC 8693 token exchange and refresh_token (identity gateway)
GET/v1/environments/:env/.well-known/jwks.json, /.well-known/openid-configurationnoneThe environment's JWK Set and issuer metadata; public
any/scim/v2/:connection/*The connection's bearerThe hosted SCIM endpoint an IdP provisions into; replayed to your scimHandler

Environment variables

VariableSet byUsed for
PERMDOCK_CLOUD_URLMarketplace provisioning or youBase URL of the environment's API. Production host is https://api.permdock.com
PERMDOCK_CLOUD_KEYMarketplace provisioning or youServer-side client key for the store, sink, snapshot, policy and catalog endpoints; rotated from the dashboard
PERMDOCK_CLOUD_ADMIN_KEYYou, from the dashboardadmin key for authoring scripts and the contract tests; never set in the application's runtime environment
PERMDOCK_CLOUD_ENVOptionalOverrides the environment name when VERCEL_ENV is absent

permdock doctor reports whether the variables are present, whether the key reaches the API, and whether a client entry imports permdock/cloud.

What stays local and what the Cloud adds

ConcernIn-process default (always available)With permdock/cloud
Decidingdecide on the bundled policyUnchanged
Pending approvalsmemoryApprovalStore()Persistent store, inbox UI scoped per tenant, delivery, approver groups, expiry jobs
Memberships and custom rolessubjectFrom*, context, your MembershipSource and RoleSource; permdock/scim writing a DirectoryStore you ownA hosted SCIM relay per tenant (IdP setup wizard, group-to-role mapping UI, sync log) that replays provisioning to your scimHandler with an RFC 7523 bearer. In Cloud-native directory mode, the Cloud directory and assignment UI instead, delivered as claims on the tokens it issues. Either way the Cloud implements neither interface and is never read at request time, so it never joins the decision path (invariant 15); in bring-your-own mode it holds no authoritative copy
Evidence and governanceon('decision') handler, a DecisionSink over your table, permdock/otel for tracesDecision log with retention tiers; access-review, agent-activity, approval-chain and tenant queries over the principal, actor, tenant, membership and via fields of every event; signed (permdock-decisions+jwt), OCSF and CSV exports for auditors, SIEMs and compliance platforms; governance actions limited to rejecting pending approvals and invalidating snapshots (audit and observability "Evidence and governance")
SnapshotsServed by the application's endpoint, plain JSON or signed with your own TokenSignerEdge distribution, always signed (permdock-snapshot+jwt, keys at the environment's /.well-known/jwks.json), invalidated by CAEP
Non-TypeScript callersYour own permdock/authzen deploymentHosted AuthZEN ADS serving the same policy to gateways and services
Catalog and usagepermdock collect and permdock usage in CIDashboards over the same catalog files, drift alerts

Hosted Authorization Decision Service

The Cloud runs the same permdock/authzen handler your app could run, against the policy you publish with permdock cloud push. Kong, Envoy, Tyk, Zuplo or a Go service calls POST /access/v1/evaluation and gets an AuthZEN answer whose context carries outcome, denial reasons and the approval token (AuthZEN). Decision semantics are identical to the embedded engine because it is the same code; an evaluations batch of a policy matrix from permdock/testing is the parity test.

Caller authentication is real authentication (threat model, invariant 9):

  • On Vercel, the caller presents its OIDC token; the ADS verifies it with permdock/jwt against Vercel's JWKS and maps sub, project_id and environment to a workload principal.
  • Elsewhere, the caller uses OAuth client credentials issued per environment in the dashboard; the token's aud is the environment URL.
  • A request with no verifiable token is answered 401; a token for another environment is 403. There is no shared static secret mode.

The ADS never reads a subject from the request body unless the caller is registered as a trusted PEP for that environment, matching the in-app endpoint rule.

Vercel Marketplace

PermDock Cloud is listed as a native Marketplace integration so a Vercel project gets an environment, its variables and billing without leaving the dashboard.

  • Resource model. One Marketplace resource is one PermDock environment, created per Vercel project and environment (production, preview, development) so preview approvals never land in the production inbox. PERMDOCK_CLOUD_URL and PERMDOCK_CLOUD_KEY are written to the project's environment variables on provisioning and rotated from either dashboard.
  • Integration server. The Cloud repository implements the Marketplace provisioning API (install, resource create, update, delete, SSO, billing webhooks and invoices) per the approval checklist; this repository only documents the variables and the doctor check.
  • Template. apps/examples/eve-agent is the deploy template the listing requires: an Eve agent with post tools, permdock/eve approvals stored in the Cloud, and the inbox linked from the deployed app (Eve adapter).
  • Plans. A free environment with a short retention window, then usage-based plans on five meters: monthly active principals (a human and each agent actor counted once), connected tenants (a tenant with a SCIM connection or a tenant-scoped inbox), resolved approvals above a free allowance at a small per-unit price, the decision retention tier, and hosted ADS evaluations. Decisions made by the embedded engine are never metered. The exact numbers belong to the Cloud repository and the landscape research, not to the library docs.

Integrations

The Cloud has an Integrations page; what each connector speaks is fixed here and catalogued on Cloud integrations. Every connector is a standard wire format or an existing PermDock interface (trusted issuers over JWKS or OIDC Discovery, CAEP transmitters, SCIM sources, Vercel provisioning and OIDC, OTLP and OCSF export, CloudEvents webhooks, signed and CSV evidence exports, Chat SDK delivery, a read-only MCP server), and none sits on the decision path. No connector adds an npm entry.

Request lifecycle

  1. The application decides locally. Nothing on this path touches the network.
  2. on('decision') events go to permdockCloud.sink, which batches and posts them to POST /v1/environments/:env/decisions with the key.
  3. An approval-required outcome causes the adapter to call permdockCloud.approvals.create, which posts the ApprovalRequest to POST /v1/environments/:env/approvals. The Cloud notifies the configured approvers.
  4. An approver resolves the request in the inbox UI, through a Slack or Teams card (Chat SDK requestApproval; the platform signature identifies the responder) or through an email link that lands on the authenticated inbox. The Cloud authenticates the approver, applies the actor and distinct-approver rules, records resolvedBy.
  5. The application's retried call reads the record via permdockCloud.approvals.get, re-runs decide locally, compares the token and proceeds. The resumed decision goes to the sink like any other.
  6. Independently of the above, an identity provider connected to the tenant's hosted SCIM endpoint pushes a provisioning operation. The Cloud records it in the sync log, applies the tenant's group-to-role mapping as the urn:permdock:scim:schemas:extension:roles:1.0 attribute, and replays the operation to the application's scimHandler with an RFC 7523 JWT bearer signed by the environment key (iss = the environment URL, aud = your endpoint, tenant = the tenant, a 60-second exp). Your DirectoryStore is updated; the next createPermDock for an affected principal reads the change through directoryMembershipSource (SCIM adapter). If your endpoint is unreachable the Cloud retries with backoff and shows the failure in the sync log; nothing is decided from the Cloud's copy.

What it validates

  • The key is present and server-side; the entry throws at construction (not at decision time) when url or key is missing, so misconfiguration is visible at boot.
  • Responses from the API are validated against the wire-format versions this package knows; an unknown v is rejected and reported, and the adapter falls back to the in-memory store for that request so decisions never depend on the API being reachable.
  • Approver identity on the Cloud side comes from the Cloud's own authentication; the client never asserts who approved.

How denials surface

  • The Cloud is not on the decision path, so it produces no denials of its own. A resume against a request the Cloud reports as rejected or expired surfaces through the calling adapter exactly as with any store (approvals adapter).
  • API unreachable during a resume: denied with detail approval-not-found and an on('error') event; the application may retry.
  • The hosted ADS answers denied evaluations with decision: false and context.permdock.outcome plus denial reasons, and transport errors as Problem Details (AuthZEN adapter).

Example app

apps/examples/eve-agent (also the Marketplace template) and a cloud variant of apps/examples/authzen-pdp in which the PEP process points at a hosted environment instead of the local Hono PDP, asserting identical decisions for the policy matrix.

Last updated on

On this page