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.
You can use this guide on its own. For stronger gates, see also Build catalog and production quality gates 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 withResponsible
,SLO
, andPCI
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 >= 1.13
- A valid nullplatform API key with roles Admin, Developer, Ops, SecOps at the Organization level
- A Kubernetes cluster with the nullplatform agent running
- The nullplatform CLI installed:
curl https://cli.nullplatform.com/install.sh | sh
- Environment variable for the CLI:
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 to add three fields to the application
entity: Responsible
(owner), PCI
(Yes/No), and SLO
(Critical/High/Medium/Low).
Replace these placeholders:
<organization=XXXX:account=XXXX:namespace=XXXX>
with your NRN.- Update the
enum
forResponsible
with names relevant to your org (examples shown).
- IaC
- cURL
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
})
}
If you prefer to use the API, send the following POST request to our Catalog API.
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
}
}'
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.
✅ Checkpoint​
Create a new application and confirm the Responsible, PCI, and SLO fields are visible in the UI.

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
.
- Create a policy that passes only when
PCI = No
. - Associate it to a deployment approval action scoped to
environment=production
. - 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.
- IaC
- cURL
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
}
If you prefer to use the API, send the following POST requests to our Approval API.
-
Create the approval action (
deployment:create
)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"
}' -
Create the PCI approval policy (passes when
PCI = "No"
)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" }
}
}' -
Associate the policies to the action.
Use the
id
returned by the action creation for<approval_action_id>
, and the id from the policy.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>
}'
Combine with build gates for layered approvals​
When you already enforce build-time quality gates (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 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):
# 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​
- Create a new application and set the PCI field to Yes.
- Trigger a production deployment. You should see a manual approval request.

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.