
Aws Amplify
- 3.1k installs
- 2.2k repo stars
- Updated August 4, 2026
- aws/agent-toolkit-for-aws
aws-amplify is an agent skill for
About
The aws-amplify skill documents agent workflows from the repository SKILL.md. It covers node.js ^18.19.0 || ^20.6.0 || =22 and npm. Key workflows include aWS credentials configured aws sts get-caller-identity succeeds. - Route to the Right Reference Step 1: Identify the Task Type | Task | Go To | | ---------------------------------------- | ------------------------------------------------------------------------ | | Create a new project | → scaffolding.md references/scaffolding.md , then Step 2 and/or Step 3 | | Add or modify a backend feature | → Step 2 Backend Features | | Connect frontend to existing backen Developers invoke aws-amplify when the task matches the triggers and reference files in SKILL.md for grounded, stepwise execution. Reference files and progressive disclosure keep context focused while preserving concrete commands, configuration fields, and validation checks copied from the upstream documentation.
- Node.js ^18.19.0 || ^20.6.0 || =22 and npm
- AWS credentials configured aws sts get-caller-identity succeeds
- For sandbox: npx ampx --version returns a valid version
- For mobile: Platform-specific tooling Xcode, Android Studio, Flutter SDK
- Web: Default to React Vite and explain the choice.
Aws Amplify by the numbers
- 3,057 all-time installs (skills.sh)
- +381 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #204 of 1,879 Marketing & SEO skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
aws-amplify capabilities & compatibility
- Capabilities
- node.js ^18.19.0 || ^20.6.0 || =22 and npm · aws credentials configured aws sts get caller id · for sandbox: npx ampx version returns a valid · for mobile: platform specific tooling xcode, and · web: default to react vite and explain the choic
- Use cases
- seo · marketing · copywriting
What aws-amplify says it does
Build and deploy full-stack web and mobile apps with AWS Amplify Gen2
npx skills add https://github.com/aws/agent-toolkit-for-aws --skill aws-amplifyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3.1k |
|---|---|
| repo stars | ★ 2.2k |
| Security audit | 3 / 3 scanners passed |
| Last updated | August 4, 2026 |
| Repository | aws/agent-toolkit-for-aws ↗ |
What problem does aws-amplify solve for developers using the documented workflows?
The aws-amplify skill documents agent workflows from the repository SKILL.md. It covers node.js ^18.19.0 || ^20.6.0 || =22 and npm. Key workflows include aWS credentials configured aws sts get-caller
Who is it for?
Developers working with aws-amplify patterns described in the skill documentation.
Skip if: Skip when docs are empty or the task is outside the skill documented scope.
When should I use this skill?
Use when
What you get
Actionable aws-amplify guidance grounded in SKILL.md workflows and reference files.
- Amplify AI route configuration
- Model selection setup
By the numbers
- Documents 2 Amplify AI route types: a.conversation() and a.generation()
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: Default to React (Vite) and explain the choice.
- Mobile: Ask which platform (Flutter, Swift, Android, or React Native) —
there is no universal mobile default, so guessing leads to wasted effort.
- Neither specified: If the user says "build an app" without clarifying web
vs. mobile, ask before proceeding — the framework choice affects every subsequent step.
- 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: Default to npm unless the user specifies yarn or pnpm.
- Language: Default to TypeScript. Gen2 backends are TypeScript-only;
frontends should follow the project's existing language.
- Next.js: Default to App Router unless the user specifies Pages Router.
- React Native: Ask whether the user uses 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 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
Read the corresponding reference for each backend feature you need:
| 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 | geo-pubsub-cdk.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
amplify/ and src/ must be siblings under the project root — placing them at different directory levels breaks sandbox detection. (Exception: in monorepos, amplify/ may be in a packages/ subdirectory — the key is that amplify_outputs.json must be accessible from the frontend entry point.)
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, gitignored — never edit or commit
└── 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 |
Framework Setup
These patterns apply to every web task — not just new projects. 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 — do not proceed (requires separate migration skill).
Frontend 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 — without it, 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:
Important:layout.tsxis a server component in App Router. Use theConfigureAmplifyClientSideclient component pattern below instead.
{ ssr: true } is a Next.js-only option (not needed by Vue, Angular, or React SPA). Both App Router and Pages Router use it, but apply it differently:
- App Router — set globally in ConfigureAmplifyClientSide client component- Pages Router — set per-file where server-side access is needed
Next.js App Router: Client-Side Configuration
Next.js App Router requires a dedicated client component to configure Amplify for browser-side operations:
// components/ConfigureAmplifyClientSide.tsx
"use client";
import { Amplify } from "aws-amplify";
import outputs from "@/amplify_outputs.json";
Amplify.configure(outputs, { ssr: true });
export default function ConfigureAmplifyClientSide() {
return null;
}Import in your root layout:
// app/layout.tsx
import ConfigureAmplifyClientSide from "@/components/ConfigureAmplifyClientSide";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<ConfigureAmplifyClientSide />
{children}
</body>
</html>
);
}Why? In App Router,layout.tsxis a server component. Client components needAmplify.configure()to run in the browser. Without this, you get "Auth UserPool not configured" errors.
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);Next.js Pages Router
Pages Router does NOT need { ssr: true } in _app.tsx. Instead, configure per-file where you need server-side access:
// pages/api/protected.ts or getServerSideProps
import { Amplify } from 'aws-amplify';
import outputs from '@/amplify_outputs.json';
Amplify.configure(outputs, { ssr: true });Key difference: App Router uses a global client component. Pages Router configures per-file.
<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 { 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 { Amplify } from 'aws-amplify';
import outputs from './amplify_outputs.json';
Amplify.configure(outputs);React Native Pitfalls
- Import order:
react-native-get-random-valuesmust be the FIRST
import in the entry file, 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.
SvelteKit
Configure Amplify in the client hooks file:
// src/hooks.client.ts
import { Amplify } from 'aws-amplify';
import outputs from '../amplify_outputs.json';
Amplify.configure(outputs);Note: No @aws-amplify/ui-* components exist for Svelte. Use core APIs directly.Unsupported Frameworks (Astro, Solid, etc.)
For frameworks without official Amplify support:
1. Use npm create amplify@latest -y to scaffold the backend (works in any project) 2. Configure Amplify inside a client-side component (not at build time)
Astro
Amplify is client-side only in Astro. Create a React component (no Astro syntax):
// src/components/AuthenticatedApp.tsx
import { Amplify } from 'aws-amplify';
import { Authenticator } from '@aws-amplify/ui-react';
import outputs from '../amplify_outputs.json';
Amplify.configure(outputs);
export default function AuthenticatedApp() {
return (
<Authenticator>
{({ signOut, user }) => <main>Hello {user?.username}</main>}
</Authenticator>
);
}Use in an Astro page with client:only:
---
// src/pages/index.astro — no Amplify imports here
---
<html>
<body>
<AuthenticatedApp client:only="react" />
</body>
</html>Must use `client:only="react"` (NOT client:load) to avoid SSR hydration errors.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 |
AI
Prerequisites: Backend defined inamplify/backend.tswithdefineBackend({ auth, data }).
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 Sonnet 4.5')For the full list of supported models, see AI Concepts: Models.
Key constraint: a.generation() routes only support Anthropic (Claude) models. a.conversation() routes work with any supported model.
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.
Bedrock Model Access
Some older or restricted models require explicit enablement in the AWS Bedrock console (Model access). On-demand foundation models (Claude Sonnet 4+, Nova) are available immediately. Amplify uses global inference profiles for cross-region model access.
If you get AccessDeniedException: Could not access the model with the specified model ID, check Bedrock → Model access in your region.
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 Sonnet 4.5'),
systemPrompt: 'You are a helpful assistant.',
})
.authorization(allow => allow.owner()),
});Backend: Generation Routes
Use a.generation() for single-turn (stateless) inference.
const schema = a.schema({
summarize: a.generation({
aiModel: a.ai.model('Claude Sonnet 4.5'),
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()),
});Authorization constraints (these cause TypeError at CDK assembly if violated):
- Conversation routes (
a.conversation()) requireallow.owner()authorization —allow.authenticated()and other non-owner strategies throw a TypeError at CDK assembly time. - Generation routes (
a.generation()) require non-owner authorization (allow.authenticated(),allow.guest(),allow.group(), orallow.publicApiKey()) —allow.owner()throws a TypeError at CDK assembly time.
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 Sonnet 4.5'),
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, 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
- 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
Prerequisites: Backend defined inamplify/backend.tswithdefineBackend({ auth, data }).
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
Use secret() for OAuth client secrets — hardcoding credentials exposes them in source control.
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 -n "<value>" | npx ampx sandbox secret set MY_OAUTH_CLIENT_ID. (The documented approach uses an interactive prompt; piping with echo -n is a practical alternative for scripts.) For provider-specific OAuth setup guides, consult AWS documentation via available tools; when unavailable, use web search or AWS CLI.
SAML / OIDC (Enterprise)
OIDC providers are configured inside loginWith.externalProviders:
import { defineAuth, secret } from '@aws-amplify/backend';
export const auth = defineAuth({
loginWith: {
email: true,
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' });Tip: Auth trigger handlers need @types/aws-lambda for TypeScript types.Trigger Lambda + Data Table Access
If a trigger Lambda (e.g., postConfirmation) needs to write to a defineData table, this can create a circular dependency. Workarounds:
1. Access via `backend.auth.resources` (avoids cycle when trigger is in auth stack):
// backend.ts
const postConfirmFn = backend.auth.resources.userPool.triggers?.postConfirmation;
const table = backend.data.resources.tables['UserProfile'];
table.grantWriteData(postConfirmFn);
postConfirmFn.addEnvironment('TABLE_NAME', table.tableName);1. Separate DynamoDB table — create via CDK (not defineData) to avoid stack coupling.
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' and add allow.guest() authorization rules:
const schema = a.schema({
Todo: a.model({
content: a.string(),
}).authorization(allow => [
allow.guest().to(['read']), // unauthenticated users can read
allow.owner(), // owners can CRUD
]),
});
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: 'iam', // required for guest access
apiKeyAuthorizationMode: { expiresInDays: 7 }, // optional alternative
},
});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. 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, manage secrets through the
Amplify console (App settings → Environment variables → Secrets). The ampx sandbox secret command only works for local sandbox environments.
Links
Auth — Mobile
Prerequisites
Initialize Amplify with the Auth plugin before using this feature:
Flutter — lib/main.dart:
await Amplify.addPlugins([AmplifyAuthCognito()]);
await Amplify.configure(amplifyConfig);Generate dart outputs: npx ampx sandbox --outputs-format dart --outputs-out-dir libSwift (Apple platforms):
try Amplify.add(plugin: AWSCognitoAuthPlugin())
try Amplify.configure(with: .amplifyOutputs)Drag amplify_outputs.json into the Xcode project navigator so it is included in the app bundle.Android:
Amplify.addPlugin(AWSCognitoAuthPlugin())
Amplify.configure(AmplifyOutputs(R.raw.amplify_outputs), applicationContext)Placeamplify_outputs.jsoninapp/src/main/res/raw/. Enable core library desugaring for API level < 26.
>
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. Custom OTP/passkey flows require additional challenge handling.>
Passwordless WebAuthn:
>
- Swift: .userChoice(preferredAuthFactor: .webAuthn)- Android: AuthenticationFlow.UserChoice(preferredFirstFactor = AuthFactorType.WEB_AUTHN)Flutter
Dependencies — add to pubspec.yaml:
flutter pub add amplify_flutter amplify_auth_cognito amplify_authenticatorUsage — wrap your MaterialApp and set its builder:
import 'package:amplify_authenticator/amplify_authenticator.dart';
import 'package:flutter/material.dart';
// After Amplify is configured (see Prerequisites above):
@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)
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() — configure Amplify (see Prerequisites above)
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:
Core library desugaring required — see Prerequisites above.
dependencies {
implementation("com.amplifyframework.ui:authenticator:<version>")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:<version>")
}INTERNET permission is required in AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET"/>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(
preferredFirstFactor = AuthFactorType.WEB_AUTHN
)
)
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
- 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
Prerequisites: Project initialized,amplify_outputs.jsonexists (fromnpx ampx sandbox), andAmplify.configure(outputs)called in app entry point.
>
Backend required: Auth must be defined in amplify/auth/resource.tsusing defineAuth — see auth-backend.md.Authenticator Component
| Framework | Package | Tag | CSS (required) |
|---|---|---|---|
| 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.
Angular
Angular cannot resolve npm CSS via @import in stylesheets. Add to angular.json instead:
"styles": [
"node_modules/@aws-amplify/ui-angular/theme.css",
"src/styles.css"
]Manual Auth Flows
Imports from aws-amplify/auth: signIn, signUp, confirmSignUp, confirmSignIn, signOut, resetPassword.
After signIn(), switch on result.nextStep.signInStep to handle each possible challenge:
| 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.
Device Tracking
import { rememberDevice, forgetDevice, fetchDevices } from 'aws-amplify/auth';
await rememberDevice(); // Remember current device for MFA
await forgetDevice(); // Forget current device
const { devices } = await fetchDevices(); // List remembered devicesNext.js Server-Side Auth
For server components and route handlers, use cookie-based auth:
For server-side auth + data access in Next.js, see data-web.md § Server-Side (Next.js).
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 — it polyfills crypto.getRandomValues() which the Amplify SDK requires for token generation and is missing in React Native's JavaScript runtime. @aws-amplify/react-native must come before aws-amplify. See SKILL.md § Framework Setup 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. 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: Call
Hub.listen('auth', ...)
to capture the OAuth redirect callback on page reload.
- Vue component syntax: Vue requires PascalCase
<Authenticator>
component syntax (not kebab-case <authenticator>).
Links
Data — Backend
Prerequisites: Backend defined inamplify/backend.tswithdefineBackend({ auth, data }).
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 });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).
`a.phone()`: Only accepts E.164 format (+15551234567). Hyphens (+1-555-0101) and short formats are rejected.
Date/Time Field Formats
| Field Type | Storage Format | Example |
|---|---|---|
a.date() | ISO date string | 2024-01-15 |
a.time() | ISO time string | 14:30:00.000Z |
a.datetime() | ISO datetime | 2024-01-15T14:30:00.000Z |
a.timestamp() | Epoch seconds (not ms!) | 1705325400 |
Pitfall:a.timestamp()is seconds, not milliseconds. UseMath.floor(Date.now() / 1000)when setting values from JavaScript.
Custom Identifiers
.identifier(['sku']) replaces the auto-generated id field entirely:
Product: a.model({
sku: a.string().required(),
name: a.string(),
}).identifier(['sku'])- All queries must use the custom identifier (
{ sku: 'ABC123' }) - Duplicate values cause DynamoDB conditional check error
- The
idfield no longer exists on this model
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.
Authorization Rule Combining
When multiple rules are applied, the most permissive wins. You cannot use deny rules — if allow.authenticated() grants full CRUD, you cannot selectively deny delete for non-owners. Structure rules from most restrictive:
.authorization(allow => [
allow.owner(), // Owner: full CRUD
allow.authenticated().to(['read', 'create']), // Others: read + create only
])Pitfall:groupsDefinedIn('fieldName')automatically creates an implicit field on the model. Do NOT also declare that field explicitly — this causes:"Implicit field conflicts with explicit field definition."
>
Type system gap: The implicit field from groupsDefinedIn('fieldName') is NOT exposed in generated TypeScript client types. To set the field programmatically, use an untyped approach:>
```typescript
await client.graphql({
query: mutations.updateProject,
variables: { id: projectId, teamGroups: ['Admins', 'Editors'] },
});
```
| Pattern | Use Case | Field Type | Who Gets Access |
|---|---|---|---|
allow.ownersDefinedIn('editors') | Multiple named users own the resource | a.string().array() — declare explicitly | Specific users listed in the array |
allow.groupsDefinedIn('teamGroups') | Access by Cognito group membership | Implicit — do NOT declare | Any user in the named Cognito group |
Relationships
Three types — reference field types must match the related model's identifier type.
Foreign key fields must use `a.id()`, nota.string(). Usinga.string()causes silent relationship resolution failures.
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.
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).
Deletion Behavior — No Referential Integrity
Deleting a parent record does NOT cascade to children and does NOT fail. Child records become orphaned silently — manually delete children first or implement a soft-delete pattern.
// Delete children before parent
const books = await client.models.Book.list({ filter: { authorId: { eq: authorId } } });
await Promise.all(books.data.map(b => client.models.Book.delete({ id: b.id })));
await client.models.Author.delete({ id: authorId });Self-Referential Models (Tree Structures)
Category: a.model({
name: a.string().required(),
parentId: a.id(),
parent: a.belongsTo('Category', 'parentId'),
children: a.hasMany('Category', 'parentId'),
})Requires explicit FK field (parentId: a.id()). Works for trees, org charts, threaded comments.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.
>
`.required()` on enums:a.enum(['A','B']).required()does NOT work —.required()doesn't exist on EnumType. Define the enum separately and usea.ref():
>
```typescript
const Priority = a.enum(['low', 'medium', 'high']);
const schema = a.schema({
Todo: a.model({
priority: a.ref('Priority').required(), // ✅ Works
// priority: Priority.required(), // ❌ Fails
})
});
```
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: see auth-backend.md § Guest Access.
Pitfalls
- Missing `ClientSchema` export: Without `export type Schema =
ClientSchema<typeof schema>, frontend generateClient<Schema>()` has no type information and all operations are untyped.
- Auth mode conflict: Using
allow.publicApiKey()in model rules but
setting defaultAuthorizationMode: 'userPool' without adding apiKeyAuthorizationMode causes API key requests to be rejected.
- Per-field auth + `.required()`: Fields with owner-only authorization (
allow.owner()) cannot be.required()— other users can't provide a value on create. Make private fields optional.
Links
Data — Mobile
Prerequisites
Initialize Amplify with Auth and API plugins before using this feature:
Flutter — lib/main.dart:
await Amplify.addPlugins([AmplifyAuthCognito(), AmplifyAPI()]);
await Amplify.configure(amplifyConfig);Generate dart outputs: npx ampx sandbox --outputs-format dart --outputs-out-dir libSwift (Apple platforms):
try Amplify.add(plugin: AWSCognitoAuthPlugin())
try Amplify.add(plugin: AWSAPIPlugin())
try Amplify.configure(with: .amplifyOutputs)Drag amplify_outputs.json into the Xcode project navigator so it is included in the app bundle.Android:
Amplify.addPlugin(AWSCognitoAuthPlugin())
Amplify.addPlugin(AWSApiPlugin())
Amplify.configure(AmplifyOutputs(R.raw.amplify_outputs), applicationContext)Placeamplify_outputs.jsoninapp/src/main/res/raw/. Enable core library desugaring for API level < 26.
>
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 shorthand request builders (.list(), .create(), .subscription(of:type:)) via GraphQLRequest extensions, unlike Flutter's explicit ModelQueries/ModelMutations classes.
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
run npx ampx generate graphql-client-code to produce typed model classes. Without this step, model types do not exist.
- 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 requires 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
Prerequisites: Project initialized,amplify_outputs.jsonexists (fromnpx ampx sandbox), andAmplify.configure(outputs)called in app entry point.
>
Backend required: Data must be defined in amplify/data/resource.tsusing defineData — see data-backend.md.Client Setup
Call generateClient<Schema>() at module scope (outside any component). Calling it inside a component creates a new client on every render, breaking subscriptions, caching, and causing memory leaks.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 call sub.unsubscribe() in cleanup.
useEffect(() => {
const sub = client.models.Todo.observeQuery().subscribe({
next: ({ items }) => setTodos(items),
});
return () => sub.unsubscribe();
}, []);Filtering Subscriptions
useEffect(() => {
const sub = client.models.Vote.observeQuery({
filter: { pollId: { eq: currentPollId } },
}).subscribe({
next: ({ items }) => setVotes(items),
});
return () => sub.unsubscribe();
}, [currentPollId]);When to Use Which
| Pattern | Best For |
|---|---|
observeQuery() | Continuously updated lists — handles pagination, filtering, deduplication. Use by default. |
onCreate / onUpdate / onDelete | Fine-grained control — animations, toasts, or single event type only. |
observeQuery does NOT support server-side sorting. Sort results client-side after receiving them.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.
Querying a Secondary Index
// Backend: define with queryField name
.secondaryIndexes(index => [
index('pollId').sortKeys(['voterId']).queryField('votesByPollAndVoter')
])
// Frontend: call by name
const { data } = await client.models.Vote.votesByPollAndVoter({
pollId: currentPollId,
voterId: { eq: currentVoterId },
});JSON Fields — Serialization Asymmetry
a.json() fields require JSON.stringify() on write but auto-parse on read:
// Write: stringify before saving
await client.models.Config.create({
metadata: JSON.stringify({ key: "val", nested: { a: 1 } })
});
// Read: auto-parsed back to object
const { data } = await client.models.Config.get({ id });
console.log(data.metadata.key); // "val" — already an objectPassing a raw object on write fails silently with: "Variable 'metadata' has an invalid value">
(Thea.json()field maps to GraphQL'sAWSJSONscalar, which expects a JSON-encoded string as input.)
Pitfalls
- Array field updates = full replacement: Array fields have no append/remove operations. You must read, modify, and write the entire array:
const item = await client.models.Todo.get({ id });
const updated = [...(item.data?.tags ?? []), 'newTag'];
await client.models.Todo.update({ id, tags: updated });Risk: Concurrent updates can overwrite each other. For frequently-modified lists, consider a separate model with a relationship instead.
- 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) — use generateServerClientUsingCookies instead.
Links
Deployment
Prerequisites: Backend defined inamplify/backend.tswithdefineBackend({ auth, data }).
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 (Gen2 uses
amplify/backend.ts+defineBackend())
Sandbox Deployment
Deploy a personal development environment:
AWS_REGION=us-east-1 npx ampx sandbox --onceUse 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)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 ci
- npm run build
artifacts:
baseDirectory: dist # Change per framework (see table above)
files:
- '**/*'
cache:
paths:
- .npm/**/*
- node_modules/**/*Note:$AWS_BRANCHand$AWS_APP_IDare automatically set by Amplify Hosting. For custom CI/CD pipelines (GitHub Actions, CodePipeline), set these manually or use your pipeline's equivalent variables.
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:
echo -n "<value>" | 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
Pipe the value via stdin — without the pipe, the command prompts interactively.
Important: Useecho -n(no trailing newline) when piping values tosecret set. (The documented approach uses an interactive prompt; piping withecho -nis a practical alternative for scripts.)
This stores the secret for your personal sandbox environment. Branch environments (production): Secrets are managed through the Amplify console (App settings → Environment variables → Secrets), NOT via CLI. The ampx sandbox secret command only works for local sandbox environments.
Alternatively, use the AWS CLI for non-secret environment variables:
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. 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.
InvalidCredentialError: Failed to load default AWS region
Despite the error name, this is a missing region, not a credential issue:
export AWS_REGION=us-east-1AppSync API Limit (25 per account)
Each sandbox creates an AppSync API. Frequent sandbox creation/deletion can leave orphaned APIs. If deployment fails with "LimitExceededException":
1. Check: AWS Console → AppSync → APIs → delete unused 2. Request limit increase via Service Quotas
Links
Functions & API
Prerequisites: Backend defined inamplify/backend.tswithdefineBackend({ auth, data }).
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 });Sharing Code Between Functions
Each Lambda function is bundled independently from its own directory. Importing from a shared directory (amplify/shared/utils.ts) fails at build time.
Options:
1. Duplicate the shared code in each function directory 2. Symlink: ln -s ../../shared/utils.ts amplify/functions/my-fn/utils.ts 3. Package: Create a local npm package and install it in each function's package.json
Handler Return Types
| Handler Type | Import | Returns |
|---|---|---|
S3Handler | @types/aws-lambda | void — async event, no response expected |
APIGatewayProxyHandler | @types/aws-lambda | { statusCode, headers?, body } |
APIGatewayProxyHandlerV2 | @types/aws-lambda | { statusCode, headers?, body } |
Common mistake: Writing an S3 handler like an API handler. S3Handler returns void, not a response object.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 -n "<value>" | npx ampx sandbox secret set MY_API_KEY.
Important: Useecho -n(no trailing newline) when piping values tosecret set.
>
Important: Theampx sandbox secret setcommand is for local/sandbox development only. For apps deployed to Amplify Hosting, secrets must be created via the Amplify console (NOTampx sandbox secret— that's local only) — sandbox secrets are NOT available in hosted environments. See: https://docs.amplify.aws/react/deploy-and-host/fullstack-branching/secrets-and-vars/#set-secrets
Environment Variables in Lambda
Recommended (type-safe):
import { env } from '$amplify/env/my-function';
const tableName = env.TABLE_NAME;Fallback (if `$amplify/env` causes esbuild bundling errors):
const tableName = process.env.TABLE_NAME!;Scheduled 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 day', 'every week', 'every month', 'every year'
// 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)]),
});Lambda + API Gateway + Data Access
When a Lambda both accesses data tables AND is exposed via API Gateway, use resourceGroupName to avoid circular dependencies:
export const myFunction = defineFunction({
name: 'my-function',
resourceGroupName: 'data', // Places in data stack to avoid circular dep
});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()]),
});How `.handler()` works:.handler()grants AppSync the permission to invoke the Lambda (AppSync→Lambda). The Lambda IS the resolver — it receives the GraphQL event directly. If the Lambda also needs to call the Data API or access DynamoDB tables for side effects, addallow.resource(fn)to the model withresourceGroupName: 'data'on the function to avoid circular dependencies.
>
```typescript
// ❌ CIRCULAR DEPENDENCY — manual table grant in backend.ts
backend.data.resources.tables["Model"].grantReadData(backend.myFn.resources.lambda);
>
// ✅ Use resourceGroupName to co-locate the function in the data stack
const myFn = defineFunction({ name: 'my-fn', resourceGroupName: 'data' });
// Then in the schema: allow.resource(myFn) on the model
```
>
Gap: The Lambda resolver receives the GraphQL event but does NOT automatically get TABLE_NAME as an environment variable. Your Lambda must either:>
1. Use the Amplify data client (generateClient()) which discovers tables automatically2. Explicitly set env vars: myFunction.addEnvironment('TABLE_NAME', backend.data.resources.tables['Todo'].tableName)>
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.
- Missing `@types/node`: Lambda functions require
@types/nodein devDependencies. Without it,process.envand Node.js globals cause TypeScript errors. Install:npm install --save-dev @types/node - `@types/aws-lambda`: Lambda handlers (
S3Handler,PreSignUpTriggerHandler, etc.) need this package for TypeScript types. Install at project root or in the function's directory if it has its ownpackage.json. - AppSync identity typing:
event.identityin custom handlers has varying types depending on auth mode. Use type assertion:
const identity = event.identity as { username?: string; sub?: string };
const userId = identity?.username || identity?.sub || 'unknown';- `dataSource: 'NONE'`: Using
a.handler.custom({ dataSource: 'NONE' })causes "Data source not found" during deployment. Use a Lambda handler instead, or create the NONE data source explicitly via CDK.
- Lambda error types lost: Custom error classes thrown in Lambda arrive at the frontend as generic
Errorwith only themessagepreserved. Error name, stack, and custom properties are stripped by AppSync. Return structured error data in the response instead.
Links
Advanced Features
Prerequisites: Backend defined inamplify/backend.tswithdefineBackend({ auth, data }).
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.
Geo / Location (Frontend)
Install: npm install @aws-amplify/geo
import { Geo } from '@aws-amplify/geo';
const results = await Geo.searchByText('Seattle');Note: Gen2 usesimport { Geo } from '@aws-amplify/geo'— NOTAmplify.Geo.*(that namespace doesn't exist).
>
Constraint: Location Service resource names have a 100-character limit. Use short static names — avoid dynamic names like ${stack.stackName}-index.For rendering maps, also install maplibre-gl-js-amplify. See AWS Amplify Geo docs for full client setup.
PubSub — Backend + Frontend
Install required:npm install @aws-amplify/pubsub— not included in the baseaws-amplifypackage.
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(); // Always unsubscribe to prevent leaksWhen using subscriptions in React, wrap in useEffect and return cleanup function to call .unsubscribe().
Retrieve the IoT endpoint programmatically: aws iot describe-endpoint --endpoint-type iot:Data-ATS.
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.
Avoiding Circular Dependencies
Complex apps with storage triggers + auth groups + data models can create circular CloudFormation dependencies. Solutions:
1. Incremental deployment: Deploy auth + data first, then add storage triggers in a subsequent deployment 2. Separate stacks: Use backend.createStack('storage-triggers') to isolate trigger resources 3. Avoid `.handler()` + manual grants: When using a.query().handler(fn), don't also call grantReadData() — .handler() auto-grants access
See: Troubleshoot circular dependencies
Storage Triggers Writing to Data Tables — BLOCKED
Storage triggers that need to write to DynamoDB tables in the data stack create a circular dependency that CANNOT be resolved with resourceGroupName.
Workarounds:
1. Have the trigger write to a separate DynamoDB table (created via CDK, not defineData) 2. Use EventBridge to decouple: trigger → EventBridge → Lambda → DynamoDB 3. Handle metadata creation client-side after upload completes
Pitfalls
- Duplicate stack names:
backend.createStack()names must be
unique across the entire backend — reusing a name silently overwrites.
- 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: Configure the correct IoT endpoint for
your region — using the wrong endpoint type causes silent connection failures.
Links
Scaffolding
Prerequisites: Node.js ^18.19.0 || ^20.6.0 || >=22, npm, and AWS credentials configured.
Starter Templates
Use official starter templates — hand-crafted structures can break Amplify Hosting deployment detection.
git clone <TEMPLATE_URL> my-app && cd my-app && rm -rf .git && git init && npm install| Framework | Template URL |
|---|---|
| React (Vite) | https://github.com/aws-samples/amplify-vite-react-template |
| Next.js (App Router) | https://github.com/aws-samples/amplify-next-template |
| Next.js (Pages Router) | https://github.com/aws-samples/amplify-next-pages-template |
| Vue | https://github.com/aws-samples/amplify-vue-template |
| Angular | https://github.com/aws-samples/amplify-angular-template |
Web — 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 -yUse the -y flag for non-interactive execution — without it, the command prompts interactively and hangs in agent/CI environments. 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 typescriptNote:aws-cdk-libandconstructsare peer dependencies — npm 7+ installs them automatically. If using--legacy-peer-deps, install them explicitly.
Then create amplify/backend.ts:
import { defineBackend } from '@aws-amplify/backend';
defineBackend({});Install the frontend library:
npm install aws-amplifyNext.js SSR:npm create amplify@latestdoes NOT install@aws-amplify/adapter-nextjs. Add manually for server-side rendering:
>
```bash
npm install @aws-amplify/adapter-nextjs
```
Web — 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 onlyMobile — 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)
Do 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
Do 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, 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.
Development Workflow
# Terminal 1 — Start sandbox (watch mode, auto-deploys on changes)
npx ampx sandbox
# Terminal 2 — Start dev server (requires amplify_outputs.json from sandbox)
npm run devSandbox modes:
npx ampx sandbox— Watch mode, continuously deploys changes (recommended for development)npx ampx sandbox --once— Single deployment then exits (for CI/CD or initial setup)
First time: Runnpx ampx sandboxand wait for it to generateamplify_outputs.jsonbefore starting your dev server.
amplify_outputs.json is gitignored — see deployment.md for generation details.
Sandbox Stack Naming
The sandbox stack name is derived from your project's root package.json name. If you clone a template, change the name to avoid collisions:
{ "name": "my-unique-app-name" }Running multiple projects with the same name simultaneously causes one sandbox to overwrite another.
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.
- React Native requires
@react-native-async-storage/async-storage— the
Amplify SDK uses it for token persistence and will fail at runtime without it.
Links
Storage — Backend
Prerequisites: Backend defined inamplify/backend.tswithdefineBackend({ auth, data }).
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.
{entity_id}must be the last path segment before/*— you cannot add path segments after it.
>
✅ 'avatar/{entity_id}/*'✅ 'documents/{entity_id}/*'❌'protected/{entity_id}/avatar/*'— fails withInvalidStorageAccessPathError
Paths must end with /* to match all objects under that prefix. Paths must not start with /.
Resource-Scoped vs User-Scoped Paths
{entity_id} resolves to the current user's identity ID, not a resource ID. For files tied to a resource (poll, post, project) rather than a user:
access: (allow) => ({
'public/polls/*': [
allow.guest.to(['read']),
allow.authenticated.to(['read', 'write', 'delete']),
],
})
// Upload with resource ID in path
await uploadData({ path: `public/polls/${pollId}/cover.jpg`, data: file });Access control is at the path-prefix level, not per-resource. The frontend must enforce which users can write to which resource paths.
Multiple Buckets
export const primaryStorage = defineStorage({ name: 'primaryFiles', isDefault: true, access: (allow) => ({ /* rules */ }) });
export const secondaryStorage = defineStorage({ name: 'secondaryFiles', access: (allow) => ({ /* rules */ }) });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. Import the trigger function into backend.ts or it won't be deployed.
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
use 'public/*' to match files under that prefix.
- Missing `{entity_id}`: Using
'private/*'instead of
'private/{entity_id}/*' exposes every user's private files to all authenticated users.
- 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.
- Missing `.to([])`:
allow.authenticatedwithout.to(['read', 'write'])causes a silent failure — no access is granted. - Leading slash: Paths must NOT start with
/. Use'photos/*'not'/photos/*'.
Links
Storage — Web
Prerequisites: Project initialized,amplify_outputs.jsonexists (fromnpx ampx sandbox), andAmplify.configure(outputs)called in app entry point.
>
Backend required: Storage must be defined in amplify/storage/resource.tsusing defineStorage — see storage-backend.md.API Reference
All imports from 'aws-amplify/storage'.
| Operation | Call |
|---|---|
| Upload | uploadData({ path: 'public/file.txt', data }) |
| Download blob | await (await downloadData({ path }).result).body.blob() |
| Presigned URL | await getUrl({ path }) (default 15 min expiry) |
| List | await list({ path: 'public/' }) → { items } |
| Remove | await remove({ path }) |
| Copy | await copy({ source: { path }, destination: { path } }) |
Security: Amplify Gen2 enables S3 server-side encryption (SSE-S3) by default. For sensitive data, consider configuring SSE-KMS with a customer-managed key via CDK overrides. Amplify also enforces HTTPS-only access to S3 buckets by default; if using custom bucket configurations, add a bucket policy with "aws:SecureTransport": "false" → Deny to ensure encryption in transit.uploadData returns a control object: .pause(), .resume(), .cancel(), .result (Promise). Progress: options.onProgress: ({ transferredBytes, totalBytes }) => ….
Custom bucket: options: { bucket: 'nameFromDefineStorage' } or { bucket: { bucketName, region } }. Raw ARN does NOT work.
React UI Components
npm add @aws-amplify/ui-react-storage — import both CSS files or components render unstyled:
import '@aws-amplify/ui-react/styles.css';
import '@aws-amplify/ui-react-storage/styles.css';WARNING: Missing either CSS import causes unstyled components.
| Component | Import from | Key props / setup |
|---|---|---|
<StorageBrowser /> | @aws-amplify/ui-react-storage/browser | createStorageBrowser({ config: createAmplifyAuthAdapter() }) — bucket specified by name string, NOT ARN |
<StorageImage /> | @aws-amplify/ui-react-storage | alt, path |
<FileUploader /> | @aws-amplify/ui-react-storage | path, maxFileCount, acceptedFileTypes |
StorageBrowser Requirements
- Must be inside an
<Authenticator>component (needs auth context) - Must have an explicit height on the container (component doesn't set its own height)
- Client-side only — use dynamic import with
ssr: falsein Next.js - Both CSS files required:
@aws-amplify/ui-react/styles.cssand@aws-amplify/ui-react-storage/styles.css
React Native
Same JS API as web — all imports from 'aws-amplify/storage':
uploadData, downloadData, getUrl, list, remove — identical signatures. Use react-native-image-picker or expo-document-picker for file selection.
Pitfalls
- `{entity_id}` paths:
protected/{entity_id}/andprivate/{entity_id}/resolve to the user's Cognito identity ID at runtime. - Upload cancellation:
result.cancel()rejects the promise — catchCanceledErrorto handle it gracefully.
Common Patterns
Displaying Uploaded Images
Use getUrl() to generate presigned URLs for display:
import { getUrl } from "aws-amplify/storage";
const result = await getUrl({
path: "photos/my-photo.jpg",
options: { expiresIn: 3600 }, // URL valid for 1 hour
});
// Use in img tag
<img src={result.url.toString()} alt="Photo" />Note: Presigned URLs expire (default 15 minutes). Use expiresIn to set a custom duration.Links
Related skills
How it compares
Pick aws-amplify over raw Bedrock SDK setup when you need managed AI routes inside an existing Amplify Gen 2 defineBackend backend.
FAQ
Who is aws-amplify for?
Developers and software engineers working with aws-amplify patterns described in the skill documentation.
When should I use aws-amplify?
When .
Is aws-amplify safe to install?
Review the Security Audits panel on this page before installing in production.