LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Port: The Internal Developer Portal That Works Out of the Box

portdeveloper-portalplatform-engineeringidpdevopskubernetesbackstageself-service

Port is an internal developer portal (IDP) built as a product rather than a framework. Where Backstage hands you a pile of React components and says “good luck,” Port hands you a working portal on day one. You model your engineering world through a flexible data model, connect your tools, and expose self-service actions — all through a UI or API, without writing and deploying custom code.

This guide covers everything a platform engineer needs to evaluate, deploy, and extend Port: the data model, blueprint system, integrations, self-service actions, scorecards, the Ocean framework, and how it honestly stacks up against Backstage.


What Port Is and Why It Exists

An internal developer portal solves the “where does this live?” problem. Services scatter across GitHub repos, infrastructure lives in Terraform state, incidents land in PagerDuty, teams are defined in Okta, and nobody has a single view of what depends on what, who owns it, or whether it’s production-ready. An IDP creates that view — and builds self-service tooling on top of it so developers can act on what they see without creating a ticket and waiting three days.

Port’s core bet is that most organizations don’t want to build and maintain a custom portal. They want to define their data model and connect their tools, not maintain a Node.js/React monolith with a custom plugin for every integration. Port is cloud-hosted SaaS: Port runs the infrastructure, you define the model.

The platform has seven core capabilities:

  • Software Catalog — a unified SDLC view of all your entities (services, clusters, environments, pipelines, etc.)
  • Blueprints — the data model that defines what entities exist and what properties they have
  • Self-Service Actions — developer-triggered operations backed by GitHub Actions, webhooks, GitLab pipelines, or Terraform
  • Scorecards — production readiness checks and maturity tracking against defined standards
  • Workflow Automation — event-driven automations triggered by catalog changes
  • AI Agents — custom agents with defined goals and access to catalog context
  • Interface Designer — custom dashboards per team or persona

Architecture: How Port Works

Port is fully cloud-hosted. There’s no backend to deploy, no database to manage. You sign up at app.getport.io, define your data model in the Builder, and connect integrations that push data in.

┌─────────────────────────────────────────────────────────────┐
│                      Port SaaS Platform                      │
│                                                             │
│  ┌──────────────┐  ┌─────────────┐  ┌────────────────────┐ │
│  │  Blueprint   │  │  Software   │  │  Self-Service      │ │
│  │  Builder     │  │  Catalog    │  │  Actions           │ │
│  └──────────────┘  └─────────────┘  └────────────────────┘ │
│                                                             │
│  ┌──────────────┐  ┌─────────────┐  ┌────────────────────┐ │
│  │  Scorecards  │  │  Workflow   │  │  Dashboards /      │ │
│  │              │  │  Automation │  │  Interface Designer│ │
│  └──────────────┘  └─────────────┘  └────────────────────┘ │
│                                                             │
│  REST API / Terraform Provider / Pulumi Provider           │
└─────────────────────────────────────────────────────────────┘
          ▲                     ▲                  ▲
          │                     │                  │
   Ocean Integrations     GitHub App         Webhooks /
   (K8s, GitLab, AWS,     (repo data,        Custom ingestion
    PagerDuty, etc.)       PR events)

Data flow: Integrations pull data from external systems (or receive webhooks) and push entity data to Port’s API. Port stores entities in its own managed database keyed by blueprint + identifier. The catalog UI reads from this database. Self-service actions send invocation payloads outward to your CI/CD systems — Port never holds your secrets or executes code directly.

API-first: Everything Port does through its UI is available via REST API. Port also provides a Terraform provider and Pulumi provider for managing the data model as code.


The Blueprint System

Blueprints are Port’s data model. A blueprint defines a type of entity — like Service, Kubernetes Cluster, Deployment, PagerDuty Service, or Cloud Account. Every entity in Port is an instance of a blueprint.

Blueprint Structure

A blueprint is a JSON document with this shape:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
{
  "identifier": "microservice",
  "title": "Microservice",
  "description": "A backend microservice",
  "icon": "Microservice",
  "schema": {
    "properties": {
      "repo_url": {
        "type": "string",
        "format": "url",
        "title": "Repository URL"
      },
      "language": {
        "type": "string",
        "title": "Language",
        "enum": ["Go", "Python", "TypeScript", "Java", "Rust"],
        "enumColors": {
          "Go": "blue",
          "Python": "yellow",
          "Rust": "orange"
        }
      },
      "tier": {
        "type": "string",
        "title": "Service Tier",
        "enum": ["tier-1", "tier-2", "tier-3"]
      },
      "on_call": {
        "type": "string",
        "format": "user",
        "title": "On-Call Engineer"
      },
      "last_deployed": {
        "type": "string",
        "format": "date-time",
        "title": "Last Deployed"
      },
      "open_incidents": {
        "type": "number",
        "title": "Open Incidents"
      },
      "readme": {
        "type": "string",
        "format": "markdown",
        "title": "README"
      }
    },
    "required": ["repo_url", "language"]
  },
  "calculationProperties": {},
  "mirrorProperties": {},
  "relations": {}
}

Property Types

