---
title: Require manual approval for applications in prod
description: "🎯 Add metadata to applications and enforce policies so sensitive apps require manual approval before production deployment."
sidebar_label: Application catalog approvals
toc_max_heading_level: 2
doc_id: c45da192-e09b-48de-b9c3-c4eb33707c82
tutorial_type: tutorial
tutorial_category: governance
tutorial_time: 20 min
tutorial_featured: false
tutorial_cover: /img/tutorials/covers/enforce-apps-production-approval.svg
keywords:
  - catalog
  - approvals
  - policies
  - applications
  - pci
  - slo
  - terraform
tags:
  - governance
  - production
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Require manual approval for applications in production

> 🎯 **Goal:** Add metadata to applications and **enforce policies** so sensitive apps require **manual approval** before production deployment.

## Introduction

This tutorial captures **application‑level** attributes and enforces a **PCI review** step for production releases. 


:::tip Best paired with build checks
You can use this guide on its own. For stronger gates, see also [Build catalog and production quality gates](./build-quality-policies-production) to validate coverage and vulnerabilities at build time. These guides are independent and can be done in any order.
:::

## What you’ll set up

You’ll:

- Define a **catalog specification** on the `application` entity with `Responsible`, `SLO`, and `PCI` fields.
- Populate those fields when creating applications.
- Attach an **approval policy** so **non‑PCI** applications auto‑approve, while **PCI apps require manual approval** before deploying to production.

---

## Prerequisites

You’ll need:

- Terraform/OpenTofu**
- A valid [**nullplatform API key**](/docs/authorization/api-keys) with roles **Admin, Developer, Ops, SecOps** at the **Organization** level
- A **Kubernetes** cluster with the **nullplatform agent** running
- The [**nullplatform CLI**](/docs/cli/) installed: `curl https://cli.nullplatform.com/install.sh | sh`
- Environment variable for the CLI:
  ```bash
  export NULLPLATFORM_API_KEY=<your_api_key_here>
  ```


## 1. Create a catalog spec for application metadata

Policies read from entity metadata, so declaring catalog schemas ensures the fields exist and are validated consistently across applications.

Run the following Terraform/OpenTofu to add three fields to the `application` entity: `Responsible` (owner), `PCI` (Yes/No), and `SLO` (Critical/High/Medium/Low). 

> Replace these placeholders:
> - `<api-key>` with your nullplatform API key.
> - `<organization=XXXX:account=XXXX:namespace=XXXX>` with your NRN.
> - Update the `enum` for `Responsible` with names relevant to your org (examples shown).

<Tabs
defaultValue="catalog-iac"
values={[
  { label: 'IaC', value: 'catalog-iac' },
  { label: 'cURL', value: 'catalog-curl' },
  ]}>
<TabItem value="catalog-iac">

```hcl
terraform {
  required_providers {
    nullplatform = {
      source = "nullplatform/nullplatform"
    }
  }
}

provider "nullplatform" {
  api_key = "<api-key>"
}

resource "nullplatform_metadata_specification" "application_metadata" {
  name        = "Application Metadata"
  description = "Owner, PCI, and SLO for applications"
  nrn         = "<organization=XXXX:account=XXXX:namespace=XXXX>"
  entity      = "application"
  metadata    = "metadata_application"

  schema = jsonencode({
    "type": "object",
    "properties": {
      "Responsible": {
        "description": "Person assigned as responsible for the application.",
        "type": "string",
        "enum": ["Jane Doe", "John Smith", "Alex Taylor"]
      },
      "PCI": {
        "description": "Is the application in PCI scope?",
        "type": "string",
        "enum": ["Yes", "No"]
      },
      "SLO": {
        "description": "Reliability class for the application.",
        "type": "string",
        "enum": ["Critical", "High", "Medium", "Low"]
      }
    },
    "required": ["PCI", "Responsible"],
    "additionalProperties": false
  })
}
```

</TabItem>
<TabItem value="catalog-curl">

If you prefer to use the API, send the following POST request to our [Catalog API](/docs/api/catalog-specification-create).

```bash
curl -X POST -L 'https://api.nullplatform.com/metadata/metadata_specification' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer <token>' \
  -d '{
    "name": "Application Metadata",
    "description": "Owner, PCI, and SLO for applications",
    "nrn": "<organization=XXXX:account=XXXX:namespace=XXXX>",
    "entity": "application",
    "metadata": "metadata_application",
    "schema": {
      "type": "object",
      "properties": {
        "Responsible": {
          "description": "Person assigned as responsible for the application.",
          "type": "string",
          "enum": ["Jane Doe", "John Smith", "Alex Taylor"]
        },
        "PCI": {
          "description": "Is the application in PCI scope?",
          "type": "string",
          "enum": ["Yes", "No"]
        },
        "SLO": {
          "description": "Reliability class for the application.",
          "type": "string",
          "enum": ["Critical", "High", "Medium", "Low"]
        }
      },
      "required": ["PCI", "Responsible"],
      "additionalProperties": false
    }
  }'
```

</TabItem>
</Tabs>


:::info
Catalog specifications aren’t applied retroactively. Existing applications won’t automatically get values for the new fields.
If you already have applications, backfill one catalog **instance** per application [via API](/docs/api/catalog-instance-single-create).
:::

#### ✅ Checkpoint

Create a new application and confirm the **Responsible**, **PCI**, and **SLO** fields are visible in the UI.

<img alt="app-catalog-example.png" src="/img/tutorials/app-catalog-example.png" width="80%" className="helper-image img-separator" />


