> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cognite.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Signing Flows custom apps from the CLI

> Step-by-step guide to generate signing keys, sign Flows custom app bundles, and publish signed deployments with the Cognite CLI.

Signing a Flows custom app proves who built the bundle and binds the signature to the exact bytes deployed. The platform rejects the deployment if anything in the bundle changes after signing, or if the signing key isn't registered.

Use this guide if you're a certified Flows builder and you deploy custom apps with the Cognite CLI.

## Signatures required to publish

Before you publish a Flows custom app, the platform may require a developer signature, an app certification signature, or both. Each signature is tied to the exact bundle bytes. Use the table to see what each one proves. This guide walks you through the developer signature only.

| Signature                   | What it proves                                                |
| --------------------------- | ------------------------------------------------------------- |
| Developer signature         | A certified developer built this bundle for this CDF project. |
| App certification signature | Cognite has reviewed and approved this production bundle.     |

Whether you need the certification signature depends on the CDF project's status. Projects with **Dev status** accept the developer signature on its own, which is useful for development, staging, and sharing prototypes with end users without going through Cognite app review. Production projects without Dev status require both signatures.

<Card title="Request Dev status for a CDF project" icon="ticket" href="https://cognite.zendesk.com/hc/en-us/requests/new?ticket_form_id=360001234312&tf_priority=normal&tf_subject=Dev+Status+Request&tf_360015295097=cognite_flows&tf_description=Dear+Cognite+Platform+Team%2C%3Cbr%3E%3Cbr%3EI+would+like+to+request+Dev+status+for+the+following+CDF+project.%3Cbr%3EThis+will+enable+us+to+share+custom+Flows+apps+with+end+users+in+this+project+without+going+through+the+Cognite+app+review+and+certification+process.%3Cbr%3EWe+understand+the+risks+and+acknowledge+the+responsibility.%3Cbr%3E%3Cbr%3ECDF+org%3A+%5BYour+org%5D%3Cbr%3ECDF+project%3A+%5BYour+project%5D%3Cbr%3E%3Cbr%3EBest+regards%2C%3Cbr%3E%5BYour+name%5D">
  Opens a pre-filled Zendesk request. Fill in your org and project name before submitting.
</Card>

## Prerequisites

* The latest Cognite CLI (`npx @cognite/cli@latest`).
* A Flows custom app with `app.json` (with `infra` set to `appsApi`).
* Access to the CDF project you'll deploy to.
* [Builder certification](/cdf/flows/guides/builder-certification), or signing instructions from the team reviewing your app.
* [Application certification](/cdf/flows/guides/application-certification) if you're deploying to a production project and need the Cognite signature.

<Info>
  A developer signature is not the same as production app approval. Production apps still go through Cognite app certification before they can be deployed to customer production projects.
</Info>

## Sign and publish

