
Amplify Workflow
- 84 installs
- 850 repo stars
- Updated August 3, 2026
- awslabs/agent-plugins
amplify-workflow is a Claude skill that builds and deploys full-stack web and mobile apps with AWS Amplify Gen2, covering Cognito auth, AppSync/DynamoDB data, S3 storage, functions, and frontend integration.
About
This skill builds and deploys full-stack web and mobile apps with AWS Amplify Gen2 using a TypeScript code-first approach. A developer uses it to define backend resources like Cognito auth, AppSync/DynamoDB data, and S3 storage, then integrate a frontend across eight supported frameworks. It routes to version-specific reference files to prevent common Amplify Gen2 mistakes.
- Builds and deploys full-stack apps with AWS Amplify Gen2 using a TypeScript code-first approach
- Covers auth (Cognito), data (AppSync/DynamoDB), storage (S3), functions, APIs, and the Amplify AI Kit with Bedrock
- Supports React, Next.js, Vue, Angular, React Native, Flutter, Swift, and Android frontends
Amplify Workflow by the numbers
- 84 all-time installs (skills.sh)
- Ranked #606 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
amplify-workflow capabilities & compatibility
- Capabilities
- api development · database · frontend
- Works with
- aws
- Use cases
- api development · database · frontend
- IDEs
- vscode
- Runs
- Local or remote
What amplify-workflow says it does
Gen2 backends are TypeScript-only
npx skills add https://github.com/awslabs/agent-plugins --skill amplify-workflowAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 84 |
|---|---|
| repo stars | ★ 850 |
| Last updated | August 3, 2026 |
| Repository | awslabs/agent-plugins ↗ |
What it does
Build and deploy full-stack web and mobile apps with AWS Amplify Gen2 auth, data, storage, and functions.
Who is it for?
Building full-stack apps on AWS Amplify Gen2 with code-first backend and frontend integration
Skip if: Amplify Gen1, standalone SAM/CDK without Amplify, or direct Bedrock without the Amplify AI Kit
When should I use this skill?
The project uses Amplify Gen2, has an amplify/ directory, or asks about defineBackend, defineAuth, defineData, or npx ampx
What you get
A deployed full-stack Amplify Gen2 app with defined auth, data, and storage backends and a connected frontend.
- Deployed full-stack Amplify Gen2 app
- Defined auth, data, and storage backends
- Frontend integration
By the numbers
- 8 supported frontend frameworks
- Node.js required: ^18.19.0 || ^20.6.0 || >=22
Files
AWS Amplify Gen2
Build and deploy full-stack applications using AWS Amplify Gen2's TypeScript code-first approach. This skill covers backend resource creation, frontend integration across 8 frameworks, and deployment workflows.
Prerequisites
- Node.js ^18.19.0 || ^20.6.0 || >=22 and npm
- AWS credentials configured (
aws sts get-caller-identitysucceeds) - For sandbox:
npx ampx --versionreturns a valid version - For mobile: Platform-specific tooling (Xcode, Android Studio, Flutter SDK)
Defaults & Assumptions
When the user does not specify a framework:
- Web: You SHOULD default to React (Vite) and explain the choice.
- Mobile: You MUST ask which platform the user wants (Flutter,
Swift, Android, or React Native). There is no universal mobile default.
- Neither specified: If the user says "build an app" without clarifying web
vs. mobile, you MUST ask before proceeding.
- Backend only: If only backend changes are requested and no frontend
framework is mentioned, skip the frontend integration step entirely.
When the user does not specify tooling or strategy:
- Package manager: You SHOULD default to npm unless the user
specifies yarn or pnpm.
- Language: You SHOULD default to TypeScript. Gen2 backends are
TypeScript-only; frontends SHOULD follow the project's existing language.
- Next.js: You SHOULD default to App Router unless the user
specifies Pages Router.
- React Native: Ask the user whether they use Expo or **bare
React Native CLI**.
- Auth: You MUST ask which login method the user wants
(email/password, social login, SAML, passwordless, etc.). Do not assume a default.
- Data authorization: default to `publicApiKey`
(allow.publicApiKey()) — this is the starter template default. When auth is added, switch to owner-based (allow.owner()) with defaultAuthorizationMode: 'userPool'.
Quick Start — Route to the Right Reference
Step 0: Read Core Reference (ALWAYS)
You MUST read the core reference for your target platform before reading any other reference file. These contain Gen2 detection, Amplify.configure() placement per framework, sandbox commands, required packages, and directory structure rules — patterns needed for all tasks, not just new projects.
- Web (React, Next.js, Vue, Angular, React Native): You MUST read
core-web.md
- Mobile (Flutter, Swift, Android): You MUST read
core-mobile.md
- Backend only (no frontend work): Skip to Step 1.
Step 1: Identify the Task Type
| Task | Go To |
|---|---|
| Create a new project | → scaffolding.md, then Step 2 and/or Step 3 |
| Add or modify a backend feature | → Step 2 (Backend Features) |
| Connect frontend to existing backend | → Step 3 (Frontend Integration) |
| Deploy the application | → deployment.md |
Step 2: Backend Features
You MUST read the corresponding reference for each backend feature:
| Feature | Reference | When to Use |
|---|---|---|
| Authentication | auth-backend.md | Email/password, social login, MFA, SAML/OIDC |
| Data Models | data-backend.md | GraphQL schema, DynamoDB, relationships, auth rules |
| File Storage | storage-backend.md | S3 uploads/downloads, access rules |
| Functions & API | functions-and-api.md | Lambda, custom resolvers, REST/HTTP APIs, calling from client |
| AI Features | ai.md | Conversation, generation, AI tools via Bedrock _(backend config + React/Next.js frontend)_ |
| Geo, PubSub, CDK | advanced-features.md | Backend-only: custom CDK stacks, overrides, custom outputs. Backend + frontend: Geo, PubSub, Face Liveness |
Each backend feature file is self-contained. Load only what you need.
Routing note: These files apply for both adding and modifying
features. Route to the same file whether the user says "add auth" or
"change auth config" — each reference covers the full define surface.
Step 3: Frontend Integration
After configuring backend resources, connect the frontend. Choose by platform and feature:
Web (React, Next.js, Vue, Angular, React Native):
| Feature | Reference |
|---|---|
| Auth UI & flows | auth-web.md |
| Data CRUD & subscriptions | data-web.md |
| Storage upload/download | storage-web.md |
Mobile (Flutter, Swift, Android):
| Feature | Reference |
|---|---|
| Auth UI & flows | auth-mobile.md |
| Data CRUD & subscriptions | data-mobile.md |
| Storage upload/download | storage-mobile.md |
Note: AI and Functions frontend patterns are included in
ai.md and
functions-and-api.md respectively —
they are not split into separate web/mobile files.
Core Concepts
Amplify Gen2 Architecture
- Code-first: All backend resources defined in TypeScript under
amplify/ - Main config:
amplify/backend.tsimports and combines all resources via
defineBackend()
- Resource files:
amplify/auth/resource.ts,amplify/data/resource.ts,
amplify/storage/resource.ts, amplify/functions/<name>/resource.ts
- Generated output:
amplify_outputs.json— consumed by frontend
Amplify.configure(). Gitignored — generated by npx ampx sandbox (local dev) or npx ampx pipeline-deploy (CI/CD), never committed.
Directory Structure
project-root/
├── amplify/
│ ├── backend.ts # defineBackend({ auth, data, ... })
│ ├── auth/resource.ts # defineAuth({ ... })
│ ├── data/resource.ts # defineData({ schema })
│ ├── storage/resource.ts # defineStorage({ ... })
│ └── functions/
│ └── my-func/
│ ├── resource.ts # defineFunction({ ... })
│ └── handler.ts # export const handler = ...
├── src/ # Frontend code
├── amplify_outputs.json # Generated — DO NOT edit or commit (gitignored)
└── package.jsonKey APIs
| Package | Purpose |
|---|---|
@aws-amplify/backend | defineAuth, defineData, defineStorage, defineFunction, defineBackend |
aws-amplify | Frontend: Amplify.configure(), generateClient(), auth/data/storage APIs |
@aws-amplify/ui-react | Pre-built UI: <Authenticator>, <StorageBrowser> |
@aws-amplify/ui-react-ai | AI UI: <AIConversation>, useAIConversation |
Documentation & Resource Verification
When you need AWS documentation (advanced CDK constructs, service limits, provider-specific auth config):
1. If AWS documentation tools are available (e.g., via AWS MCP), you SHOULD use them to search and retrieve relevant documentation pages. 2. If AWS documentation tools are unavailable, you MUST fall back to web search or the aws CLI for resource verification.
Why conditional: Amplify Gen2 is code-first — the primary workflow is
editing TypeScript files and running npx ampx commands. AWS MCP toolsare useful for post-deployment verification but are not required.
Security Considerations
- Use
secret()for all credentials and API keys — never hardcode or use plain environment variables for sensitive values - Review
allow.guest()exposure carefully — guest access is enabled by default and grants unauthenticated users access to IAM-authorized resources - Scope IAM policies to specific resource ARNs — avoid
resources: ['*']in production - Never log secrets or include them in error messages
- Enable CloudTrail and CloudWatch alarms for monitoring Amplify-deployed resources; enable access logging on S3, AppSync, and API Gateway
- Configure security headers for web apps — set CSP, HSTS, X-Frame-Options, and X-Content-Type-Options via
customHeadersinamplify.yml - Attach AWS WAF to public-facing AppSync APIs and API Gateway endpoints for defense in depth
- Enable throttling and rate limiting on API Gateway and AppSync APIs to prevent abuse
- Use IAM roles with ephemeral credentials for CI/CD pipelines and Lambda execution roles — never long-lived access keys
- Encrypt CloudWatch Logs groups with KMS (aws:kms) when they may contain PII, tokens, or secrets; enable log retention policies
- Enable AppSync schema validation and API Gateway request validators to reject malformed input at the edge
- Use ACM-managed TLS certificates for custom domains on Amplify Hosting — configure via
customDomainin deployment config
Links
All documentation links usereactas the default platform slug. Replace/react/in any URL with your target framework:
| Framework | Slug |
|---|---|
| React | react |
| Next.js | nextjs |
| Vue | vue |
| Angular | angular |
| React Native | react-native |
| Flutter | flutter |
| Swift | swift |
| Android | android |
Advanced Features
Geo (Location) — Backend + Frontend
Add map display and location search using CDK constructs in amplify/backend.ts:
import { defineBackend } from '@aws-amplify/backend';
import * as geo from 'aws-cdk-lib/aws-location';
const backend = defineBackend({ auth });
const geoStack = backend.createStack('GeoStack');
const placeIndex = new geo.CfnPlaceIndex(geoStack, 'PlaceIndex', {
dataSource: 'Esri',
indexName: 'myPlaceIndex',
});
const map = new geo.CfnMap(geoStack, 'Map', {
mapName: 'myMap',
configuration: { style: 'VectorEsriNavigation' },
});
backend.addOutput({
geo: {
aws_region: geoStack.region,
maps: { items: { [map.mapName]: { style: 'VectorEsriNavigation' } } },
search_indices: { items: [placeIndex.indexName] },
},
});Grant authenticated users access via IAM policy on the geo resources.
Frontend: Install @aws-amplify/geo and maplibre-gl-js-amplify. Use Amplify.Geo.searchByText() for search and AmplifyMapLibreRequest for rendering maps. See AWS Amplify Geo docs for full client setup.
PubSub — Backend + Frontend
Real-time messaging via AWS IoT Core. Configure an IoT endpoint and attach an IAM policy for authenticated users in amplify/backend.ts:
import * as iam from 'aws-cdk-lib/aws-iam';
const pubsubStack = backend.createStack('PubSubStack');
backend.auth.resources.authenticatedUserIamRole.addToPrincipalPolicy(
new iam.PolicyStatement({
actions: ['iot:Connect', 'iot:Publish', 'iot:Subscribe', 'iot:Receive'],
resources: [
`arn:aws:iot:*:*:client/\${cognito-identity.amazonaws.com:sub}`,
`arn:aws:iot:*:*:topic/amplify/*`,
`arn:aws:iot:*:*:topicfilter/amplify/*`,
],
})
);
backend.addOutput({
custom: { iotEndpoint: 'your-iot-endpoint.iot.region.amazonaws.com' },
});Frontend — subscribe and publish:
import { PubSub } from '@aws-amplify/pubsub';
const sub = PubSub.subscribe({ topics: ['myTopic'] }).subscribe({
next: (data) => console.log('Message:', data),
error: (err) => console.error(err),
});
await PubSub.publish({ topics: ['myTopic'], message: { msg: 'hello' } });
sub.unsubscribe(); // MUST unsubscribe to prevent leaksWhen using subscriptions in React, MUST wrap in useEffect and return cleanup function to call .unsubscribe().
Retrieve the IoT endpoint programmatically: aws iot describe-endpoint --endpoint-type iot:Data-ATS. See AWS Amplify PubSub docs for connection configuration.
Custom CDK Stacks — Backend Only
Create additional CloudFormation stacks for resources not natively supported by Amplify:
const backend = defineBackend({ auth, data });
const customStack = backend.createStack('AnalyticsStack');
// Use any CDK construct in the custom stack
import * as sns from 'aws-cdk-lib/aws-sns';
const topic = new sns.Topic(customStack, 'NotificationTopic');
// Access Amplify resources from custom stack
const userPool = backend.auth.resources.userPool;Stack names MUST be unique within the backend — duplicate names cause deployment failures. Use descriptive names like 'EmailStack', 'AnalyticsStack'.
Backend Overrides — Backend Only
Access and modify underlying CloudFormation resources when Amplify's high-level API does not expose a needed property:
const backend = defineBackend({ auth, data });
// Auth override: Access the underlying CFN user pool resource
const cfnUserPool = backend.auth.resources.cfnResources.cfnUserPool;
cfnUserPool.policies = {
passwordPolicy: {
minimumLength: 12,
requireLowercase: true,
requireUppercase: true,
requireNumbers: true,
requireSymbols: true,
},
};
// DynamoDB override: Access underlying CFN resources
const { cfnResources } = backend.data.resources;
// Enable point-in-time recovery
cfnResources.amplifyDynamoDbTables['Todo'].pointInTimeRecoveryEnabled = true;
// Change billing mode
cfnResources.amplifyDynamoDbTables['Todo'].billingMode = 'PAY_PER_REQUEST';
// Set TTL
cfnResources.amplifyDynamoDbTables['Todo'].timeToLiveAttribute = {
attributeName: 'ttl',
enabled: true,
};The entry point for DynamoDB table overrides is backend.data.resources.cfnResources.amplifyDynamoDbTables['ModelName'], which exposes L1 CFN properties directly.
To add a Global Secondary Index, use .secondaryIndexes() in the schema definition (the Amplify-native approach) rather than CDK overrides:
const schema = a.schema({
Todo: a.model({
content: a.string(),
status: a.string(),
createdAt: a.datetime(),
})
.secondaryIndexes(index => [
index('status').sortKeys(['createdAt']),
])
.authorization(allow => [allow.owner()]),
});See AWS Amplify Override docs for the full override API.
Custom Outputs — Backend Only
Expose custom resource values to the frontend via amplify_outputs.json:
backend.addOutput({
custom: {
analyticsTopicArn: topic.topicArn,
apiEndpoint: 'https://api.example.com',
},
});Values appear under the custom key in amplify_outputs.json. Frontend reads them from the Amplify configuration after Amplify.configure().
Face Liveness — Backend + Frontend
Verify user identity with Amazon Rekognition Face Liveness. Add IAM permissions in amplify/backend.ts:
import * as iam from 'aws-cdk-lib/aws-iam';
backend.auth.resources.authenticatedUserIamRole.addToPrincipalPolicy(
new iam.PolicyStatement({
actions: [
'rekognition:CreateFaceLivenessSession',
'rekognition:StartFaceLivenessSession',
'rekognition:GetFaceLivenessSessionResults',
],
resources: ['*'], // Rekognition session ARNs are generated at runtime — scope with conditions if needed
})
);Frontend (React):
npm install @aws-amplify/ui-react-livenessimport { FaceLivenessDetector } from '@aws-amplify/ui-react-liveness';
<FaceLivenessDetector
sessionId={sessionId}
region="us-east-1"
onAnalysisComplete={async () => { /* fetch results */ }}
/>Create the session server-side via Rekognition SDK or a Lambda function, then pass the sessionId to the component. See AWS Amplify Liveness docs for the full integration guide.
Frontend (Swift — iOS 14+):
Requires the amplify-ui-swift-liveness package and camera permission (NSCameraUsageDescription in Info.plist). Add the package via Xcode SPM: https://github.com/aws-amplify/amplify-ui-swift-liveness.
The backend must include a Cognito Identity Pool with an IAM role that grants rekognition:StartFaceLivenessSession and rekognition:GetFaceLivenessSessionResults.
See Swift Liveness docs for the full SwiftUI integration guide.
Frontend (Android — API 24+):
Add the dependency to app/build.gradle.kts:
dependencies {
implementation("com.amplifyframework.ui:liveness:1.+")
}Requires Jetpack Compose. The backend must include a Cognito Identity Pool with an IAM role that grants rekognition:StartFaceLivenessSession and rekognition:GetFaceLivenessSessionResults.
See Android Liveness docs for the full Compose integration guide.
Pitfalls
- Duplicate stack names:
backend.createStack()names MUST be
unique across the entire backend — reusing a name causes deployment failures.
- Missing IAM permissions: Geo, PubSub, and Face Liveness all require
explicit IAM policies — Amplify does not auto-grant access to these services.
- Geo CDK setup: Geo (maps, place search, geofencing) requires CDK
constructs — there is no defineGeo() in Amplify Gen2. Use aws-cdk-lib/aws-location directly as shown above.
- PubSub endpoint: You MUST configure the correct IoT endpoint for
your region; using the wrong endpoint type causes silent connection failures.
Links
AI
Model Selection
Use a.ai.model() to select an AI model in both a.conversation() and a.generation() routes. Pass a human-readable model name string:
aiModel: a.ai.model('Claude 3.5 Sonnet v2')a.ai.model() accepts any supported model name:
- Anthropic:
'Claude 3 Haiku','Claude 3 Sonnet','Claude 3 Opus','Claude 3.5 Haiku','Claude 3.5 Sonnet','Claude 3.5 Sonnet v2','Claude 3.7 Sonnet','Claude Opus 4','Claude Sonnet 4','Claude Haiku 4.5','Claude Sonnet 4.5','Claude Opus 4.5','Claude Sonnet 4.6','Claude Opus 4.6' - Amazon:
'Amazon Nova Pro','Amazon Nova Lite','Amazon Nova Micro' - Meta:
'Llama 3.1 405B Instruct','Llama 3.1 70B Instruct','Llama 3.1 8B Instruct' - Cohere:
'Cohere Command R+','Cohere Command R' - Mistral:
'Mistral Large 2','Mistral Large','Mistral Small'
For models not in the supported list, use the raw escape hatch: aiModel: { resourcePath: '<bedrock-model-id>' }.
Availability depends on the AWS region and Bedrock model access enablement.
Note:a.generation()routes only support Anthropic (Claude) models.a.conversation()routes work with any supported model.
Backend: Conversation Routes
Define multi-turn conversation routes in your data schema using a.conversation():
// amplify/data/resource.ts
import { a, type ClientSchema } from '@aws-amplify/backend';
const schema = a.schema({
chat: a.conversation({
aiModel: a.ai.model('Claude 3.5 Sonnet v2'),
systemPrompt: 'You are a helpful assistant.',
})
.authorization(allow => allow.owner()),
});Backend: Generation Routes
Use a.generation() for single-turn (stateless) inference.
MUST: Only Anthropic (Claude) models supporta.generation()routes. Non-Anthropic models (Amazon Nova, Meta Llama, Cohere, Mistral) work witha.conversation()only.
const schema = a.schema({
summarize: a.generation({
aiModel: a.ai.model('Claude 3.5 Sonnet v2'),
systemPrompt: 'Summarize the provided text concisely.',
inferenceConfiguration: { maxTokens: 500, temperature: 0.3 },
})
.arguments({ text: a.string().required() })
.returns(a.customType({ summary: a.string() }))
.authorization(allow => allow.authenticated()),
});CRITICAL — Authorization Constraints:
- Conversation routes (
a.conversation()) MUST useallow.owner()authorization —allow.authenticated()and other non-owner strategies throw a TypeError at CDK assembly time (before deployment even begins). - Generation routes (
a.generation()) MUST use non-owner authorization (allow.authenticated(),allow.guest(),allow.group(), orallow.publicApiKey()) —allow.owner()throws a TypeError at CDK assembly time (before deployment even begins).
These constraints are asymmetric and frequently confused. Getting them wrong causes the CDK synthesis to fail with a non-obvious TypeError.
Security: Conversation history sent to Amazon Bedrock may contain PII. Do not log full request/response payloads in production. Enable CloudWatch Logs encryption (KMS) and set appropriate retention policies for any logs that may capture inference data.
Backend Integration
AI conversation and generation routes are part of your data schema. Import into amplify/backend.ts:
import { defineBackend } from '@aws-amplify/backend';
import { data } from './data/resource';
defineBackend({ data }); // AI routes live inside the data schemaBackend: AI Tools
Attach Lambda functions as tools to conversation routes so the AI model can invoke them:
import { myToolFunc } from '../functions/my-tool/resource';
const schema = a.schema({
chat: a.conversation({
aiModel: a.ai.model('Claude 3.5 Sonnet v2'),
systemPrompt: 'You are a helpful assistant with tool access.',
tools: [
{
name: 'getWeather',
query: a.ref('getWeather'),
description: 'Get current weather for a city',
},
],
})
.authorization(allow => allow.owner()),
getWeather: a.query()
.arguments({ city: a.string().required() })
.returns(a.customType({ temp: a.float(), condition: a.string() }))
.handler(a.handler.function(myToolFunc))
.authorization(allow => allow.authenticated()),
});Define the tool function with defineFunction (see functions-and-api.md).
Frontend: React AI UI
Install the AI UI package:
npm install @aws-amplify/ui-react-aiSet up hooks and render the conversation component:
import { generateClient } from 'aws-amplify/data';
import { createAIHooks, AIConversation } from '@aws-amplify/ui-react-ai';
import type { Schema } from '../amplify/data/resource';
const client = generateClient<Schema>();
const { useAIConversation } = createAIHooks(client);
export default function Chat() {
const [
{ data: { messages }, isLoading },
handleSendMessage,
] = useAIConversation('chat');
return (
<AIConversation
messages={messages}
isLoading={isLoading}
handleSendMessage={handleSendMessage}
/>
);
}Frontend: Manual Client
For programmatic access without the pre-built UI:
const client = generateClient<Schema>();
// List conversations
const { data: conversations } = await client.conversations.chat.list();
// Create a new conversation
const { data: conversation } = await client.conversations.chat.create();
// Send a message
const { data: message } = await conversation.sendMessage({
content: [{ text: 'Hello!' }],
});Pagination: use limit and nextToken parameters on .list().
Streaming
Subscribe to streaming responses for real-time token delivery:
In React, MUST wrap in useEffect and return the cleanup function:
useEffect(() => {
const sub = conversation.onStreamEvent({
next: (event) => console.log(event),
error: (err) => console.error(err),
});
return () => sub.unsubscribe();
}, [conversation]);UI note: Amplify AI Kit provides pre-built UI components for React and
React Native only. Flutter, Swift, and Android apps can invoke AI
conversation/generation routes via manual GraphQL client calls — see
data-mobile.md patterns for the equivalent approach.
Pitfalls
- Conversation auth MUST be `allow.owner()`: Using
allow.authenticated() or any other non-owner strategy on a.conversation() throws a TypeError at CDK assembly time.
- Generation auth MUST NOT be `allow.owner()`: Using
allow.owner() on a.generation() throws a TypeError at CDK assembly time. Use allow.authenticated(), allow.guest(), or allow.group().
- Missing AI route in data schema: The conversation or generation
route MUST be defined in your a.schema() — without it, the frontend client has no AI endpoint to call.
- Model availability: Not all Bedrock models are enabled by default —
you MUST enable model access in the AWS console (Bedrock → Model access) before using a model in a.ai.model().
- Message content structure: Both
sendMessage('Hello')(string) and
sendMessage({ content: [{ text: 'Hello' }] }) (object) are valid. Use the object form when sending images or tool results.
Links
Auth — Backend
Basic Auth Setup
Define authentication in amplify/auth/resource.ts:
import { defineAuth } from '@aws-amplify/backend';
export const auth = defineAuth({
loginWith: {
email: true,
// phone: true, // SMS-based login
},
userAttributes: {
preferredUsername: { required: false },
},
});Import into amplify/backend.ts:
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
defineBackend({ auth });MFA Configuration
export const auth = defineAuth({
loginWith: { email: true },
multifactor: {
mode: 'REQUIRED', // or 'OPTIONAL'
totp: true,
sms: true,
email: true,
},
});Set mode: 'REQUIRED' to enforce MFA for all users. 'OPTIONAL' lets users enable it themselves.
Frontend impact: When MFA is enabled, the Authenticator component handles all MFA steps automatically. For custom UI, see auth-web.md for signInStep handling.
Passwordless Authentication
Passwordless login methods can coexist with traditional password-based auth.
Email OTP:
export const auth = defineAuth({
loginWith: {
email: {
otpLogin: true,
},
},
});SMS OTP:
export const auth = defineAuth({
loginWith: {
phone: {
otpLogin: true,
},
},
});WebAuthn / Passkeys:
export const auth = defineAuth({
loginWith: {
webAuthn: true,
},
});These passwordless methods can be combined with each other and with password-based login in the same defineAuth configuration.
Social Login
You MUST use secret() for OAuth client secrets — never hardcode credentials.
import { defineAuth, secret } from '@aws-amplify/backend';
export const auth = defineAuth({
loginWith: {
email: true,
externalProviders: {
google: {
clientId: secret('GOOGLE_CLIENT_ID'),
clientSecret: secret('GOOGLE_CLIENT_SECRET'),
scopes: ['email', 'profile', 'openid'],
attributeMapping: {
email: 'email', // values are strings, NOT objects
fullname: 'name',
},
},
facebook: { clientId: secret('FB_CLIENT_ID'), clientSecret: secret('FB_CLIENT_SECRET') },
signInWithApple: {
clientId: secret('APPLE_CLIENT_ID'),
teamId: secret('APPLE_TEAM_ID'),
keyId: secret('APPLE_KEY_ID'),
privateKey: secret('APPLE_PRIVATE_KEY'),
},
loginWithAmazon: { clientId: secret('AMAZON_CLIENT_ID'), clientSecret: secret('AMAZON_CLIENT_SECRET') },
callbackUrls: ['http://localhost:3000/', 'https://myapp.com/'],
logoutUrls: ['http://localhost:3000/', 'https://myapp.com/'],
},
},
});Set secrets via CLI: echo "<value>" | npx ampx sandbox secret set GOOGLE_CLIENT_ID. For provider-specific OAuth setup guides, SHOULD consult AWS documentation via available tools; when unavailable, MUST use web search or AWS CLI.
SAML / OIDC (Enterprise)
OIDC providers are configured directly in externalProviders:
externalProviders: {
oidc: [{
name: 'MyOIDC',
clientId: secret('OIDC_CLIENT_ID'),
clientSecret: secret('OIDC_CLIENT_SECRET'),
issuerUrl: 'https://idp.example.com',
attributeMapping: { email: 'email' },
}],
callbackUrls: ['http://localhost:3000/'],
logoutUrls: ['http://localhost:3000/'],
}SAML is NOT supported in defineAuth — the ExternalProviderSpecificFactoryProps type has no saml property. The lower-level auth-construct package supports SAML, but it was never wired up to the high-level API. Use CDK escape hatches via backend.auth.resources to configure SAML providers:
// In backend.ts — SAML requires CDK-level configuration
const { cfnUserPool } = backend.auth.resources.cfnResources;
// Configure SAML identity provider via CfnUserPoolIdentityProviderConsult AWS documentation for CfnUserPoolIdentityProvider SAML configuration properties.
Cognito Triggers
import { defineAuth } from '@aws-amplify/backend';
import { preSignUp } from './pre-sign-up/resource';
import { postConfirmation } from './post-confirmation/resource';
export const auth = defineAuth({
loginWith: { email: true },
triggers: {
preSignUp,
postConfirmation,
// Also: preAuthentication, postAuthentication,
// createAuthChallenge, defineAuthChallenge, verifyAuthChallengeResponse,
// preTokenGeneration, customMessage, userMigration
},
});Define each trigger with defineFunction:
// amplify/auth/pre-sign-up/resource.ts
import { defineFunction } from '@aws-amplify/backend';
export const preSignUp = defineFunction({ name: 'pre-sign-up' });Guest (Unauthenticated) Access
Guest access is enabled by default in Amplify Gen2 — the Cognito Identity Pool is created with allowUnauthenticatedIdentities: true automatically.
To use guest access in your data models, set defaultAuthorizationMode to 'iam':
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: 'iam',
},
});Security: Guest access grants unauthenticated users IAM-authorized access. For production, explicitly evaluate whether guest access is needed and prefer allow.authenticated() as the default. If guest access is required, scope it to read-only on non-sensitive models only.To disable guest access, use a CDK override in backend.ts:
const { cfnIdentityPool } = backend.auth.resources.cfnResources;
cfnIdentityPool.allowUnauthenticatedIdentities = false;Pitfalls
- Trigger not registered (silent no-op): Defining a trigger function
with defineFunction but NOT adding it to triggers: {} in defineAuth causes a silent no-op — the function deploys but never fires. You MUST both define AND register: triggers: { preSignUp, postConfirmation }.
- Hardcoded secrets: Using string literals instead of
secret()for
OAuth credentials exposes them in source control.
- Missing scopes: Social providers default to minimal scopes — add
'email', 'profile' explicitly or user attributes won't populate.
- Google attribute mapping: The Google claim
namemaps to Cognito
fullname (NOT name). The attributeMapping values are plain strings, NOT objects: { email: 'email', fullname: 'name' }.
- MFA method mismatch: Enabling
sms: truein MFA requires a phone
number attribute on the user pool — add phone_number to user attributes. Similarly, email: true in MFA requires an email attribute on the user pool.
- Secrets in CI/CD: For branch environments, use:
npx ampx secret set KEY_NAME --branch main --app-id APP_ID.
Links
Auth — Mobile
Backend required: Auth must be defined in amplify/auth/resource.tsusing defineAuth — see auth-backend.md.Authenticator Component (Recommended)
All three mobile platforms provide a drop-in Authenticator component that handles sign-in, sign-up, MFA, social login, passwordless, password reset, and all intermediate auth states automatically. Use it unless you need a fully custom UI. Zero manual signInStep handling is required.
Passwordless: The Authenticator component handles passwordless flows (email OTP, SMS OTP, and WebAuthn/passkey) automatically when configured in defineAuth. No custom UI code needed for passwordless authentication. To default to passkeys, see the platform-specific "Passwordless / user-choice flow" examples below. Custom OTP/passkey flows require additional challenge handling.Flutter
Dependencies — add to pubspec.yaml:
dependencies:
amplify_flutter: ^2.0.0
amplify_auth_cognito: ^2.0.0
amplify_authenticator: ^2.0.0Usage — wrap your MaterialApp and set its builder:
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_authenticator/amplify_authenticator.dart';
import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:flutter/material.dart';
import 'amplify_outputs.dart';
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
_configureAmplify();
}
void _configureAmplify() async {
try {
await Amplify.addPlugin(AmplifyAuthCognito());
await Amplify.configure(amplifyOutputs);
} on Exception catch (e) {
safePrint('Error configuring Amplify: $e');
}
}
@override
Widget build(BuildContext context) {
return Authenticator(
child: MaterialApp(
builder: Authenticator.builder(),
home: const Scaffold(
body: Center(child: Text('You are logged in!')),
),
),
);
}
}Swift (Apple platforms)
Supports iOS 13+, macOS 12+, tvOS 13+, watchOS 9+, visionOS 1+ (preview).
Passkeys require iOS 17.4+, macOS 13.5+, or visionOS 1.0+.
Dependencies — add both SPM packages in Xcode (File > Add Packages…):
| Package | URL | Libraries |
|---|---|---|
| Amplify Library for Swift | https://github.com/aws-amplify/amplify-swift | Amplify, AWSCognitoAuthPlugin |
| Amplify UI Swift Authenticator | https://github.com/aws-amplify/amplify-ui-swift-authenticator | Authenticator |
SPM versioning: For both packages, select "Up to Next Major Version" in Xcode's dependency rule. Do NOT pin to a specific branch (e.g., main) — use "Up to Next Major Version" to get compatible updates automatically.Usage — SwiftUI entry point:
import Amplify
import Authenticator
import AWSCognitoAuthPlugin
import SwiftUI
@main
struct MyApp: App {
init() {
do {
try Amplify.add(plugin: AWSCognitoAuthPlugin())
try Amplify.configure(with: .amplifyOutputs)
} catch {
print("Unable to configure Amplify \(error)")
}
}
var body: some Scene {
WindowGroup {
Authenticator { state in
VStack {
Text("Hello, \(state.user.username)")
Button("Sign out") {
Task { await state.signOut() }
}
}
}
}
}
}Passwordless / user-choice flow:
Authenticator(authenticationFlow: .userChoice(
preferredAuthFactor: .webAuthn
)) { state in
Text("Welcome \(state.user.username)!")
}Android (Kotlin)
Dependencies — add to your app's build.gradle.kts:
// Enable Jetpack Compose
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
buildFeatures { compose = true }
composeOptions { kotlinCompilerExtensionVersion = "1.5.3" }
}
dependencies {
implementation("com.amplifyframework.ui:authenticator:1.4.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
}INTERNET permission is required in AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET"/>Configure — in your Application.onCreate():
try {
Amplify.addPlugin(AWSCognitoAuthPlugin())
Amplify.configure(AmplifyOutputs(R.raw.amplify_outputs), applicationContext)
} catch (error: AmplifyException) {
Log.e("MyApp", "Could not initialize Amplify", error)
}Usage — Jetpack Compose:
import com.amplifyframework.ui.authenticator.ui.Authenticator
import com.amplifyframework.ui.authenticator.SignedInState
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Authenticator { state ->
Column {
Text("Signed in as ${state.user.username}")
val scope = rememberCoroutineScope()
Button(onClick = { scope.launch { state.signOut() } }) {
Text("Sign Out")
}
}
}
}
}
}Passwordless / user-choice flow:
val authenticatorState = rememberAuthenticatorState(
authenticationFlow = AuthenticationFlow.UserChoice(
preferredAuthFactor = AuthFactor.WebAuthn
)
)
Authenticator(state = authenticatorState) { state ->
Text("Welcome ${state.user.username}!")
}Custom UI
Use the low-level Auth APIs when you need full control over the UI. Each platform returns a nextStep from signIn / signUp — switch on it and call confirmSignIn as needed. The Authenticator handles all these steps automatically; the list below is for reference when building custom flows.
Flutter
import 'package:amplify_flutter/amplify_flutter.dart';Sign in:
final result = await Amplify.Auth.signIn(
username: username,
password: password,
);
if (result.isSignedIn) {
safePrint('Sign in complete');
} else {
// Handle result.nextStep.signInStep — e.g.:
// confirmSignInWithSmsMfaCode → prompt for SMS code, call confirmSignIn
// confirmSignInWithTotpMfaCode → prompt for TOTP code, call confirmSignIn
// confirmSignInWithNewPassword → prompt new password, call confirmSignIn
// done → authenticated
}Confirm sign-in (for MFA / challenge steps):
final result = await Amplify.Auth.confirmSignIn(
confirmationValue: codeFromUser,
);Sign up:
final result = await Amplify.Auth.signUp(
username: username,
password: password,
options: SignUpOptions(
userAttributes: {AuthUserAttributeKey.email: email},
),
);
if (result.nextStep.signUpStep == AuthSignUpStep.confirmSignUp) {
// Prompt for confirmation code
}Confirm sign-up:
await Amplify.Auth.confirmSignUp(
username: username,
confirmationCode: code,
);Swift (Apple platforms)
Uses async/await.
import AmplifySign in:
do {
let result = try await Amplify.Auth.signIn(
username: username,
password: password
)
switch result.nextStep {
case .done:
print("Sign in succeeded")
case .confirmSignInWithSMSMFACode(let details, _):
print("SMS code sent to \(details.destination)")
// Prompt user, then call confirmSignIn
case .confirmSignInWithTOTPCode:
// Prompt for TOTP code, then call confirmSignIn
default:
print("Next step: \(result.nextStep)")
}
} catch let error as AuthError {
print("Sign in failed: \(error)")
}Confirm sign-in:
let result = try await Amplify.Auth.confirmSignIn(
challengeResponse: codeFromUser
)Sign up:
let options = AuthSignUpRequest.Options(
userAttributes: [AuthUserAttribute(.email, value: email)]
)
let result = try await Amplify.Auth.signUp(
username: username,
password: password,
options: options
)
if case .confirmUser(let details, _, _) = result.nextStep {
print("Confirmation sent to \(String(describing: details))")
}Confirm sign-up:
try await Amplify.Auth.confirmSignUp(
for: username,
confirmationCode: code
)Android (Kotlin)
Android supports both Kotlin coroutines and callbacks. Coroutines are recommended.
import com.amplifyframework.kotlin.core.Amplify
import com.amplifyframework.auth.AuthUserAttributeKey
import com.amplifyframework.auth.options.AuthSignUpOptionsSign in (coroutines — recommended):
try {
val result = Amplify.Auth.signIn("username", "password")
if (result.isSignedIn) {
Log.i("Auth", "Sign in succeeded")
} else {
// Handle result.nextStep.signInStep — e.g.:
// CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE → prompt SMS code
// CONFIRM_SIGN_IN_WITH_TOTP_CODE → prompt TOTP code
// DONE → authenticated
Log.i("Auth", "Next step: ${result.nextStep.signInStep}")
}
} catch (error: AuthException) {
Log.e("Auth", "Sign in failed", error)
}Sign in (callbacks — alternative):
import com.amplifyframework.core.Amplify // Java facade for callback style
Amplify.Auth.signIn("username", "password",
{ result -> Log.i("Auth", "Signed in: ${result.isSignedIn}") },
{ error -> Log.e("Auth", "Sign in failed", error) }
)Confirm sign-in (coroutines):
try {
val result = Amplify.Auth.confirmSignIn("code from user")
Log.i("Auth", "Confirmed: $result")
} catch (error: AuthException) {
Log.e("Auth", "Confirm failed", error)
}Sign up (coroutines):
val options = AuthSignUpOptions.builder()
.userAttributes(listOf(
AuthUserAttribute(AuthUserAttributeKey.email(), email)
))
.build()
try {
val result = Amplify.Auth.signUp("username", "password", options)
Log.i("Auth", "Sign up step: ${result.nextStep.signUpStep}")
} catch (error: AuthException) {
Log.e("Auth", "Sign up failed", error)
}Confirm sign-up (coroutines):
try {
Amplify.Auth.confirmSignUp("username", "123456")
} catch (error: AuthException) {
Log.e("Auth", "Confirm sign-up failed", error)
}Social Login on Mobile
Social sign-in uses an OAuth web UI redirect. Callback URLs must match the callbackUrls configured in your defineAuth backend resource.
Flutter:
final result = await Amplify.Auth.signInWithWebUI(
provider: AuthProvider.google,
);Platform setup for Flutter OAuth:
- Android: Add
<intent-filter>with your callback scheme toMainActivityinAndroidManifest.xml. - iOS: No additional platform configuration required.
- macOS: Enable App Sandbox → "Incoming Connections (Server)" in Xcode.
Swift:
let result = try await Amplify.Auth.signInWithWebUI(
for: .google,
presentationAnchor: window
)Platform setup: Add callback URL scheme to Info.plist under CFBundleURLSchemes.
Android (coroutines):
try {
val result = Amplify.Auth.signInWithSocialWebUI(
AuthProvider.google(), activity
)
Log.i("Auth", "Social sign-in OK: $result")
} catch (error: AuthException) {
Log.e("Auth", "Social sign-in failed", error)
}Platform setup: Add HostedUIRedirectActivity with your callback scheme to AndroidManifest.xml:
<activity
android:name="com.amplifyframework.auth.cognito.activities.HostedUIRedirectActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" />
</intent-filter>
</activity>Pitfalls
- Plugin order:
addPlugin()/add(plugin:)MUST be called
before configure() on all platforms — see core-mobile.md.
- Missing INTERNET permission (Android): Without
<uses-permission android:name="android.permission.INTERNET"/> in AndroidManifest.xml, all auth calls fail with a network error.
- Callback URL mismatch (social login): OAuth redirect URLs configured
in the native app (Info.plist / AndroidManifest.xml / Flutter scheme) MUST match the callbackUrls in your defineAuth backend resource. A mismatch causes a silent redirect failure.
- Unhandled auth steps (Custom UI only): When building custom sign-in
flows, the nextStep returned from signIn must be handled. Ignoring steps like MFA confirmation causes the auth flow to stall silently. The Authenticator component handles all steps automatically.
Links
- Authenticator (Android)
- Authenticator (Swift)
- Authenticator (Flutter)
- Auth Overview (Android)
- Sign In (Android)
- External Identity Providers (Android)
- Multi-Factor Authentication (Android)
- Auth Overview (Swift)
- Sign In (Swift)
- External Identity Providers (Swift)
- Multi-Factor Authentication (Swift)
- Auth Overview (Flutter)
- Sign In (Flutter)
- External Identity Providers (Flutter)
- Multi-Factor Authentication (Flutter)
Auth — Web
Backend required: Auth must be defined in amplify/auth/resource.tsusing defineAuth — see auth-backend.md.Authenticator Component
| Framework | Package | Tag | CSS (MUST import) |
|---|---|---|---|
| React / Next.js | @aws-amplify/ui-react | <Authenticator> | @aws-amplify/ui-react/styles.css |
| Vue | @aws-amplify/ui-vue | <Authenticator> | @aws-amplify/ui-vue/styles.css |
| Angular | @aws-amplify/ui-angular | <amplify-authenticator> + AmplifyAuthenticatorModule | @aws-amplify/ui-angular/theme.css |
Props: loginMechanisms={['email']}, socialProviders={['google']}. Slot: {({ signOut, user }) => ...} — access user?.signInDetails?.loginId. Next.js SSR: wrap layout in <Authenticator.Provider>, use useAuthenticator hook.
Manual Auth Flows
Imports from aws-amplify/auth: signIn, signUp, confirmSignUp, confirmSignIn, signOut, resetPassword.
After signIn(), you MUST switch on result.nextStep.signInStep:
| signInStep value | Action |
|---|---|
DONE | Authenticated |
CONFIRM_SIGN_UP | Call confirmSignUp() |
CONFIRM_SIGN_IN_WITH_TOTP_CODE | Prompt TOTP, call confirmSignIn({ challengeResponse }) |
CONFIRM_SIGN_IN_WITH_SMS_CODE | Prompt SMS code, same |
CONFIRM_SIGN_IN_WITH_EMAIL_CODE | Prompt email code, same |
CONTINUE_SIGN_IN_WITH_TOTP_SETUP | Show QR URI, call confirmSignIn() |
CONTINUE_SIGN_IN_WITH_MFA_SELECTION | `confirmSignIn({ challengeResponse: 'TOTP' \ |
RESET_PASSWORD | Call resetPassword() |
CONFIRM_SIGN_IN_WITH_NEW_PASSWORD_REQUIRED | confirmSignIn({ challengeResponse: newPassword }) |
CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE | confirmSignIn({ challengeResponse }) |
CONFIRM_SIGN_IN_WITH_PASSWORD | confirmSignIn({ challengeResponse: password }) |
CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION | `confirmSignIn({ challengeResponse: 'TOTP' \ |
CONTINUE_SIGN_IN_WITH_EMAIL_SETUP | Prompt email, call confirmSignIn() |
CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION | confirmSignIn({ challengeResponse: selectedFactor }) |
OAuth/social: signInWithRedirect({ provider: 'Google' }).
Session Management
API (from aws-amplify/auth) | Returns |
|---|---|
getCurrentUser() | { userId, username, signInDetails? } |
fetchAuthSession() | { tokens?, credentials?, identityId?, userSub? } — access .tokens?.idToken, .tokens?.accessToken |
fetchUserAttributes() | { email, phone_number, ... } |
Tokens refresh automatically.
Next.js Server-Side Auth
For server components and route handlers, use cookie-based auth:
import { generateServerClientUsingCookies } from '@aws-amplify/adapter-nextjs/data';
import { cookies } from 'next/headers';
import outputs from '@/amplify_outputs.json';
const client = generateServerClientUsingCookies({ config: outputs, cookies });Critical:generateServerClientUsingCookiesfrom@aws-amplify/adapter-nextjs/datais the ONLY way to access authenticated data in Next.js server components. Do NOT usegenerateClient()on the server side — it has no access to the user's session cookies.
For server actions and middleware, use createServerRunner from @aws-amplify/adapter-nextjs:
import { createServerRunner } from '@aws-amplify/adapter-nextjs';
import outputs from '@/amplify_outputs.json';
export const { runWithAmplifyServerContext } = createServerRunner({ config: outputs });React Native
React Native uses the same aws-amplify auth APIs as web. All manual auth flows (signIn, signUp, confirmSignIn, etc.) and session management APIs work identically.
Setup
Import order matters: react-native-get-random-values MUST be the FIRST import in the entry file, @aws-amplify/react-native MUST come before aws-amplify. See core-web.md for the full required import order.
npm install @aws-amplify/ui-react-native @react-native-async-storage/async-storageSame <Authenticator> prop API as web React (from @aws-amplify/ui-react-native). @react-native-async-storage/async-storage is required for token persistence.
Social Login
signInWithRedirect({ provider: 'Google' }) — same as web. Ensure callback URLs in defineAuth include your Expo scheme.
Pitfalls
- Missing CSS import: Without the
styles.cssimport, the
<Authenticator> renders as unstyled HTML.
- Unhandled sign-in steps: Not switching on ALL
signInStepvalues
causes the flow to silently stall on MFA or password-reset challenges. You MUST handle every possible value — missing any causes the auth flow to hang with no visible error.
- MFA timing: Calling
updateMFAPreference()before authentication
completes fails silently because the user is not yet authenticated. Wait until signInStep is 'DONE'.
- OAuth in multi-page apps: You MUST call
Hub.listen('auth', ...)
to capture the OAuth redirect callback on page reload.
- Vue component syntax: Vue MUST use PascalCase
<Authenticator>
component syntax (not kebab-case <authenticator>).
Links
Core — Mobile
Critical Rules
These patterns apply to every task — not just new projects. You MUST verify each one before implementing any feature.
Gen2 Detection
Before modifying any code, check if the project is already Gen2:
1. amplify/ directory exists with backend.ts 2. amplify_flutter in pubspec.yaml (Flutter) or @aws-amplify/backend in package.json devDependencies (for projects using a JS/TS-based Amplify backend alongside a native mobile frontend)
If both are true, the project is already Gen2 — skip to feature implementation. If amplify/.config/ exists instead, this is a Gen1 project — MUST NOT proceed (requires separate migration skill).
Plugin Initialization Order
Flutter, Swift, and Android all require plugins to be added before calling configure. Reversing the order causes a runtime exception.
Flutter:
await Amplify.addPlugins([AmplifyAuthCognito()]);
await Amplify.configure(amplifyOutputs);Swift:
try Amplify.add(plugin: AWSCognitoAuthPlugin())
try Amplify.configure(with: .amplifyOutputs)Android (Kotlin):
Amplify.addPlugin(AWSCognitoAuthPlugin())
Amplify.configure(AmplifyOutputs(R.raw.amplify_outputs), applicationContext)Required Packages
Flutter — pubspec.yaml:
dependencies:
amplify_flutter: ^2.0.0
amplify_auth_cognito: ^2.0.0Swift: Add via Xcode SPM: https://github.com/aws-amplify/amplify-swift (Up to Next Major Version).
Android — app/build.gradle.kts:
dependencies {
implementation("com.amplifyframework:core:2.+")
implementation("com.amplifyframework:aws-auth-cognito:2.+")
}Configure Entry Points
Flutter — lib/main.dart:
import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'amplify_outputs.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Amplify.addPlugins([AmplifyAuthCognito()]);
await Amplify.configure(amplifyOutputs);
runApp(const MyApp());
}Swift (SwiftUI) — MyApp.swift:
import SwiftUI
import Amplify
import AWSCognitoAuthPlugin
@main
struct MyApp: App {
init() {
do {
try Amplify.add(plugin: AWSCognitoAuthPlugin())
try Amplify.configure(with: .amplifyOutputs)
} catch {
print("Failed to configure Amplify: \(error)")
}
}
var body: some Scene {
WindowGroup { ContentView() }
}
}Swift (UIKit) — AppDelegate.swift:
import Amplify
import AWSCognitoAuthPlugin
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
do {
try Amplify.add(plugin: AWSCognitoAuthPlugin())
try Amplify.configure(with: .amplifyOutputs)
} catch {
print("Failed to configure Amplify: \(error)")
}
return true
}Android — Application class:
import com.amplifyframework.core.Amplify
import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin
import com.amplifyframework.core.configuration.AmplifyOutputs
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
Amplify.addPlugin(AWSCognitoAuthPlugin())
Amplify.configure(AmplifyOutputs(R.raw.amplify_outputs), applicationContext)
}
}For Android, amplify_outputs.json MUST go in app/src/main/res/raw/, not in the project root.
Platform-Specific MUST Steps
Android: Core Library Desugaring
Core library desugaring MUST be enabled for Android API level < 26. The agent MUST provide explicit step-by-step desugaring instructions to the customer — do not just mention it. See: https://docs.amplify.aws/android/start/quickstart/#5-install-dependencies
In app/build.gradle.kts:
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
}Swift (Apple platforms): Xcode Project Configuration
Supported: iOS 13+, macOS 12+, tvOS 13+, watchOS 9+, visionOS 1+ (preview).
For iOS/Swift projects, amplify_outputs.json MUST be added to the Xcode project. The agent should instruct the user to drag the amplify_outputs.json file into their Xcode project navigator so it is included in the app bundle.
Flutter: Dart Output Format
For Flutter, amplify_outputs.dart MUST be generated by specifying the dart output format when running sandbox or deploy commands:
npx ampx sandbox --outputs-format dart --outputs-out-dir lib
npx ampx generate outputs --format dart --out-dir libLinks
Core — Web
Critical Rules
These patterns apply to every task — not just new projects. You MUST verify each one before implementing any feature.
Gen2 Detection
Before modifying any code, check if the project is already Gen2:
1. amplify/ directory exists with backend.ts 2. @aws-amplify/backend in package.json devDependencies
If both are true, the project is already Gen2 — skip to feature implementation. If amplify/.config/ exists instead, this is a Gen1 project — MUST NOT proceed (requires separate migration skill).
Directory Structure
amplify/ and src/ MUST be siblings under the project root. Placing them at different directory levels breaks sandbox detection.
project-root/
├── amplify/
│ ├── backend.ts
│ ├── auth/resource.ts
│ ├── data/resource.ts
│ └── storage/resource.ts
├── src/
├── amplify_outputs.json # Generated — DO NOT edit
└── package.jsonFrontend Configuration
Import the generated outputs and configure Amplify in the correct entry point for your framework. Placing this in the wrong file causes silent failures — Amplify API calls return undefined or empty responses with no error.
WARNING: amplify_outputs.json MUST exist before the app can compile. If missing, the build fails with a module-not-found error. Run npx ampx sandbox (or npx ampx sandbox --once) first to generate it. See scaffolding.md for the correct sequence. React (Vite) — src/main.tsx:
import { Amplify } from 'aws-amplify';
import outputs from '../amplify_outputs.json';
Amplify.configure(outputs);Next.js (App Router) — app/layout.tsx:
import { Amplify } from 'aws-amplify';
import outputs from '@/amplify_outputs.json';
Amplify.configure(outputs, { ssr: true });`{ ssr: true }` applies only to Next.js App Router. All other frameworks (Vue, Angular, React SPA) omit this option. Vue — src/main.js:
import { Amplify } from 'aws-amplify';
import outputs from '../amplify_outputs.json';
Amplify.configure(outputs);Angular — src/main.ts:
import { Amplify } from 'aws-amplify';
import outputs from '../amplify_outputs.json';
Amplify.configure(outputs);Data Client Best Practices
See data-web.md for generateClient setup and module-scope rules.
For Next.js Server Components, use generateServerClientUsingCookies from @aws-amplify/adapter-nextjs/data — NOT generateClient. Server components have no browser session, so generateClient fails silently. <Authenticator.Provider> is required in layout.tsx for auth context.
React Native
React Native uses the same aws-amplify JS package as web frameworks (it is part of amplify-js, not the native mobile SDKs). All web APIs apply to RN with the additions below.
Required Packages
npm install aws-amplify @aws-amplify/react-native \
@react-native-async-storage/async-storage \
react-native-get-random-values@react-native-async-storage/async-storage is required — the Amplify SDK uses it for token persistence and will fail at runtime without it.
Configure Entry Points
No plugin registration needed — configure only.
React Native (Expo) — App.tsx:
import 'react-native-get-random-values'; // MUST be first
import '@aws-amplify/react-native'; // MUST come before aws-amplify
import { Amplify } from 'aws-amplify';
import outputs from './amplify_outputs.json';
Amplify.configure(outputs);React Native (Bare CLI) — index.js (before AppRegistry.registerComponent):
import 'react-native-get-random-values'; // MUST be first
import '@aws-amplify/react-native'; // MUST come before aws-amplify
import { Amplify } from 'aws-amplify';
import outputs from './amplify_outputs.json';
Amplify.configure(outputs);Gen2 Detection (React Native)
Same as web — check for amplify/ directory with backend.ts and @aws-amplify/backend in package.json devDependencies.
React Native Pitfalls
- Import order:
react-native-get-random-valuesMUST be the FIRST
import in the entry file, @aws-amplify/react-native MUST come before aws-amplify. Reversing the order causes cryptographic failures at runtime.
- Missing AsyncStorage: Without
@react-native-async-storage/async-storage, auth tokens are not persisted and users must re-authenticate on every app restart.
Pitfalls
- Forgetting to import
amplify_outputs.jsonin the entry point — the app
will load but all Amplify API calls will fail silently.
Links
Data — Backend
Schema Definition
Define your data models in amplify/data/resource.ts:
import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
const schema = a.schema({
Todo: a.model({
content: a.string().required(),
priority: a.enum(['low', 'medium', 'high']),
done: a.boolean().default(false),
dueDate: a.date(),
owner: a.string(),
}).authorization(allow => [allow.owner()]),
});
export type Schema = ClientSchema<typeof schema>;
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: 'userPool',
},
});Import into amplify/backend.ts:
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
import { data } from './data/resource';
defineBackend({ auth, data });You MUST export Schema as ClientSchema<typeof schema> — without this export, frontend clients lose all type inference. Field types: a.string(), a.integer(), a.float(), a.boolean(), a.date(), a.datetime(), a.timestamp(), a.time(), a.email(), a.url(), a.phone(), a.ipAddress(), a.json(), a.id(), a.enum([...]). Chain .required() or .array() on any field; .default(value) on scalar fields only (not enums — see Pitfalls).
Authorization Rules
Six strategies, applied per-model or per-field:
WARNING: In data authorization rules, allow.guest() is a method call (with parentheses). In storage access rules, allow.guest is a property (no parentheses). Mixing these up causes TypeScript errors.
a.model({ /* fields */ }).authorization(allow => [
allow.publicApiKey().to(['read']), // API key: public read
allow.guest().to(['read']), // Requires defaultAuthorizationMode: 'iam'
allow.owner(), // Creator has full CRUD
allow.authenticated().to(['read']), // Any signed-in user can read
allow.group('Admins'), // Named Cognito group
allow.custom(), // Lambda authorizer
])Security note:allow.guest()andallow.publicApiKey()both permit unauthenticated access. Only use for intentionally public, non-sensitive data. Preferallow.authenticated()orallow.owner()for sensitive resources. See Amplify authorization best practices and Amazon Cognito Identity Pool security for guidance on choosing the right authorization strategy.
Per-field authorization overrides model-level rules:
Post: a.model({
title: a.string(),
secret: a.string().authorization(allow => [allow.owner()]),
}).authorization(allow => [allow.authenticated().to(['read'])])Multi-owner: Use allow.ownersDefinedIn('editors') with an editors: a.string().array() field to grant multiple users ownership. Dynamic groups: Use allow.groupsDefinedIn('teamGroups') with a string field to control access via group names stored on each record.
Relationships
Three types — reference field types MUST match the related model's identifier type.
const schema = a.schema({
Team: a.model({
name: a.string().required(),
members: a.hasMany('Member', 'teamId'),
}).authorization(allow => [allow.owner()]),
Member: a.model({
name: a.string().required(),
teamId: a.id().required(),
team: a.belongsTo('Team', 'teamId'),
profile: a.hasOne('Profile', 'memberId'),
}).authorization(allow => [allow.owner()]),
Profile: a.model({
bio: a.string(),
memberId: a.id().required(),
member: a.belongsTo('Member', 'memberId'),
}).authorization(allow => [allow.owner()]),
});The second argument to hasMany/belongsTo/hasOne is the foreign key field name. That field MUST be declared explicitly on the child model.
You MUST declare both sides of every relationship — the parent model needs a.hasMany('Child', 'fkField') AND the child model needs a.belongsTo('Parent', 'fkField'). Omitting either side causes silent query failures (e.g., lazy-loading the relation returns undefined).
The foreign-key field MUST use a.id() — NOT a.string() — to match the related model's identifier type. Using a.string() causes runtime relationship resolution failures.
// CORRECT — both sides declared, FK uses a.id()
Team: a.model({
name: a.string().required(),
members: a.hasMany('Member', 'teamId'), // parent side
})
Member: a.model({
name: a.string().required(),
teamId: a.id().required(), // FK: a.id(), NOT a.string()
team: a.belongsTo('Team', 'teamId'), // child side — REQUIRED
})Secondary Indexes
Todo: a.model({
content: a.string(),
status: a.string(),
createdAt: a.datetime(),
}).secondaryIndexes(index => [
index('status').sortKeys(['createdAt']).queryField('listByStatus'),
])Indexes enable client.models.Todo.listByStatus({ status: 'active' }). Composite sort keys allow multi-field sorting within a partition. You SHOULD name the queryField descriptively — it becomes the typed client method name.
Enum Types
Define enums with a.enum() at the top level of a.schema(), then reference them in model fields with a.ref():
const schema = a.schema({
Priority: a.enum(['low', 'medium', 'high']),
Task: a.model({
title: a.string().required(),
priority: a.ref('Priority'),
}).authorization(allow => [allow.owner()]),
});You can also use a.enum() inline on a model field:
Todo: a.model({
content: a.string().required(),
priority: a.enum(['low', 'medium', 'high']),
})⚠️ Pitfall:.default()does not work ona.enum()fields — default values are only supported on scalar types (a.string(),a.integer(), etc.). Applying.default()to an enum field silently fails at deployment.
Custom Types
Custom types group related fields into a reusable structure:
const schema = a.schema({
Location: a.customType({ lat: a.float(), lng: a.float() }),
Task: a.model({
title: a.string().required(),
location: a.ref('Location'),
}).authorization(allow => [allow.owner()]),
});Use a.ref('TypeName') to reference custom types or enums in model fields.
Custom Queries and Mutations
Expose Lambda-backed operations through the schema:
const schema = a.schema({
// ... models ...
echo: a.query()
.arguments({ message: a.string().required() })
.returns(a.string())
.handler(a.handler.function('echoHandler'))
.authorization(allow => [allow.authenticated()]),
placeOrder: a.mutation()
.arguments({ productId: a.id().required(), qty: a.integer() })
.returns(a.json())
.handler(a.handler.function('orderHandler'))
.authorization(allow => [allow.authenticated()]),
});The handler function name MUST match a defineFunction name imported into backend.ts.
Authorization Modes
Configure default and additional auth modes in defineData:
Starter template default (public access):
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: 'apiKey',
apiKeyAuthorizationMode: { expiresInDays: 30 },
},
});With auth (user-scoped access):
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: 'userPool',
apiKeyAuthorizationMode: { expiresInDays: 30 },
// lambdaAuthorizationMode: { function: myAuthFn },
},
});The defaultAuthorizationMode MUST match at least one strategy used in your model authorization() rules (e.g., userPool ↔ owner() / authenticated() / group(); apiKey ↔ publicApiKey(); iam ↔ guest()).
Guest access is enabled by default in Amplify Gen2 — see auth-backend.md for details and how to disable it.
Guest access configuration (with allow.guest()):
// amplify/data/resource.ts — set IAM as default auth mode for guest access
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: 'iam',
},
});Pitfalls
- Missing `ClientSchema` export: Without `export type Schema =
ClientSchema<typeof schema>, frontend generateClient<Schema>()` has no type information and all operations are untyped.
- FK field type `a.string()` instead of `a.id()`: Using
a.string()
for foreign key fields causes relationship resolution to fail silently — queries return null for related models. Always use a.id() for FK fields.
- Missing relationship side: Omitting
belongsToon the child model
(or hasMany on the parent) causes lazy-loading the relation to return undefined with no error.
- Guest access auth mode:
allow.guest()requires
defaultAuthorizationMode: 'iam' in defineData. Guest access (unauthenticated identities) is enabled by default in Amplify Gen2.
- Auth mode conflict: Using
allow.publicApiKey()in model rules but
setting defaultAuthorizationMode: 'userPool' without adding apiKeyAuthorizationMode causes API key requests to be rejected.
- Forgetting `defineBackend`: Defining
datawithout importing it
into backend.ts means the schema is never deployed.
- `.default()` on enum fields:
.default()does not work on
a.enum() fields — default values are only supported on scalar types (a.string(), a.integer(), a.float(), a.boolean(), etc.). Applying .default() to an enum field silently fails at deployment.
Links
Data — Mobile
Backend required: Data must be defined in amplify/data/resource.tsusing defineData — see data-backend.md.Flutter
Import package:amplify_flutter/amplify_flutter.dart. All operations go through Amplify.API.
Queries: Amplify.API.query(request: ModelQueries.list(Todo.classType)) — response in .response.data?.items. Same pattern for .get().
Mutations: Amplify.API.mutate(request: ModelMutations.create(todo)) — same shape for .update(), .delete(). Build updated models with todo.copyWith(done: true).
Subscriptions: Amplify.API.subscribe(ModelSubscriptions.onCreate(Todo.classType)) → returns a stream. Listen with .listen(), cancel with sub.cancel().
Swift (Apple platforms)
Supported: iOS 13+, macOS 12+, tvOS 13+, watchOS 9+, visionOS 1+ (preview).
Uses Amplify.API.query/mutate with async/await. Swift uses ModelQueries, ModelMutations, and ModelSubscriptions (plural, like Flutter).
Queries: try await Amplify.API.query(request: .list(Todo.self)) — result is .success(let todos).
Mutations: try await Amplify.API.mutate(request: .create(newTodo)) — same for .update(), .delete(). Modify models directly: updated.done = true.
Subscriptions: Amplify.API.subscribe(request: .subscription(of: Todo.self, type: .onCreate)) → use for try await event in subscription. Cancel via task.cancel() when the view disappears.
Android (Kotlin)
Android supports both callback-based and coroutine-based APIs. Coroutine example (recommended):
Queries:
suspend fun getTodo(id: String) {
try {
val response = Amplify.API.query(ModelQuery.get(Todo::class.java, id))
Log.i("MyAmplifyApp", response.data.name)
} catch (error: ApiException) {
Log.e("MyAmplifyApp", "Query failed", error)
}
}Mutations:
val todo = Todo.builder()
.name("My todo")
.build()
try {
val response = Amplify.API.mutate(ModelMutation.create(todo))
Log.i("MyAmplifyApp", "Todo with id: ${response.data.id}")
} catch (error: ApiException) {
Log.e("MyAmplifyApp", "Create failed", error)
}Same pattern for .update() and .delete(). Build models via Todo.builder().name("text").build(); update via todo.copyOfBuilder().done(true).build().
Subscriptions (coroutine — uses Kotlin Flow):
val job = scope.launch {
try {
Amplify.API.subscribe(ModelSubscription.onCreate(Todo::class.java))
.catch { Log.e("MyAmplifyApp", "Error on subscription", it) }
.collect { Log.i("MyAmplifyApp", "Todo created: ${it.data.name}") }
} catch (error: ApiException) {
Log.e("MyAmplifyApp", "Subscription not established", error)
}
}
// When done:
job.cancel()Callback alternative: all operations also accept onSuccess/onError lambdas — e.g. Amplify.API.query(ModelQuery.list(Todo::class.java), { response -> ... }, { error -> ... }).
Pitfalls
- Missing codegen for native platforms: Flutter, Swift, and Android
MUST run npx ampx generate graphql-client-code to produce typed model classes. Without this step, model types do not exist. You SHOULD use typed model classes for compile-time safety.
- GraphQL vs REST confusion: All data operations use the GraphQL API
(Amplify.API.query/mutate), not REST. Using REST methods for model CRUD returns errors.
- Subscription cleanup: Every platform MUST perform explicit
subscription cleanup (.cancel() on Swift tasks, job.cancel() for Kotlin coroutines, subscription.cancel() for callbacks, or sub.cancel() for Flutter). Missing cleanup causes connection leaks and stale data.
- Offline sync (Flutter/Swift/Android): DataStore is a separate API
from direct API operations. Do not mix DataStore.query() with Amplify.API.query() in the same model workflow.
Links
- Data Overview (Android)
- Set Up Data (Android)
- Connect to Existing Data Sources (Android)
- Data Client (Android)
- Data Overview (Swift)
- Set Up Data (Swift)
- Connect to Existing Data Sources (Swift)
- Data Client (Swift)
- Data Overview (Flutter)
- Set Up Data (Flutter)
- Connect to Existing Data Sources (Flutter)
- Data Client (Flutter)
Data — Web
Backend required: Data must be defined in amplify/data/resource.tsusing defineData — see data-backend.md.Client Setup
`generateClient<Schema>()` MUST be called at module scope (outside any React component). Calling it inside a component creates a new client per render, breaking subscriptions and caching.
import { generateClient } from 'aws-amplify/data';
import type { Schema } from '../amplify/data/resource';
// Module scope — called once
const client = generateClient<Schema>();The <Schema> generic gives full type inference on all model operations.
CRUD Operations
All operations return { data, errors }. You SHOULD check errors before using data.
const { data, errors } = await client.models.Todo.create({ content: 'Ship feature', priority: 'high' });Same shape for .list(), .get({ id }), .update({ id, done: true }), .delete({ id }). .list() accepts an optional filter: { filter: { done: { eq: false } } }.
Error Handling
You SHOULD handle both GraphQL-level errors and network failures:
try {
const { data, errors } = await client.models.Todo.create({ content: 'New todo' });
if (errors) { /* handle GraphQL field/validation errors */ }
} catch (err) {
/* handle network or unexpected errors */
}Real-Time
- `observeQuery()` — auto-updating list, returns
{ items }snapshots. Recommended default. - `onCreate()` / `onUpdate()` / `onDelete()` — per-event subscriptions.
Both return an observable; call .subscribe({ next }) and MUST call sub.unsubscribe() in cleanup.
useEffect(() => {
const sub = client.models.Todo.observeQuery().subscribe({
next: ({ items }) => setTodos(items),
});
return () => sub.unsubscribe();
}, []);Server-Side (Next.js)
import { generateServerClientUsingCookies } from '@aws-amplify/adapter-nextjs/data';
import { cookies } from 'next/headers';
import outputs from '@/amplify_outputs.json';
import type { Schema } from '@/amplify/data/resource';
const cookieClient = generateServerClientUsingCookies<Schema>({ config: outputs, cookies });Use cookieClient.models.* the same as the browser client. Works in Server Components, Server Actions, and App Router API routes.
React Native
Identical to the web client — uses generateClient<Schema>() from aws-amplify/data. All CRUD, observeQuery(), and subscription APIs (onCreate, onUpdate, onDelete) are the same.
Pitfalls
- Subscription memory leaks:
useEffectMUST return
() => sub.unsubscribe() as a cleanup function. Without it, subscriptions accumulate across re-renders, causing memory leaks and duplicate data updates.
- Wrong auth mode for subscriptions: Subscriptions require a
WebSocket-compatible auth mode (userPool or iam). API key auth on subscriptions fails silently.
- Missing `<Schema>` generic:
generateClient()without<Schema>
returns an untyped client — all operations lose autocomplete and type checking.
- Server client without cookies: Using
generateClient()in Next.js
server components fails (no browser session) — you MUST use generateServerClientUsingCookies.
Links
Deployment
Prerequisites
Before deploying, verify:
npx ampx --versionreturns a valid versionaws sts get-caller-identitysucceeds- Node.js ≥ 18.x installed
.gitignoreincludesnode_modules/,.env*,amplify_outputs.json,
.amplify/
`amplify_outputs.json` is gitignored — it is generated at build time, NOT committed to source control:
- Local dev:
npx ampx sandboxgenerates it automatically - CI/CD:
npx ampx pipeline-deploygenerates it during the build phase - Other frontend apps in a monorepo: Use
npx ampx generate outputs --app-id <backend-app-id> to generate it
- Project is a Gen2 project — see
core-web.md or core-mobile.md for detection logic (Gen2 uses amplify/backend.ts + defineBackend())
Sandbox Deployment
Deploy a personal development environment:
AWS_REGION=us-east-1 npx ampx sandbox --onceYou MUST use the --once flag in agent and CI environments — without it, the command starts a file watcher that never exits. If prompted to bootstrap, run npx ampx sandbox --once again after bootstrapping completes.
Verify amplify_outputs.json was generated in the project root.
CI/CD Setup
Create the Amplify App
REPO="github.com/<user>/<repo>"
APP_ID=$(aws amplify create-app \
--name my-app \
--repository "$REPO" \
--access-token "$(gh auth token)" \
--query 'app.appId' --output text)You MUST use github.com/user/repo format — not https://.
IAM Service Role
Create a dedicated role for Amplify backend deployments:
ROLE_NAME="AmplifyBackendRole-${APP_ID}"
# 1. Create the role with Amplify trust policy
aws iam create-role --role-name "$ROLE_NAME" --assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "amplify.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
# 2. Attach the backend deploy policy
aws iam attach-role-policy --role-name "$ROLE_NAME" \
--policy-arn arn:aws:iam::aws:policy/service-role/AmplifyBackendDeployFullAccess
# 3. Attach the role to the app
ROLE_ARN=$(aws iam get-role --role-name "$ROLE_NAME" --query 'Role.Arn' --output text)
aws amplify update-app --app-id "$APP_ID" --iam-service-role-arn "$ROLE_ARN"All three steps are required — missing the role causes AccessDeniedException during deployment.
Create Branch
aws amplify create-branch --app-id "$APP_ID" --branch-name mainamplify.yml
Create amplify.yml in the project root. Set baseDirectory per framework:
| Framework | baseDirectory |
|---|---|
| Vite (React/Vue) | dist |
| CRA | build |
| Next.js (export) | out |
| Next.js (SSR) | .next |
| Angular | dist/<project-name>/browser |
Wrong `baseDirectory` = blank page in production (silent failure). Always match the framework table above.
version: 1
backend:
phases:
build:
commands:
- npm ci --cache .npm --prefer-offline
- npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
frontend:
phases:
build:
commands:
- npm run build
artifacts:
baseDirectory: dist # Change per framework (see table above)
files:
- '**/*'
cache:
paths:
- .npm/**/*
- node_modules/**/*Monorepo Configuration
For monorepos, set appRoot in amplify.yml to the subdirectory containing the Amplify app:
appRoot: packages/webWARNING: appRoot must have NO leading slash. appRoot: packages/web (correct) vs appRoot: /packages/web (wrong)
Monorepo rules:
- Only ONE app runs
npx ampx pipeline-deploy; other apps use
npx ampx generate outputs --app-id <backend-app-id> to get their amplify_outputs.json.
- Run
npm ciat the repo root, NOT insideappRoot.
Trigger Deployment
aws amplify start-job --app-id "$APP_ID" --branch-name main --job-type RELEASESecrets Management
Sandbox: Set secrets via CLI:
npx ampx sandbox secret set MY_API_KEYSecurity: Avoid passing secret values as CLI arguments or viaecho— these appear in shell history and/proc. Instead, usenpx ampx sandbox secret set MY_SECRETwhich prompts for input interactively, or pipe from a secure source:aws ssm get-parameter --name /path/to/secret --with-decryption --query Parameter.Value --output text | npx ampx sandbox secret set MY_SECRET --from-stdin
This stores the secret for your personal sandbox environment. Branch environments (production): Set secrets via the ampx CLI:
npx ampx secret set MY_API_KEY --branch main --app-id $APP_IDOr via the Amplify console under App settings → Environment variables, or via the AWS CLI:
aws amplify update-app --app-id "$APP_ID" \
--environment-variables MY_API_KEY=<value>Important: --environment-variables stores values as plain text.For sensitive values (API keys, tokens), use npx ampx sandbox secret set(sandbox) or npx ampx secret set --branch (production) which stores inSSM SecureString.
>
Note: Under the hood, Amplify Gen2secret()references are backed by AWS Systems Manager Parameter Store (SecureString parameters). Review access policies on the/amplify/parameter path in your account to ensure only authorized roles can read production secrets.
Reference secrets in functions using secret() — see functions-and-api.md for the pattern.
Multi-Environment
Use branch-based environments — each Git branch deploys independently:
# Create a staging branch
git checkout -b staging
git push origin staging
aws amplify create-branch --app-id "$APP_ID" --branch-name staging
aws amplify start-job --app-id "$APP_ID" --branch-name staging --job-type RELEASEEach branch gets isolated backend resources (Cognito pool, AppSync API, DynamoDB tables). Set branch-specific secrets separately.
Custom Domains
Associate a custom domain with the Amplify app:
aws amplify create-domain-association \
--app-id "$APP_ID" \
--domain-name example.com \
--sub-domain-settings '[
{"prefix": "", "branchName": "main"},
{"prefix": "staging", "branchName": "staging"}
]'Amplify auto-provisions an SSL certificate. You MUST add the provided CNAME records to your DNS for verification. Check status:
aws amplify get-domain-association --app-id "$APP_ID" --domain-name example.comAmplify Hosting
Amplify Hosting provides framework-aware builds with SSR support for Next.js. The build pipeline auto-detects the framework from package.json. For SSR apps, Amplify deploys a Lambda@Edge or CloudFront function — no manual CloudFront configuration needed.
Production URL format: https://<branch>.<app-id>.amplifyapp.com
Deployment Validation
After deployment, check job status with aws amplify list-jobs --app-id "$APP_ID" --branch-name main --query 'jobSummaries[0].status' and verify amplify_outputs.json endpoints match expected values.
Post-Deployment
Rollback: Revert via Git and redeploy:
git revert HEAD --no-edit
git push origin main
# Amplify auto-triggers a new build from the pushFor CI/CD, manually trigger: aws amplify start-job --app-id "$APP_ID" --branch-name main --job-type RELEASE.
Pitfalls
- Missing `--once` flag: Without
--once, sandbox starts a file
watcher that never exits — agent sessions and CI pipelines hang indefinitely. MUST use npx ampx sandbox --once in any non-interactive environment.
- Repo format: You MUST use
github.com/user/repo— the
https:// prefix causes create-app to fail silently.
- Missing IAM service role: Skipping role creation causes
AccessDeniedException on every backend deployment.
- Wrong `baseDirectory`: Using
buildfor a Vite app (which outputs
to dist) causes a blank page in production — match the framework table above. This is a silent failure with no error message.
- Monorepo `appRoot` leading slash:
appRoot: packages/webvs
appRoot: /packages/web — leading slash breaks path resolution.
- `amplify_outputs.json` not committed: This file is gitignored and
generated at build time. CI uses pipeline-deploy to generate it; local dev uses sandbox.
- Not bootstrapping: First sandbox run in a new account/region
requires CDK bootstrapping — follow prompts or run npx ampx sandbox --once again after bootstrap.
Links
Functions & API
Lambda Functions
Define a function in amplify/functions/<name>/resource.ts:
import { defineFunction } from '@aws-amplify/backend';
export const myFunc = defineFunction({
name: 'my-func',
entry: './handler.ts',
timeoutSeconds: 30, // default 3, max 900
memoryMB: 512, // default 512
runtime: 22, // Node.js version (18, 20, 22, 24); default 22
environment: {
TABLE_NAME: 'my-table',
REGION: 'us-east-1',
},
});Create the handler at amplify/functions/<name>/handler.ts:
import type { Handler } from 'aws-lambda';
import { env } from '$amplify/env/my-func';
export const handler: Handler = async (event) => {
const table = env.TABLE_NAME; // typed, from defineFunction environment
return { statusCode: 200, body: JSON.stringify({ table }) };
};Import into amplify/backend.ts:
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
import { myFunc } from './functions/my-func/resource';
defineBackend({ auth, myFunc });Environment Variables & Secrets
You SHOULD import environment variables from $amplify/env/<function-name> — this provides type-safe access to values defined in defineFunction. Values are also available at runtime via process.env.VAR_NAME, but the $amplify/env import is preferred because it gives you compile-time type checking and autocompletion.
For sensitive values, use secret():
import { defineFunction, secret } from '@aws-amplify/backend';
export const myFunc = defineFunction({
name: 'my-func',
entry: './handler.ts',
environment: {
API_KEY: secret('MY_API_KEY'),
},
});Set secrets via CLI: echo "<value>" | npx ampx sandbox secret set MY_API_KEY.
IMPORTANT: The ampx sandbox secret set command is for local/sandbox development only. For apps deployed to Amplify Hosting, secrets MUST be created via the Hosting console or CLI — sandbox secrets are NOT available in hosted environments. See: https://docs.amplify.aws/react/deploy-and-host/fullstack-branching/secrets-and-vars/#set-secretsScheduled Functions
Use schedule to invoke a function on a cron or natural-language schedule:
import { defineFunction } from '@aws-amplify/backend';
export const cronJob = defineFunction({
name: 'cron-job',
entry: './handler.ts',
schedule: 'every 1h', // natural-language shorthand
// Valid shorthands: 'every 5m', 'every 1h', 'every 6h', 'every 1d'
// OR: schedule: '0 */1 * * ? *', // cron expression — same property
});The handler MUST use EventBridgeHandler type:
import type { EventBridgeHandler } from 'aws-lambda';
export const handler: EventBridgeHandler<'Scheduled Event', void, void> = async () => {
// scheduled logic
};Resource Access
Grant a function access to other Amplify resources:
const backend = defineBackend({ auth, data, storage, myFunc });
// Grant function access to auth, data, and storage
backend.myFunc.resources.lambda.addEnvironment(
'USER_POOL_ID', backend.auth.resources.userPool.userPoolId
);
backend.data.resources.tables['Todo'].grantReadData(backend.myFunc.resources.lambda);
backend.storage.resources.bucket.grantReadWrite(backend.myFunc.resources.lambda);For data schema access, use allow.resource() in authorization rules:
const schema = a.schema({
Todo: a.model({
content: a.string(),
}).authorization(allow => [allow.resource(myFunc)]),
});Custom Queries and Mutations
Use a.query() and a.mutation() with .handler() to add custom server-side logic through AppSync (no API Gateway needed):
// amplify/data/resource.ts
const schema = a.schema({
// Custom query with Lambda handler
summarize: a.query()
.arguments({ text: a.string().required() })
.returns(a.string())
.handler(a.handler.function(summarizeHandler))
.authorization(allow => [allow.authenticated()]),
// Custom mutation with Lambda handler
processOrder: a.mutation()
.arguments({ orderId: a.string().required() })
.returns(a.json())
.handler(a.handler.function(processOrderHandler))
.authorization(allow => [allow.authenticated()]),
});When to use which:
>
-a.query()/a.mutation()with.handler()— AppSync-native, type-safe, uses the data schema. Preferred for most custom logic.
- API Gateway + Lambda — Use when you need REST endpoints, webhooks, or third-party integrations that require a specific URL.
REST API (API Gateway)
Create a REST API using CDK in amplify/backend.ts:
import { defineBackend } from '@aws-amplify/backend';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import { myFunc } from './functions/my-func/resource';
const backend = defineBackend({ auth, myFunc });
const apiStack = backend.createStack('RestApiStack');
const api = new apigateway.RestApi(apiStack, 'MyRestApi', {
restApiName: 'my-rest-api',
deployOptions: { stageName: 'prod' },
});
api.root.addResource('items').addMethod(
'GET', new apigateway.LambdaIntegration(backend.myFunc.resources.lambda)
);
backend.addOutput({ custom: { restApiUrl: api.url } });The handler MUST use APIGatewayProxyHandler type for REST API (v1):
import type { APIGatewayProxyHandler } from 'aws-lambda';HTTP API (API Gateway v2)
For a lightweight HTTP API:
import type { APIGatewayProxyHandlerV2 } from 'aws-lambda';
import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2';
import { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations';
const httpApi = new apigwv2.HttpApi(apiStack, 'MyHttpApi', {
corsPreflight: { allowOrigins: ['*'], allowMethods: [apigwv2.CorsHttpMethod.GET] },
});
httpApi.addRoutes({
path: '/items',
methods: [apigwv2.HttpMethod.GET],
integration: new HttpLambdaIntegration('GetItems', backend.myFunc.resources.lambda),
});
backend.addOutput({ custom: { httpApiUrl: httpApi.url! } });The handler MUST use APIGatewayProxyHandlerV2 type for HTTP API (v2).
Backend Outputs
Use backend.addOutput() to expose custom values to the frontend via amplify_outputs.json:
backend.addOutput({ custom: { apiUrl: api.url, region: 'us-east-1' } });Frontend reads custom outputs from the configured Amplify outputs.
Calling from Client
For custom queries and mutations defined via a.query() or a.mutation(), call them from the client:
const { data } = await client.queries.summarize({ text: '...' });For REST/HTTP API outputs added via backend.addOutput(), read the endpoint URL from amplify_outputs.json and use standard HTTP clients.
Pitfalls
- `runtime` must be an integer: Use
runtime: 22, NOT
runtime: "nodejs22.x". String format causes build errors.
- Wrong handler type: REST API (v1) requires
APIGatewayProxyHandler
with event.httpMethod; HTTP API (v2) requires APIGatewayProxyHandlerV2 with event.requestContext.http.method. Mixing them causes malformed responses. Both return { statusCode, body }.
- Missing resource access: A function without explicit grants cannot
access auth, data, or storage resources — add grants in backend.ts.
- Secrets in plain `environment`: Sensitive values MUST use
secret(), not string literals.
- `createStack` name collision: Stack names passed to
backend.createStack() MUST be unique across the backend. Duplicate names cause deployment failures.
Links
Scaffolding
Web — Greenfield
You MUST use official starter templates. You MUST NOT manually scaffold the project structure — hand-crafted structures MAY break Amplify Hosting deployment detection.
React (Vite)
git clone https://github.com/aws-samples/amplify-vite-react-template.git my-app
cd my-app && rm -rf .git && git init
npm installNext.js
App Router (default):
git clone https://github.com/aws-samples/amplify-next-template.git my-app
cd my-app && rm -rf .git && git init
npm installPages Router:
git clone https://github.com/aws-samples/amplify-next-pages-template.git my-app
cd my-app && rm -rf .git && git init
npm installVue
git clone https://github.com/aws-samples/amplify-vue-template.git my-app
cd my-app && rm -rf .git && git init
npm installAngular
git clone https://github.com/aws-samples/amplify-angular-template.git my-app
cd my-app && rm -rf .git && git init
npm installWeb — Brownfield
For existing web projects, add Amplify Gen2 without overwriting application code. You SHOULD use the create command for automatic setup:
npm create amplify@latest -yYou MUST use the -y flag for non-interactive execution. This scaffolds the amplify/ directory and installs backend dependencies.
For monorepos or custom build pipelines where the create command conflicts, install manually:
npm install --save-dev @aws-amplify/backend@latest @aws-amplify/backend-cli@latest typescriptThen create amplify/backend.ts:
import { defineBackend } from '@aws-amplify/backend';
defineBackend({});Install the frontend library:
npm install aws-amplifyWeb — React Native
Expo
npx --yes create-expo-app@latest my-app
cd my-app
npm create amplify@latest -y
npm install aws-amplify @aws-amplify/react-native @react-native-async-storage/async-storage react-native-get-random-valuesBare CLI
npx --yes @react-native-community/cli init MyApp --pm npm
cd MyApp
npm create amplify@latest -y
npm install aws-amplify @aws-amplify/react-native @react-native-async-storage/async-storage react-native-get-random-values
npx --yes pod-install # iOS onlyYou MUST use the -y flag with npm create amplify@latest for non-interactive execution.
Mobile — Flutter
flutter create --platforms ios,android my_app
cd my_app
npm create amplify@latest -yAdd dependencies to pubspec.yaml:
dependencies:
amplify_flutter: ^2.0.0
amplify_auth_cognito: ^2.0.0Then run flutter pub get.
Mobile — Swift (Apple platforms)
You MUST NOT create the Xcode project from the CLI — assume an existing Xcode project is open in Xcode.
1. In the project root (where .xcodeproj lives), run: npm create amplify@latest -y 2. Add the Swift package via Xcode: File → Add Package Dependencies → https://github.com/aws-amplify/amplify-swift (Up to Next Major Version). 3. Add amplify_outputs.json to the Xcode project (drag into navigator, check "Copy items if needed").
Mobile — Android
You MUST NOT create the Android project from the CLI — assume an existing Android Studio project.
1. In the project root, run: npm create amplify@latest -y 2. Add dependencies to app/build.gradle.kts:
dependencies {
implementation("com.amplifyframework:core:2.+")
implementation("com.amplifyframework:aws-auth-cognito:2.+")
}3. Copy amplify_outputs.json into app/src/main/res/raw/.
Generate amplify_outputs
For mobile projects, this step must be completed before the app can build.
Run the sandbox before opening the mobile project.
WARNING: After scaffolding, you MUST run npx ampx sandbox --once (or npx ampx sandbox for local dev) before npm run dev. This generates amplify_outputs.json, which the frontend imports at build time. Without it, the app fails to compile because import outputs from '../amplify_outputs.json' resolves to nothing.
# After npm install:
npx ampx sandbox --once # generates amplify_outputs.json
npm run dev # NOW the app can compile
# Flutter requires the Dart output format (see core-mobile.md):
npx ampx sandbox --once --outputs-format dart --outputs-out-dir libamplify_outputs.json is gitignored — see deployment.md for generation details.
Pitfalls
- Using the wrong template for a web framework causes broken build configs.
Always match template to framework exactly.
- Forgetting
npm create amplify@latest -yafter the framework scaffold
is the most common mistake — without it, there is no amplify/ directory.
- Running `npm run dev` before `npx ampx sandbox`: The app cannot
compile without amplify_outputs.json — always run sandbox first.
- React Native requires
@react-native-async-storage/async-storage— the
Amplify SDK uses it for token persistence and will fail at runtime without it.
- For Android,
amplify_outputs.jsongoes inapp/src/main/res/raw/— see core-mobile.md.
Links
Storage — Backend
Basic Setup
Define storage in amplify/storage/resource.ts:
import { defineStorage } from '@aws-amplify/backend';
export const storage = defineStorage({
name: 'myFiles',
access: (allow) => ({
'public/*': [
allow.guest.to(['read']),
allow.authenticated.to(['read', 'write', 'delete']),
],
'protected/{entity_id}/*': [
allow.authenticated.to(['read']),
allow.entity('identity').to(['read', 'write', 'delete']),
],
'private/{entity_id}/*': [
allow.entity('identity').to(['read', 'write', 'delete']),
],
}),
});Import into amplify/backend.ts:
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
import { storage } from './storage/resource';
defineBackend({ auth, storage });Access Rules
Path patterns control who can access files. The {entity_id} placeholder resolves to the authenticated user's identity ID at runtime — each user gets an isolated directory.
Actions: 'read', 'write', 'delete' (granular: 'get' and 'list' instead of 'read'). Subjects: allow.guest.to([...]), allow.authenticated.to([...]), allow.groups(['Admins']).to([...]), allow.entity('identity').to([...]). Every rule MUST end with .to() specifying the permitted actions — omitting .to() means NO permissions are granted.
WARNING: Storage access rules use allow.guest (PROPERTY, no parentheses) and allow.authenticated (PROPERTY). Data authorization rules use allow.guest() (METHOD, with parentheses). Mixing these up causes TypeScript errors.
WARNING: {entity_id} MUST be paired with allow.entity('identity'). Using {entity_id} in a path without allow.entity('identity') in that path's rules has no effect.
Paths MUST end with /* to match all objects under that prefix. Paths MUST NOT start with /.
Multiple Buckets
export const primaryStorage = defineStorage({ name: 'primaryFiles', isDefault: true, access: (allow) => ({ /* rules */ }) });
export const secondaryStorage = defineStorage({ name: 'secondaryFiles', access: (allow) => ({ /* rules */ }) });You MUST set isDefault: true on exactly one bucket when defining multiple. Each bucket MUST have a unique name property. The name is what clients reference when targeting a non-default bucket.
Event Triggers
import { defineFunction, defineStorage } from '@aws-amplify/backend';
const onUploadHandler = defineFunction({ entry: './on-upload-handler.ts' });
export const storage = defineStorage({
name: 'myFiles',
triggers: { onUpload: onUploadHandler, onDelete: onUploadHandler },
access: (allow) => ({ 'public/*': [allow.authenticated.to(['read', 'write'])] }),
});The trigger handler receives an S3Handler event with bucket name and object key. You MUST import the trigger function into backend.ts.
Typed handler example:
import type { S3Handler } from 'aws-lambda';
export const handler: S3Handler = async (event) => {
const objectKeys = event.Records.map((record) => record.s3.object.key);
console.log(`Upload handler invoked for objects [${objectKeys.join(', ')}]`);
};Pitfalls
- *Paths without `/
:** A path like'public'` matches nothing — you
MUST use 'public/*' to match files under that prefix.
- Missing `.to([])`: Omitting
.to(['read', 'write'])from an access
rule grants NO permissions — the rule is silently ignored.
- Missing `{entity_id}`: Using
'private/*'instead of
'private/{entity_id}/*' exposes every user's private files to all authenticated users.
- Leading slash: Paths MUST NOT start with
/— use'public/*',
not '/public/*'.
- Forgetting `isDefault`: With multiple buckets and no
isDefault: true,
client operations fail because no default bucket is resolved.
- `grantReadWrite()` path argument: Do NOT pass a path argument to
grantReadWrite(lambda) — it operates on the whole bucket. There is no per-path grant API.
Links
Storage — Mobile
Backend required: Storage must be defined in amplify/storage/resource.tsusing defineStorage — see storage-backend.md.Flutter
Imports: amplify_flutter + amplify_storage_s3. All paths wrapped with StoragePath.fromString().
| Operation | Call |
|---|---|
| Upload file | Amplify.Storage.uploadFile(localFile: AWSFile.fromPath(path), path: const StoragePath.fromString('public/photo.jpg')) |
| Download file | Amplify.Storage.downloadFile(path: const StoragePath.fromString('public/photo.jpg'), localFile: localFile) |
| List | Amplify.Storage.list(path: const StoragePath.fromString('public/')) → .result.items |
| Presigned URL | Amplify.Storage.getUrl(path: const StoragePath.fromString('public/file.jpg')) |
| Remove | Amplify.Storage.remove(path: const StoragePath.fromString('public/file.jpg')) |
Security: Amplify Gen2 enables S3 server-side encryption (SSE-S3) by default. All transfers use HTTPS (TLS in transit). For sensitive data, configure SSE-KMS with a customer-managed key via CDK overrides.
Upload progress — use the onProgress callback parameter:
final op = Amplify.Storage.uploadFile(
localFile: AWSFile.fromPath('/path/to/file'),
path: const StoragePath.fromString('public/photos/photo.jpg'),
onProgress: (p) => print('fraction: ${p.fractionCompleted}'),
);
final result = await op.result;MUST use const with StoragePath.fromString() for compile-time constant paths.
Swift (Apple platforms)
Supported: iOS 13+, macOS 12+, tvOS 13+, watchOS 9+, visionOS 1+ (preview).
Uses Amplify.Storage with async/await. Import: Amplify.
| Operation | Call |
|---|---|
| Upload data | Amplify.Storage.uploadData(path: .fromString("public/file.txt"), data: data) → try await task.value |
| Upload file | Amplify.Storage.uploadFile(path: .fromString("public/file.txt"), local: fileUrl) → try await task.value |
| Download data | Amplify.Storage.downloadData(path: .fromString("public/file.txt")) → .value returns Data |
| Download file | Amplify.Storage.downloadFile(path: .fromString("public/path"), local: fileUrl) → try await task.value |
| List | try await Amplify.Storage.list(path: .fromString("public/")) → .items |
| Presigned URL | try await Amplify.Storage.getURL(path: .fromString("public/file.jpg")) |
| Remove | try await Amplify.Storage.remove(path: .fromString("public/file.jpg")) |
Download with progress tracking:
let downloadTask = Amplify.Storage.downloadData(
path: .fromString("public/example.jpg")
)
Task {
for await progress in await downloadTask.progress {
print("Progress: \(progress.fractionCompleted)")
}
}
let data = try await downloadTask.valueUpload with progress tracking:
let uploadTask = Amplify.Storage.uploadData(
path: .fromString("public/photo.jpg"),
data: imageData
)
Task {
for await progress in await uploadTask.progress {
print("Progress: \(progress)")
}
}
let result = try await uploadTask.valueUse SwiftUI's PhotosPicker (from import PhotosUI) to obtain image data, then pass to uploadData.
Android (Kotlin)
Android supports both callback-based and coroutine-based APIs. Import: com.amplifyframework.core.Amplify, com.amplifyframework.storage.StoragePath.
Coroutine example (recommended):
private suspend fun uploadFile() {
val exampleFile = File(applicationContext.filesDir, "example")
exampleFile.writeText("Example file contents")
val upload = Amplify.Storage.uploadFile(
StoragePath.fromString("public/example"), exampleFile
)
try {
val result = upload.result()
Log.i("MyAmplifyApp", "Successfully uploaded: ${result.path}")
} catch (error: StorageException) {
Log.e("MyAmplifyApp", "Upload failed", error)
}
}private suspend fun downloadFile() {
val download = Amplify.Storage.downloadFile(
StoragePath.fromString("public/example"), localFile
)
try {
val result = download.result()
Log.i("MyAmplifyApp", "Successfully downloaded: ${result.file.name}")
} catch (error: StorageException) {
Log.e("MyAmplifyApp", "Download failed", error)
}
}| Operation (coroutine) | Call |
|---|---|
| Upload file | Amplify.Storage.uploadFile(StoragePath.fromString("public/photo.jpg"), file) → .result() |
| Upload stream | Amplify.Storage.uploadInputStream(StoragePath.fromString("public/example"), stream) → .result() |
| Download file | Amplify.Storage.downloadFile(StoragePath.fromString("public/photo.jpg"), localFile) → .result() |
| List | Amplify.Storage.list(StoragePath.fromString("public/")) → .items |
| Presigned URL | Amplify.Storage.getUrl(StoragePath.fromString("public/file.jpg")) → .url |
| Remove | Amplify.Storage.remove(StoragePath.fromString("public/file.jpg")) |
Callback alternative: all operations also accept onSuccess/onError lambdas — e.g. Amplify.Storage.uploadFile(StoragePath.fromString("public/photo.jpg"), file, { result -> ... }, { error -> ... }).
Permissions
For authenticated user paths, use protected/{entity_id}/ or private/{entity_id}/ — the {entity_id} resolves to the user's Cognito identity ID at runtime.
- Android: Verify
INTERNETpermission is declared inAndroidManifest.xml(usually present by default). If the app accesses the camera, addCAMERA; for gallery access, addREAD_MEDIA_IMAGES(API 33+) orREAD_EXTERNAL_STORAGE(older). - Apple (iOS/macOS): No special permissions for S3 storage operations. If the app accesses the camera, add
NSCameraUsageDescriptioninInfo.plist. If the app accesses the photo library, addNSPhotoLibraryUsageDescription. - Flutter: Follows Android/iOS rules above — add permissions in
AndroidManifest.xmlandInfo.plistrespectively.
Pitfalls
- Swift SDK uses `getURL` (capital URL), not `getUrl`: Using the
wrong casing (lowercase l) causes compile errors. JS/web uses getUrl (lowercase), but Swift uses getURL.
- Wrong file wrapper per platform: Flutter requires
AWSFile.fromPath(), Swift uses Data (for uploadData) or a file URL (for uploadFile), Android uses File. Using the wrong type causes compile errors — check the platform's expected input.
- Missing `StoragePath.fromString()`: Flutter and Android require
StoragePath.fromString('path') to wrap path strings. Passing a raw string literal does not compile.
- Large file uploads on mobile: For files over 5 MB, the SDK
automatically uses multipart upload. You SHOULD implement progress tracking (onProgress in Flutter, for await progress in ... in Swift, transferObserver or progress callback in Android) to show upload progress to the user.
Links
Related skills
FAQ
What backend language does Amplify Gen2 use?
TypeScript; Gen2 backends are TypeScript-only in a code-first approach.
How many frontend frameworks does it support?
Eight: React, Next.js, Vue, Angular, React Native, Flutter, Swift, and Android.