## 2. Require manual approval for PCI apps in production

PCI systems typically require human oversight. The policy below **auto‑approves non‑PCI apps** and **prompts for manual approval** when `PCI = Yes`.

1. Create a policy that **passes only when `PCI = No`**.
2. Associate it to a **deployment approval action** scoped to `environment=production`.
3. Configure the action to **approve on policy success** and require **manual** on policy fail.

> Replace:
> - `<api-key>` with your nullplatform API key.
> - `<organization=XXXX:account=XXXX:namespace=XXXX>` with your NRN.

<Tabs
defaultValue="policy-iac"
values={[
  { label: 'IaC', value: 'policy-iac' },
  { label: 'cURL', value: 'policy-curl' },
  ]}>
<TabItem value="policy-iac">

```hcl
terraform {
  required_providers {
    nullplatform = {
      source = "nullplatform/nullplatform"
    }
  }
}

provider "nullplatform" {
  api_key = "<api-key>"
}

# Policy: passes for non-PCI apps (PCI = No)
resource "nullplatform_approval_policy" "PCI" {
  nrn  = "<organization=XXXX:account=XXXX:namespace=XXXX>"
  name = "PCI"
  conditions = jsonencode({
    "application.metadata.metadata_application.PCI" = "No"
  })
}

# Action: deployment_create
resource "nullplatform_approval_action" "deployment_create" {
  nrn    = "<organization=XXXX:account=XXXX:namespace=XXXX>"
  entity = "deployment"
  action = "deployment:create"

  dimensions = {
    environment = "production"
  }

  # If policy passes (non-PCI) ⇒ approve. If it fails (PCI) ⇒ manual.
  on_policy_success = "approve"
  on_policy_fail    = "manual"
}

# Attach policy to action
resource "nullplatform_approval_action_policy_association" "PCI" {
  approval_action_id = nullplatform_approval_action.deployment_create.id
  approval_policy_id = nullplatform_approval_policy.PCI.id
}
```

</TabItem>
<TabItem value="policy-curl">

If you prefer to use the API, send the following POST requests to our [Approval API](/docs/approvals-api-index).

1. Create the approval action (`deployment:create`)

    ```bash
    curl -X POST -L 'https://api.nullplatform.com/approval/action' \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json' \
      -H 'Authorization: Bearer <token>' \
      -d '{
        "nrn": "<organization=XXXX:account=XXXX:namespace=XXXX>",
        "entity": "deployment",
        "action": "deployment:create",
        "dimensions": {
          "environment": "production"
        },
        "on_policy_success": "approve",
        "on_policy_fail": "manual"
      }'
    ```

2. Create the PCI approval policy (passes when `PCI = "No"`)

    ```bash
    curl -X POST -L 'https://api.nullplatform.com/approval/policy' \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json' \
      -H 'Authorization: Bearer <token>' \
      -d '{
        "nrn": "<organization=XXXX:account=XXXX:namespace=XXXX>",
        "name": "PCI",
        "conditions": {
          "application.metadata.metadata_application.PCI": { "$eq": "No" }
        }
      }'
    ```

3. Associate the policies to the action.

    Use the `id` returned by the action creation for `<approval_action_id>`, and the id from the policy.

    ```bash
    curl -X POST -L 'https://api.nullplatform.com/approval/action/<approval_action_id>/policy' \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json' \
      -H 'Authorization: Bearer <token>' \
      -d '{
        "policy_id": <pci_policy_id>
      }'
    ```
</TabItem>
</Tabs>

### Combine with build gates for layered approvals

When you [**already enforce build-time quality gates**](/docs/tutorials/build-quality-policies-production) (e.g., coverage, vulnerabilities), you can layer this PCI approval on
the same production deployment action. In practice, you keep your existing action and add this PCI policy alongside
your build-quality policies so they’re evaluated together at deployment time.

If your Terraform/OpenTofu for build gates already defines the deployment approval action and associations, you only need to
**add this policy resource** (and, if not handled elsewhere, an association to the same action):

```hcl
# Policy: passes for non-PCI apps (PCI = No)
resource "nullplatform_approval_policy" "PCI" {
  nrn  = "<organization=XXXX:account=XXXX:namespace=XXXX>"
  name = "PCI"
  conditions = jsonencode({
    "application.metadata.metadata_application.PCI" = "No"
  })
}

# (Optional if you don’t already associate policies programmatically)
# Attach to the same production deployment action used by build gates
# resource "nullplatform_approval_action_policy_association" "PCI" {
#   approval_action_id = nullplatform_approval_action.deployment_create.id
#   approval_policy_id = nullplatform_approval_policy.PCI.id
# }
```

## Test that it works

1. Create a new application and set the PCI field to **Yes**.
2. Trigger a **production** deployment. You should see a **manual approval** request.
   
<img alt="pci-policy-prod.png" src="/img/tutorials/pci-policy-prod.png" width="80%" className="helper-image img-separator" />

## Wrap‑up 🎉

All done! Now you have:

- Modeled **Owner**, **SLO**, and **PCI** as **application metadata**.
- Enforced that **PCI‑scoped applications** require **manual approval** in **production**.

## What's next

- [Enforce build quality gates for prod deployments](/docs/tutorials/build-quality-policies-production): combine application metadata with build metrics for a complete production policy
- [Approve secret access by environment](/docs/tutorials/secrets-approval-by-environment): extend the approval model to secret parameter values

---

## Related docs

- [Catalog and metadata](/docs/catalog/getting-started)
- [Agent](/docs/agent/overview)
- [Approvals](/docs/approvals/)