<Steps>
  <Step title="Generate a signing key">
    ```bash theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
    npx @cognite/cli@latest keys generate
    ```

    The CLI prompts for a key ID (`kid`), the email tied to the key, and an expiry. On macOS, it stores the private key in **Keychain** by default and writes the public key to `~/.cognite-cli/keys/<kid>.pub.pem` so you can share it for registration.

    <Warning>
      The private key stays on your machine. Don't share it, commit it, export it from Keychain, or send it to Cognite.
    </Warning>
  </Step>

  <Step title="Register the public key">
    `keys generate` finishes by printing a pre-filled Zendesk URL for registration. Pick one when prompted:

    * **Open in browser** — submits the request directly.
    * **Copy link** — paste it into the browser later. Pass `--copy-link` to skip the prompt and copy straight to the clipboard.

    The request contains your `kid`, email, expiry, and the YAML block Cognite needs to add to the public key registry. You can publish before the key is registered, but `apps status` will show your signature as `KEY_NOT_IN_REGISTRY` until Cognite adds it to the registry.
  </Step>

  <Step title="Build and deploy">
    From your app folder:

    ```bash theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
    npm run build
    npx @cognite/cli@latest apps deploy --interactive
    ```

    `apps deploy` packages `dist/`, uploads it as a DRAFT, and keeps a copy of the exact bytes at `.cognite-bundles/<externalId>-<versionTag>.zip` so signing operates on what the platform actually received. The app scaffold gitignores `.cognite-bundles/`. Don't commit bundle files from that folder.
  </Step>

  <Step title="Sign the bundle">
    ```bash theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
    npx @cognite/cli@latest apps sign
    ```

    With no arguments, `apps sign` picks `app.json`'s `externalId` and `versionTag`, finds the matching `.cognite-bundles/<app>-<version>.zip`, writes `<bundle>.dev.sig` next to it, and submits the signature to the platform. Override with `--signing-identity <kid>`, `--scope <org>/<project>`, or an explicit bundle path.

    Run `apps status` to verify the signature was accepted before you publish.

    <Tip>
      Commit before signing so the source-to-signature link stays auditable. Tag the commit with the app version:

      ```bash theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
      git tag "v$(jq -r .versionTag app.json)" -m "Signed deployment"
      git push origin --tags
      ```
    </Tip>
  </Step>

  <Step title="Publish">
    ```bash theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
    npx @cognite/cli@latest apps publish .
    ```

    `apps publish` transitions the version from DRAFT to PUBLISHED. Signatures were already submitted by `apps sign`; publish re-submits any local `.dev.sig` (and `.cert.sig` if present) as a safety net, then flips the lifecycle state.
  </Step>

  <Step title="Confirm">
    ```bash theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
    npx @cognite/cli@latest apps status .
    ```

    The command shows the lifecycle state and every signature the platform has on record for the current version. Each entry includes the signing identity (`kid`), the role (`developer` or `certifier`), the time the signature was issued, and the platform's verdict.

    You'll see one of three shapes depending on where you are in the flow:

    Before `apps deploy`:

    ```text theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
    Status:  not deployed yet

    Run `npx @cognite/cli apps deploy` to upload this version.
    ```

    After `apps deploy`, before `apps sign` + `apps publish`:

    ```text theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
    Status:  DRAFT

    Signatures: none stored

    Run `npx @cognite/cli apps publish .` to publish this version.
    ```

    After a clean publish:

    ```text theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
    App:     Sign Test App (sign-test-app)
    Version: 0.0.3  (local)
    Status:  PUBLISHED

    Signatures:
      ✅ acme-dev-001    (developer, signed 2026-05-27T13:22:37.000Z) — VALID
      ✅ cognite-cert-1  (certifier, signed 2026-05-27T13:25:10.000Z) — VALID
    ```

    A ❌ next to a signature means the platform won't accept that signature for serve-time verification. The table below explains each verdict and what to do.

    | Verdict                    | What it means                                                                                | What to do                                                                            |
    | -------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
    | `VALID`                    | Signature verifies and the key is in the registry and in date.                               | Nothing — this is the expected state.                                                 |
    | `REVOKED`                  | The signing key has been revoked since the signature was submitted.                          | Re-sign with a current key, then publish a new version.                               |
    | `EXPIRED`                  | The signing key was in date when signed, but has since expired.                              | Rotate the key (see [Rotate or replace a key](#rotate-or-replace-a-key)) and re-sign. |
    | `SIGNED_BEFORE_KEY_ISSUED` | The signature's `iat` timestamp predates the key's registration. Clock skew, or a stale key. | Re-sign with the current system clock. If the clock is correct, contact Cognite.      |
    | `IAT_IN_FUTURE`            | The signature's `iat` timestamp is in the future. Usually clock skew on the signing machine. | Fix the signing machine's clock, then re-sign and re-publish.                         |
    | `BUNDLE_TOO_OLD`           | The bundle's signature is older than the platform's minimum-age policy.                      | Re-sign the current bundle and re-publish.                                            |
    | `KEY_NOT_IN_REGISTRY`      | The `kid` you signed with isn't in the registry.                                             | Confirm the key is registered before re-signing.                                      |

    <Check>
      Verify the version is **PUBLISHED** and your developer signature appears as `VALID` for the expected `kid`. Anything else needs action before the version will serve.
    </Check>
  </Step>
</Steps>

## Production certification (Cognite signature)

Production CDF projects without Dev status need both a developer signature and an app certification signature. The two signatures are independent — Cognite signs the same bundle bytes with their own key and returns the signature to you. Complete the [Sign and publish steps](#sign-and-publish) until **Publish**, then add these steps before you publish the application:

<Steps>
  <Step title="Submit the app for review">
    From your app folder, on a clean commit:

    ```bash theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
    npx @cognite/cli@latest apps submit
    ```

    `apps submit` packages a source archive (`src-<sha>.zip`), copies the deploy bundle into `dist/submit/`, and opens a pre-filled Zendesk request with the app metadata. Attach everything in `dist/submit/` plus a screen recording of the app, then submit.

    <Note>
      The command will fail if your working tree has uncommitted changes — the source archive has to match the committed code exactly. Commit first, then re-run.
    </Note>
  </Step>

  <Step title="Receive the certification signature from Cognite">
    After Cognite reviews and approves the bundle, your Zendesk contact replies with a `.cert.sig` file. Save it to your app's `.cognite-bundles/` folder next to the deploy bundle, named `<externalId>-<versionTag>.cert.sig`.

    For example, for app `sign-test-app` version `0.0.3`:

    ```text theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
    .cognite-bundles/
      sign-test-app-0.0.3.zip          ← created by apps deploy
      sign-test-app-0.0.3.zip.dev.sig  ← created by apps sign
      sign-test-app-0.0.3.zip.cert.sig ← from Cognite, save here
    ```

    To open `.cognite-bundles/` in your file manager from the app folder:

    <Tabs>
      <Tab title="macOS">
        ```bash theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
        open .cognite-bundles
        ```
      </Tab>

      <Tab title="Windows">
        ```powershell theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
        explorer .cognite-bundles
        ```
      </Tab>

      <Tab title="Linux">
        ```bash theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
        xdg-open .cognite-bundles
        ```
      </Tab>
    </Tabs>

    Don't commit `.cert.sig` to git. The scaffold gitignores `.cognite-bundles/` for this reason.
  </Step>

  <Step title="Publish">
    ```bash theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
    npx @cognite/cli@latest apps publish .
    ```

    `apps publish` picks up both `.dev.sig` and `.cert.sig`, submits the certifier signature, and transitions the version to PUBLISHED.

    Run `apps status .` to confirm both signatures show as `VALID`.
  </Step>
</Steps>

## Sign again when the bundle changes

The signature is bound to the exact bytes of the bundle. Anything that changes the bundle invalidates the signature:

* Re-running `npm run build`.
* Editing files in `dist/` or `manifest.json`.
* Bumping the app version.

Re-run `apps deploy`, `apps sign`, and `apps publish` in order. `deploy` overwrites the bundle file at the same path, so the next `sign` picks up the new bytes automatically.

## Rotate or replace a key

Generate a new key when the old one expires, you move to a new machine, or you suspect the private key is compromised:

1. `npx @cognite/cli@latest keys generate`.
2. Register the new public key.
3. Sign new bundles with the new key.
4. Ask your Cognite contact to revoke the old key if needed.

Previously published apps stay published. Rotating your local key doesn't invalidate signatures already accepted by the platform.

## Troubleshooting

<AccordionGroup>
  <Accordion title="No signing identities were found">
    Run `npx @cognite/cli@latest keys generate`, then retry the signing command.
  </Accordion>

  <Accordion title="Publish fails with `bundle hash mismatch`">
    The bytes you signed don't match what the platform received. Run these commands in order:

    1. `npx @cognite/cli@latest apps deploy` — refreshes `.cognite-bundles/<app>-<version>.zip` with the bytes the platform has.
    2. `npx @cognite/cli@latest apps sign` — signs the refreshed bundle.
    3. `npx @cognite/cli@latest apps publish .` — submits the new signature.

    Don't manually repackage the bundle.
  </Accordion>

  <Accordion title="`apps status` shows ❌ KEY_NOT_IN_REGISTRY">
    Publish accepts the signature, but the platform can't verify it until your key is registered. Confirm with your Cognite contact that the key has been added, then re-run `apps status` — the verdict flips to `VALID` once the registry catches up. No need to re-sign or re-publish.
  </Accordion>

  <Accordion title="`apps status` shows ❌ IAT_IN_FUTURE or SIGNED_BEFORE_KEY_ISSUED">
    The signature timestamp doesn't line up with the key's registration window. Almost always clock skew on the signing machine. Make sure your system clock is correct, then re-run `apps sign` and `apps publish`.
  </Accordion>

  <Accordion title="Publish fails with `two signatures required`">
    You're targeting a production project that requires both a developer and an app certification signature. Follow [Production certification](#production-certification-cognite-signature) to submit for review and add the `.cert.sig` Cognite returns.
  </Accordion>

  <Accordion title="Multiple signing keys">
    Run `npx @cognite/cli@latest keys list`, find the correct `kid`, and pass it with `--signing-identity <kid>`.
  </Accordion>

  <Accordion title="Lost access to Mac or Keychain">
    Generate a new key on the replacement machine and register the public key. Ask your Cognite contact to revoke the old key if it may be compromised.
  </Accordion>
</AccordionGroup>

## Related topics

* [Become a certified Flows builder](/cdf/flows/guides/builder-certification)
* [Deploying Flows custom apps](/cdf/flows/guides/deploying)
* [Flows custom apps quality guidelines](/cdf/flows/guides/quality-guidelines)