Port supports a rich set of property types:

Type Format Use Case
string Free text
string url Clickable links
string email Email addresses
string user Reference to a Port user
string team Reference to a Port team
string date-time Timestamps
string timer TTL / expiry countdown
string markdown Rendered Markdown content
string yaml YAML object viewer
number Integers and floats
boolean True/false flags
array Lists of values
object Arbitrary JSON objects
string enum Dropdown with defined values

One important constraint: the type of a property is immutable after creation. Plan your schema carefully — changing a property type requires deleting and recreating it, which loses existing data for that field.

Calculation Properties

Calculation properties derive their value from a formula applied to other properties. They’re defined as jq expressions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
"calculationProperties": {
  "days_since_deploy": {
    "title": "Days Since Last Deploy",
    "type": "number",
    "calculation": "(.properties.last_deployed | if . == null then null else (now - (. | fromdateiso8601)) / 86400 | floor end)"
  },
  "is_stale": {
    "title": "Is Stale",
    "type": "boolean",
    "calculation": "(.properties.last_deployed | if . == null then true else (now - (. | fromdateiso8601)) > 2592000 end)"
  }
}

Mirror Properties

Mirror properties pull values from related entities into the current blueprint. For example, if a Deployment is related to a Service, you can mirror Service.tier onto Deployment so it’s visible without navigating the relation:

1
2
3
4
5
6
"mirrorProperties": {
  "service_tier": {
    "title": "Service Tier",
    "path": "service.$tier"
  }
}

Relations Between Blueprints

Relations connect blueprints to build a graph of your engineering world. There are two relation types:

  • Single (many: false) — one-to-one: a deployment relates to exactly one service
  • Many (many: true) — one-to-many: a service depends on multiple packages
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
"relations": {
  "service": {
    "title": "Service",
    "target": "microservice",
    "required": false,
    "many": false
  },
  "dependencies": {
    "title": "Dependencies",
    "target": "microservice",
    "required": false,
    "many": true
  }
}

One constraint: a relation cannot have both required: true and many: true simultaneously.

Relations are how Port builds context. A Kubernetes Pod blueprint relates to a Kubernetes Node, which relates to a Kubernetes Cluster, which relates to a Cloud Account. Navigate the graph in the UI and you get instant context: which pods are running, on which nodes, in which cluster, owned by which team, with which on-call engineer.


Software Catalog: Populating Entities

The catalog is only useful if it stays current. Port provides three main paths to populate it.

1. Ocean Integrations (Pull-Based)

Ocean integrations are the primary mechanism. Each integration is a standalone process that connects to an external system, transforms data with JQ mappings, and pushes entities to Port’s API. Most integrations can be hosted by Port (zero infrastructure overhead) or self-hosted in your own cluster.

Install the Kubernetes integration via Helm:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
helm repo add port-labs https://port-labs.github.io/helm-charts
helm repo update

helm install port-k8s-exporter port-labs/port-k8s-exporter \
  --create-namespace \
  --namespace port-k8s-exporter \
  --set secret.secrets.portClientId="YOUR_CLIENT_ID" \
  --set secret.secrets.portClientSecret="YOUR_CLIENT_SECRET" \
  --set portBaseUrl="https://api.getport.io" \
  --set stateKey="k8s-exporter" \
  --set eventListenerType="POLLING"

After install, the exporter runs a continuous sync loop: on startup it does a full resync, then watches the Kubernetes API for create/update/delete events and pushes changes to Port in real-time.

The integration maps Kubernetes objects to blueprints through a YAML configuration that lives in Port’s UI (editable post-install):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
resources:
  - kind: apps/v1/deployments
    selector:
      query: .metadata.namespace | startswith("kube") | not
    port:
      entity:
        mappings:
          identifier: .metadata.name + "-" + .metadata.namespace
          title: .metadata.name
          blueprint: '"k8s_deployment"'
          properties:
            namespace: .metadata.namespace
            image: .spec.template.spec.containers[0].image
            replicas: .spec.replicas
            available_replicas: .status.availableReplicas
            labels: .metadata.labels
          relations:
            namespace: .metadata.namespace

The selector.query is a JQ expression that filters which resources to ingest. The mappings section maps Kubernetes object fields to blueprint properties using JQ.

2. GitHub Integration

The GitHub integration installs as a GitHub App and syncs repositories, pull requests, workflow runs, issues, deployments, branches, tags, Dependabot alerts, code scanning alerts, and file contents.

Per-repo or org-wide configuration lives in port-app-config.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# .github/port-app-config.yml  (per-repo)
# OR .github-private/port-app-config.yml  (org-wide)

deleteDependentEntities: true
createMissingRelatedEntities: true

