# Cloud integrations

Source: https://permdock.com/docs/adapters/cloud-integrations

The connectors PermDock Cloud offers on its Integrations page, what each one speaks and what it never does; every connector is a standard wire format or an existing PermDock interface, none is an npm entry, and none sits on the decision path.

Status: planned
PermDock Cloud has an Integrations page where an environment is connected to the platforms around it: identity providers, Vercel, observability backends, SIEMs, webhooks, compliance platforms, your own database, chat surfaces and an MCP host. This page is the contract for that UI. It fixes what each connector speaks; the `PermDock-Cloud` repository implements it. Two rules from the rest of the docs apply to every row ([adapters](/docs/adapters), [PermDock Cloud](/docs/adapters/cloud), invariant 15):

* A connector is a standard wire format (AuthZEN, OCSF, CloudEvents, OTLP, SCIM, CAEP, JOSE, OIDC) or an existing PermDock interface (`ApprovalStore`, `DecisionSink`, `SnapshotSource`, `DirectoryStore`). There is no `permdock/auth0`, `permdock/datadog` or `permdock/vanta`, and connecting a platform never adds a dependency to your application.
* No connector sits on the decision path. Inbound connectors change who the Cloud authenticates, what your `DirectoryStore` contains, or (in Cloud-native directory mode) which claims the Cloud mints into the tokens your app verifies; outbound connectors copy evidence and notifications out. Nothing any connector does is consulted by `can`, `decide`, `filter`, `where` or `simulate`.

