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.

Status: planned Phase: 2

permdock/cloud is one implementation of the interfaces the rest of the package defines. cloud({ url, key }) returns an ApprovalStore with an inbox UI behind it, a DecisionSink with a queryable decision log, 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 (ADR 0021). 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 (URL to follow once the repository is public). This page defines what the client speaks; that repository implements it.

Purpose

Three things are worth paying for around an embedded decision engine: a place where pending approvals wait for a person with a UI and notifications; a place where decisions accumulate so "which agent did what for whom" is a query rather than a log grep; and a way for services that do not run TypeScript, and for API gateways, to enforce the same policy. PermDock Cloud provides all three as an Authorization Decision Service plus control plane, and permdock/cloud is the client. 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).

API

import { cloud } from 'permdock/cloud'

const pd = 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 / snapshots
export const { getPermDock, getPermission, PermDockProvider, permdockHandler } = createPermDock(policy, {
  subject,
  store: pd.approvals,       // ApprovalStore: persistent, inbox UI, delivery
  sink: pd.sink,             // DecisionSink: batched, retried, queryable
  snapshots: pd.snapshots,   // SnapshotSource: distributed, CAEP-invalidated (optional)
})
  • cloud() returns a frozen object; approvals, sink and snapshots are the three interfaces. There is no decide on it.
  • sink batches events in memory and flushes on an interval, on size, and at the end of a request when the runtime exposes waitUntil. Delivery failure never affects a decision and is reported through on('error').
  • approvals speaks the same ApprovalRequest wire shape as the in-memory store; 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).
  • Zero required dependencies: the client uses fetch and the wire formats from core. It is server-only and tests/bundle asserts no client entry reaches it.

Environment variables

VariableSet byUsed for
PERMDOCK_CLOUD_URLMarketplace provisioning or youBase URL of the environment's API
PERMDOCK_CLOUD_KEYMarketplace provisioning or youServer-side key for the store, sink and snapshot endpoints; rotated from the dashboard
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 RoleSourceNothing. The Cloud never implements either interface, so it never joins the decision path (invariant 15); a hosted SCIM receiver is considered, not scheduled (commercial landscape)
Auditon('decision') handler, permdock/otelDecision log with retention, per-actor, per-principal and per-tenant queries (the tenant, membership and via fields of every event), anomaly views
SnapshotsServed by the application's endpointEdge distribution, signed, 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 (a CLI command in Phase 2). Kong, Envoy, Tyk, Zuplo or a Go service calls POST /access/v1/evaluation and gets an AuthZEN answer with the PermDock Decision under context.permdock (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 an in-memory-equivalent retention window, then usage-based plans on stored approvals, retained decisions and ADS evaluations; the exact numbers belong to the Cloud repository and the commercial landscape research, not to the library docs.

Request lifecycle

  1. The application decides locally. Nothing on this path touches the network.
  2. on('decision') events go to pd.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 pd.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 pd.approvals.get, re-runs decide locally, compares the token and proceeds. The resumed decision goes to the sink like any other.

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 the PermDock Decision under context.permdock, 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.

Open questions

  • Whether snapshots belongs in Phase 2 with the store and sink, or later, since it is the only capability that changes a client-visible path.
  • How a self-hosted PermDock Cloud (the repository is open) advertises itself so that cloud({ url }) works against it with no Marketplace involvement; the target is "point PERMDOCK_CLOUD_URL at your deployment".
  • Whether the sink should also accept permdock/otel spans, or whether OTel export to the Cloud is a separate collector configuration.
  • Whether the hosted snapshots source should serve per-tenant snapshots (refresh({ tenant })) from the edge or only the active-tenant snapshot the application issued; either way it distributes what the application computed and never resolves memberships itself.
  • The permdock cloud push command shape and whether policy publication should be a build step (createPermDockPlugin) rather than a CLI command.

On this page