resources:
  - kind: repository
    selector:
      query: "true"
    port:
      entity:
        mappings:
          identifier: .name
          title: .name
          blueprint: '"microservice"'
          properties:
            repo_url: .html_url
            language: .language
            default_branch: .default_branch
            description: .description
          relations:
            team: .teams[0].name

  - kind: pull-request
    selector:
      query: .state == "open"
    port:
      entity:
        mappings:
          identifier: (.base.repo.name + "-" + (.number | tostring))
          title: .title
          blueprint: '"pull_request"'
          properties:
            status: .state
            author: .user.login
            url: .html_url
            created_at: .created_at
          relations:
            service: .base.repo.name

  - kind: file
    selector:
      query: "true"
      files:
        - path: "service.yaml"
    port:
      itemsToParse: .file.content | fromjson | .dependencies
      entity:
        mappings:
          identifier: .item.name
          title: .item.name
          blueprint: '"package"'
          properties:
            version: .item.version

The file kind is powerful: it lets you ingest structured data from YAML/JSON files in your repos — service manifests, dependency lists, SLO definitions — and create entities from them.

Note: The legacy GitHub integration sunsets July 15, 2026. Migrate to the Ocean-powered GitHub integration before then.

3. Port’s REST API

For custom data sources — internal databases, legacy CMDBs, homegrown tooling — push entities directly via the API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
curl -X POST "https://api.getport.io/v1/blueprints/microservice/entities" \
  -H "Authorization: Bearer $PORT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "identifier": "payments-service",
    "title": "Payments Service",
    "properties": {
      "repo_url": "https://github.com/acme/payments-service",
      "language": "Go",
      "tier": "tier-1",
      "last_deployed": "2026-03-28T14:23:00Z",
      "open_incidents": 2
    },
    "relations": {
      "team": "platform-team"
    }
  }'

Get a token first:

1
2
3
4
PORT_TOKEN=$(curl -s -X POST "https://api.getport.io/v1/auth/access_token" \
  -H "Content-Type: application/json" \
  -d "{\"clientId\": \"$PORT_CLIENT_ID\", \"clientSecret\": \"$PORT_CLIENT_SECRET\"}" \
  | jq -r '.accessToken')

The API supports upsert semantics: POST with upsert=true creates or updates based on identifier, making it idempotent for CI/CD pipelines.


Self-Service Actions

Self-service actions let developers trigger predefined operations from the Port UI (or API) without needing access to the underlying infrastructure. The key design principle: Port sends a payload to your backend — it doesn’t execute code itself. Your existing CI/CD systems remain the execution engine.

Action Anatomy

Every action has three parts:

  1. Frontend inputs — the form developers fill out (validated before submission)
  2. Backend invocation — how Port triggers the execution
  3. Audit trail — Port records every execution, its inputs, status, and output link

Defining an Action (JSON)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
{
  "identifier": "scaffold_service",
  "title": "Scaffold New Service",
  "icon": "Github",
  "description": "Create a new microservice from a template",
  "trigger": {
    "type": "self-service",
    "operation": "CREATE",
    "userInputs": {
      "properties": {
        "service_name": {
          "type": "string",
          "title": "Service Name",
          "pattern": "^[a-z][a-z0-9-]{2,39}$"
        },
        "language": {
          "type": "string",
          "title": "Language",
          "enum": ["go", "python", "typescript"]
        },
        "team": {
          "type": "string",
          "format": "entity",
          "blueprint": "team",
          "title": "Owning Team"
        },
        "tier": {
          "type": "string",
          "title": "Service Tier",
          "enum": ["tier-1", "tier-2", "tier-3"],
          "default": "tier-2"
        }
      },
      "required": ["service_name", "language", "team"]
    }
  },
  "invocationMethod": {
    "type": "GITHUB",
    "org": "acme-corp",
    "repo": "platform-automation",
    "workflow": "scaffold-service.yml",
    "reportWorkflowStatus": true,
    "workflowInputs": {
      "service_name": "{{ .inputs.service_name }}",
      "language": "{{ .inputs.language }}",
      "team": "{{ .inputs.team.identifier }}",
      "tier": "{{ .inputs.tier }}",
      "port_run_id": "{{ .run.id }}",
      "triggered_by": "{{ .trigger.by.user.email }}"
    }
  }
}

GitHub Actions Backend

The GitHub workflow receives Port’s payload via workflow_dispatch inputs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# .github/workflows/scaffold-service.yml
name: Scaffold Service

on:
  workflow_dispatch:
    inputs:
      service_name:
        required: true
        type: string
      language:
        required: true
        type: string
      team:
        required: true
        type: string
      tier:
        required: true
        type: string
      port_run_id:
        required: true
        type: string
      triggered_by:
        required: true
        type: string

