Launchpad
A multi-tenant platform design that turns AI-builder / vibe-coded apps into production ECS Fargate deployments, for owners who aren't engineers.
A capstone-scale system-design project from the same DevOps Institute cohort as Procal on EKS. Launchpad rents out its own AWS account: every user's app runs as an ECS Fargate service billed to Launchpad, not to the user, which is why the design leans so heavily on strict cost gating and a resource ledger that can never lose track of what actually exists in AWS. The focus here is the infrastructure layer: the control-plane/data-plane split, the provisioning engine, and the pipeline that turns an upload into a running service.
A pipeline with a cost gate
- 1
Login & register
GitHub OAuth. No password, no Cognito.
- 2
Upload & scan
The app's source (zip or GitHub repo) goes to S3, then deterministic marker-file detection (package.json, go.mod, and so on) identifies the runtime. Zero billable AWS resources so far.
- 3
Config generated
A Dockerfile and runtime config are generated from a per-runtime template, then optionally reviewed by an AI pass for framework-specific gaps the template doesn't know about. Still zero commitment.
- 4
Paymentbillable
A subscription is created and a payment method verified. This is the gate: nothing past this point runs without an active subscription, enforced in SQL with an EXISTS clause, not just app-layer code.
- 5
Build imagebillable
CodeBuild builds the image and pushes it to a per-application ECR repository.
- 6
Infra provisioningbillable
Step Functions calls the AWS SDK directly to create the task definition, target group, listener rule, and ECS service for this one application.
- 7
Deploybillable
The service comes up behind the shared ALB and becomes reachable.
- 8
Live & dashboard
CloudWatch-backed health and metrics, surfaced back to the app owner.
Stages 1–3 are ordinary API requests. Stages 4–7 are meant to run as one Step Functions execution, kicked off by a single endpoint: POST /applications/{id}/deployments, the only route that starts a job with a real AWS bill attached.
Control plane and data plane
Control plane — Launchpad itself
Data plane — one tenant app
Frontend and API share one CloudFront distribution and origin, so the session cookie is HttpOnly + Secure + SameSite=Strict with no CORS at all. It carries a session id, never a self-contained JWT, so a session can be revoked instantly. Tenant apps deliberately share one ALB via per-app listener rules and target groups instead of provisioning an ALB per app.
Provisioning: AWS SDK, not Terraform
Launchpad's own baseline infrastructure, VPC, ECS clusters, IAM, RDS, is Terraform in the launchpad-infra repo. But a tenant's application infrastructure is created and torn down on demand, so it's provisioned by calling the AWS SDK for Go directly, not by running Terraform per app. The engine that replaces Terraform state for that path is a single table:
Write-ahead, always. A row in resources is inserted with status='creating' before the AWS API call, and its ARN is filled in only after that call succeeds. A crash mid-provisioning can never leave a resource that exists in AWS but isn't tracked, and quietly keeps billing.
Idempotent by constraint. Every insert uses UNIQUE (application_id, resource_type, logical_name) with an ON CONFLICT, the actual basis for retrying a Step Functions state, rather than a check-then-insert that races.
Teardown, strictly reversed. Resources are deleted from the highest dependency_order down to the lowest, never any other order; anything that fails to delete is marked orphaned instead of silently disappearing from the record.
Orchestration: Step Functions
Build → provision → deploy → release, along with their compensating (rollback) actions, are wrapped in one Step Functions state machine, so a failure partway through is corrected the same way a success is finished: by the state machine, not by a person. Each state maps to one AWS SDK call guarded by the write-ahead ledger above, so a retried state is safe to run twice.
Security and tenancy rules
- Secret values never touch the database, an API response, or a log. Only the ARN is stored; ECS tasks read the secret at runtime through the task definition's secrets block.
- Tenant scoping always comes from the session, never from a client-supplied parameter, for both database queries and CloudWatch queries.
- A resource that belongs to a different tenant returns 404, not 403, so its existence is never leaked.
- An uploaded app archive is untrusted input. Nothing inside it (a main.tf, a launchpad.yml, an unusual Dockerfile instruction) is ever read as infrastructure configuration.
- AI output is never an infrastructure parameter. It's used only to classify an undetected runtime and to review a generated Dockerfile against the app's real manifest, never to choose CPU, memory, or health-check paths.
- Every application image is referenced by digest, never by tag, since a tag can move and a production record shouldn't lose its meaning.