AWS Amplify Gen 2 in Production: Architecture Decisions That Matter
A practical guide to AWS Amplify Gen 2 for production applications — authentication, data modeling, custom resolvers, and the limitations to know before you build.
AWS Amplify Gen 2 is a meaningful rearchitecting of the platform. The TypeScript-first, code-first model eliminates most of the friction that made Gen 1 difficult to work with in complex production environments — the YAML configuration fragmentation, the opaque CLI-generated resources, the difficulty of customizing beyond what the CLI anticipated.
But Amplify Gen 2 is still Amplify, which means it makes strong assumptions about your architecture, and those assumptions are not always the right ones for every application. Before you commit to Amplify for a production deployment, you need to understand both what it does well and where it runs out of road.
This is a practical guide written for teams evaluating Amplify Gen 2 for a Next.js application that needs to survive production.
What Changed from Gen 1 to Gen 2
The most significant change is the move from a CLI-driven, YAML-based configuration model to a TypeScript-first, code-first model. In Gen 1, you ran amplify add auth and got a generated configuration file that was difficult to read, harder to modify, and nearly impossible to version meaningfully. Drift between environments was common and painful.
In Gen 2, your entire backend is defined in TypeScript files in your repository:
// amplify/auth/resource.ts
import { defineAuth } from "@aws-amplify/backend";
export const auth = defineAuth({
loginWith: {
email: true,
},
multiFactor: {
mode: "OPTIONAL",
totp: true,
},
});This is a genuine improvement. Your backend infrastructure is now a first-class part of your codebase — version controlled, code-reviewed, and deployable through the same pipeline as your application code. The configuration is readable by engineers who were not present when it was created.
Gen 2 also ships with a unified data modeling layer built on AppSync and DynamoDB, a cleaner authentication integration with Cognito, and sandbox environments for local development that provision real AWS resources in an isolated account context.
What did not fundamentally change: Amplify is still an opinionated platform built on AppSync + DynamoDB for data, Cognito for auth, and CloudFront + S3 (or Lambda) for hosting. If your requirements live comfortably in that stack, Gen 2 is a significant improvement. If your requirements push outside it, you will hit the same walls.
Authentication Setup
Amplify Gen 2 authentication is built on Cognito, and the Gen 2 configuration model gives you reasonable control over the Cognito setup through the TypeScript definition layer.
Standard Configuration
The baseline auth setup handles email/password authentication, MFA, and the standard social providers (Google, Facebook, Apple):
export const auth = defineAuth({
loginWith: {
email: {
verificationEmailStyle: "CODE",
verificationEmailSubject: "Verify your email",
},
externalProviders: {
google: {
clientId: secret("GOOGLE_CLIENT_ID"),
clientSecret: secret("GOOGLE_CLIENT_SECRET"),
},
callbackUrls: ["https://yourapp.com/auth/callback"],
logoutUrls: ["https://yourapp.com"],
},
},
multiFactor: {
mode: "REQUIRED",
totp: true,
sms: true,
},
userAttributes: {
givenName: { required: true, mutable: true },
familyName: { required: true, mutable: true },
},
});Custom Auth Flows
Cognito supports custom authentication challenges — magic links, device-based authentication, custom verification logic — through Lambda triggers. Gen 2 exposes these through a triggers configuration:
export const auth = defineAuth({
loginWith: { email: true },
triggers: {
createAuthChallenge: defineFunction({
entry: "./create-auth-challenge.ts",
}),
defineAuthChallenge: defineFunction({
entry: "./define-auth-challenge.ts",
}),
verifyAuthChallengeResponse: defineFunction({
entry: "./verify-auth-challenge.ts",
}),
},
});This is how you implement magic link authentication or hardware key flows. The trigger functions are standard Lambda functions with Cognito event shapes. The integration is cleaner in Gen 2 than Gen 1, but the Cognito trigger model itself is unchanged — the same constraints around session management and challenge sequencing apply.
Production Auth Considerations
A few things that matter in production and are easy to miss:
Token expiry configuration — Amplify defaults are not always appropriate for your application. The ID token, access token, and refresh token have separate expiry windows. Define these explicitly based on your security requirements. For applications handling sensitive data, shorter-lived tokens with well-implemented refresh flows are preferable to long-lived sessions.
Advanced Security Features — Cognito's advanced security features (compromised credential detection, adaptive authentication) are not enabled by default and are not configurable through the standard Amplify Gen 2 auth definition at the time of writing. Enabling them requires a custom CDK construct via defineBackend. This is not a blocking issue, but it is something to account for if you are building a security-sensitive application.
User pool limits — Cognito scales well but has default limits on API calls per second that can affect authentication under high concurrency. If you are building for significant scale, review the Cognito service quotas early.
Data Modeling with DynamoDB
Amplify Gen 2 data modeling is built on AppSync with DynamoDB as the underlying store. The TypeScript model definition is the most significant improvement over Gen 1's GraphQL SDL-in-YAML approach:
// amplify/data/resource.ts
import { defineData, a } from "@aws-amplify/backend";
const schema = a.schema({
Organization: a
.model({
name: a.string().required(),
plan: a.enum(["starter", "professional", "enterprise"]),
members: a.hasMany("User", "organizationId"),
createdAt: a.datetime(),
})
.authorization((allow) => [
allow.owner(),
allow.group("admin"),
]),
User: a
.model({
email: a.string().required(),
organizationId: a.id().required(),
organization: a.belongsTo("Organization", "organizationId"),
role: a.enum(["member", "admin", "viewer"]),
})
.authorization((allow) => [
allow.owner(),
allow.authenticated().to(["read"]),
]),
});
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: "userPool",
},
});The authorization model is one of the stronger aspects of Amplify Gen 2. Field-level and model-level authorization rules compile down to DynamoDB condition expressions and VTL resolvers, which means the enforcement is at the data layer — not just in your application logic.
What DynamoDB Access Patterns Require
Amplify abstracts DynamoDB but does not eliminate its fundamental access pattern constraints. DynamoDB performs well for queries by partition key, poorly for ad-hoc queries across arbitrary dimensions.
The Amplify data layer adds a set of secondary indexes automatically, but complex relational queries require either:
- Designing your schema to match the access patterns you need (the correct DynamoDB approach)
- Layering a custom query with a Lambda resolver for complex filtering
- Accepting that some queries will result in a full table scan through AppSync filters (which works at small scale and fails badly at large scale)
If your application requires complex relational queries — reporting, multi-dimensional filtering, analytics — DynamoDB through Amplify is the wrong storage layer for those use cases. A common production pattern is DynamoDB for transactional data + Aurora Serverless or RDS Proxy for reporting workloads, connected via custom resolvers.
Custom Business Logic: Lambda Resolvers and Custom Queries
Amplify Gen 2 supports two paths for custom business logic: Lambda functions as custom resolvers in the AppSync layer, and HTTP endpoints via the Amplify function definition.
Custom Resolvers
Custom resolvers replace the auto-generated AppSync resolvers for specific operations. Use them when the default CRUD behavior is insufficient — when you need to enforce business rules, trigger side effects, or integrate with external services:
// amplify/data/resource.ts
const schema = a.schema({
// ...
createOrderWithInventoryCheck: a
.mutation()
.arguments({
productId: a.id().required(),
quantity: a.integer().required(),
})
.returns(a.ref("Order"))
.handler(
a.handler.function(
defineFunction({ entry: "./create-order-handler.ts" })
)
)
.authorization((allow) => [allow.authenticated()]),
});The handler function receives the AppSync event shape and has access to the full AWS SDK. This is where you put logic that the auto-generated resolvers cannot express — inventory checks, external payment API calls, conditional workflows.
Function Definitions
For operations that live outside the AppSync layer — scheduled jobs, event-driven processing, webhook handlers — Amplify Gen 2 provides function definitions that deploy as Lambda functions and can be connected to EventBridge, SQS, or HTTP endpoints:
// amplify/functions/process-webhook/resource.ts
import { defineFunction } from "@aws-amplify/backend";
export const processWebhook = defineFunction({
name: "process-webhook",
entry: "./handler.ts",
timeoutSeconds: 30,
environment: {
STRIPE_WEBHOOK_SECRET: secret("STRIPE_WEBHOOK_SECRET"),
},
});The function connects to API Gateway through the backend definition, giving you a deployable webhook endpoint with the Amplify environment variables and IAM context already configured.
Deployment Pipelines
Amplify Gen 2 integrates with Amplify Hosting for CI/CD. The deployment model provisions sandbox environments (development) from developer machines and deploys production through connected branches in Amplify Hosting.
Branch-Based Environments
Each Git branch can map to an isolated Amplify environment with its own provisioned resources. This gives you true environment parity between staging and production — the same AppSync API, the same Cognito user pool configuration, the same DynamoDB tables, just with separate provisioned instances.
The configuration in amplify.yml:
version: 1
backend:
phases:
build:
commands:
- npm ci
- npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
frontend:
phases:
build:
commands:
- npm run build
artifacts:
baseDirectory: .next
files:
- "**/*"
cache:
paths:
- node_modules/**/*Custom Domains and Edge Configuration
Custom domain configuration in Amplify Hosting is functional but limited compared to a CloudFront distribution you control directly. Advanced edge behaviors — custom cache policies, origin request policies, Lambda@Edge functions — require either going through the Amplify console (which exposes a subset of CloudFront options) or managing the CloudFront distribution outside Amplify.
For Next.js applications that require sophisticated edge configuration — geo-based routing, A/B testing at the edge, advanced cache invalidation — this is a meaningful constraint.
Limitations and When Not to Use Amplify
Amplify Gen 2 is the right tool for applications that map cleanly onto its assumptions. It is the wrong tool when those assumptions conflict with your requirements.
Complex relational data models. If your application is fundamentally relational — complex joins, ad-hoc reporting, transactions across multiple entities — DynamoDB is not the right storage layer. Amplify does not support PostgreSQL or MySQL as a primary data store through its standard data layer.
Strict infrastructure control requirements. Some organizations require specific VPC configurations, custom KMS key management, fine-grained IAM policies that differ from what Amplify provisions, or integration with existing AWS infrastructure that predates the Amplify deployment. Amplify supports CDK customization via defineBackend, but the further you go down that path, the more you are managing raw CDK rather than Amplify.
Multi-region deployments. Amplify Hosting is primarily a single-region deployment model. Global multi-region active-active architectures are not a natural fit for Amplify's deployment model.
High-throughput APIs with complex rate limiting requirements. AppSync is a capable GraphQL endpoint, but organizations with sophisticated API rate limiting requirements, complex quota management, or high per-second throughput needs may find that a custom API Gateway + Lambda setup gives them more control.
Teams that need to own the infrastructure. Amplify abstracts significant infrastructure complexity. That is its value proposition. For teams that need deep operational visibility into their infrastructure, that abstraction can be an obstacle rather than an asset.
Production Readiness Considerations
Logging and Observability
Amplify deploys CloudWatch logging for AppSync and Lambda functions by default. For production applications, this is necessary but not sufficient.
A useful production observability stack on Amplify:
- CloudWatch Logs Insights for structured log querying across Lambda and AppSync
- CloudWatch Alarms on error rates, latency percentiles, and function throttles
- X-Ray for distributed tracing across AppSync resolvers and Lambda functions — enable this at the AppSync level and instrument Lambda handlers
- RUM (Real User Monitoring via CloudWatch) for frontend performance data
The Amplify console exposes a subset of these metrics. For serious production monitoring, go directly to CloudWatch and build the dashboards and alarms you need there.
Custom Domain Setup
Amplify Hosting manages ACM certificate provisioning for custom domains. The setup is straightforward for domains managed in Route 53. For domains managed externally, you will need to add CNAME validation records manually, and the console workflow for this is functional but not fast.
One practical consideration: Amplify uses CloudFront under the hood, but the distribution is managed by Amplify. If you have existing CloudFront behavior configurations you want to apply, check what Amplify exposes through the console before assuming it is configurable.
Environment Variable Management
Amplify Gen 2 introduced the secret() function for referencing sensitive values. Secrets are stored in AWS Secrets Manager and injected at build time and runtime — they are not in your TypeScript code or your environment variable files.
For non-secret configuration that varies by environment (feature flags, API endpoint URLs, tier-specific configuration), use Amplify environment variables configured per branch in the Amplify console or in amplify.yml. Do not put environment-specific configuration in your TypeScript backend definition files — it defeats the purpose of having branch-based environment parity.
Frequently Asked Questions
Should I use Amplify Gen 2 or CDK directly?
If you want the managed CI/CD, the integrated auth and data layers, and the Gen 2 TypeScript configuration model, Amplify Gen 2 is reasonable. If you have complex infrastructure requirements or need precise control over every AWS resource, CDK directly gives you more flexibility at the cost of more responsibility. Teams that start with Amplify often migrate specific concerns to CDK via defineBackend as the application grows — this is a supported pattern.
Can I use Amplify Gen 2 with an existing Next.js application?
Yes. The Amplify backend is separate from your Next.js application. You can add Amplify to an existing Next.js project, connect it to Amplify Hosting, and adopt the auth and data layers incrementally. The most common starting point for an existing application is adding Amplify Hosting for CI/CD, then optionally adopting the auth and data layers.
How does Amplify Gen 2 handle database migrations?
It does not, in the traditional sense. DynamoDB is schemaless, so there is no migration runner. Schema changes in Amplify Gen 2 that affect DynamoDB (new fields, new indexes) are applied by deploying the updated schema definition. Removing a field from the schema does not delete data from DynamoDB — it stops the auto-generated resolvers from reading it. Managing backward compatibility during schema evolution is your responsibility.
Amplify Gen 2 is worth evaluating seriously for Next.js applications that align with its model. The TypeScript-first approach is a genuine improvement, and the integrated auth and data layers save real time compared to assembling those components manually. The constraints are real but predictable — knowing them in advance lets you design around them or make an informed decision to use a different stack.
If you are evaluating Amplify Gen 2 for a production application and want a second opinion on whether the stack fits your requirements, start with an architecture conversation. The answer is usually clear within a focused discussion.