jobs:
  scaffold:
    runs-on: ubuntu-latest
    steps:
      - name: Report start to Port
        uses: port-labs/port-github-action@v1
        with:
          clientId: ${{ secrets.PORT_CLIENT_ID }}
          clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
          operation: PATCH_RUN
          runId: ${{ inputs.port_run_id }}
          status: IN_PROGRESS
          logMessage: "Scaffolding ${{ inputs.service_name }}..."

      - name: Create repository from template
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            await github.rest.repos.createUsingTemplate({
              template_owner: 'acme-corp',
              template_repo: 'service-template-${{ inputs.language }}',
              owner: 'acme-corp',
              name: '${{ inputs.service_name }}',
              private: true
            });

      - name: Register entity in Port
        uses: port-labs/port-github-action@v1
        with:
          clientId: ${{ secrets.PORT_CLIENT_ID }}
          clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
          operation: UPSERT
          identifier: ${{ inputs.service_name }}
          blueprint: microservice
          properties: |
            {
              "repo_url": "https://github.com/acme-corp/${{ inputs.service_name }}",
              "language": "${{ inputs.language }}",
              "tier": "${{ inputs.tier }}"
            }
          relations: |
            {
              "team": "${{ inputs.team }}"
            }

      - name: Report success to Port
        if: success()
        uses: port-labs/port-github-action@v1
        with:
          clientId: ${{ secrets.PORT_CLIENT_ID }}
          clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
          operation: PATCH_RUN
          runId: ${{ inputs.port_run_id }}
          status: SUCCESS
          logMessage: "Service ${{ inputs.service_name }} created successfully"
          link: "https://github.com/acme-corp/${{ inputs.service_name }}"

      - name: Report failure to Port
        if: failure()
        uses: port-labs/port-github-action@v1
        with:
          clientId: ${{ secrets.PORT_CLIENT_ID }}
          clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
          operation: PATCH_RUN
          runId: ${{ inputs.port_run_id }}
          status: FAILURE
          logMessage: "Failed to scaffold service"

Backend Types Compared

Backend Type Value Best For
GitHub Actions GITHUB Most CI/CD workflows
GitLab Pipelines GITLAB GitLab-native orgs
Azure DevOps AZURE_DEVOPS Microsoft-heavy shops
Webhook WEBHOOK Jenkins, custom APIs, n8n
Kafka KAFKA Event-driven, async operations
Upsert Entity UPSERT_ENTITY Simple catalog updates, no external backend needed

Webhook Backend Example

For operations backed by Jenkins, n8n, or any HTTP endpoint:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "invocationMethod": {
    "type": "WEBHOOK",
    "url": "https://jenkins.acme.internal/generic-webhook-trigger/invoke",
    "method": "POST",
    "synchronized": false,
    "headers": {
      "Authorization": "Bearer {{ .secrets.JENKINS_TOKEN }}",
      "Content-Type": "application/json"
    },
    "body": {
      "action": "{{ .action.identifier }}",
      "service": "{{ .entity.identifier }}",
      "environment": "{{ .inputs.environment }}",
      "port_run_id": "{{ .run.id }}"
    }
  }
}

Action Approvals

For sensitive operations — delete a service, modify production — Port supports multi-step approval flows. Define approvers by team or role; the action queues until approved, then executes. All approvals and rejections are captured in the audit log.


Scorecards

Scorecards answer the question: “Are our services production-ready, and how do we measure progress?” They evaluate entities against defined rules and surface compliance status across your entire catalog.

How Scorecards Work

A scorecard attaches to a blueprint and defines:

  • Levels — hierarchical stages (default: Basic, Bronze, Silver, Gold)
  • Rules — conditions that must pass to reach a given level
  • Filters — optional JQ expressions to scope which entities are evaluated

An entity starts at Basic and advances only when it passes all rules for a level. This creates a clear progression path and prevents cherry-picking easy wins.

Production Readiness Scorecard (Full Example)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
{
  "identifier": "ProductionReadiness",
  "title": "Production Readiness",
  "blueprint": "microservice",
  "levels": [
    { "color": "red",    "title": "Basic"  },
    { "color": "orange", "title": "Bronze" },
    { "color": "yellow", "title": "Silver" },
    { "color": "green",  "title": "Gold"   }
  ],
  "rules": [
    {
      "identifier": "has_readme",
      "title": "Has README",
      "level": "Bronze",
      "query": {
        "combinator": "and",
        "conditions": [
          {
            "operator": "isNotEmpty",
            "property": "readme"
          }
        ]
      }
    },
    {
      "identifier": "not_stale",
      "title": "Deployed Within 365 Days",
      "level": "Bronze",
      "query": {
        "combinator": "and",
        "conditions": [
          {
            "operator": "isNotEmpty",
            "property": "last_deployed"
          },
          {
            "operator": ">",
            "property": "days_since_deploy",
            "value": 0
          }
        ]
      }
    },
    {
      "identifier": "has_team_owner",
      "title": "Has Team Owner",
      "level": "Silver",
      "query": {
        "combinator": "and",
        "conditions": [
          {
            "operator": "isNotEmpty",
            "property": "$team"
          }
        ]
      }
    },
    {
      "identifier": "recent_deploy",
      "title": "Deployed Within 90 Days",
      "level": "Silver",
      "query": {
        "combinator": "and",
        "conditions": [
          {
            "operator": "<",
            "property": "days_since_deploy",
            "value": 90
          }
        ]
      }
    },
    {
      "identifier": "has_on_call",
      "title": "Has On-Call Defined",
      "level": "Gold",
      "query": {
        "combinator": "and",
        "conditions": [
          {
            "operator": "isNotEmpty",
            "property": "on_call"
          }
        ]
      }
    },
    {
      "identifier": "low_incidents",
      "title": "Fewer Than 5 Open Incidents",
      "level": "Gold",
      "query": {
        "combinator": "and",
        "conditions": [
          {
            "operator": "<",
            "property": "open_incidents",
            "value": 5
          }
        ]
      }
    },
    {
      "identifier": "very_recent_deploy",
      "title": "Deployed Within 30 Days",
      "level": "Gold",
      "query": {
        "combinator": "and",
        "conditions": [
          {
            "operator": "<",
            "property": "days_since_deploy",
            "value": 30
          }
        ]
      }
    }
  ]
}

