From laptop to live: an event-driven AWS stack with Wayfinder
One wf up deploys a real production-shape AWS application: five Lambda functions behind API Gateway and CloudFront, fanning out through SNS into DynamoDB and S3, with a React back-office SPA. All built and pushed straight from your laptop. No hand-coded IAM. No separate CI pipeline. One manifest.
What you'll deploy
Orderly, an event-driven order processing backend you'd recognise from any real production codebase. A hot path that captures orders and fans out through SNS. A warm path that aggregates sales rollups in real time. A cold path that streams every event into an append-only audit bucket. All wired together by one Wayfinder manifest.
The walkthrough
Five steps: prerequisites, the manifest, the local build, the deploy, and the iteration loop. Click through and read at your own pace.
Wayfinder handles the AWS provisioning, the workload identity wiring, the cross-region ACM cert and the access policies. You bring the laptop and the code. No Terraform install, no AWS provider versions to align, no hand-crafted IAM, no CI/CD pipeline to stand up.
- Sign up for a Wayfinder account. Create your tenant and your first workspace. Start free.
- Connect AWS via CloudAccess. Allow DynamoDB, SNS, S3, Lambda, ECR, API Gateway v2, CloudFront, Route53, ACM and IAM.
-
Attach a DNS zone to your environment. So CloudFront can serve
<stack>-<instance>.<your-domain>. -
Install the
wfCLI and runwf login. Wayfinder's command-line client, authenticated against your tenant. -
Install Docker (with BuildKit) and the
awsCLI. Used by the local build step. Wayfinder vends short-lived, resource-scoped credentials to the AWS CLI just in time. - Have Go 1.25+ and Node 20+ available. For the handler binary and the React SPA.
One Wayfinder.yaml describes every component and how they connect. Each block names a plan (a curated, versioned Terraform module owned by your platform team) and the access it needs from its neighbours. There is no IAM in this file, only intent. Three DynamoDB tables, one SNS topic, one S3 audit bucket, an ECR repo, five Lambda functions, an API Gateway and a CloudFront-fronted SPA. Below is the storage layer plus the first Lambda; the rest follows the same shape.
apiVersion: v1
name: orderly
description: "Event-driven order processing"
components:
# ── Storage ──────────────────────────────────────────────
orders:
type: CloudResource
plan: aws-dynamodb-table@1.0.0
inputs:
- name: suffix
value: orders
- name: hash_key
value: order_id
auditbucket:
type: CloudResource
plan: aws-s3-data-bucket@1.0.0
inputs:
- name: suffix
value: audit
# ── Event bus ────────────────────────────────────────────
orderevents:
type: CloudResource
plan: aws-sns-topic@1.0.0
# ── Hot path: checkout ───────────────────────────────────
checkout:
type: CloudResource
plan: aws-lambda-go-handler@1.0.0
needs: [pushimages]
workloadIdentity:
access:
- to: orders
consumptionPolicy: write
- to: orderevents
consumptionPolicy: publish
inputs:
- name: suffix
value: checkout
- name: container_image_uri
value: "${{ .Components.ecr.repository_url }}:${VERSION}"
terraform:
tfVarsTemplate: |-
env = {
HANDLER = "checkout"
ORDERS_TABLE = "${{ .Components.orders.table_name }}"
TOPIC_ARN = "${{ .Components.orderevents.topic_arn }}"
}
Read that workloadIdentity.access block carefully. checkout declares "I write to orders and publish to orderevents". Wayfinder turns that into a workload identity, a scoped IAM role, the trust policy for Lambda and the resource-policy attachments, all derived from the plan's curated permission set. The cold-path auditledger Lambda uses consumptionPolicy: write-only against the audit bucket: the IAM literally cannot read what it wrote, a direct SOC 2 / PCI compliance story you couldn't reasonably construct by hand without a long IAM review.
Some steps in a deployment have to run from a real machine: building a container image, pushing it to ECR, bundling a React app, syncing it to S3 and invalidating CloudFront. Wayfinder calls these LocalActions. They run on your laptop, but the credentials, the ordering and the audit trail are all owned by the platform. The pushimages LocalAction builds the mono-binary Go image once and pushes it to ECR; all five Lambdas share that image and branch on a HANDLER env var, so one build serves the whole stack.
pushimages:
type: LocalAction
withAccess:
- to: ecr
consumptionPolicy: push
localAction:
run: |-
aws ecr get-login-password --region ${{ .Cloud.Region }} \
| docker login --username AWS --password-stdin \
${{ .Components.ecr.repository_url }}
make -C ../app build VERSION=${VERSION}
docker tag orderly:${VERSION} \
${{ .Components.ecr.repository_url }}:${VERSION}
docker push ${{ .Components.ecr.repository_url }}:${VERSION}
Brokered access, not stored secrets: Wayfinder issues a short-lived AWS session scoped only to this ECR repository with only push permissions, for the duration of the action. The credentials never land on disk, never appear in a CI variable, and expire automatically. The deployspa LocalAction does the same trick for the front end (npm build, aws s3 sync, then a CloudFront invalidation), running only after the spa CloudFront distribution exists (declared via needs: [spa]).
deployspa:
type: LocalAction
needs: [spa]
withAccess:
- to: spa
consumptionPolicy: deploy
localAction:
run: |-
npm --prefix ../app/spa install
VITE_API_BASE=/api npm --prefix ../app/spa run build
aws s3 sync ../app/spa/dist \
s3://${{ .Components.spa.spa_bucket_id }}/ \
--delete --sse
aws cloudfront create-invalidation \
--distribution-id ${{ .Components.spa.distribution_id }} \
--paths "/*"
Why this matters
- No separate CI pipeline. Build and deploy happen in the same dependency graph as everything else.
- No long-lived AWS keys. Wayfinder vends per-action, resource-scoped credentials.
- Recorded runs. Every LocalAction execution is captured and visible in Wayfinder: what command ran, by whom, against what version.
- It still works when you graduate to CI. The same manifest runs unchanged from your laptop or a runner.
One command turns the manifest into a running application. Wayfinder resolves the dependency graph, runs components in parallel where it can and serialises where it must. Before the very first deploy, register the curated CloudResourcePlans with your tenant; only needed once per tenant or after a plan version bump.
wf apply -f ./plans/
wf use workspace <your-workspace>
wf use environment <your-environment>
export VERSION=$(date +%Y%m%d-%H%M%S)
wf up -f ./aws/Wayfinder.yaml \
-i orderly-demo \
--dns-zone <your-dns-zone> \
--env-var VERSION=$VERSION
What Wayfinder does, in order
- Storage, bus and ECR access resolve in parallel. DynamoDB tables, the SNS topic and the S3 audit bucket. ~1 min.
-
pushimagesbuilds the mono-binary and pushes once to ECR. ~30 s on a warm BuildKit cache. - All 5 Lambdas deploy in parallel, pointing at the same image. ~1 min each.
- API Gateway wires the routes, internal-only, fronted by CloudFront.
- CloudFront, ACM cert (us-east-1) and Route53 records. The slowest step at 3 to 8 minutes, dominated by CloudFront propagation.
-
deployspabuilds the React SPA, syncs to S3 and invalidates CloudFront.
wf get stackinstance orderly-demo -w
The inner-loop story is where Wayfinder pays for itself. Once the stack exists, changing a handler is a single command and ~30 s on the wire.
- YAML-only tweak. Seconds. No rebuild, just a plan and apply.
- One handler's Go code changed. ~30 s. The Go cache mount skips unchanged packages; only the changed Lambda redeploys with the new image tag.
- SPA-only change. ~1 min, dominated by the CloudFront invalidation.
-
Adding a new resource. Declare it in the manifest, run
wf upagain. Wayfinder produces a diff and applies only what changed.
When you're done (after recording the demo video, for example) clean teardown is one command.
wf destroy stack orderly --instance orderly-demo
Tables, the SNS topic, the Lambdas, API Gateway, CloudFront, the ACM cert, the Route53 records, both S3 buckets: gone. For a real compliance story, flip the audit bucket's disable_force_destroy input to true and Wayfinder will refuse to delete it while objects remain. The same component shapes (Lambda → API Gateway → CloudFront, fanning out through SNS to DDB and S3) port to Azure Functions, GCP Cloud Run or an EKS-based variant by swapping plan names. Same YAML structure, different cloud. That's the destination.
Run this walkthrough yourself
Book a 20-minute demo and we'll set up a workspace, walk you through the manifest, and watch you deploy the whole thing from your own laptop.