# OpenAPI Overlay

Source: https://permdock.com/docs/standards/openapi-overlay

How permdock openapi --format overlay emits an Overlay 1.1.0 document (or, behind --overlay 1.2, the pinned Overlay 1.2 draft with reusable actions) that adds security, securitySchemes and x-permdock-* fields to an OpenAPI description without mutating it, how to apply and check it in CI, and why the Overlay never removes security.

Draft posture: build (Overlay 1.2 pinned to `v1.2-dev` at commit `edd4adea`, 8 August 2026, recorded in `x-permdock-catalog.drafts.overlay`; 1.1.0 stays the default output)

## What it is [#what-it-is]

The [Overlay Specification 1.1.0](https://spec.openapis.org/overlay/v1.1.0.html) (released 14 January 2026; 1.0.0 was 17 October 2024) describes a document that repeatably applies transformations to one or many OpenAPI descriptions: adding metadata, removing elements before sharing a description, updating descriptions. An Overlay document has:

| Field | Meaning |
| --- | --- |
| `overlay` | The Overlay specification version, `1.1.0` |
| `info` | `title` and `version` of the Overlay itself |
| `extends` | URI of the OpenAPI description the Overlay applies to (optional; an applier may be given the target explicitly) |
| `actions` | Ordered list of actions. Each has a `target` JSONPath expression selecting nodes in the description, plus one of `update` (merge a value into every selected node), `remove` (delete every selected node) or, new in 1.1, `copy` (copy a value from another location) |

An applier resolves each `target` against the description and applies the actions in order; the output is a new OpenAPI description. The source is never edited.

**Overlay 1.2** (in development on the [`v1.2-dev` branch](https://github.com/OAI/Overlay-Specification/blob/v1.2-dev/src/overlay.md), release date "TBD" in its revision history) adds one construct: reusable actions. A root `components` object holds `components.actions`, a map of Reusable Action Objects, each with a `description` and a `fields` object that is an Action Object without `target`. An entry in `actions` may then be a Reusable Action Reference Object instead of an Action Object: `$ref` (a same-document JSON Pointer restricted to `#/components/actions/`), a required `target`, and an optional `description` that overrides the one in `fields`. Nothing else on the object may be overridden; the `update`, `remove` and `copy` come from the referenced action. Earlier notes on this page and elsewhere described a `targetFormat` field for AsyncAPI targets; that idea is not in the `v1.2-dev` text at the pinned commit and PermDock does not emit it.

## Why PermDock prefers an Overlay [#why-permdock-prefers-an-overlay]

`permdock openapi emit` can write the mutated document, and that is still the default (`--format document`) because it is the simplest thing to serve. The Overlay form is preferred whenever the OpenAPI description is owned by someone other than the team running PermDock, or whenever the security metadata should be reviewable on its own:

* **The source stays owned by the API team.** A generator (`@hono/zod-openapi`, `hono-openapi`, `@orpc/openapi`, `trpc-to-openapi`, next-openapi-gen) or a hand-written file remains the single source of truth for paths, schemas and descriptions. PermDock adds authorization metadata beside it instead of rewriting it.
* **It is how PermDock reaches producers it has no hook in.** Next.js route handlers, and every framework next-openapi-gen scans, have no in-process place to call `describe()`. The Overlay is the hand-off: the producer generates, PermDock overlays, the producer's applier merges. For generated descriptions the Overlay is therefore the documented default even though `--format document` stays the CLI default.
* **PermDock's output is reviewable.** The Overlay contains exactly what PermDock decided: which operations require which scopes, which keys they map to, which scheme they use. A reviewer reads a few dozen lines instead of diffing a whole regenerated description.
* **Repeatable and diffable in CI.** The Overlay is a deterministic function of the catalog and the source's `operationId`s. `permdock openapi emit --format overlay --check` fails when the committed Overlay no longer matches, which catches a permission renamed without the document following, and a document edited to change what PermDock emitted.
* **One Overlay, many descriptions.** A monorepo with several APIs that share one catalog can apply one `securitySchemes` Overlay to each, which answers the "shared scheme document" question on the [adapter page](/docs/adapters/openapi) without URI-referenced schemes (a 3.2-only feature).

The Overlay is the delivery mechanism; the semantics of every field it writes are on [OpenAPI](/docs/standards/openapi) and [OpenAPI registries](/docs/standards/openapi-registry).

## How PermDock uses it [#how-permdock-uses-it]

### Output [#output]

```bash
permdock openapi emit --doc openapi.json --format overlay --out permdock.overlay.json
```

```json
{
  "overlay": "1.1.0",
  "info": {
    "title": "PermDock authorization metadata",
    "version": "sha256:..."
  },
  "extends": "./openapi.json",
  "actions": [
    {
      "target": "$.components.securitySchemes",
      "description": "PermDock security schemes",
      "update": {
        "permdockOAuth": {
          "type": "oauth2",
          "oauth2MetadataUrl": "https://auth.example.com/.well-known/oauth-authorization-server",
          "x-permdock-securityProfile": "fapi2",
          "flows": {
            "authorizationCode": {
              "authorizationUrl": "https://auth.example.com/authorize",
              "tokenUrl": "https://auth.example.com/token",
              "scopes": {
                "post:read": "Read a post",
                "post:update": "Edit a post you own",
                "post:delete": "Delete a post you own"
              }
            }
          }
        }
      }
    },
    {
      "target": "$.paths.*[?@.operationId == 'deletePost']",
      "description": "post.delete",
      "update": {
        "security": [{ "permdockOAuth": ["post:delete"] }],
        "x-permdock-permissions": ["post.delete"],
        "x-permdock-conditions": {
          "post.delete": {
            "op": "eq",
            "field": "authorId",
            "value": { "ref": "principal.id" }
          }
        },
        "x-permdock-approval": { "post.delete": { "reason": "human" } }
      }
    },
    {
      "target": "$.paths.*[?@.operationId == 'updatePost']",
      "description": "post.update",
      "update": {
        "security": [{ "permdockOAuth": ["post:update"] }],
        "x-permdock-permissions": ["post.update"]
      }
    },
    {
      "target": "$",
      "description": "PermDock catalog pin",
      "update": {
        "x-permdock-catalog": { "v": 1, "generator": "permdock/openapi" }
      }
    }
  ]
}
```

Rules the emitter follows:

* Operations are targeted by `operationId`, never by path and method, so the Overlay survives a route being moved. Operations without an `operationId` are reported as findings (exit `1`) rather than targeted by position.
* Which operations appear comes from the same source as the document form: `x-permdock-permissions` already present in the source (written by the framework hooks), or the route registry when the CLI runs inside the project.
* Actions are sorted (`securitySchemes`, then `securityProfileRequirements` on `--target 3.3`, then operations by `operationId`, then root) and `info.version` is a `sha256:` fingerprint of the catalog's permission keys and scopes, so two runs over the same inputs produce byte-identical output. Each target is an RFC 9535 filter on the path items' operations, `$.paths.*[?@.operationId == '<id>']`, with `'` and `\\` escaped in the literal.
* **The Overlay never contains a `remove` action on `security` or `securitySchemes`**, and never an `update` that sets `security` to `[]` unless the permission is explicitly public in the catalog. Loosening a document's security is not something PermDock does on anyone's behalf.
* `--target 3.1` swaps the 3.2 fields for the registered fallbacks (`x-oai-deviceAuthorization`, `x-oai-deviceAuthorizationUrl`, `x-oai-deprecated`) and `x-permdock-oauth2MetadataUrl`, exactly as in document mode.

### Overlay 1.2 output [#overlay-12-output]

`--overlay 1.2` (adapter: `overlay({ version: '1.2' })`) emits the Overlay 1.2 draft at the pinned `v1.2-dev` commit instead of 1.1.0. It is the *build* posture of [watch list](/docs/standards/watch-list) applied to a document format rather than to a field: PermDock builds it because the 1.1 form repeats the same `update` body once per operation, the draft's shape is small enough to write fixtures against, and the stable twin is the 1.1 form itself, one flag away and carrying exactly the same information. `1.1` stays the default until 1.2.0 is released and the appliers in the table below accept it.

The 1.2 form differs from the 1.1 form in exactly one way: every operation `update` body becomes a reusable action, keyed by the permission keys it grants, and the operation entries become references to it. Operations that grant the same permissions share one reusable action; the `securitySchemes`, `securityProfileRequirements` and root catalog actions are unchanged because each has one target.

```json
{
  "overlay": "1.2.0",
  "info": {
    "title": "PermDock authorization metadata",
    "version": "sha256:..."
  },
  "extends": "./openapi.json",
  "components": {
    "actions": {
      "post.delete": {
        "description": "post.delete",
        "fields": {
          "update": {
            "security": [{ "permdockOAuth": ["post:delete"] }],
            "x-permdock-permissions": ["post.delete"],
            "x-permdock-conditions": {
              "post.delete": {
                "op": "eq",
                "field": "authorId",
                "value": { "ref": "principal.id" }
              }
            },
            "x-permdock-approval": { "post.delete": { "reason": "human" } }
          }
        }
      },
      "post.update": {
        "description": "post.update",
        "fields": {
          "update": {
            "security": [{ "permdockOAuth": ["post:update"] }],
            "x-permdock-permissions": ["post.update"]
          }
        }
      }
    }
  },
  "actions": [
    {
      "target": "$.components.securitySchemes",
      "description": "PermDock security schemes",
      "update": {
        "permdockOAuth": {
          "type": "oauth2",
          "flows": { "...": "as in the 1.1 example" }
        }
      }
    },
    {
      "$ref": "#/components/actions/post.delete",
      "target": "$.paths.*[?@.operationId == 'deletePost']",
      "description": "deletePost"
    },
    {
      "$ref": "#/components/actions/post.update",
      "target": "$.paths.*[?@.operationId == 'updatePost']",
      "description": "updatePost"
    },
    {
      "$ref": "#/components/actions/post.update",
      "target": "$.paths.*[?@.operationId == 'patchPost']",
      "description": "patchPost"
    },
    {
      "target": "$",
      "description": "PermDock catalog pin",
      "update": {
        "x-permdock-catalog": {
          "v": 1,
          "generator": "permdock/openapi",
          "drafts": { "overlay": "1.2-dev@<commit>" }
        }
      }
    }
  ]
}
```

Rules specific to 1.2:

* The reusable-action key is the sorted, comma-joined list of permission keys the operation grants (`post.delete`; `post.read,post.update` for an operation with two). Keys are JSON Pointer tokens, so `~` and `/` are escaped as `~0` and `~1` in the `$ref`, per RFC 6901; permission keys are resource and action identifiers joined by dots ([naming](/docs/getting-started/naming)), so in practice the key appears verbatim.
* `components.actions` is sorted by key, references keep the same `operationId` order as 1.1 output, and each reference's `description` is the `operationId` so a reviewer can still read the list of covered operations without resolving the pointers. Output is byte-identical across runs, and `--check` compares a committed 1.2 Overlay against a regenerated 1.2 Overlay, never across versions.
* Only `target` and `description` appear on a reference; `update`, `remove` and `copy` are never written on one, matching the draft's rule. A committed Overlay with a `remove` anywhere on `security`, `securitySchemes` or `securityProfileRequirements`, inline or inside `components.actions`, still fails `--check`.
* The pin `drafts.overlay` names the `v1.2-dev` commit the shape was checked against, in the root `x-permdock-catalog` action. `--check` fails and `permdock doctor` (`PD012`) warns when a committed 1.2 Overlay carries a different pin; the fix is to regenerate. 1.1 output carries no `drafts` entry for the Overlay.
* The 1.2 form validates against the `schemas/v1.2-dev` JSON Schema at the pinned commit; `redocly lint` and the shipped ruleset accept both `overlay: 1.1.0` and `overlay: 1.2.0`.
* **Release switch.** When 1.2.0 is published, `--overlay 1.2` emits the released shape in the next PermDock minor, `drafts.overlay` disappears, and any difference between draft and release is absorbed by the emitter (the importer reads nothing back from an Overlay, so there is no dual-read window). The default moves from `1.1` to `1.2` no earlier than one further minor and only once next-openapi-gen, Redocly CLI, Bump.sh, Speakeasy and `overlays-js` all accept `components.actions`. If reusable actions are dropped before release, `--overlay 1.2` emits the 1.1 shape with `overlay: 1.2.0` and this page records that.

### Applying [#applying]

Any Overlay 1.x applier can consume the file: it takes the OpenAPI description (from `extends` or a command-line argument) and the Overlay and writes the combined description. Run it as a build step and serve or publish the result; the source description in the repository stays unchanged. PermDock does not ship its own applier; the document form covers projects that do not want one.

Each operation action targets the operation object by `operationId` and sets `security` on it, one action per route. Because the producer must not write `security` on covered operations (below), there is no existing array for an applier to merge with, and the operation ends with exactly the requirements PermDock wrote.

| Applier | Where it runs | Notes |
| --- | --- | --- |
| [next-openapi-gen](https://github.com/tazo90/next-openapi-gen/blob/main/docs/overlay.md) `overlay.apply` | Inside `openapi-gen generate`, for Next.js, TanStack Start, React Router, Remix, SvelteKit, Nuxt, Astro, Hono and Express projects | Applied before the spec is written, so the Scalar UI it scaffolds and the Arazzo files it compiles see the applied description. Accepts Overlay 1.0, 1.1 and 1.2. The Next.js recipe |
| [Redocly CLI](https://redocly.com/docs/cli/commands) `join --overlay` | Lint, bundle and `generate-client` pipelines | Experimental flag; `redocly lint permdock.overlay.json` validates the Overlay itself |
| Scalar CLI | Docs and registry pipelines | Overlay support is on Scalar's roadmap; apply with one of the above before `scalar registry publish` |

Whichever applier runs, the producer must not also write `security` on the operations PermDock covers (next-openapi-gen `@auth` and `authPresets`, for example). One owner per field: PermDock owns `security`, the producer owns paths and schemas. `--check` reports operations whose source already carries `security` the Overlay would replace.

**`operationId` is the join key.** Producers generate it, the Overlay targets it, Arazzo steps reference it. A producer configured to omit `operationId`s cannot be overlaid; `--check` fails on the first operation without one rather than falling back to path-and-method targeting.

**Which Overlay version to emit.** An applier that does not know `components.actions` rejects a 1.2 Overlay at `overlay: 1.2.0` or, worse, ignores the references. Keep the default `1.1` for Redocly CLI, Bump.sh, Speakeasy and `overlays-js` pipelines until their release notes name Overlay 1.2; next-openapi-gen documents 1.0 to 1.2 and is the first applier the 1.2 form is tested against. The version is a per-pipeline choice; a monorepo can commit both files from one catalog.

### CI recipe [#ci-recipe]

```yaml
# .github/workflows/permissions.yml
- run: pnpm exec permdock collect --check
- run: pnpm exec permdock openapi emit --doc openapi.json --format overlay --out permdock.overlay.json --check
# apply permdock.overlay.json to openapi.json with your Overlay applier, then publish the result
```

`--check` regenerates the Overlay in memory and compares it byte-for-byte with the committed file. It fails on a renamed or removed permission, a new protected route with no entry, a changed `operationId`, and any hand edit to the committed Overlay, including one that adds a `remove` action.

## Threat: an Overlay that removes security [#threat-an-overlay-that-removes-security]

An Overlay is code that rewrites an API description, and `remove` is a legitimate action in the specification. An attacker who can modify the Overlay in a pipeline, or substitute their own, can strip `security` from every operation in the published description:

```json
{ "target": "$.paths.*.*.security", "remove": true }
```

The published description would then claim the API is public. Enforcement is unaffected (PermDock's `protect` does not read the document), but clients, gateways and agents that trust the description would attempt unauthenticated calls, and a gateway that derives its policy from the description would stop enforcing.

Mitigations:

* PermDock's Overlay **never** contains `remove` on `security` or `securitySchemes`, so the presence of one is a signal, not a configuration choice.
* `permdock openapi emit --format overlay --check` fails on any difference from the regenerated Overlay, including an added `remove`. Run it in the same job that applies the Overlay.
* Treat the Overlay like any other build input: commit it, review it, and do not fetch it from a mutable URL at build time. PermDock does not sign the Overlay; repository review plus `--check` is the integrity control.
* Keep enforcement independent of the document. PermDock's `protect` guards routes from the policy, not from the OpenAPI description, so a tampered description cannot open a route ([threat model](/docs/security/threat-model)).

## Mapping table [#mapping-table]

| Overlay concept | PermDock concept |
| --- | --- |
| `extends` | `--doc` |
| `actions[].target` on `$.components.securitySchemes` | `securitySchemes()` from the adapter, every catalog `scope` |
| `actions[].target` by `operationId` | One protected route; `describe(permission)` output |
| `update.security` | `security(permission)` |
| `update.x-permdock-*` | Permission keys, portable conditions, approval metadata, security profile |
| `remove` | Never emitted for security fields |
| `copy` (1.1) | Not used |
| `components.actions` and `$ref` references (1.2 draft, `--overlay 1.2`) | One reusable action per distinct set of granted permission keys; one reference per operation |
| Deterministic output | `--check` in CI |

## Sources [#sources]

* [Overlay Specification 1.1.0](https://spec.openapis.org/overlay/v1.1.0.html).
* [Overlay Specification `v1.2-dev`](https://github.com/OAI/Overlay-Specification/blob/v1.2-dev/src/overlay.md) and its `schemas/v1.2-dev` schema, at the commit named in the `Draft posture` line.
* [OpenAPI Initiative registries](https://spec.openapis.org/registry/), for the extension names the Overlay writes.

## Related [#related]

* [OpenAPI](/docs/standards/openapi): the fields the Overlay adds, and what changes when a native security-profile construct exists.
* [OpenAPI registries](/docs/standards/openapi-registry): the `x-permdock-*` namespace.
* [Arazzo workflows](/docs/standards/arazzo): reading the applied description back to pre-flight a workflow.
* [OpenAPI adapter](/docs/adapters/openapi), [Next.js adapter](/docs/adapters/next) and [CLI: openapi](/docs/cli/openapi).
* [OpenAPI ecosystem](/docs/research/ecosystem-index): producers, appliers and consumers.
* [Adapters](/docs/adapters).
* [Watch list](/docs/standards/watch-list): the Overlay 1.2 row.