Scorecard Operators

Operator Meaning
= Equals
!= Not equals
< Less than
> Greater than
<= Less than or equal
>= Greater than or equal
contains String contains
doesNotContain String does not contain
isEmpty Property is null or empty
isNotEmpty Property has a value
in Value is in array
notIn Value is not in array

DORA Metrics Scorecard

Port provides a built-in DORA metrics integration. Once you’ve connected GitHub (for deployment data) and PagerDuty (for incident data), you can configure a DORA scorecard benchmarked against the official DORA report thresholds:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
{
  "identifier": "DoraMetrics",
  "title": "DORA Metrics",
  "blueprint": "microservice",
  "levels": [
    { "color": "red",    "title": "Low"   },
    { "color": "orange", "title": "Medium" },
    { "color": "yellow", "title": "High"   },
    { "color": "green",  "title": "Elite"  }
  ],
  "rules": [
    {
      "identifier": "deployment_frequency_medium",
      "title": "Deploys At Least Once Per Week",
      "level": "Medium",
      "query": {
        "combinator": "and",
        "conditions": [
          {
            "operator": ">=",
            "property": "deployments_last_30_days",
            "value": 4
          }
        ]
      }
    },
    {
      "identifier": "lead_time_high",
      "title": "Lead Time Under 1 Week",
      "level": "High",
      "query": {
        "combinator": "and",
        "conditions": [
          {
            "operator": "<",
            "property": "avg_lead_time_days",
            "value": 7
          }
        ]
      }
    },
    {
      "identifier": "deployment_frequency_elite",
      "title": "Deploys Multiple Times Per Day",
      "level": "Elite",
      "query": {
        "combinator": "and",
        "conditions": [
          {
            "operator": ">=",
            "property": "deployments_last_30_days",
            "value": 60
          }
        ]
      }
    }
  ]
}

Scorecard Automation

Scorecards can trigger automations. When a service degrades below a threshold (e.g., drops from Silver to Bronze), Port can automatically open a Jira ticket, send a Slack notification to the owning team, or trigger a GitHub Action to run a health check. This closes the loop from “we see the problem” to “someone is fixing it.”


Key Integrations

Port’s integration catalog covers over 50 systems. Here’s what the major ones provide.

GitHub

  • Repositories, branches, pull requests, issues, releases, tags
  • Workflow runs and workflow definitions
  • Code scanning alerts and Dependabot alerts
  • Deployments and environments
  • File ingestion (parse YAML/JSON from repos into entities)
  • Real-time sync via GitHub App webhooks

Configuration is per-repo (port-app-config.yml in .github/) or org-wide (.github-private/).

GitLab

  • Projects, groups, merge requests, pipelines
  • File kind support (similar to GitHub file ingestion)
  • GitLab V2 integration simplifies token setup and improves performance
  • Periodic sync and real-time webhook mode

Kubernetes

  • Namespaces, nodes, pods, deployments, DaemonSets, StatefulSets, ReplicaSets, services
  • CRDs from ArgoCD, Istio, Knative, OpenShift
  • Deployed as Helm chart in-cluster, watches the Kubernetes API
  • Automatic health status (compares desired vs available replicas)
  • Container security analysis (privileged mode, resource limits)

Terraform

  • Track Terraform Cloud/Enterprise workspaces and runs
  • Import infrastructure resources (EC2, RDS, S3, etc.) into the catalog
  • Terraform provider for managing Port’s own data model as code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
terraform {
  required_providers {
    port = {
      source  = "port-labs/port-labs"
      version = "~> 2.0"
    }
  }
}

provider "port" {
  client_id     = var.port_client_id
  client_secret = var.port_client_secret
}

resource "port_blueprint" "microservice" {
  title      = "Microservice"
  identifier = "microservice"
  icon       = "Microservice"

  properties = {
    string_props = {
      "repo_url" = {
        title  = "Repository URL"
        format = "url"
      }
      "language" = {
        title  = "Language"
        enum   = ["Go", "Python", "TypeScript"]
      }
    }
    number_props = {
      "open_incidents" = {
        title = "Open Incidents"
      }
    }
  }
}

PagerDuty

  • Services, incidents, schedules, on-call assignments, escalation policies, users
  • Service analytics: MTTR, mean time to first acknowledge
  • Incident analytics per service
  • Real-time sync via webhooks for incident state changes
  • Critical for on-call visibility: surface oncall_user from PagerDuty as a property on Service blueprints

AWS, GCP, Azure

Port has cloud integrations that ingest:

  • AWS: EC2 instances, S3 buckets, ECR repositories, CloudFormation stacks, EKS clusters
  • GCP: Projects, compute instances, GCS buckets
  • Azure: Subscriptions, resource groups, AKS clusters, Azure DevOps work items

