AI-narrated article. Prose is preserved with pronunciation adjustments; code and image cues refer to the written article.
Build an Internal Developer Platform End to End: From Service Request to Owned Runtime
This is the AI-narrated article. The prose follows the written post; pronunciation is adjusted, and code and images are announced separately.
Build a platform path that survives partial failure: authorize service requests, scaffold an owned repository, connect reviewed delivery, and expose precise runtime status.
TL;DR: An internal developer platform must connect service requests to owned delivery and runtime operations. This article defines admission, repository, CI, GitOps, and operational boundaries, then provides a complete Backstage repository-onboarding template. It separates catalog ownership from authorization and repository creation from deployment success. Practical guidance covers backend credentials, reviewed template revisions, Argo CD project restrictions, partial-success recovery, durable operations, and retirement policies. The template is a bounded onboarding example with explicit prerequisites; the architecture is a design proposal, and no production deployment or sandbox trial is claimed.
The platform is finished when the service can be operated
A developer clicks Create Service. The repository appears, the catalog card looks polished, and the platform reports success. A week later, the team discovers that nobody owns the alert route, deployment permissions are broader than expected, and a failed provisioning step must be repaired by hand.
The gap is the service lifecycle. An internal developer platform needs a coherent path from authenticated intent through owned delivery and runtime evidence. Build one complete path for one service class before adding more portal features.
This article proposes a Backstage, GitHub, and Argo CD architecture, then provides a complete repository-onboarding template. It does not claim that the template alone creates a production service.
Start with a contract that survives the portal
Define the product: a team can create a private service repository, receive a reviewed delivery configuration, deploy to an approved environment, and find operational ownership from the catalog.
Table. Columns: Boundary, Owner, Evidence of completion.
Row 1. Boundary: Request admission; Owner: Platform team; Evidence of completion: Authorized requester, approved target, operation ID.
Row 2. Boundary: Repository creation; Owner: Platform integration; Evidence of completion: Repository URL and template revision.
Row 3. Boundary: Build and review; Owner: Service team; Evidence of completion: Required checks and reviewed artifact.
Row 4. Boundary: Runtime reconciliation; Owner: Delivery platform; Evidence of completion: Desired revision, sync and health state.
Row 5. Boundary: Operation; Owner: Service team; Evidence of completion: Alert route, runbook, SLO, escalation.
End of table.
Keep the service identifier stable across the catalog, repository, deployment manifests, and telemetry. Assign the team before provisioning resources. A catalog owner relation helps discovery; it does not authorize repository writes or Kubernetes access.
Use a backend admission boundary for privileged requests. Resolve the signed-in principal, validate membership and allowed environments, reserve the target identity, then record an operation. A client-supplied owner or namespace must never become authority merely because it passed a form validator.
Image. Platform architecture with request admission, delivery execution, and separate status projections
Put credentials where the decisions are enforced
Backstage supports Scaffolder permissions for tasks, parameters, steps, and actions. Use those controls alongside target-specific authorization in the backend or custom actions. Hiding a field or limiting a picker improves the interface; it is not sufficient enforcement for direct API calls.
Use a GitHub App integration where its permissions and installation scope fit the operation. Keep credentials in backend configuration or a secret manager. Restrict installed repositories and permissions to the required actions. A new-repository workflow may need organization-level scope; review that separately from ordinary writes to existing repositories.
CI should build and attest artifacts with its own bounded identity. The GitOps controller should deploy from approved repositories into approved destinations. Neither should inherit the portal integration's credentials.
A complete repository-onboarding example
The example uses the v1beta3 template API documented by Backstage. Use an existing Backstage installation aligned with the 1.55.3 release, with Scaffolder and Catalog enabled. Check your application lockfile and installed action schemas before adopting it; Backstage plugins have their own package versions.
Prerequisites are an existing GitHub integration authorized to create private repositories in your chosen organization, existing group:default/checkout and group:default/platform catalog entities, and a trusted template repository. Replace example-org with your permitted organization. Do not use production integration credentials for the first trial.
Install the GitHub action module using the built-in action instructions and align dependencies through your application's normal release process:
Code example. Refer to the written article for the complete code.
Add the module to the existing backend entry point, retaining its other registrations:
Code example. Refer to the written article for the complete code.
Confirm fetch:template, publish:github, and catalog:register appear on your instance's /create/actions page. Place template.yaml beside a skeleton directory in the trusted template repository:
Code example. Refer to the written article for the complete code.
Save skeleton/catalog-info.yaml:
Code example. Refer to the written article for the complete code.
Save skeleton/README.md:
Code example. Refer to the written article for the complete code.
The entity descriptor specification defines the Component fields. Experimental describes this example's lifecycle; it is not a security restriction.
Import the template URL through the catalog's registration flow, ensuring your catalog rules allow Template entities. Use a reviewed commit reference for the template and skeleton so a moving branch cannot silently change the generated service. The template authoring guide explains relative skeleton URLs and step outputs.
First inspect rendered files using the template editor's dry-run mode. Dry-run behavior depends on action support; verify each installed action's implementation, especially custom actions. Then perform one authorized trial in a sandbox organization and confirm repository visibility, descriptor content, and catalog processing. This article records static review, not a claimed trial result.
For cleanup, unregister the test catalog location and archive the trial repository through the owning team's process. Unregistering an entity does not remove its repository or cloud resources. Remove the template location if the pilot ends; retain task records needed for audit.
Extend onboarding through reviewed delivery
The next platform increment should add an approved application skeleton and CI contract: dependency checks, tests, immutable artifact identity, and deployment manifests. Require reviews on delivery configuration before exposing production targets.
For Argo CD, define an AppProject with specific source repositories, destination clusters/namespaces, and allowed resource kinds. The project documentation describes those restrictions. Avoid treating the permissive default project as a production tenancy boundary.
Namespace restrictions need careful interpretation: permission to deploy into the namespace hosting Argo CD itself can expose administrative power. Enforce cluster-scoped resource policy separately, and verify destination permissions against the controller's actual Kubernetes identity.
Publish distinct status fields in the portal: repository created, checks passed, desired revision accepted, deployment healthy, and ownership complete. A successful Git write must not imply a healthy workload.
Recover the partial success without creating a second service
Consider an illustrative failure: publish:github succeeds, but catalog registration fails because the integration cannot read the generated descriptor. Retrying the whole template can hit an existing-repository error, obscuring the successful first step.
Keep the original task and repository URL. Check the descriptor's presence, default branch, catalog-reader credentials, and entity validation errors. Repair the read path, then register that same location. Do not delete the repository to make a green task screenshot.
For workflows spanning infrastructure, deployment, and external APIs, design durable operations with checkpoints and idempotency. Reserve a unique service/environment target, persist external identifiers immediately, and make retries reconcile those identifiers. A Scaffolder step sequence is not an atomic transaction across GitHub and the catalog.
Choose compensation deliberately. Reverting a delivery change, unregistering a location, and destroying a database have different ownership and data implications. Require explicit retention and deletion policies before adding a Delete Service button.
Operate the platform as a product
Prioritize the requests that consume team time: service creation, environment access, promotion, credentials, and retirement. Track task failures by boundary, time spent waiting for ownership decisions, and unsupported paths. Count a service as operational only when its runtime and ownership criteria are met.
Keep a documented escape path for supported exceptions. A platform that cannot explain a failed request will move work back into chat and tickets, regardless of how attractive the portal is.
FAQ
Is Backstage the entire internal developer platform? It can provide discovery and workflow entry points. Delivery, authorization, infrastructure state, and runtime operations still need explicit owners and implementations.
Can a template's owner field enforce access? No. It describes ownership; backend permission checks must enforce the caller's authority.
Should a failed task always be retried from the beginning? No. Inspect persisted external outcomes first, then recover the failed boundary without duplicating successful resources.