Full-Stack Deployment with AWS Amplify Gen 2 and CDK
Moving from Amplify Gen 1 to Gen 2 — TypeScript-first backends, CDK under the hood, and per-developer sandboxes that actually work.

Why Amplify Gen 2 Changes the Equation
Amplify Gen 1 had a reputation problem. The CLI-driven workflow generated CloudFormation templates that were opaque, hard to customize, and nearly impossible to debug when deployments failed. Teams would start with Amplify for speed, then rip it out at the exact moment they needed it most — when complexity grew beyond the happy path.
Amplify Gen 2 is a fundamentally different product wearing the same brand. The backend is defined in TypeScript, CDK constructs are first-class citizens, and the deployment pipeline is Git-based with per-developer sandbox environments. It's what Amplify should have been from the start.
Architecture: TypeScript-First Backend Definition
The Gen 2 backend lives in an amplify/ directory at your project root. Instead of CLI-generated JSON, you write TypeScript that compiles to CDK constructs:
// amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
import { data } from './data/resource';
import { storage } from './storage/resource';
import { processOrder } from './functions/process-order/resource';
const backend = defineBackend({
auth,
data,
storage,
processOrder,
});
// Access underlying CDK constructs for customization
const dataStack = backend.data.stack;
const table = backend.data.resources.tables['Order'];
// Add a GSI that Amplify's schema DSL doesn't natively support
table.addGlobalSecondaryIndex({
indexName: 'byCustomerDate',
partitionKey: { name: 'customerId', type: AttributeType.STRING },
sortKey: { name: 'createdAt', type: AttributeType.STRING },
projectionType: ProjectionType.ALL,
});
The critical difference from Gen 1: you can always escape to raw CDK. Amplify provides high-level abstractions for common patterns (auth, data, storage), but when you need a custom GSI, a specific Lambda configuration, or an integration that Amplify doesn't model — you reach into the CDK construct tree directly.
Data Modeling: The Schema DSL
Amplify Gen 2's data layer uses a TypeScript DSL that generates DynamoDB tables, AppSync resolvers, and client-side types in one pass:
// amplify/data/resource.ts
import { defineData, a, type ClientSchema } from '@aws-amplify/backend';
const schema = a.schema({
Order: a.model({
customerId: a.string().required(),
items: a.json().required(),
total: a.float().required(),
status: a.enum(['PENDING', 'PROCESSING', 'SHIPPED', 'DELIVERED']),
createdAt: a.datetime(),
shippedAt: a.datetime(),
})
.secondaryIndexes((index) => [
index('customerId').sortKeys(['createdAt']).name('byCustomerDate'),
])
.authorization((allow) => [
allow.owner(),
allow.group('admin').to(['read', 'update', 'delete']),
allow.authenticated().to(['read']),
]),
Customer: a.model({
email: a.email().required(),
name: a.string().required(),
tier: a.enum(['FREE', 'PRO', 'ENTERPRISE']),
orders: a.hasMany('Order', 'customerId'),
})
.authorization((allow) => [
allow.owner(),
allow.group('admin'),
]),
});
export type Schema = ClientSchema<typeof schema>;
export const data = defineData({ schema });
The authorization rules compile to AppSync resolver logic — no Lambda authorizers needed for standard CRUD patterns. The type export (Schema) generates client-side types that flow through to your frontend components.
Per-Developer Sandbox Environments
The feature that sold our team: every developer gets an isolated AWS environment for their branch. No more "don't deploy to staging, I'm testing something":
# Start a personal sandbox (creates isolated resources)
npx ampx sandbox
# Sandbox watches for file changes and hot-deploys
# Each developer gets their own:
# - Cognito User Pool
# - DynamoDB tables
# - AppSync API
# - S3 buckets
# - Lambda functions
Sandboxes use your personal AWS credentials and create resources with a developer-specific prefix. The sandbox process watches your amplify/ directory and hot-deploys changes in under 20 seconds for most modifications.
Benchmarks: Gen 1 vs Gen 2 Developer Experience
We migrated a mid-size application (12 models, 8 Lambda functions, Cognito auth, S3 storage) from Gen 1 to Gen 2:
| Metric | Amplify Gen 1 | Amplify Gen 2 | Improvement |
|---|---|---|---|
| Initial deployment time | 8-12 min | 3-5 min | -60% |
| Incremental deploy (data model change) | 4-6 min | 15-45 sec | -90% |
| Time to add custom CDK resource | Hours (eject required) | Minutes (inline) | Dramatic |
| CI/CD pipeline complexity | High (amplify.yml + hooks) | Low (Git-based) | Simplified |
| Type safety (backend → frontend) | Partial (codegen) | Full (schema DSL) | Complete |
| Debugging failed deploys | Painful (nested CFN) | Clear (CDK errors) | Significantly better |
The incremental deploy improvement is the game-changer for developer velocity. Gen 1 would re-synthesize and diff the entire CloudFormation stack for any change. Gen 2's sandbox uses targeted hot-swap deployments that only update the changed resource.
Production Deployment: Git-Based Pipeline
For production, Amplify Gen 2 uses a Git-based deployment pipeline that's configured in the Amplify console or via CDK:
// amplify/backend.ts — production configuration
import { defineBackend } from '@aws-amplify/backend';
import * as cdk from 'aws-cdk-lib';
const backend = defineBackend({ auth, data, storage });
// Production-specific configuration
const authStack = backend.auth.stack;
// Add WAF to Cognito hosted UI
const waf = new cdk.aws_wafv2.CfnWebACL(authStack, 'AuthWaf', {
scope: 'REGIONAL',
defaultAction: { allow: {} },
rules: [
{
name: 'RateLimit',
priority: 1,
action: { block: {} },
statement: {
rateBasedStatement: {
limit: 100,
aggregateKeyType: 'IP',
},
},
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'AuthRateLimit',
},
},
],
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'AuthWaf',
},
});
Migration Strategy: Gen 1 to Gen 2
Our migration took 3 weeks for a team of 2. The approach:
- Week 1: Recreate the data schema in Gen 2's TypeScript DSL. Run Gen 1 and Gen 2 in parallel.
- Week 2: Migrate auth configuration, custom Lambda functions, and storage buckets. Verify parity with integration tests.
- Week 3: Cut over DNS, decommission Gen 1 resources, clean up.
The hardest part was migrating Cognito User Pools — Gen 2 creates new pools rather than importing existing ones. We used a Lambda migration trigger to transparently re-authenticate users on first login.
Operational Considerations
What Gen 2 handles well:
- Rapid prototyping with immediate type safety
- Multi-environment isolation (sandbox per developer)
- Standard CRUD patterns with authorization
- Frontend framework integration (React, Next.js, Vue)
Where you'll hit limits:
- Complex event-driven architectures (beyond simple Lambda triggers)
- Multi-region deployments (Amplify is single-region)
- Advanced networking (VPC configurations are possible but not ergonomic)
- Large teams with many microservices (Amplify is best for bounded applications)
Conclusion
Amplify Gen 2 is the right tool for full-stack applications where the backend is an enabler for the frontend, not the core product. If your value is in the user experience and your backend is standard patterns (auth, CRUD, storage, functions), Gen 2 eliminates weeks of CDK boilerplate while preserving escape hatches.
The TypeScript-first approach means your backend definition is type-checked, auto-completed, and version-controlled the same way as your application code. Per-developer sandboxes eliminate environment conflicts. And when you need something Amplify doesn't model — you just write CDK.
Don't use it for microservice architectures or complex event-driven systems. Do use it for products where shipping features matters more than infrastructure novelty.
Recommended reading

Per-Team Cost Allocation in Shared Kubernetes Clusters: From Chaos to Clarity
Implementing accurate per-namespace cost allocation in multi-tenant Kubernetes clusters, covering request vs. usage attribution, shared resource amortization, and building showback dashboards that drive accountability.

Measuring and Eliminating Toil: From 40% to 12% of Engineering Time
A systematic approach to identifying, measuring, and automating toil—the repetitive operational work that scales linearly with service growth and prevents engineers from doing creative work.

Serverless Postgres in Production: Branching, Scale-to-Zero, and the End of Database Provisioning
Running Neon serverless Postgres in production for 8 months — covering database branching workflows, scale-to-zero economics, connection pooling, and migration from RDS.

Comments
No comments yet. Be the first to share your thoughts.