AWS integration supports a “hosted-by-Port” mode (OAuth, minimal setup) and self-hosted mode.

ArgoCD

  • Applications, projects, deployment status
  • Maps ArgoCD Application resources to Port entities
  • Correlate ArgoCD deployment status with your service catalog entries

Security Integrations

  • Snyk: Vulnerabilities, projects, license compliance
  • SonarQube/SonarCloud: Code quality metrics, coverage, technical debt
  • Wiz: Cloud security findings, misconfigurations
  • Trivy: Container vulnerability scanning results

The Ocean Integration Framework

Ocean is the open-source Python framework that powers all of Port’s integrations. It’s available on GitHub (port-labs/ocean) under Apache 2.0. If Port doesn’t have a native integration for your tool, Ocean is how you build one.

Architecture

Ocean has two layers:

  1. Ocean Core (port_ocean/) — the shared infrastructure: Port API authentication, entity ingestion pipelines, JQ transformation engine, event listeners (polling, webhooks, Kafka), resync orchestration
  2. Integrations (integrations/) — individual implementations for each third-party system

An integration developer writes only the third-party system logic: how to authenticate, how to fetch resources, how to handle webhooks. Ocean Core handles everything else.

How an Integration Works

Every Ocean integration follows a lifecycle:

  1. Startup: Load configuration, authenticate with the external system
  2. Initial resync: Fetch all current resources, map them with JQ, push to Port
  3. Event listening: Subscribe to webhooks or poll for changes
  4. Real-time updates: Push delta changes to Port as events arrive
  5. Periodic resync: Re-run full sync on a configurable schedule

Building a Custom Integration

An Ocean integration has this structure:

my-integration/
├── main.py                    # Core logic
├── integration.py             # Configuration schema
├── pyproject.toml             # Dependencies
└── .port/
    ├── spec.yaml              # Integration metadata
    └── resources/
        ├── blueprints.json    # Default blueprints to create
        └── port-app-config.yaml  # Default entity mappings

A minimal main.py for a custom system:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from port_ocean.context.ocean import ocean
from port_ocean.core.ocean_types import ASYNC_GENERATOR_RESYNC_TYPE

# Resync handler: called on startup and periodic resync
@ocean.on_resync("project")
async def resync_projects(kind: str) -> ASYNC_GENERATOR_RESYNC_TYPE:
    async for page in my_client.get_projects_paginated():
        yield page  # List of raw objects; Ocean applies JQ mappings

# Webhook handler: called when external system sends event
@ocean.router.post("/webhook")
async def handle_webhook(request: dict) -> None:
    event_type = request.get("action")

    if event_type in ("created", "updated"):
        project = await my_client.get_project(request["project_id"])
        await ocean.register_raw("project", [project])

    elif event_type == "deleted":
        await ocean.unregister_raw("project", [{"id": request["project_id"]}])

The port-app-config.yaml defines the JQ mappings — no hardcoded field names in the Python code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
resources:
  - kind: project
    selector:
      query: "true"
    port:
      entity:
        mappings:
          identifier: .id | tostring
          title: .name
          blueprint: '"project"'
          properties:
            status: .status
            owner: .owner.email
            created_at: .created_at
            url: .web_url

Running an Ocean Integration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Install Ocean CLI
pip install port-ocean[cli]

# Create a new integration scaffold
ocean new my-integration

# Run locally for development
ocean sail

# Or deploy as Docker container
docker run -e PORT_CLIENT_ID=xxx -e PORT_CLIENT_SECRET=xxx \
  -e OCEAN__SECRET__MY_API_TOKEN=yyy \
  port-labs/my-integration:latest

Port vs. Backstage: Honest Comparison

Both tools solve the same problem but make fundamentally different trade-offs.

Philosophy

Aspect Port Backstage
Type SaaS product Open-source framework
Deployment Port-hosted Self-hosted (you manage it)
Data model Flexible, UI/API-driven YAML files in repos (catalog-info.yaml)
Extensibility Configuration-first Code-first (plugins)
Infrastructure cost SaaS subscription Compute + PostgreSQL + maintenance

Feature-by-Feature

Software Catalog

Both handle this well. Backstage uses catalog-info.yaml files committed alongside code — the catalog is Git-backed by default, which some teams strongly prefer for auditability. Port uses integrations and API pushes — the catalog is dynamic and can reflect real-time state (current pod counts, active incidents) rather than just what was committed.

Self-Service / Scaffolding

Backstage’s Software Templates are purpose-built for scaffolding: define a Nunjucks template, wire up a multi-step wizard, and Backstage creates files, repos, and catalog entries. It’s polished for the “create a new service” use case specifically.

Port’s actions are more general-purpose. You’re wiring Port’s UI to your existing GitHub Actions, Jenkins pipelines, or webhooks. More flexibility, slightly more setup. Port doesn’t have a built-in template rendering engine — you bring your own scaffolding logic in the workflow.

Documentation (TechDocs)