<Mermaid
  chart="flowchart LR
  subgraph inbound [Inbound: identity and platform]
    Idp[&#x22;Auth0, Okta, Entra ID, Clerk, Supabase Auth, WorkOS, Better Auth, Google Workspace&#x22;]
    NativeDir[&#x22;Cloud-native directory and assignment UI&#x22;]
    Vercel[&#x22;Vercel: Marketplace, OIDC, env vars&#x22;]
  end
  Cloud[&#x22;PermDock Cloud: evidence log, inbox, hosted ADS, SCIM relay, snapshots&#x22;]
  subgraph outbound [Outbound: evidence, delivery, agents]
    Otel[&#x22;OTLP: Datadog, Grafana Cloud, Honeycomb, Axiom, New Relic&#x22;]
    Siem[&#x22;OCSF: Splunk, Sentinel, Security Lake, Google SecOps, Elastic&#x22;]
    Hooks[&#x22;Webhooks: CloudEvents, signed JWS batches&#x22;]
    Compliance[&#x22;Vanta, Drata, Secureframe&#x22;]
    Chat[&#x22;Slack, Teams, Discord, email, PagerDuty&#x22;]
    Mcp[&#x22;MCP server: read-only evidence and catalog&#x22;]
    Db[&#x22;Supabase Postgres, Neon, your Postgres: evidence export&#x22;]
  end
  Idp -->|&#x22;trusted issuer (JWKS), federated login, CAEP transmitter, SCIM source&#x22;| Cloud
  NativeDir -->|&#x22;claims minted into issued tokens&#x22;| Cloud
  Vercel -->|&#x22;provisioning, caller identity&#x22;| Cloud
  Cloud --> Otel
  Cloud --> Siem
  Cloud --> Hooks
  Cloud --> Compliance
  Cloud --> Chat
  Cloud --> Mcp
  Cloud --> Db"
/>

Each table below has the same columns. **Connector** is what the Integrations page shows. **Standard or interface** is the only thing the connector speaks. **What it carries** is the data that crosses. **Never** is the thing the connector is not allowed to do, and each of those is a row in the [threat model](/docs/security/threat-model).

## Inbound: identity providers [#inbound-identity-providers]

One identity provider can be connected in up to four roles. They are independent: a tenant may use Okta as a SCIM source while its end users authenticate with Clerk.

The environment's directory mode decides which rows apply. In Cloud-native mode the Cloud directory is the source of the claims it mints, and upstream providers connect only as federated login. In bring-your-own mode your provider or your tables stay authoritative: end users either log in through the Cloud issuer, which federates to the provider, or bypass the Cloud entirely and your app uses the provider's `subjectFrom*`; the SCIM relay feeds your `DirectoryStore`.

| Connector | Standard or interface | What it carries | Never |
| --- | --- | --- | --- |
| Trusted issuer (Auth0, Okta, Entra ID, Clerk, Supabase Auth, WorkOS AuthKit, Better Auth, Google Identity, any JWKS-publishing issuer) | OIDC Discovery or RFC 8414 metadata, JWKS, RFC 9068 access tokens; verified by `permdock/jwt` with the same `discovery`, `accept`, `algorithms` and `profile` rules the application uses | Who the hosted ADS and the snapshot edge accept as an end user: `iss`, `sub`, `aud`, `roles`, `groups`, `org_id`, `act`, `cnf`, mapped to a `Subject` exactly as [`subjectFromJwt`](/docs/adapters/jwt) does in your app | Widen what the embedded engine decides; accept a token whose tenant claim has no onboarded connection (no tenant, never a default); read memberships from the provider's management API |
| CAEP transmitter (Okta, Entra ID, Google Workspace, Auth0, any SSF transmitter) | Shared Signals Framework: RFC 8935 push or RFC 8936 poll into the Cloud's SSF receiver; OIDC Back-Channel Logout as the alternative | `session-revoked`, `credential-change`, `assurance-level-change` events keyed on the subject; `logout_token` for Back-Channel Logout | Change a decision; the event invalidates signed snapshots served by the Cloud edge and nothing else ([Shared Signals](/docs/standards/shared-signals-caep), [SSF adapter](/docs/adapters/ssf)) |
| SCIM source (Okta, Entra ID, Google Workspace, WorkOS Directory Sync, any SCIM 2.0 client) | SCIM 2.0 (RFC 7643, RFC 7644, RFC 9865) into the hosted relay; RFC 7523 JWT bearer from the relay to your `scimHandler` | `User` and `Group` resources, the tenant's group-to-role mapping as the `urn:permdock:scim:schemas:extension:roles:1.0` attribute, one sync-log entry per operation | Hold an authoritative copy of what it relays (your `DirectoryStore` stays the record in bring-your-own mode); implement `MembershipSource` or `RoleSource`; grant a role the policy did not declare `assignable` ([SCIM adapter](/docs/adapters/scim)) |
| Federated upstream (Clerk, Auth0, WorkOS, Google, Enterprise SSO, any OIDC Discovery issuer) | OIDC authorization code plus PKCE from the Cloud issuer to the upstream | The upstream's authentication of the user; the Cloud then issues the access token your app verifies with `subjectFromJwt({ discovery: PERMDOCK_CLOUD_URL })` | Become the application's session layer; copy upstream roles into claims without the directory mode's mapping; accept an upstream token without its `iss` and `aud` checks |
| Cloud-native directory (the Cloud's own users, groups and assignment UI) | PermDock JWT claims ([JWT authorization claims](/docs/standards/jwt-authorization-claims)) on Cloud-issued tokens; `dev.permdock.membership` events for every change | `roles`, `groups`, `entitlements` and the tenant claim for each principal, taken from declared `assignable` roles and plans in the uploaded catalog | Be read at request time; implement `MembershipSource` or `RoleSource`; mint a role or plan the catalog does not declare; hand out a role the acting admin does not hold in that tenant |

"Connect Supabase" on the identity side means Supabase Auth as a trusted issuer (its project JWKS, `role` and `app_metadata` handled as on the [Supabase adapter](/docs/adapters/supabase) page); the Cloud never reads your Supabase tables or RLS policies. "Connect Clerk", "Connect Auth0" and "Connect WorkOS" mean the same three roles with the provider's discovery URL, SSF transmitter and SCIM client.

## Inbound: Vercel [#inbound-vercel]

The Vercel connector is already specified on the [Cloud adapter](/docs/adapters/cloud) page ("Vercel Marketplace" and "Hosted Authorization Decision Service"); the Integrations page links those sections rather than duplicating them.

| Connector | Standard or interface | What it carries | Never |
| --- | --- | --- | --- |
| Vercel Marketplace | Marketplace provisioning API (install, resource, SSO, billing) | One PermDock environment per Vercel project and environment; `PERMDOCK_CLOUD_URL` and `PERMDOCK_CLOUD_KEY` written to the project | Write a `NEXT_PUBLIC_*` variable; share one environment between `preview` and `production` |
| Vercel OIDC | Vercel's OIDC federation token verified against Vercel's JWKS | ADS caller identity: `sub`, `project_id`, `environment` mapped to a workload principal | Accept a static shared secret in place of the token; accept a token for another environment |
| Vercel Chat SDK | Chat SDK `requestApproval` (below, delivery) | Approval cards on Slack and Teams with signature-verified responders | Resolve an approval from a message body |

## Outbound: observability (OTLP) [#outbound-observability-otlp]

| Connector | Standard or interface | What it carries | Never |
| --- | --- | --- | --- |
| OTLP collector endpoint (Datadog, Grafana Cloud, Honeycomb, Axiom, New Relic, any OpenTelemetry Collector) | OTLP/HTTP log records and spans with the `permdock.*` attribute set from the [OpenTelemetry adapter](/docs/adapters/otel) (`permdock.outcome`, `permdock.permission`, `permdock.subject.id`, `permdock.actor.id`, `permdock.actor.kind`, `permdock.matched.role`, `permdock.denials.count`) | A copy of every event in the evidence log as an OTLP log record, and the hosted ADS's own `permdock.decide` spans | Be the evidence. Collectors sample and drop, retention is short, a span is not signed; the sink's log and its signed export are the evidence ([audit and observability](/docs/concepts/audit-and-observability) "OpenTelemetry is not the evidence path") |

Because the attribute names are the same set `permdock/otel` emits from inside your application, a backend cannot tell an app-side span from a Cloud-side log record, and one dashboard serves both.

## Outbound: SIEM (OCSF) [#outbound-siem-ocsf]

| Connector | Standard or interface | What it carries | Never |
| --- | --- | --- | --- |
| SIEM or security lake (Splunk HTTP Event Collector, Microsoft Sentinel data collector, AWS Security Lake, Google SecOps, Elastic ingest, Datadog Cloud SIEM) | The OCSF Authorize Session projection `toOcsf(event)` from [wire formats](/docs/concepts/wire-formats), posted over the destination's ingest HTTP API | Every decision and approval event, pinned to one OCSF version | Diverge from the projection a self-hosted `DecisionSink` recipe emits; the Cloud runs the documented projection, it does not own a private one |

## Outbound: webhooks [#outbound-webhooks]

Webhooks are the generic escape hatch: anything not listed on this page receives CloudEvents over HTTP.

| Connector | Standard or interface | What it carries | Never |
| --- | --- | --- | --- |
| HTTP webhook | CloudEvents 1.0 events in a batch signed as a compact JWS with `typ: permdock-decisions+jwt` under the `events` claim, posted as `application/jwt` ([wire formats](/docs/concepts/wire-formats) "Signed outputs"); there is no unsigned mode | Events of type `dev.permdock.decision`, `dev.permdock.approval`, `dev.permdock.directory` (a SCIM operation replayed), `dev.permdock.membership` (a role change) and `dev.permdock.catalog` (a policy publish or a drift finding); `source` is the emitting service, `subject` the permission key, resource id or principal id | Deliver an approval resolution: a webhook tells a receiver that an approval is pending or resolved, and the receiver cannot answer it |

A receiver verifies the JWS against the environment's JWK Set, `<PERMDOCK_CLOUD_URL>/v1/environments/<env>/.well-known/jwks.json` (`cloudEndpoints().jwks` from `permdock/cloud`), with `aud` equal to itself and `exp`; the batch `jti` and each event `id` make replay detectable. Retries use exponential backoff with the same `jti` and `id`, so a receiver deduplicates on them. A TypeScript receiver uses `verifyWebhook` from `permdock/cloud`; any other language uses its JOSE library the same way.

```ts
import { cloudEndpoints, verifyWebhook } from "permdock/cloud";
import { memoryReplayStore } from "permdock/ssf";

const replay = memoryReplayStore();

export async function POST(request: Request) {
  const delivery = await verifyWebhook(request, {
    jwks: cloudEndpoints().jwks,
    audience: "https://app.example.com/api/permdock-webhook",
    replay,
  });
  if (!delivery.ok) return new Response(null, { status: 401 });
  for (const event of delivery.events) {
    if (event.type === "dev.permdock.catalog" && event.data.kind === "publish")
      await permdockCloud.policies.refresh();
  }
  return new Response(null, { status: 204 });
}
```

`verifyWebhook` never throws. It answers `{ ok: false, reason }` with `unsigned` (the body is not a compact JWS), `invalid-token` (with the verifier's `cause`), `invalid-events` (an event outside the closed `type` list, a data shape that does not match its type, or a `__proto__`, `constructor` or `prototype` key) or `replayed` (a `jti` the `ReplayStore` already holds). `parseCloudEvent` is the same per-event check for events read from a queue.

Signed only, because a webhook receiver is usually a public URL. An unsigned mode with a shared secret or an optional HTTP signature would be the mode everyone ships first, and a forged `catalog` or `membership` event then triggers a refresh or an alert on attacker input. The batch reuses the decision-export envelope so a receiver has one `typ`, one key set and one verifier for everything the Cloud sends.

## Outbound: compliance evidence [#outbound-compliance-evidence]

| Connector | Standard or interface | What it carries | Never |
| --- | --- | --- | --- |
| Compliance platform (Vanta, Drata, Secureframe) | The Cloud export endpoint: a signed batch (`permdock-decisions+jwt`) or CSV pulled on the review window with a scoped API key | Access-review evidence with the columns from the [Evidence and governance](/docs/concepts/audit-and-observability) contract: `subject.principal`, `actor`, `permission`, `outcome`, `tenant`, `matched.role`, `via` | Return sampled data; the evidence log writes every event and the export is complete for its window |

## Outbound: data destinations [#outbound-data-destinations]

| Connector | Standard or interface | What it carries | Never |
| --- | --- | --- | --- |
| Your database (Supabase Postgres, Neon, any Postgres) | Either a scheduled pull of signed batches by a job you run, or a CloudEvents webhook into a function you own (a Supabase Edge Function, a route handler) that inserts rows | The same events, into a table with your schema; the [sink recipes](/docs/concepts/audit-and-observability) show the columns | Read from your database. The Cloud has no credential to your Postgres, never reads a table or an RLS policy, and never uses the `service_role` key |

This is how "connect Supabase" reads on the data side: Supabase is a destination that receives evidence, not a source the Cloud queries.

## Outbound: approval delivery [#outbound-approval-delivery]

| Connector | Standard or interface | What it carries | Never |
| --- | --- | --- | --- |
| Slack, Microsoft Teams, Discord | Vercel Chat SDK `requestApproval`, the recipe from the [approvals adapter](/docs/adapters/approvals) "Delivery" section run by the Cloud | An approval card per `ApprovalRequest`; the platform signature identifies the responder, the Cloud maps the responder to an approver and applies the actor and distinct-approver rules | Take an approver identity from the message body; let the request's `actor` approve its own call |
| Email | A link to the authenticated inbox | Notification that an approval is pending | Resolve from the link alone; the approver signs in to the inbox (the signed-link question stays open on [approvals](/docs/security/approvals)) |
| Paging (PagerDuty, Opsgenie, any incident tool with an events API) | A CloudEvents webhook for `dev.permdock.approval` with `phase: requested` | The page; the on-call person resolves in the inbox or a chat card | Resolve an approval |

## Outbound: MCP [#outbound-mcp]

| Connector | Standard or interface | What it carries | Never |
| --- | --- | --- | --- |
| PermDock Cloud MCP server (connected from Claude, Cursor, ChatGPT or any MCP host at [mcp.permdock.com](https://mcp.permdock.com)) | MCP over streamable HTTP with OAuth 2.1 per [MCP authorization](/docs/standards/mcp-authorization); the Cloud is the resource server and its own identity is the authorization server | Read-only tools: query evidence (the review questions above), list pending approvals, read the published catalog and drift findings, all scoped to the token's subject and tenants | Resolve an approval, publish a policy or change a connector. Approver identity must come from the Cloud's own authentication, and an agent approving an agent's action defeats the human-in-the-loop guarantee ([approvals](/docs/security/approvals)) |

## What the page does not offer [#what-the-page-does-not-offer]

* A connector that resolves memberships or roles from a provider API. That is a `MembershipSource` or `RoleSource` in your application, or SCIM into a `DirectoryStore` you own ([tenancy](/docs/concepts/tenancy), invariant 15).
* A connector that pushes a policy into the application. Policies are code in your bundle; `permdock cloud push` publishes a copy to the hosted ADS and the catalog, in one direction.
* Per-vendor npm entries or per-vendor OpenAPI extensions.
* An authentication product or a gateway. Trusted issuers are how the Cloud consumes authentication, not how it provides it.
* An MCP `explain` tool that evaluates a permission for a subject. Explaining a past decision needs no evaluation: the stored event already carries `matched`, `via` and `denials`, and the evidence query tool returns it. Evaluating a hypothetical one would make the Cloud's MCP server take a subject as an argument, which invariant 13 forbids, and would answer from the pushed copy of the policy rather than the code the application runs. What-if questions belong to `simulate` in the application or a `describePolicy` test.

## Example app [#example-app]

No example app of its own: `apps/examples/eve-agent` (the Marketplace template) exercises the Vercel, Slack and evidence-export connectors against a Cloud environment; `apps/examples/scim` exercises the SCIM relay.

## Related standards [#related-standards]

* [OpenID Connect](/docs/standards/openid-connect), [JOSE](/docs/standards/jose) and [JWT authorization claims](/docs/standards/jwt-authorization-claims): trusted issuers.
* [Shared Signals and CAEP](/docs/standards/shared-signals-caep): CAEP transmitters.
* [SCIM 2.0](/docs/standards/scim): SCIM sources and the relay.
* [MCP authorization](/docs/standards/mcp-authorization): the Cloud's MCP server.
* [Wire formats](/docs/concepts/wire-formats): CloudEvents types, the OCSF projection and `permdock-decisions+jwt`.
* [adapters](/docs/adapters), [PermDock Cloud](/docs/adapters/cloud).
