Skip to content

Platform as Product

Platform teams face a fundamental tension: they need to provide stable, secure infrastructure to downstream teams while maintaining the velocity to iterate and improve. Traditional approaches force a choice—either lock everything down and slow everyone down, or give teams direct access and risk chaos.

Yaffle enables a third path: live infrastructure as an API surface.

This isn’t about publishing reusable Terraform modules. Platform teams don’t hand downstream teams a “blessed way to run RDS” and wish them luck. Instead:

Platform teams run the infrastructure. Downstream teams consume its outputs.

Platform Team Workspaces Running Infrastructure Yaffle Registry Product Teams manages manages manages subnet_ids db_endpoint redis_url network workspace database workspace cache workspace VPC + Subnets RDS Instance ElastiCache Live Outputs Team A Team B

When a product team references the platform’s database workspace, they get:

module "platform_database" {
source = "yaffle.dev/acme--platform/platform--database/yaffle"
}
resource "aws_ecs_service" "api" {
environment = [
{
name = "DATABASE_URL"
value = module.platform_database.connection_string # Live RDS endpoint
}
]
}

That connection_string isn’t a template—it’s the actual endpoint of a running RDS instance that the platform team manages.

The platform team’s workspace outputs define the API surface:

platform/database/outputs.tf
output "connection_string" {
description = "PostgreSQL connection string (without credentials)"
value = "postgresql://${aws_db_instance.main.endpoint}/${aws_db_instance.main.db_name}"
}
output "security_group_id" {
description = "Security group to allow database access"
value = aws_security_group.db_access.id
}
output "secret_arn" {
description = "Secrets Manager ARN for database credentials"
value = aws_secretsmanager_secret.db_creds.arn
}

Downstream teams don’t need to know:

  • What RDS instance class is used
  • How backups are configured
  • What the maintenance window is
  • How replicas are set up

They just need the connection string and a way to authenticate.

The cross-repo contract is producer-owned in yaffle.toml:

[[workspaces]]
path = "platform/database"
environments = ["*"]
outputs.connection_string = { visibility = "public", consumers = ["applications/apps/*"] }
outputs.secret_arn = { visibility = "public", consumers = ["applications/apps/*"] }

That gives the platform team a clean product boundary:

  • same-repo platform workspaces can still consume all outputs
  • cross-repo product teams only see the curated public outputs
  • cross-org module sharing is not supported

Each named environment runs its own copy of the platform infrastructure:

# platform team's yaffle.toml
[[environments]]
name = "dev"
[[environments]]
name = "staging"
[[environments]]
name = "prod"
[[workspaces]]
path = "database"
environments = ["*"]

When the platform applies to staging, the database workspace creates/updates a staging RDS instance. The outputs for staging reflect that specific instance.

When a product team’s workspace runs in staging, they automatically get staging outputs:

# This resolves to the staging RDS endpoint when running in staging,
# and the prod RDS endpoint when running in prod
module "platform_database" {
source = "yaffle.dev/acme--platform/platform--database/yaffle"
}

No environment switching. No variable juggling. The right infrastructure for the right environment, automatically.

When a platform engineer opens a PR to change the database configuration:

  1. Yaffle creates an isolated preview environment (pr-42)
  2. The database workspace applies to preview infrastructure—a separate RDS instance
  3. Downstream teams are unaffected—they still consume stable named environment outputs
  4. Platform team validates changes against real infrastructure
  5. On merge, changes promote to the target environment
Preview Staging Prod PR opened merge to main promotion always consume always consume Platform Engineer Product Teams pr-42 RDS staging RDS prod RDS

Product teams never see preview infrastructure. They consume stable, named environments while the platform team iterates safely.

A typical platform-as-product offers:

WorkspaceOutputs consumed by product teams
networkvpc_id, private_subnet_ids, public_subnet_ids
databaseconnection_string, secret_arn, security_group_id
cacheredis_url, redis_port
dnszone_id, domain_name
monitoringlog_group_name, metrics_namespace
secretskms_key_arn

Product teams compose these outputs into their own infrastructure:

module "platform" {
source = "yaffle.dev/acme--platform/platform--network/yaffle"
}
module "platform_db" {
source = "yaffle.dev/acme--platform/platform--database/yaffle"
}
resource "aws_ecs_service" "api" {
network_configuration {
subnets = module.platform.private_subnet_ids
security_groups = [module.platform_db.security_group_id]
}
}

Platform teams stay fast because:

  1. Preview environments are real infrastructure—test against actual RDS, not mocks
  2. No coordination required—previews are isolated from consumers
  3. Promotion is explicit—merge to main triggers staging, manual promotion to prod

Product teams stay unblocked because:

  1. No tickets to file—consume outputs directly
  2. No surprises—named environment outputs only change on explicit applies
  3. No configuration drift—everyone gets the same VPC, same subnets, same security groups
platform-infra/
├── network/ # VPC, subnets, security groups
│ ├── main.tf
│ └── outputs.tf # Contract: vpc_id, subnet_ids, etc.
├── database/ # RDS instances
│ ├── main.tf
│ └── outputs.tf # Contract: connection_string, secret_arn
├── cache/ # ElastiCache
│ ├── main.tf
│ └── outputs.tf # Contract: redis_url
└── yaffle.toml
yaffle.toml
[[environments]]
name = "dev"
[[environments]]
name = "staging"
[[environments]]
name = "prod"
[[workspaces]]
path = "network"
environments = ["*"]
[[workspaces]]
path = "database"
environments = ["*"]
[[workspaces]]
path = "cache"
environments = ["*"]
# Triggers
[[triggers.github.push]]
branch = "main"
environment = "staging"
[[triggers.github.pull_request]]
branch_pattern = "*"

Yaffle enables platform teams to operate as product teams—but the product is running infrastructure, not reusable modules:

Traditional IaCPlatform as Product with Yaffle
Publish module, teams instantiate their ownRun infrastructure, teams consume outputs
”Here’s how to create an RDS instance""Here’s your database endpoint”
Teams manage their own statePlatform manages state, teams consume results
Drift between team configurationsConsistency across all consumers
Coordination to change shared patternsPreview environments for safe iteration

The result: platform teams own the infrastructure lifecycle end-to-end, downstream teams get stable APIs to build on, and everyone ships with confidence.