Dan Matthew

Program note · Writing · 16 Aug 2026

A deploy provider should not know what language it is deploying

Packkit generators emit a language-neutral deployment contract. A provider reads that contract and deploys it, and never asks whether a Node, Python, or Go program wrote it.

#packkit #deployment #aws #terraform #architecture

Most deploy tooling I have used is language-shaped. It knows how to ship a Node app, or a Python app, and the knowledge is baked in so deep that adding a language means teaching the deployer a new dialect. I did not want that in Packkit, because the whole point of the platform is that a Node service, a Python service, and a Go service are the same kind of thing wearing different clothes.

So the deployers, which I call providers, are built on one rule: a provider decides what it can deploy by reading the deployment contract, never by looking at the language. This post is how that plays out with two very different providers, one that calls an API and one that writes infrastructure code.

The contract is the whole interface

Every generator returns files plus a small tagged object that says what was built: static, service, or worker. That object is the only thing a provider sees. A React app is static. A FastAPI app, an Express app, and a Go net/http app are all service, differing only in a runtime string. A background job in any language is worker.

A provider implements a tiny contract of its own: which deployment types it supports, and a prepare or plan that turns a supported project into a deployment. Core ships a runProviderConformanceSuite, the same idea as the generator suite, so a provider either behaves like a Packkit provider or it does not.

contract: static contract: service contract: worker provider reads the type S3 plus CloudFront App Runner ECS Fargate
⤢ Enlarge

Two providers, same interface, different mechanics

The Netlify provider is API driven. It can plan and it can apply, because Netlify has an API you call to make a deploy happen. You hand it a static contract and it does the work.

The AWS provider is deliberately different. It does not call AWS at deploy time and it never holds your credentials. It reads the contract and emits Terraform, plus a GitHub Actions workflow that runs the Terraform using short-lived OIDC, no long-lived keys stored anywhere. The provider's job is to write correct infrastructure code, and the pipeline runs it. That keeps the provider credential-free, which for a tool that other people will point at their own AWS accounts felt like the only defensible design.

Here is where the one code path pays off. The AWS provider maps a contract to an archetype, and dispatches on the archetype, not the language:

  • static becomes a private, versioned S3 bucket behind CloudFront with Origin Access Control. The Terraform state uses S3 native locking, so there is no DynamoDB table to stand up and pay for.
  • service becomes AWS App Runner, not Fargate behind a load balancer. App Runner maps cleanly to the service contract, it gives you managed HTTPS, and it has no VPC, NAT, or ALB sitting there billing you when nothing is happening.
  • worker becomes ECS Fargate on a minimal VPC with public subnets and an egress-only security group, so there is no NAT gateway, which is the roughly thirty dollars a month you pay by accident on your first ECS setup.

Because the dispatch is on the archetype, one provider deploys a Node service, a Python service, and a Go service from the same code path. That is the payoff of making core forget languages: the thing at the far end of the pipeline forgets them too.

What is still rough

The AWS provider only plans and emits, it does not apply. There is no runtime AWS call in it at all, by choice, because holding cloud credentials to run applies is a responsibility I did not want the library to carry. The deploy path is the emitted pipeline running under your own OIDC. An injected apply runner is on the roadmap, but it stays optional and off by default.

The other honest limit is that the emitted infrastructure is opinionated toward cheap and simple, App Runner over Fargate, no NAT, price-class-100 CloudFront. That is the right default for a portfolio or a small service and the wrong one for something that needs a VPC and private subnets. When it is wrong, it is wrong in a readable Terraform file you own and can edit, which is the best I can offer a generated starting point.

Both providers pass the conformance suite and live under the PackkitLabs org as provider-netlify and provider-aws.