Backstage wins here. TechDocs is a first-class feature: point Backstage at a mkdocs.yml, and it renders your docs inside the portal. Engineers write Markdown, Backstage handles the rest.

Port supports Markdown properties and embedded URLs, but there’s no equivalent to TechDocs. Documentation lives outside Port; you link to it.

Scorecards

Port wins. Scorecards are native, configurable, and automatically evaluated against live catalog data. Backstage requires third-party plugins (Cortex, Roadie) or custom development for equivalent functionality.

Kubernetes Visibility

Port wins in practice. The Kubernetes integration installs cleanly via Helm and syncs cluster resources in minutes. Backstage’s Kubernetes plugin requires more configuration and frequently needs TLS certificate adjustments to work in real clusters.

Setup Time

Port is significantly faster to get to value. A working catalog with GitHub + Kubernetes integrations can be done in a day. Backstage typically takes 6–12 months to reach the same level of customization and coverage, and requires ongoing maintenance as Backstage versions update.

Customization Ceiling

Backstage’s ceiling is higher. Because it’s a React/Node.js framework, you can build anything — custom UI components, novel backend integrations, arbitrary data flows. Port’s configurability is deep (custom blueprints, JQ transformations, custom Ocean integrations) but you’re constrained to Port’s data model and UI paradigms.

Cost

Backstage is open-source (free), but “free” hides the engineering cost of building and maintaining it. Port costs money directly (see pricing section), but has a predictable cost model and no infrastructure to maintain.

RBAC

Both support role-based access control. Port’s RBAC is configured through the UI and supports entity ownership-based permissions. Backstage’s permissions system is more complex to configure but more flexible for unusual access patterns.

When to Choose Port

  • Your platform team is small (under 5 FTEs)
  • You need a working portal in weeks, not months
  • You want to avoid maintaining a React/Node.js app
  • You value real-time catalog data (current incidents, live cluster state)
  • You want native scorecards without additional plugins
  • Budget is available for SaaS

When to Choose Backstage

  • You have strong open-source requirements or data sovereignty constraints
  • You need deep UI customization beyond what Port’s interface designer supports
  • Documentation-as-code (TechDocs) is a core requirement
  • You have a dedicated platform engineering team with React experience
  • You need a feature that doesn’t exist in Port and won’t be custom-integrated
  • The 200+ community plugins cover your specific tech stack

Getting Started

1. Create a Free Account

Go to app.getport.io. No credit card required. The free tier gives you up to 15 seats, 10,000 entities, and 500 automation runs per month.

2. Define Your Data Model

Port’s onboarding wizard offers pre-built templates for common stacks (Kubernetes, GitHub, cloud resources). Start with a template and customize, or start from scratch in the Builder.

Navigate to Builder in the left sidebar. You’ll see default blueprints Port created. Add properties, customize relations, and define your entity types.

3. Install Integrations

Go to Builder → Integrations and click Add Integration. For most integrations, choose “Hosted by Port” to have Port run the integration process:

  • GitHub: Installs a GitHub App — authorize Port to read your org, configure port-app-config.yml or use the UI global config
  • Kubernetes: Generates a helm install command pre-populated with your credentials
  • PagerDuty: OAuth flow — authorize Port to read your PagerDuty org

4. Verify the Catalog

After integrations run their initial sync (minutes for GitHub/PagerDuty, a few minutes for Kubernetes), check Catalog in the left sidebar. You should see entities appearing under your blueprints.

5. Create Your First Action

In Builder → Self-Service, click New Action. Walk through the wizard:

  1. Choose the blueprint (entity type) the action applies to
  2. Define input properties (the form developers fill out)
  3. Choose backend (GitHub, webhook, etc.)
  4. Configure the invocation payload
  5. Save and test

6. Create a Scorecard

In Builder, select a blueprint → click Scorecards tab → New Scorecard. Define your levels and add rules. Port evaluates all existing entities immediately and shows compliance across your catalog.

Managing Port as Code

For production environments, manage Port’s data model through Terraform or the API rather than clicking around the UI:

1
2
3
4
5
6
7
8
# Initialize Terraform with Port provider
terraform init

# Plan data model changes
terraform plan

# Apply blueprint/relation/scorecard changes
terraform apply

Port’s Terraform provider resource types: port_blueprint, port_action, port_scorecard, port_entity, port_team, port_webhook.


Real-World Use Cases

Software Catalog

The starting point for most Port deployments. Connect GitHub (repositories + teams), Kubernetes (cluster resources), and PagerDuty (on-call + incidents). Within hours you have:

  • Every service listed with its repo URL, language, owning team, and on-call engineer
  • Each service linked to its running Kubernetes deployments
  • Current open incidents surfaced directly on the service page
  • No more “who owns this?” searches across Slack, GitHub, and Confluence

Developer Self-Service: Day-2 Operations

Beyond scaffolding, Port shines for day-2 operations. Common action patterns:

  • Scale a deployment: Developer selects a service, inputs desired replica count → GitHub Action calls kubectl scale and reports back
  • Trigger a deployment: Developer selects service + environment → GitHub Action runs the deployment pipeline
  • Create a feature flag: Developer inputs flag name + rollout % → webhook calls LaunchDarkly API
  • Add a DNS record: Developer inputs subdomain → Terraform action provisions in Route53 → entity created in catalog
  • Create a database: Developer selects service → GitHub Action runs Terraform to provision RDS instance → links to service entity

Each of these replaces a ticket, a Slack message, or direct infrastructure access.

On-Call Management

With PagerDuty integrated:

  • Every service shows its current on-call engineer and open incidents
  • Scorecards flag services with more than N open incidents
  • Dashboards show on-call load per team
  • Actions let engineers acknowledge, escalate, or resolve incidents from Port without opening PagerDuty

DORA Metrics Dashboard

Connect GitHub (deployment events) and PagerDuty (incident data). Port’s DORA guide walks you through creating calculation properties for:

  • Deployment Frequency: deployments per day/week aggregated from GitHub workflow runs
  • Lead Time for Changes: time from PR open to deployment, from PR data
  • Change Failure Rate: deployments followed by incidents within N hours
  • Mean Time to Recovery: incident open-to-resolve duration from PagerDuty

Create a scorecard against official DORA benchmarks (Low/Medium/High/Elite). Build a dashboard showing DORA scores per team. Share with leadership as a live engineering intelligence report.

Production Readiness Reviews

Before a service goes to production, use a scorecard to enforce:

  • Has README (Bronze)
  • Has owning team (Bronze)
  • Branch protection enabled (Silver)
  • Code owners defined (Silver)
  • On-call schedule exists (Gold)
  • Fewer than 3 open Snyk high-severity findings (Gold)
  • Deployed within 30 days (Gold)

Services that don’t meet Gold can’t be promoted to production — enforced via a Port automation that fires when a service’s tier is set to tier-1 but its scorecard level is below Silver.


Pricing

Port’s pricing is per-seat (users who log into the portal):

Tier Price Seats Entities Automation Runs
Free $0 Up to 15 Up to 10,000 500/month
Basic ~$30/seat/mo Up to 50 Up to 50,000 Standard
Standard ~$40/seat/mo Up to 200 Up to 250,000 Up to 2,000/month
Enterprise Custom Unlimited Custom (1M+) Custom

Key Free tier details:

  • No credit card required, no expiration
  • Full platform access (blueprints, actions, scorecards, all integrations)
  • Community support via Slack and documentation
  • 10,000 entities is enough for a medium-sized org: hundreds of services, repos, pipelines, and K8s resources

Standard adds SSO (SAML/OIDC), dynamic permissions (entity-level RBAC), and 5 workspaces (isolated environments for different teams or stages).

Enterprise adds SCIM provisioning (auto-sync users from Okta/Azure AD), Private Link, IP allowlisting, and 99.9% SLA with 4-hour critical support response.

Entity count is the main scaling constraint to watch. A single Kubernetes cluster with 200 services, 400 deployments, 600 pods, 10 nodes, and 20 namespaces generates ~1,230 entities. A large org with 10 clusters, 500 services, and full PagerDuty + GitHub sync can push toward 50,000 entities.


Limitations and Gotchas

Data sovereignty: Port is SaaS — your entity data lives in Port’s infrastructure. If your security posture requires on-premises data storage, Port isn’t the right fit (Backstage self-hosted is).

JQ learning curve: Port’s transformation layer is JQ throughout. If your team isn’t familiar with JQ, there’s a learning curve for writing selectors and mappings. Port’s UI helps, but complex transformations require JQ knowledge.

Static rule values in scorecards: Scorecard rules compare properties against static values only — you can’t write property_a > property_b. Work around this with calculation properties that compute the comparison result as a boolean.

GitHub workflow input limit: GitHub’s workflow_dispatch trigger supports maximum 10 inputs. For actions requiring more parameters, bundle them into a single JSON object input.

Scorecard identifier immutability: Once you create a scorecard with a given identifier, you can’t change it. Renaming requires creating a new scorecard and deleting the old one.

Integration sunset: The legacy GitHub integration sunsets July 15, 2026. If you’re running it, migrate to the Ocean-powered GitHub integration before then.


Summary

Port solves the “build vs. buy” dilemma for internal developer portals decisively: most organizations should not build and maintain a custom Backstage deployment unless they have a specific reason to. Port’s blueprint system is flexible enough to model any engineering organization’s reality, and the Ocean integration framework covers essentially everything in a modern tech stack.

The free tier is genuinely useful — 10,000 entities and full platform access means you can build a real, production-worthy catalog for a mid-sized organization without spending anything. The SaaS model means zero infrastructure overhead: no Backstage React app to keep current, no PostgreSQL to manage, no Node.js deployment pipeline to maintain.

The trade-off is real: Port is not open-source, and your data lives in Port’s cloud. For organizations with strict data sovereignty requirements, Backstage remains the answer. For everyone else, Port is the fastest path to a working, maintained internal developer portal that developers actually use.

Start with app.getport.io, connect your GitHub integration, and you’ll have a live service catalog in under an hour. The blueprint builder and scorecard system make it easy to grow from there into a full platform engineering hub.

Comments