diff --git a/aws-amplify/POWER.md b/aws-amplify/POWER.md index 63a1a22..2420e62 100644 --- a/aws-amplify/POWER.md +++ b/aws-amplify/POWER.md @@ -1,43 +1,316 @@ --- name: "aws-amplify" -displayName: "Build full-stack apps with AWS Amplify" -description: "Build and extend full-stack applications with AWS Amplify Gen 2 using type-safe TypeScript, guided workflows, and best practices. Covers adding features to existing Amplify backends, authentication, data models, storage, serverless functions, and AI/ML integration." -keywords: ["amplify", "aws-amplify", "amplify gen 2", "gen2", "fullstack", "full-stack", "lambda", "graphql", "cognito", "sandbox", "backend", "auth", "authentication", "storage", "data model", "react", "nextjs", "next.js", "vue", "nuxt", "angular", "react native", "flutter", "swift", "android", "ios", "deploy", "deployment", "production"] +displayName: "Build full-stack apps with AWS Amplify Gen2" +description: "Build and deploy full-stack web and mobile apps with AWS Amplify Gen2 (TypeScript code-first). Covers auth (Cognito), data (AppSync/DynamoDB), storage (S3), functions, APIs, and AI (Amplify AI Kit with Bedrock). Supports React, Next.js, Vue, Angular, React Native, Flutter, Swift, and Android." +keywords: ["amplify", "gen2", "fullstack", "cognito", "appsync", "dynamodb", "s3", "lambda", "bedrock", "react", "nextjs", "flutter", "swift", "android"] author: "AWS" --- -# AWS Amplify Gen 2 +# AWS Amplify Gen2 ## Overview -Build full-stack applications with AWS Amplify Gen 2 using TypeScript code-first development. This power provides guided workflows for: +AWS Amplify Gen2 is a TypeScript code-first developer experience for building full-stack web and mobile applications. All backend resources — authentication (Cognito), data (AppSync/DynamoDB), storage (S3), serverless functions (Lambda), and AI (Bedrock via Amplify AI Kit) — are defined in TypeScript under an `amplify/` directory. A single `amplify_outputs.json` file is generated to configure frontends. -- Creating backend resources (auth, data, storage, functions) -- Deploying to sandbox and production environments -- Integrating frontend frameworks (React, Next.js, Vue, Angular, Flutter, Swift) -- Following Amplify Gen 2 best practices +**Supported frameworks:** React (Vite), Next.js, Vue, Angular, React Native, Flutter, Swift, Android. + +**Key differences from Gen1:** No CLI wizards, no `amplify push` — everything is code-defined and deployed via `npx ampx sandbox` (dev) or `npx ampx pipeline-deploy` (CI/CD). ## Getting Started -**IMPORTANT: You MUST read and follow the steering file for ANY Amplify work.** Do not improvise or skip the workflow. +### Prerequisites + +- 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) + +### Quick Start -**For AI agents helping users build Amplify apps:** +```bash +# Create a new Amplify Gen2 project +npm create amplify@latest -ALWAYS read the workflow steering file first: +# Start local sandbox (watches for changes, hot-deploys to AWS) +npx ampx sandbox +``` + +### Project Structure ``` -Call action "readSteering" with powerName="aws-amplify", steeringFile="amplify-workflow.md" +project-root/ +├── amplify/ +│ ├── backend.ts # defineBackend({ auth, data, ... }) +│ ├── auth/resource.ts # defineAuth({ ... }) +│ ├── data/resource.ts # defineData({ schema }) +│ ├── storage/resource.ts # defineStorage({ ... }) +│ └── functions/ +│ └── my-func/ +│ ├── resource.ts # defineFunction({ ... }) +│ └── handler.ts # export const handler = ... +├── src/ # Frontend code +├── amplify_outputs.json # Generated — DO NOT edit or commit +└── package.json ``` -The workflow will guide you through: -1. Validating prerequisites (Node.js, npm, AWS credentials) -2. Understanding the project's current state -3. Determining which phases apply to the user's request -4. Presenting a plan and getting confirmation -5. Executing phases one at a time with user confirmation between each +### Key Packages + +| 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: ``, `` | +| `@aws-amplify/ui-react-ai` | AI UI: ``, `useAIConversation` | ## When to Load Steering Files -- Any Amplify Gen 2 work -> `amplify-workflow.md` +**IMPORTANT:** Always load the appropriate steering file(s) before starting any Amplify work. Do not improvise — these files contain validated, version-specific patterns. + +### Step 0: Always Load the Core Reference First + +Before reading any feature-specific steering file, you **MUST** load the core reference for your target platform. These contain Gen2 detection, `Amplify.configure()` placement per framework, sandbox commands, required packages, and directory structure rules. + +| Platform | Steering File | When | +|----------|---------------|------| +| Web (React, Next.js, Vue, Angular, React Native) | `core-web.md` | Any web/RN frontend work | +| Mobile (Flutter, Swift, Android) | `core-mobile.md` | Any native mobile frontend work | +| Backend only (no frontend) | Skip to Step 1 | No frontend changes needed | + +### Step 1: Project Scaffolding + +| Task | Steering File | +|------|---------------| +| Create a new Amplify Gen2 project | `scaffolding.md` → then continue to Step 2 and/or Step 3 | + +### Step 2: Backend Features + +Load the steering file for each backend feature you need to add or modify: + +| Feature | Steering File | Covers | +|---------|---------------|--------| +| Authentication | `auth-backend.md` | Email/password, social login, MFA, SAML/OIDC, user groups, custom attributes | +| Data Models | `data-backend.md` | GraphQL schema, DynamoDB, relationships, enum types, authorization rules | +| File Storage | `storage-backend.md` | S3 buckets, access rules (guest/authenticated/groups/entity), paths | +| Functions & APIs | `functions-and-api.md` | Lambda functions, custom resolvers, REST/HTTP APIs, environment variables | +| AI Features | `ai.md` | Conversation routes, generation routes, AI tools via Bedrock (backend + React/Next.js frontend) | +| Geo, PubSub, CDK | `advanced-features.md` | Custom CDK stacks, overrides, custom outputs, Geo, PubSub, Face Liveness | + +### Step 3: Frontend Integration + +After configuring backend resources, load the frontend steering file for your platform and feature: + +**Web** (React, Next.js, Vue, Angular, React Native): + +| Feature | Steering File | +|---------|---------------| +| Auth UI & flows | `auth-web.md` | +| Data CRUD & subscriptions | `data-web.md` | +| Storage upload/download | `storage-web.md` | + +**Mobile** (Flutter, Swift, Android): + +| Feature | Steering File | +|---------|---------------| +| 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. + +### Step 4: Deployment + +| Task | Steering File | +|------|---------------| +| Deploy to sandbox or production | `deployment.md` | + +### Quick Routing Examples + +| User Says | Load | +|-----------|------| +| "Build a full-stack app" | `core-web.md` → `scaffolding.md` → backend files → frontend files → `deployment.md` | +| "Add authentication" | `auth-backend.md` (+ `core-web.md` → `auth-web.md` if frontend needed) | +| "Add a data model" | `data-backend.md` | +| "Connect my React app to Amplify" | `core-web.md` → relevant frontend files | +| "Deploy to production" | `deployment.md` | +| "Add AI chat" | `ai.md` (includes both backend and React/Next.js frontend) | +| "Build a Flutter app with auth" | `core-mobile.md` → `scaffolding.md` → `auth-backend.md` → `auth-mobile.md` | + +## Available MCP Tools + +When AWS documentation MCP tools are available, use them to look up advanced CDK constructs, service limits, or provider-specific configuration. + +| Tool | Use For | +|------|---------| +| `search_documentation` | Find Amplify Gen2 documentation pages by topic | +| `read_documentation` | Read specific Amplify documentation pages | +| `recommend` | Get related documentation recommendations | + +**Tip:** Amplify's LLM-optimized docs are at [https://docs.amplify.aws/ai/llms.txt](https://docs.amplify.aws/ai/llms.txt) + +## Common Workflows + +### 1. Scaffold a New Full-Stack Project + +```bash +npm create amplify@latest +cd my-amplify-app +npx ampx sandbox +``` + +Load `scaffolding.md` for framework-specific setup, directory structure, and starter template details. + +### 2. Add Authentication + +In `amplify/auth/resource.ts`: +```typescript +import { defineAuth } from '@aws-amplify/backend'; + +export const auth = defineAuth({ + loginWith: { + email: true, // or phone, or external providers + }, +}); +``` + +Register in `amplify/backend.ts`: +```typescript +import { defineBackend } from '@aws-amplify/backend'; +import { auth } from './auth/resource'; + +defineBackend({ auth }); +``` + +Load `auth-backend.md` for MFA, social login, SAML/OIDC, custom attributes, and user groups. + +### 3. Add a Data Model + +In `amplify/data/resource.ts`: +```typescript +import { type ClientSchema, a, defineData } from '@aws-amplify/backend'; + +const schema = a.schema({ + Todo: a.model({ + content: a.string(), + isDone: a.boolean(), + }).authorization(allow => [allow.publicApiKey()]), +}); + +export type Schema = ClientSchema; +export const data = defineData({ schema }); +``` + +Load `data-backend.md` for relationships, enum types, secondary indexes, and authorization rules. + +### 4. Deploy to Production + +```bash +# CI/CD pipeline deployment +npx ampx pipeline-deploy --branch main --app-id +``` + +Load `deployment.md` for Amplify Hosting, custom CI/CD pipelines, environment management, and fullstack branch deployments. + +## Defaults & Assumptions + +When the user does not specify preferences: + +| Choice | Default | Notes | +|--------|---------|-------| +| Web framework | React (Vite) | Explain the choice; user can override | +| Mobile framework | **ASK** | No default — must ask Flutter/Swift/Android/RN | +| Package manager | npm | Unless user specifies yarn or pnpm | +| Language | TypeScript | Gen2 backends are TS-only; frontends follow project convention | +| Next.js router | App Router | Unless user specifies Pages Router | +| Auth login method | Email/password | Unless user specifies social/SAML/other | +| Data authorization | `publicApiKey` | Switch to `owner`-based when auth is added | + +**Critical:** If the user says "build an app" without specifying web vs. mobile, you **MUST** ask before proceeding. + +## Best Practices + +1. **Always use the `amplify/` directory** — all backend resources are TypeScript files under `amplify/`. Never use CLI wizards or manual AWS console configuration. + +2. **Never edit `amplify_outputs.json`** — this file is auto-generated by `npx ampx sandbox` (dev) or `npx ampx pipeline-deploy` (CI/CD). It should be in `.gitignore`. + +3. **One `defineBackend()` call** — in `amplify/backend.ts`, combine all resources into a single `defineBackend({ auth, data, storage })` call. + +4. **Use `a.schema()` for data models** — define your GraphQL schema using the type-safe `a` builder from `@aws-amplify/backend`. Avoid writing raw GraphQL SDL. + +5. **Authorization rules are mandatory** — every model needs `.authorization(allow => [...])`. Start with `allow.publicApiKey()` for prototyping, switch to `allow.owner()` or `allow.groups()` for production. + +6. **Configure Amplify once at the app root** — call `Amplify.configure(outputs)` in your root layout/entry file, never in individual components. + +7. **Use pre-built UI components** — `` from `@aws-amplify/ui-react` handles the entire auth flow. Don't build custom login forms unless you have specific requirements. + +8. **Sandbox for development** — `npx ampx sandbox` creates an isolated cloud environment per developer. Use `--identifier` for multiple sandboxes. + +## Troubleshooting + +### Sandbox Won't Start + +**Symptoms:** `npx ampx sandbox` fails with credential or bootstrap errors. + +**Solutions:** +1. Verify AWS credentials: `aws sts get-caller-identity` +2. Bootstrap CDK if first time: `npx ampx sandbox` will prompt automatically +3. Check Node.js version: must be ^18.19.0, ^20.6.0, or >=22 +4. Ensure no other sandbox is running with the same identifier + +### "Cannot find module" or Import Errors + +**Cause:** Missing dependencies or incorrect import paths. + +**Solutions:** +1. Run `npm install` to ensure all packages are installed +2. Check that `@aws-amplify/backend` is in `devDependencies` (not `dependencies`) +3. Check that `aws-amplify` is in `dependencies` for frontend code +4. Verify import paths match the package names exactly + +### Data Model Authorization Errors + +**Symptoms:** "Not Authorized" errors when querying or mutating data. + +**Solutions:** +1. Ensure every model has `.authorization(allow => [...])` rules +2. Check `defaultAuthorizationMode` in `defineData()` matches your auth setup +3. For authenticated access, ensure the user is signed in before making API calls +4. For `owner`-based auth, the `owner` field is auto-managed — don't set it manually + +### `amplify_outputs.json` Not Found + +**Cause:** Sandbox hasn't been started, or the file is gitignored (expected). + +**Solutions:** +1. Run `npx ampx sandbox` to generate the file locally +2. For CI/CD, `npx ampx pipeline-deploy` generates it during build +3. Ensure your `.gitignore` includes `amplify_outputs.json` — it should NOT be committed + +### Next.js SSR/SSG Issues + +**Symptoms:** `Amplify.configure()` errors in server components, hydration mismatches. + +**Solutions:** +1. Call `Amplify.configure()` in a client-side wrapper component (`'use client'`) +2. For server-side data access, use `generateServerClientUsingCookies()` from `aws-amplify/adapter-nextjs` +3. Never import `aws-amplify` in server components directly + +## Resources + +- [Amplify Gen2 Documentation](https://docs.amplify.aws/react/) +- [Amplify Docs for LLMs](https://docs.amplify.aws/ai/llms.txt) +- [Getting Started Guide](https://docs.amplify.aws/react/start/) +- [Quickstart Tutorial](https://docs.amplify.aws/react/start/quickstart/) +- [Account Setup](https://docs.amplify.aws/react/start/account-setup/) +- [How Amplify Works](https://docs.amplify.aws/react/how-amplify-works/) +- [Build a Backend](https://docs.amplify.aws/react/build-a-backend/) +- [Deploy and Host](https://docs.amplify.aws/react/deploy-and-host/) +- [CLI Reference](https://docs.amplify.aws/react/reference/cli-commands/) +- [Project Structure Reference](https://docs.amplify.aws/react/reference/project-structure/) +- [Amplify UI Components](https://ui.docs.amplify.aws/) +- [Amplify GitHub](https://github.com/aws-amplify) + +> All documentation links use `react` as the default platform slug. Replace `/react/` with `/nextjs/`, `/vue/`, `/angular/`, `/react-native/`, `/flutter/`, `/swift/`, or `/android/` for other frameworks. + +--- -**Do NOT load phase steering files directly.** The orchestrator (`amplify-workflow.md`) determines which phases apply and loads them in sequence. Phase files (`phase1-backend.md`, `phase2-sandbox.md`, `phase3-frontend.md`, `phase4-production.md`) are internal and should only be loaded when the orchestrator or a previous phase instructs you to. +This power integrates with [`awslabs.aws-documentation-mcp-server@latest`](https://github.com/awslabs/mcp) for documentation search and retrieval. diff --git a/aws-amplify/steering/advanced-features.md b/aws-amplify/steering/advanced-features.md new file mode 100644 index 0000000..7aebc9a --- /dev/null +++ b/aws-amplify/steering/advanced-features.md @@ -0,0 +1,274 @@ +# Advanced Features + +## Geo (Location) — Backend + Frontend + +Add map display and location search using CDK constructs in +`amplify/backend.ts`: + +```typescript +import { defineBackend } from '@aws-amplify/backend'; +import * as geo from 'aws-cdk-lib/aws-location'; + +const backend = defineBackend({ auth }); +const geoStack = backend.createStack('GeoStack'); + +const placeIndex = new geo.CfnPlaceIndex(geoStack, 'PlaceIndex', { + dataSource: 'Esri', + indexName: 'myPlaceIndex', +}); + +const map = new geo.CfnMap(geoStack, 'Map', { + mapName: 'myMap', + configuration: { style: 'VectorEsriNavigation' }, +}); + +backend.addOutput({ + geo: { + aws_region: geoStack.region, + maps: { items: { [map.mapName]: { style: 'VectorEsriNavigation' } } }, + search_indices: { items: [placeIndex.indexName] }, + }, +}); +``` + +Grant authenticated users access via IAM policy on the geo resources. + +**Frontend:** Install `@aws-amplify/geo` and `maplibre-gl-js-amplify`. +Use `Amplify.Geo.searchByText()` for search and `AmplifyMapLibreRequest` +for rendering maps. See +[AWS Amplify Geo docs](https://docs.amplify.aws/react/build-a-backend/add-aws-services/geo/) +for full client setup. + +## PubSub — Backend + Frontend + +Real-time messaging via AWS IoT Core. Configure an IoT endpoint and +attach an IAM policy for authenticated users in `amplify/backend.ts`: + +```typescript +import * as iot from 'aws-cdk-lib/aws-iot'; +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:Subscribe', 'iot:Publish', 'iot:Receive'], + resources: ['*'], + }) +); + +backend.addOutput({ + custom: { iotEndpoint: 'your-iot-endpoint.iot.region.amazonaws.com' }, +}); +``` + +**Frontend** — subscribe and publish: + +```typescript +import { PubSub } from '@aws-amplify/pubsub'; + +const sub = PubSub.subscribe({ topics: ['myTopic'] }).subscribe({ + next: (data) => console.log('Message:', data), + error: (err) => console.error(err), +}); + +await PubSub.publish({ topics: ['myTopic'], message: { msg: 'hello' } }); +sub.unsubscribe(); // MUST unsubscribe to prevent leaks +``` + +When using subscriptions in React, **MUST** wrap in `useEffect` and return +cleanup function to call `.unsubscribe()`. + +Retrieve the IoT endpoint programmatically: +`aws iot describe-endpoint --endpoint-type iot:Data-ATS`. +See [AWS Amplify PubSub docs](https://docs.amplify.aws/react/build-a-backend/add-aws-services/pubsub/) +for connection configuration. + +## Custom CDK Stacks — Backend Only + +Create additional CloudFormation stacks for resources not natively +supported by Amplify: + +```typescript +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: + +```typescript +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: + +```typescript +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](https://docs.amplify.aws/react/build-a-backend/add-aws-services/overriding-resources/) +for the full override API. + +## Custom Outputs — Backend Only + +Expose custom resource values to the frontend via `amplify_outputs.json`: + +```typescript +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`: + +```typescript +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: ['*'], + }) +); +``` + +**Frontend (React):** + +```bash +npm install @aws-amplify/ui-react-liveness +``` + +```tsx +import { FaceLivenessDetector } from '@aws-amplify/ui-react-liveness'; + + { /* 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](https://ui.docs.amplify.aws/react/connected-components/liveness) +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](https://ui.docs.amplify.aws/swift/connected-components/liveness) +for the full SwiftUI integration guide. + +**Frontend (Android — API 24+):** + +Add the dependency to `app/build.gradle.kts`: + +```kotlin +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](https://ui.docs.amplify.aws/android/connected-components/liveness) +for the full Compose integration guide. + +## 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:** You **MUST** configure the correct IoT endpoint for + your region; using the wrong endpoint type causes silent connection + failures. + +## Links + +- [Add AWS Services](https://docs.amplify.aws/react/build-a-backend/add-aws-services/) +- [Custom Resources](https://docs.amplify.aws/react/build-a-backend/add-aws-services/custom-resources/) +- [Overriding Resources](https://docs.amplify.aws/react/build-a-backend/add-aws-services/overriding-resources/) diff --git a/aws-amplify/steering/ai.md b/aws-amplify/steering/ai.md new file mode 100644 index 0000000..015a98e --- /dev/null +++ b/aws-amplify/steering/ai.md @@ -0,0 +1,211 @@ +# AI + +## Model Selection + +Use `a.ai.model()` to select an AI model in both `a.conversation()` and `a.generation()` routes. Pass a human-readable model name string: + +```typescript +aiModel: a.ai.model('Claude 3.5 Sonnet v2') +``` + +`a.ai.model()` accepts any supported model name: +- **Anthropic**: `'Claude 3 Haiku'`, `'Claude 3 Sonnet'`, `'Claude 3 Opus'`, `'Claude 3.5 Haiku'`, `'Claude 3.5 Sonnet'`, `'Claude 3.5 Sonnet v2'`, `'Claude 3.7 Sonnet'`, `'Claude Opus 4'`, `'Claude Sonnet 4'`, `'Claude Haiku 4.5'`, `'Claude Sonnet 4.5'`, `'Claude Opus 4.5'`, `'Claude Sonnet 4.6'`, `'Claude Opus 4.6'` +- **Amazon**: `'Amazon Nova Pro'`, `'Amazon Nova Lite'`, `'Amazon Nova Micro'` +- **Meta**: `'Llama 3.1 405B Instruct'`, `'Llama 3.1 70B Instruct'`, `'Llama 3.1 8B Instruct'` +- **Cohere**: `'Cohere Command R+'`, `'Cohere Command R'` +- **Mistral**: `'Mistral Large 2'`, `'Mistral Large'`, `'Mistral Small'` + +For models not in the supported list, use the raw escape hatch: `aiModel: { resourcePath: '' }`. + +Availability depends on the AWS region and Bedrock model access enablement. + +> **Note:** `a.generation()` routes only support Anthropic (Claude) models. `a.conversation()` routes work with any supported model. + +## Backend: Conversation Routes + +Define multi-turn conversation routes in your data schema using +`a.conversation()`: + +```typescript +// amplify/data/resource.ts +import { a, type ClientSchema } from '@aws-amplify/backend'; + +const schema = a.schema({ + chat: a.conversation({ + aiModel: a.ai.model('Claude 3.5 Sonnet v2'), + systemPrompt: 'You are a helpful assistant.', + }) + .authorization(allow => allow.owner()), +}); +``` + +## Backend: Generation Routes + +Use `a.generation()` for single-turn (stateless) inference. + +> **MUST:** Only Anthropic (Claude) models support `a.generation()` routes. Non-Anthropic models (Amazon Nova, Meta Llama, Cohere, Mistral) work with `a.conversation()` only. + +```typescript +const schema = a.schema({ + summarize: a.generation({ + aiModel: a.ai.model('Claude 3.5 Sonnet v2'), + systemPrompt: 'Summarize the provided text concisely.', + inferenceConfiguration: { maxTokens: 500, temperature: 0.3 }, + }) + .arguments({ text: a.string().required() }) + .returns(a.customType({ summary: a.string() })) + .authorization(allow => allow.authenticated()), +}); +``` + +**CRITICAL — Authorization Constraints:** +- **Conversation routes** (`a.conversation()`) **MUST** use `allow.owner()` authorization — `allow.authenticated()` and other non-owner strategies throw a TypeError at CDK assembly time (before deployment even begins). +- **Generation routes** (`a.generation()`) **MUST** use non-owner authorization (`allow.authenticated()`, `allow.guest()`, `allow.group()`, or `allow.publicApiKey()`) — `allow.owner()` throws a TypeError at CDK assembly time (before deployment even begins). + +These constraints are asymmetric and frequently confused. Getting them wrong +causes the CDK synthesis to fail with a non-obvious TypeError. + +### Backend Integration + +AI conversation and generation routes are part of your data schema. Import into `amplify/backend.ts`: + +```typescript +import { defineBackend } from '@aws-amplify/backend'; +import { data } from './data/resource'; + +defineBackend({ data }); // AI routes live inside the data schema +``` + +## Backend: AI Tools + +Attach Lambda functions as tools to conversation routes so the AI model +can invoke them: + +```typescript +import { myToolFunc } from '../functions/my-tool/resource'; + +const schema = a.schema({ + chat: a.conversation({ + aiModel: a.ai.model('Claude 3.5 Sonnet v2'), + systemPrompt: 'You are a helpful assistant with tool access.', + tools: [ + { + name: 'getWeather', + query: a.ref('getWeather'), + description: 'Get current weather for a city', + }, + ], + }) + .authorization(allow => allow.owner()), + + getWeather: a.query() + .arguments({ city: a.string().required() }) + .returns(a.customType({ temp: a.float(), condition: a.string() })) + .handler(a.handler.function(myToolFunc)) + .authorization(allow => allow.authenticated()), +}); +``` + +Define the tool function with `defineFunction` (see +[functions-and-api.md](functions-and-api.md)). + +## Frontend: React AI UI + +Install the AI UI package: + +```bash +npm install @aws-amplify/ui-react-ai +``` + +Set up hooks and render the conversation component: + +```tsx +import { generateClient } from 'aws-amplify/api'; +import { createAIHooks, AIConversation } from '@aws-amplify/ui-react-ai'; +import type { Schema } from '../amplify/data/resource'; + +const client = generateClient(); +const { useAIConversation } = createAIHooks(client); + +export default function Chat() { + const [ + { data: { messages }, isLoading }, + handleSendMessage, + ] = useAIConversation('chat'); + + return ( + + ); +} +``` + +## Frontend: Manual Client + +For programmatic access without the pre-built UI: + +```typescript +const client = generateClient(); + +// List conversations +const { data: conversations } = await client.conversations.chat.list(); + +// Create a new conversation +const { data: conversation } = await client.conversations.chat.create(); + +// Send a message +const { data: message } = await conversation.sendMessage({ + content: [{ text: 'Hello!' }], +}); +``` + +Pagination: use `limit` and `nextToken` parameters on `.list()`. + +## Streaming + +Subscribe to streaming responses for real-time token delivery: + +In React, **MUST** wrap in `useEffect` and return the cleanup function: + +```tsx +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](data-mobile.md) patterns for the equivalent approach. + +## Pitfalls + +- **Conversation auth MUST be `allow.owner()`:** Using + `allow.authenticated()` or any other non-owner strategy on + `a.conversation()` throws a TypeError at CDK assembly time. +- **Generation auth MUST NOT be `allow.owner()`:** Using + `allow.owner()` on `a.generation()` throws a TypeError at CDK assembly + time. Use `allow.authenticated()`, `allow.guest()`, or `allow.group()`. +- **Missing AI route in data schema:** The conversation or generation + route **MUST** be defined in your `a.schema()` — without it, the + frontend client has no AI endpoint to call. +- **Model availability:** Not all Bedrock models are enabled by default — + you **MUST** enable model access in the AWS console (Bedrock → Model + access) before using a model in `a.ai.model()`. +- **Message content structure:** Both `sendMessage('Hello')` (string) and + `sendMessage({ content: [{ text: 'Hello' }] })` (object) are valid. Use + the object form when sending images or tool results. + +## Links + +- [AI Overview](https://docs.amplify.aws/react/ai/) +- [Set Up AI](https://docs.amplify.aws/react/ai/set-up-ai/) +- [Conversation UI](https://docs.amplify.aws/react/frontend/ai/conversation/) +- [Generation UI](https://docs.amplify.aws/react/frontend/ai/generation/) diff --git a/aws-amplify/steering/amplify-workflow.md b/aws-amplify/steering/amplify-workflow.md deleted file mode 100644 index 0f32d9c..0000000 --- a/aws-amplify/steering/amplify-workflow.md +++ /dev/null @@ -1,184 +0,0 @@ -# Amplify Workflow - -Orchestrated workflow for AWS Amplify Gen 2 development. - -## When to Use This Workflow - -Use for any Amplify Gen 2 work: -- Building a new full-stack application -- Adding features to an existing backend -- Connecting frontend to backend -- Deploying to sandbox or production - -The workflow determines which phases apply based on your request. - ---- - -## Step 1: Validate Prerequisites - -Run these checks before proceeding: - -1. **Node.js 18.x or later** - - ```bash - node --version - ``` - -2. **npm available** - - ```bash - npm --version - ``` - -3. **AWS credentials configured** (CRITICAL) - - ```bash - AWS_PAGER="" aws sts get-caller-identity - ``` - -If the AWS credentials check fails, **STOP** and present this message to the user: - -``` -## AWS Credentials Required - -I can't proceed without AWS credentials configured. Please set up your credentials first: - -**Setup Guide:** https://docs.amplify.aws/react/start/account-setup/ - -**Quick options:** -- Run `aws configure` to set up access keys -- Run `aws sso login` if using AWS IAM Identity Center - -Once your credentials are configured, **come back and start a new conversation** to continue building with Amplify. -``` - -**Do NOT proceed with Amplify work until credentials are configured.** The user must restart the conversation after setting up credentials. - ---- - -## Step 2: Understand the Project - -Once all prerequisites pass: - -1. Read all necessary project files (e.g., `amplify/`, `package.json`, existing code) to understand the current state -2. If unsure about Amplify capabilities or best practices, use documentation tools to search and read AWS Amplify docs - -Do this BEFORE proposing a plan. - ---- - -## Step 3: Determine Applicable Phases - -Based on the user's request and project state, determine which phases apply: - -| Phase | Applies when | Steering file | -| ------------------ | -------------------------------------------------------- | -------------------- | -| 1: Backend | User needs to create or modify Amplify backend resources | `phase1-backend.md` | -| 2: Sandbox | Deploy to sandbox for testing | `phase2-sandbox.md` | -| 3: Frontend & Test | Frontend needs to connect to Amplify backend | `phase3-frontend.md` | -| 4: Production | Deploy to production | `phase4-production.md` | - -Common patterns: -- **New full-stack app:** 1 -> 2 -> 3 -> 4 -- **Backend only (no frontend):** 1 -> 2 -- **Add feature to existing backend:** 1 -> 2 -- **Redeploy after changes:** 2 only -- **Connect existing frontend:** 3 only -- **Deploy to production:** 4 only - -**IMPORTANT: Only include phases that the user actually needs.** If the user asks for backend work only (e.g., "add auth", "create a data model", "add storage"), do NOT include Phase 3 (Frontend & Test). Frontend phases should only be included when the user explicitly asks for frontend work, a full-stack app, or to connect a frontend to Amplify. - ---- - -## Step 4: Present Plan and Confirm - -Present to the user: - -``` -## Plan - -### What I understood -- [Brief summary of what the user wants] - -### Features -[list features if applicable] - -### Framework -[framework if known] - -### Phases I'll execute -1. [Phase name] - [one-line description] -> SOP: [sop-name] -2. [Phase name] - [one-line description] -> SOP: [sop-name] -... -(Include SOP name for phases 1 and 3. Phases 2 and 4 use the amplify-deployment-guide SOP.) - -Ready to get started? -``` - -**WAIT for user confirmation before proceeding.** - -**Once the user approves the plan, you MUST stick to it. Do not deviate from the planned phases or SOPs unless the user explicitly asks for changes.** - ---- - -## Step 5: Execute Phases - -After the user confirms the plan, read **ONLY the first phase's steering file** using readSteering: - -``` -Call action "readSteering" with powerName="aws-amplify", steeringFile="" -``` - -Where `` is the steering file for the first phase in the plan (from the table in Step 3). - -**Do NOT read any other phase steering files yet.** - -### Resuming After a Phase Completes - -When a phase completes, it will summarize what it did and stop. The orchestrator takes over: - -1. Tell the user which phase just finished -2. If there are more phases in the plan, ask: - -``` -[Phase name] is complete. Ready to proceed to [next phase name]? -``` - -3. **WAIT for the user to confirm before proceeding.** -4. After the user confirms, read the next phase's steering file: - -``` -Call action "readSteering" with powerName="aws-amplify", steeringFile="" -``` - -**If there are no more phases in the plan, the workflow is complete.** Tell the user all phases are done. - -Do NOT re-run prerequisites or re-present the plan. Simply dispatch the next phase. - ---- - -## Critical Rules - -1. **Always follow SOPs completely** - Do not improvise or skip steps -2. **Never use Gen 1 patterns** - This power is for Amplify Gen 2 only (TypeScript code-first, `defineAuth`/`defineData`/`defineStorage`/`defineFunction`) -3. **One phase at a time** - Read only one phase steering file at a time. Do not read ahead. -4. **Wait for confirmation between phases** - After each phase completes, ask the user to confirm before dispatching the next phase. Do not proceed until the user confirms. -5. **If you encounter an error or get sidetracked:** - - Fix the immediate issue - - Return to the SOP and continue from where you left off - - Do NOT abandon the SOP or start improvising -6. **If you lose track of where you were in the SOP:** - - Use the SOP retrieval tool to get the SOP again - - Identify which step you completed last - - Continue from the next step - ---- - -## Troubleshooting - -If issues occur during any phase: -1. Check the SOP's troubleshooting section first -2. Use documentation tools to search AWS Amplify docs for the error message -3. Read the relevant documentation page - -**After resolving the issue, immediately return to the SOP and continue from where you left off. Do not abandon the workflow.** diff --git a/aws-amplify/steering/auth-backend.md b/aws-amplify/steering/auth-backend.md new file mode 100644 index 0000000..12431a3 --- /dev/null +++ b/aws-amplify/steering/auth-backend.md @@ -0,0 +1,233 @@ +# Auth — Backend + +## Basic Auth Setup + +Define authentication in `amplify/auth/resource.ts`: + +```typescript +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`: + +```typescript +import { defineBackend } from '@aws-amplify/backend'; +import { auth } from './auth/resource'; +defineBackend({ auth }); +``` + +## MFA Configuration + +```typescript +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:** + +```typescript +export const auth = defineAuth({ + loginWith: { + email: { + otpLogin: true, + }, + }, +}); +``` + +**SMS OTP:** + +```typescript +export const auth = defineAuth({ + loginWith: { + phone: { + otpLogin: true, + }, + }, +}); +``` + +**WebAuthn / Passkeys:** + +```typescript +export const auth = defineAuth({ + loginWith: { + webAuthn: true, + }, +}); +``` + +These passwordless methods can be combined with each other and with +password-based login in the same `defineAuth` configuration. + +## Social Login + +You **MUST** use `secret()` for OAuth client secrets — never hardcode +credentials. + +```typescript +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 "" | npx ampx sandbox secret set GOOGLE_CLIENT_ID`. +For provider-specific OAuth setup guides, **SHOULD** consult AWS +documentation via available tools; when unavailable, **MUST** use web +search or AWS CLI. + +## SAML / OIDC (Enterprise) + +OIDC providers are configured directly in `externalProviders`: + +```typescript +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: + +```typescript +// In backend.ts — SAML requires CDK-level configuration +const { cfnUserPool } = backend.auth.resources.cfnResources; +// Configure SAML identity provider via CfnUserPoolIdentityProvider +``` + +Consult AWS documentation for `CfnUserPoolIdentityProvider` SAML configuration properties. + +## Cognito Triggers + +```typescript +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`: + +```typescript +// amplify/auth/pre-sign-up/resource.ts +import { defineFunction } from '@aws-amplify/backend'; +export const preSignUp = defineFunction({ name: 'pre-sign-up' }); +``` + +### Guest (Unauthenticated) Access + +Guest access is **enabled by default** in Amplify Gen2 — the Cognito Identity Pool is created with `allowUnauthenticatedIdentities: true` automatically. + +To use guest access in your data models, set `defaultAuthorizationMode` to `'iam'`: + +```typescript +export const data = defineData({ + schema, + authorizationModes: { + defaultAuthorizationMode: 'iam', + }, +}); +``` + +To **disable** guest access, use a CDK override in `backend.ts`: + +```typescript +const { cfnIdentityPool } = backend.auth.resources.cfnResources; +cfnIdentityPool.allowUnauthenticatedIdentities = false; +``` + +## Pitfalls + +- **Trigger not registered (silent no-op):** Defining a trigger function + with `defineFunction` but NOT adding it to `triggers: {}` in `defineAuth` + causes a **silent no-op** — the function deploys but never fires. You + **MUST** both define AND register: `triggers: { preSignUp, postConfirmation }`. +- **Hardcoded secrets:** Using string literals instead of `secret()` for + OAuth credentials exposes them in source control. +- **Missing scopes:** Social providers default to minimal scopes — add + `'email'`, `'profile'` explicitly or user attributes won't populate. +- **Google attribute mapping:** The Google claim `name` maps to Cognito + `fullname` (NOT `name`). The `attributeMapping` values are plain strings, + NOT objects: `{ email: 'email', fullname: 'name' }`. +- **MFA method mismatch:** Enabling `sms: true` in MFA requires a phone + number attribute on the user pool — add `phone_number` to user attributes. + Similarly, `email: true` in MFA requires an email attribute on the user pool. +- **Secrets in CI/CD:** For branch environments, use: + `npx ampx secret set KEY_NAME --branch main --app-id APP_ID`. + +## Links + +- [Auth Overview](https://docs.amplify.aws/react/build-a-backend/auth/) +- [Set Up Auth](https://docs.amplify.aws/react/build-a-backend/auth/set-up-auth/) +- [External Identity Providers](https://docs.amplify.aws/react/build-a-backend/auth/concepts/external-identity-providers/) +- [Multi-Factor Authentication](https://docs.amplify.aws/react/build-a-backend/auth/concepts/multi-factor-authentication/) +- [Passwordless Authentication](https://docs.amplify.aws/react/build-a-backend/auth/concepts/passwordless/) +- [User Attributes](https://docs.amplify.aws/react/build-a-backend/auth/concepts/user-attributes/) +- [Grant Access to Auth Resources](https://docs.amplify.aws/react/build-a-backend/auth/grant-access-to-auth-resources/) diff --git a/aws-amplify/steering/auth-mobile.md b/aws-amplify/steering/auth-mobile.md new file mode 100644 index 0000000..59913f9 --- /dev/null +++ b/aws-amplify/steering/auth-mobile.md @@ -0,0 +1,481 @@ +# Auth — Mobile + +> **Backend required:** Auth must be defined in `amplify/auth/resource.ts` +> using `defineAuth` — see [auth-backend.md](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. On Swift/Android, use `authenticationFlow: .userChoice(preferredAuthFactor: .webAuthn)` to default to passkeys. Custom OTP/passkey flows require additional challenge handling. + +### Flutter + +**Dependencies** — add to `pubspec.yaml`: + +```yaml +dependencies: + amplify_flutter: ^2.0.0 + amplify_auth_cognito: ^2.0.0 + amplify_authenticator: ^2.0.0 +``` + +**Usage** — wrap your `MaterialApp` and set its `builder`: + +```dart +import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; +import 'package:amplify_authenticator/amplify_authenticator.dart'; +import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:flutter/material.dart'; + +import 'amplifyconfiguration.dart'; + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + void initState() { + super.initState(); + _configureAmplify(); + } + + void _configureAmplify() async { + try { + await Amplify.addPlugin(AmplifyAuthCognito()); + await Amplify.configure(amplifyconfig); + } on Exception catch (e) { + safePrint('Error configuring Amplify: $e'); + } + } + + @override + Widget build(BuildContext context) { + return Authenticator( + child: MaterialApp( + builder: Authenticator.builder(), + home: const Scaffold( + body: Center(child: Text('You are logged in!')), + ), + ), + ); + } +} +``` + +### Swift (Apple platforms) + +> Supports iOS 13+, macOS 12+, tvOS 13+, watchOS 9+, visionOS 1+ (preview). +> Passkeys require iOS 17.4+, macOS 13.5+, or visionOS 1.0+. + +**Dependencies** — add both SPM packages in Xcode (**File > Add Packages…**): + +| Package | URL | Libraries | +|---|---|---| +| Amplify Library for Swift | `https://github.com/aws-amplify/amplify-swift` | `Amplify`, `AWSCognitoAuthPlugin` | +| Amplify UI Swift Authenticator | `https://github.com/aws-amplify/amplify-ui-swift-authenticator` | `Authenticator` | + +> **SPM versioning:** For both packages, select **"Up to Next Major Version"** in Xcode's dependency rule. Do NOT pin to a specific branch (e.g., `main`) — use "Up to Next Major Version" to get compatible updates automatically. + +**Usage** — SwiftUI entry point: + +```swift +import Amplify +import Authenticator +import AWSCognitoAuthPlugin +import SwiftUI + +@main +struct MyApp: App { + init() { + do { + try Amplify.add(plugin: AWSCognitoAuthPlugin()) + try Amplify.configure() + } catch { + print("Unable to configure Amplify \(error)") + } + } + + var body: some Scene { + WindowGroup { + Authenticator { state in + VStack { + Text("Hello, \(state.user.username)") + Button("Sign out") { + Task { await state.signOut() } + } + } + } + } + } +} +``` + +**Passwordless / user-choice flow:** + +```swift +Authenticator(authenticationFlow: .userChoice( + preferredAuthFactor: .webAuthn +)) { state in + Text("Welcome \(state.user.username)!") +} +``` + +### Android (Kotlin) + +**Dependencies** — add to your app's `build.gradle`: + +```groovy +// Enable Jetpack Compose +android { + compileOptions { + coreLibraryDesugaringEnabled true + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + buildFeatures { compose true } + composeOptions { kotlinCompilerExtensionVersion '1.5.3' } +} + +dependencies { + implementation 'com.amplifyframework.ui:authenticator:1.4.0' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' +} +``` + +`INTERNET` permission is required in `AndroidManifest.xml`: +```xml + +``` + +**Configure** — in your `Application.onCreate()`: + +```kotlin +try { + Amplify.addPlugin(AWSCognitoAuthPlugin()) + Amplify.configure(applicationContext) +} catch (error: AmplifyException) { + Log.e("MyApp", "Could not initialize Amplify", error) +} +``` + +**Usage** — Jetpack Compose: + +```kotlin +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:** + +```kotlin +val authenticatorState = rememberAuthenticatorState( + authenticationFlow = AuthenticationFlow.UserChoice( + preferredAuthFactor = AuthFactor.WebAuthn + ) +) +Authenticator(state = authenticatorState) { state -> + Text("Welcome ${state.user.username}!") +} +``` + +## Custom UI + +Use the low-level Auth APIs when you need full control over the UI. Each +platform returns a `nextStep` from `signIn` / `signUp` — switch on it and +call `confirmSignIn` as needed. The Authenticator handles all these steps +automatically; the list below is for reference when building custom flows. + +### Flutter + +```dart +import 'package:amplify_flutter/amplify_flutter.dart'; +``` + +**Sign in:** +```dart +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): +```dart +final result = await Amplify.Auth.confirmSignIn( + confirmationValue: codeFromUser, +); +``` + +**Sign up:** +```dart +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:** +```dart +await Amplify.Auth.confirmSignUp( + username: username, + confirmationCode: code, +); +``` + +### Swift (Apple platforms) + +Uses async/await. + +```swift +import Amplify +``` + +**Sign in:** +```swift +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:** +```swift +let result = try await Amplify.Auth.confirmSignIn( + challengeResponse: codeFromUser +) +``` + +**Sign up:** +```swift +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:** +```swift +try await Amplify.Auth.confirmSignUp( + for: username, + confirmationCode: code +) +``` + +### Android (Kotlin) + +Android supports **both** Kotlin coroutines and callbacks. Coroutines are +recommended. + +```kotlin +import com.amplifyframework.core.Amplify +import com.amplifyframework.auth.AuthUserAttributeKey +import com.amplifyframework.auth.options.AuthSignUpOptions +``` + +**Sign in (coroutines — recommended):** +```kotlin +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):** +```kotlin +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):** +```kotlin +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):** +```kotlin +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):** +```kotlin +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:** +```dart +final result = await Amplify.Auth.signInWithWebUI( + provider: AuthProvider.google, +); +``` + +Platform setup for Flutter OAuth: +- **Android:** Add `` with your callback scheme to `MainActivity` in `AndroidManifest.xml`. +- **iOS:** No additional platform configuration required. +- **macOS:** Enable App Sandbox → "Incoming Connections (Server)" in Xcode. + +**Swift:** +```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):** +```kotlin +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`: +```xml + + + + + + + + +``` + +## Pitfalls + +- **Plugin order:** `addPlugin()` / `add(plugin:)` **MUST** be called + before `configure()` on all platforms — see [core-mobile.md](core-mobile.md). +- **Missing INTERNET permission (Android):** Without + `` 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)](https://ui.docs.amplify.aws/android/connected-components/authenticator) +- [Authenticator (Swift)](https://ui.docs.amplify.aws/swift/connected-components/authenticator) +- [Authenticator (Flutter)](https://ui.docs.amplify.aws/flutter/connected-components/authenticator) +- [Auth Overview (Android)](https://docs.amplify.aws/android/build-a-backend/auth/) +- [Sign In (Android)](https://docs.amplify.aws/android/frontend/auth/sign-in/) +- [External Identity Providers (Android)](https://docs.amplify.aws/android/build-a-backend/auth/concepts/external-identity-providers/) +- [Multi-Factor Authentication (Android)](https://docs.amplify.aws/android/build-a-backend/auth/concepts/multi-factor-authentication/) +- [Auth Overview (Swift)](https://docs.amplify.aws/swift/build-a-backend/auth/) +- [Sign In (Swift)](https://docs.amplify.aws/swift/frontend/auth/sign-in/) +- [External Identity Providers (Swift)](https://docs.amplify.aws/swift/build-a-backend/auth/concepts/external-identity-providers/) +- [Multi-Factor Authentication (Swift)](https://docs.amplify.aws/swift/build-a-backend/auth/concepts/multi-factor-authentication/) +- [Auth Overview (Flutter)](https://docs.amplify.aws/flutter/build-a-backend/auth/) +- [Sign In (Flutter)](https://docs.amplify.aws/flutter/frontend/auth/sign-in/) +- [External Identity Providers (Flutter)](https://docs.amplify.aws/flutter/build-a-backend/auth/concepts/external-identity-providers/) +- [Multi-Factor Authentication (Flutter)](https://docs.amplify.aws/flutter/build-a-backend/auth/concepts/multi-factor-authentication/) diff --git a/aws-amplify/steering/auth-web.md b/aws-amplify/steering/auth-web.md new file mode 100644 index 0000000..f8295f0 --- /dev/null +++ b/aws-amplify/steering/auth-web.md @@ -0,0 +1,127 @@ +# Auth — Web + +> **Backend required:** Auth must be defined in `amplify/auth/resource.ts` +> using `defineAuth` — see [auth-backend.md](auth-backend.md). + +## Authenticator Component + +| Framework | Package | Tag | CSS (MUST import) | +|---|---|---|---| +| React / Next.js | `@aws-amplify/ui-react` | `` | `@aws-amplify/ui-react/styles.css` | +| Vue | `@aws-amplify/ui-vue` | `` | `@aws-amplify/ui-vue/styles.css` | +| Angular | `@aws-amplify/ui-angular` | `` + `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 ``, use `useAuthenticator` hook. + +## Manual Auth Flows + +Imports from `aws-amplify/auth`: `signIn`, `signUp`, `confirmSignUp`, `confirmSignIn`, `signOut`, `resetPassword`. + +After `signIn()`, you **MUST** switch on `result.nextStep.signInStep`: + +| signInStep value | Action | +|---|---| +| `DONE` | Authenticated | +| `CONFIRM_SIGN_UP` | Call `confirmSignUp()` | +| `CONFIRM_SIGN_IN_WITH_TOTP_CODE` | Prompt TOTP, call `confirmSignIn({ challengeResponse })` | +| `CONFIRM_SIGN_IN_WITH_SMS_CODE` | Prompt SMS code, same | +| `CONFIRM_SIGN_IN_WITH_EMAIL_CODE` | Prompt email code, same | +| `CONTINUE_SIGN_IN_WITH_TOTP_SETUP` | Show QR URI, call `confirmSignIn()` | +| `CONTINUE_SIGN_IN_WITH_MFA_SELECTION` | `confirmSignIn({ challengeResponse: 'TOTP'\|'SMS'\|'EMAIL' })` | +| `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'\|'EMAIL' })` | +| `CONTINUE_SIGN_IN_WITH_EMAIL_SETUP` | Prompt email, call `confirmSignIn()` | +| `CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION` | `confirmSignIn({ challengeResponse: selectedFactor })` | + +OAuth/social: `signInWithRedirect({ provider: 'Google' })`. + +## Session Management + +| API (from `aws-amplify/auth`) | Returns | +|---|---| +| `getCurrentUser()` | `{ userId, username, signInDetails? }` | +| `fetchAuthSession()` | `{ tokens?, credentials?, identityId?, userSub? }` — access `.tokens?.idToken`, `.tokens?.accessToken` | +| `fetchUserAttributes()` | `{ email, phone_number, ... }` | + +Tokens refresh automatically. + +## Next.js Server-Side Auth + +For server components and route handlers, use cookie-based auth: + +```typescript +import { generateServerClientUsingCookies } from '@aws-amplify/adapter-nextjs/data'; +import { cookies } from 'next/headers'; +import { amplifyOutputs } from '@/amplify_outputs'; + +const client = generateServerClientUsingCookies({ config: amplifyOutputs, cookies }); +``` + +> **Critical:** `generateServerClientUsingCookies` from `@aws-amplify/adapter-nextjs/data` is the ONLY way to access authenticated data in Next.js server components. Do NOT use `generateClient()` on the server side — it has no access to the user's session cookies. + +For server actions and middleware, use `createServerRunner` from `@aws-amplify/adapter-nextjs`: + +```typescript +import { createServerRunner } from '@aws-amplify/adapter-nextjs'; +import { amplifyOutputs } from '@/amplify_outputs'; + +export const { runWithAmplifyServerContext } = createServerRunner({ config: amplifyOutputs }); +``` + +## React Native + +React Native uses the same `aws-amplify` auth APIs as web. All manual auth +flows (`signIn`, `signUp`, `confirmSignIn`, etc.) and session management +APIs work identically. + +### Setup + +**Import order matters:** `react-native-get-random-values` **MUST** be +the FIRST import in the entry file, `@aws-amplify/react-native` **MUST** +come before `aws-amplify`. See [core-web.md](core-web.md) for the +full required import order. + +```bash +npm install @aws-amplify/ui-react-native @react-native-async-storage/async-storage +``` + +Same `` 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.css` import, the + `` renders as unstyled HTML. +- **Unhandled sign-in steps:** Not switching on ALL `signInStep` values + causes the flow to silently stall on MFA or password-reset challenges. + You **MUST** handle every possible value — missing any causes the auth + flow to hang with no visible error. +- **MFA timing:** Calling `updateMFAPreference()` before authentication + completes fails silently because the user is not yet authenticated. + Wait until `signInStep` is `'DONE'`. +- **OAuth in multi-page apps:** You **MUST** call `Hub.listen('auth', ...)` + to capture the OAuth redirect callback on page reload. +- **Vue component syntax:** Vue **MUST** use PascalCase `` + component syntax (not kebab-case ``). + +## Links + +- [Auth Overview (React)](https://docs.amplify.aws/react/build-a-backend/auth/) +- [Set Up Auth (React)](https://docs.amplify.aws/react/build-a-backend/auth/set-up-auth/) +- [Connect Auth Frontend (React)](https://docs.amplify.aws/react/frontend/auth/) +- [Auth Overview (Next.js)](https://docs.amplify.aws/nextjs/build-a-backend/auth/) +- [Set Up Auth (Next.js)](https://docs.amplify.aws/nextjs/build-a-backend/auth/set-up-auth/) +- [Connect Auth Frontend (Next.js)](https://docs.amplify.aws/nextjs/frontend/auth/) +- [Auth Overview (React Native)](https://docs.amplify.aws/react-native/build-a-backend/auth/) +- [Set Up Auth (React Native)](https://docs.amplify.aws/react-native/build-a-backend/auth/set-up-auth/) +- [Connect Auth Frontend (React Native)](https://docs.amplify.aws/react-native/frontend/auth/) diff --git a/aws-amplify/steering/core-mobile.md b/aws-amplify/steering/core-mobile.md new file mode 100644 index 0000000..ae5c93b --- /dev/null +++ b/aws-amplify/steering/core-mobile.md @@ -0,0 +1,185 @@ +# Core — Mobile + +## Critical Rules + +These patterns apply to **every** task — not just new projects. You **MUST** +verify each one before implementing any feature. + +### Gen2 Detection + +Before modifying any code, check if the project is already Gen2: + +1. `amplify/` directory exists with `backend.ts` +2. `amplify_flutter` in `pubspec.yaml` (Flutter) or + `@aws-amplify/backend` in `package.json` devDependencies (for projects using a JS/TS-based Amplify backend alongside a native mobile frontend) + +If both are true, the project is already Gen2 — skip to feature +implementation. If `amplify/.config/` exists instead, this is a Gen1 +project — **MUST NOT** proceed (requires separate migration skill). + +### Plugin Initialization Order + +Flutter, Swift, and Android all require plugins to be added **before** +calling configure. Reversing the order causes a runtime exception. + +**Flutter:** + +```dart +await Amplify.addPlugins([AmplifyAuthCognito()]); +await Amplify.configure(amplifyConfig); +``` + +**Swift:** + +```swift +try Amplify.add(plugin: AWSCognitoAuthPlugin()) +try Amplify.configure(with: .amplifyOutputs) +``` + +**Android (Kotlin):** + +```kotlin +Amplify.addPlugin(AWSCognitoAuthPlugin()) +Amplify.configure(AmplifyOutputs(R.raw.amplify_outputs), applicationContext) +``` + +### Required Packages + +**Flutter** — `pubspec.yaml`: + +```yaml +dependencies: + amplify_flutter: ^2.0.0 + amplify_auth_cognito: ^2.0.0 +``` + +**Swift:** Add via Xcode SPM: `https://github.com/aws-amplify/amplify-swift` +(Up to Next Major Version). + +**Android** — `app/build.gradle.kts`: + +```kotlin +dependencies { + implementation("com.amplifyframework:core:2.+") + implementation("com.amplifyframework:aws-auth-cognito:2.+") +} +``` + +### Configure Entry Points + +**Flutter** — `lib/main.dart`: + +```dart +import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; +import 'amplify_outputs.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Amplify.addPlugins([AmplifyAuthCognito()]); + await Amplify.configure(amplifyConfig); + runApp(const MyApp()); +} +``` + +**Swift (SwiftUI)** — `MyApp.swift`: + +```swift +import SwiftUI +import Amplify +import AWSCognitoAuthPlugin + +@main +struct MyApp: App { + init() { + do { + try Amplify.add(plugin: AWSCognitoAuthPlugin()) + try Amplify.configure(with: .amplifyOutputs) + } catch { + print("Failed to configure Amplify: \(error)") + } + } + var body: some Scene { + WindowGroup { ContentView() } + } +} +``` + +**Swift (UIKit)** — `AppDelegate.swift`: + +```swift +import Amplify +import AWSCognitoAuthPlugin + +func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + do { + try Amplify.add(plugin: AWSCognitoAuthPlugin()) + try Amplify.configure(with: .amplifyOutputs) + } catch { + print("Failed to configure Amplify: \(error)") + } + return true +} +``` + +**Android** — Application class: + +```kotlin +import com.amplifyframework.core.Amplify +import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin +import com.amplifyframework.core.configuration.AmplifyOutputs + +class MyApp : Application() { + override fun onCreate() { + super.onCreate() + Amplify.addPlugin(AWSCognitoAuthPlugin()) + Amplify.configure(AmplifyOutputs(R.raw.amplify_outputs), applicationContext) + } +} +``` + +For Android, `amplify_outputs.json` **MUST** go in `app/src/main/res/raw/`, +not in the project root. + +## Platform-Specific MUST Steps + +### Android: Core Library Desugaring + +Core library desugaring **MUST** be enabled for Android API level < 26. The agent **MUST** provide explicit step-by-step desugaring instructions to the customer — do not just mention it. See: https://docs.amplify.aws/android/start/quickstart/#5-install-dependencies + +In `app/build.gradle.kts`: + +```kotlin +android { + compileOptions { + isCoreLibraryDesugaringEnabled = true + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} +dependencies { + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") +} +``` + +### Swift (Apple platforms): Xcode Project Configuration + +> Supported: iOS 13+, macOS 12+, tvOS 13+, watchOS 9+, visionOS 1+ (preview). + +For iOS/Swift projects, `amplify_outputs.json` **MUST** be added to the Xcode project. The agent should instruct the user to drag the `amplify_outputs.json` file into their Xcode project navigator so it is included in the app bundle. + +### Flutter: Dart Output Format + +For Flutter, `amplify_outputs.dart` **MUST** be generated by specifying the dart output format when running sandbox or deploy commands: + +```bash +npx ampx sandbox --outputs-format dart --outputs-out-dir lib +npx ampx generate outputs --format dart --out-dir lib +``` + +## Links + +- [Android Quickstart](https://docs.amplify.aws/android/start/quickstart/) +- [Swift Quickstart](https://docs.amplify.aws/swift/start/quickstart/) +- [Flutter Quickstart](https://docs.amplify.aws/flutter/start/quickstart/) diff --git a/aws-amplify/steering/core-web.md b/aws-amplify/steering/core-web.md new file mode 100644 index 0000000..c97ec27 --- /dev/null +++ b/aws-amplify/steering/core-web.md @@ -0,0 +1,151 @@ +# Core — Web + +## Critical Rules + +These patterns apply to **every** task — not just new projects. You **MUST** +verify each one before implementing any feature. + +### Gen2 Detection + +Before modifying any code, check if the project is already Gen2: + +1. `amplify/` directory exists with `backend.ts` +2. `@aws-amplify/backend` in `package.json` devDependencies + +If both are true, the project is already Gen2 — skip to feature +implementation. If `amplify/.config/` exists instead, this is a Gen1 +project — **MUST NOT** proceed (requires separate migration skill). + +### Directory Structure + +`amplify/` and `src/` **MUST** be siblings under the project root. Placing +them at different directory levels breaks sandbox detection. + +``` +project-root/ +├── amplify/ +│ ├── backend.ts +│ ├── auth/resource.ts +│ ├── data/resource.ts +│ └── storage/resource.ts +├── src/ +├── amplify_outputs.json # Generated — DO NOT edit +└── package.json +``` + +### 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. If missing, the build fails with a module-not-found error. +Run `npx ampx sandbox` (or `npx ampx sandbox --once`) first to +generate it. See [scaffolding.md](scaffolding.md) for the correct sequence. +**React (Vite)** — `src/main.tsx`: + +```typescript +import { Amplify } from 'aws-amplify'; +import outputs from '../amplify_outputs.json'; +Amplify.configure(outputs); +``` +**Next.js (App Router)** — `app/layout.tsx`: + +```typescript +import { Amplify } from 'aws-amplify'; +import outputs from '@/amplify_outputs.json'; +Amplify.configure(outputs, { ssr: true }); +``` + +**`{ ssr: true }` applies only to Next.js App Router.** All other frameworks +(Vue, Angular, React SPA) omit this option. +**Vue** — `src/main.js`: + +```javascript +import { Amplify } from 'aws-amplify'; +import outputs from '../amplify_outputs.json'; +Amplify.configure(outputs); +``` +**Angular** — `src/main.ts`: + +```typescript +import { Amplify } from 'aws-amplify'; +import outputs from '../amplify_outputs.json'; +Amplify.configure(outputs); +``` + +## Data Client Best Practices + +See [data-web.md](data-web.md) for `generateClient` setup and module-scope rules. + +For Next.js Server Components, use `generateServerClientUsingCookies` from +`@aws-amplify/adapter-nextjs/data` — NOT `generateClient`. Server +components have no browser session, so `generateClient` fails silently. +`` 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 + +```bash +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`: + +```typescript +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`): + +```typescript +import 'react-native-get-random-values'; // MUST be first +import { Amplify } from 'aws-amplify'; +import outputs from './amplify_outputs.json'; +Amplify.configure(outputs); +``` + +### Gen2 Detection (React Native) + +Same as web — check for `amplify/` directory with `backend.ts` and +`@aws-amplify/backend` in `package.json` devDependencies. + +### React Native Pitfalls + +- **Import order:** `react-native-get-random-values` **MUST** 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. + +## Pitfalls + +- Forgetting to import `amplify_outputs.json` in the entry point — the app + will load but all Amplify API calls will fail silently. + +## Links + +- [React Quickstart](https://docs.amplify.aws/react/start/quickstart/) +- [Next.js Quickstart](https://docs.amplify.aws/nextjs/start/quickstart/) +- [Angular Quickstart](https://docs.amplify.aws/angular/start/quickstart/) +- [Vue Quickstart](https://docs.amplify.aws/vue/start/quickstart/) +- [React Native Quickstart](https://docs.amplify.aws/react-native/start/quickstart/) diff --git a/aws-amplify/steering/data-backend.md b/aws-amplify/steering/data-backend.md new file mode 100644 index 0000000..494c108 --- /dev/null +++ b/aws-amplify/steering/data-backend.md @@ -0,0 +1,297 @@ +# Data — Backend + +## Schema Definition + +Define your data models in `amplify/data/resource.ts`: + +```typescript +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; +export const data = defineData({ + schema, + authorizationModes: { + defaultAuthorizationMode: 'apiKey', + apiKeyAuthorizationMode: { + expiresInDays: 30, + }, + }, +}); +``` + +Import into `amplify/backend.ts`: + +```typescript +import { defineBackend } from '@aws-amplify/backend'; +import { auth } from './auth/resource'; +import { data } from './data/resource'; +defineBackend({ auth, data }); +``` + +You **MUST** export `Schema` as `ClientSchema` — 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()`, `.default(value)`, or `.array()` on +any field. + +## 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. + +```typescript +a.model({ /* fields */ }).authorization(allow => [ + allow.publicApiKey().to(['read']), // API key: public read + allow.guest().to(['read']), // Requires defaultAuthorizationMode: 'iam' — NOTE: method call () + 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 +]) +``` + +Per-field authorization overrides model-level rules: + +```typescript +Post: a.model({ + title: a.string(), + secret: a.string().authorization(allow => [allow.owner()]), +}).authorization(allow => [allow.authenticated().to(['read'])]) +``` +**Multi-owner:** Use `allow.ownersDefinedIn('editors')` with an +`editors: a.string().array()` field to grant multiple users ownership. +**Dynamic groups:** Use `allow.groupsDefinedIn('teamGroups')` with a +string field to control access via group names stored on each record. + +## Relationships + +Three types — reference field types **MUST** match the related model's +identifier type. + +```typescript +const schema = a.schema({ + Team: a.model({ + name: a.string().required(), + members: a.hasMany('Member', 'teamId'), + }).authorization(allow => [allow.owner()]), + + Member: a.model({ + name: a.string().required(), + teamId: a.id().required(), + team: a.belongsTo('Team', 'teamId'), + profile: a.hasOne('Profile', 'memberId'), + }).authorization(allow => [allow.owner()]), + + Profile: a.model({ + bio: a.string(), + memberId: a.id().required(), + member: a.belongsTo('Member', 'memberId'), + }).authorization(allow => [allow.owner()]), +}); +``` + +The second argument to `hasMany`/`belongsTo`/`hasOne` is the foreign key +field name. That field **MUST** be declared explicitly on the child model. + +You **MUST** declare **both sides** of every relationship — the parent model +needs `a.hasMany('Child', 'fkField')` AND the child model needs +`a.belongsTo('Parent', 'fkField')`. Omitting either side causes silent +query failures (e.g., lazy-loading the relation returns `undefined`). + +The foreign-key field **MUST** use `a.id()` — NOT `a.string()` — to match +the related model's identifier type. Using `a.string()` causes runtime +relationship resolution failures. + +```typescript +// CORRECT — both sides declared, FK uses a.id() +Team: a.model({ + name: a.string().required(), + members: a.hasMany('Member', 'teamId'), // parent side +}) + +Member: a.model({ + name: a.string().required(), + teamId: a.id().required(), // FK: a.id(), NOT a.string() + team: a.belongsTo('Team', 'teamId'), // child side — REQUIRED +}) +``` + +## Secondary Indexes + +```typescript +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()`: + +```typescript +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: + +```typescript +Todo: a.model({ + content: a.string().required(), + priority: a.enum(['low', 'medium', 'high']), +}) +``` + +> ⚠️ **Pitfall:** `.default()` does not work on `a.enum()` fields — default values are only supported on scalar types (`a.string()`, `a.integer()`, etc.). Applying `.default()` to an enum field silently fails at deployment. + +## Custom Types + +Custom types group related fields into a reusable structure: + +```typescript +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: + +```typescript +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): + +```typescript +export const data = defineData({ + schema, + authorizationModes: { + defaultAuthorizationMode: 'apiKey', + apiKeyAuthorizationMode: { expiresInDays: 30 }, + }, +}); +``` + +**With auth** (user-scoped access): + +```typescript +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](auth-backend.md) for details and how to disable it. + +**Guest access configuration** (with `allow.guest()`): + +```typescript +// amplify/data/resource.ts — set IAM as default auth mode for guest access +export const data = defineData({ + schema, + authorizationModes: { + defaultAuthorizationMode: 'iam', + }, +}); +``` + +## Pitfalls + +- **Missing `ClientSchema` export:** Without `export type Schema = + ClientSchema`, frontend `generateClient()` has no + type information and all operations are untyped. +- **FK field type `a.string()` instead of `a.id()`:** Using `a.string()` + for foreign key fields causes relationship resolution to fail silently — + queries return `null` for related models. Always use `a.id()` for FK fields. +- **Missing relationship side:** Omitting `belongsTo` on the child model + (or `hasMany` on the parent) causes lazy-loading the relation to return + `undefined` with no error. +- **Guest access auth mode:** `allow.guest()` requires + `defaultAuthorizationMode: 'iam'` in `defineData`. Guest access + (unauthenticated identities) is enabled by default in Amplify Gen2. +- **Auth mode conflict:** Using `allow.publicApiKey()` in model rules but + setting `defaultAuthorizationMode: 'userPool'` without adding + `apiKeyAuthorizationMode` causes API key requests to be rejected. +- **Forgetting `defineBackend`:** Defining `data` without importing it + into `backend.ts` means the schema is never deployed. +- **`.default()` on enum fields:** `.default()` does not work on + `a.enum()` fields — default values are only supported on scalar types + (`a.string()`, `a.integer()`, `a.float()`, `a.boolean()`, etc.). + Applying `.default()` to an enum field silently fails at deployment. + +## Links + +- [Data Overview](https://docs.amplify.aws/react/build-a-backend/data/) +- [Set Up Data](https://docs.amplify.aws/react/build-a-backend/data/set-up-data/) +- [Data Modeling](https://docs.amplify.aws/react/build-a-backend/data/data-modeling/) +- [Data Modeling — Relationships](https://docs.amplify.aws/react/build-a-backend/data/data-modeling/relationships/) +- [Data Modeling — Add Fields](https://docs.amplify.aws/react/build-a-backend/data/data-modeling/add-fields/) +- [Customize Authorization](https://docs.amplify.aws/react/build-a-backend/data/customize-authz/) +- [Connect to Existing Data Sources](https://docs.amplify.aws/react/build-a-backend/data/connect-to-existing-data-sources/) diff --git a/aws-amplify/steering/data-mobile.md b/aws-amplify/steering/data-mobile.md new file mode 100644 index 0000000..67f9ecc --- /dev/null +++ b/aws-amplify/steering/data-mobile.md @@ -0,0 +1,117 @@ +# Data — Mobile + +> **Backend required:** Data must be defined in `amplify/data/resource.ts` +> using `defineData` — see [data-backend.md](data-backend.md). + +## Flutter + +Import `package:amplify_flutter/amplify_flutter.dart`. All operations go through `Amplify.API`. + +**Queries:** `Amplify.API.query(request: ModelQueries.list(Todo.classType))` — response in `.response.data?.items`. +Same pattern for `.get()`. + +**Mutations:** `Amplify.API.mutate(request: ModelMutations.create(todo))` — same shape for `.update()`, `.delete()`. +Build updated models with `todo.copyWith(done: true)`. + +**Subscriptions:** `Amplify.API.subscribe(ModelSubscriptions.onCreate(Todo.classType))` → returns a stream. Listen with `.listen()`, cancel with `sub.cancel()`. + +## Swift (Apple platforms) + +> Supported: iOS 13+, macOS 12+, tvOS 13+, watchOS 9+, visionOS 1+ (preview). + +Uses `Amplify.API.query/mutate` with async/await. +Swift uses `ModelQueries`, `ModelMutations`, and `ModelSubscriptions` (plural, like Flutter). + +**Queries:** `try await Amplify.API.query(request: .list(Todo.self))` — result is `.success(let todos)`. + +**Mutations:** `try await Amplify.API.mutate(request: .create(newTodo))` — same for `.update()`, `.delete()`. +Modify models directly: `updated.done = true`. + +**Subscriptions:** `Amplify.API.subscribe(request: .subscription(of: Todo.self, type: .onCreate))` → use `for try await event in subscription`. Cancel via `task.cancel()` when the view disappears. + +## Android (Kotlin) + +Android supports both callback-based and coroutine-based APIs. +Coroutine example (recommended): + +**Queries:** + +```kotlin +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:** + +```kotlin +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):** + +```kotlin +val job = scope.launch { + try { + Amplify.API.subscribe(ModelSubscription.onCreate(Todo::class.java)) + .catch { Log.e("MyAmplifyApp", "Error on subscription", it) } + .collect { Log.i("MyAmplifyApp", "Todo created: ${it.data.name}") } + } catch (error: ApiException) { + Log.e("MyAmplifyApp", "Subscription not established", error) + } +} +// When done: +job.cancel() +``` + +**Callback alternative:** all operations also accept `onSuccess`/`onError` lambdas — e.g. +`Amplify.API.query(ModelQuery.list(Todo::class.java), { response -> ... }, { error -> ... })`. + +## Pitfalls + +- **Missing codegen for native platforms:** Flutter, Swift, and Android + **MUST** run `npx ampx generate graphql-client-code` to produce typed model + classes. Without this step, model types do not exist. You **SHOULD** use + typed model classes for compile-time safety. +- **GraphQL vs REST confusion:** All data operations use the GraphQL API + (`Amplify.API.query`/`mutate`), not REST. Using REST methods for model + CRUD returns errors. +- **Subscription cleanup:** Every platform **MUST** perform explicit + subscription cleanup (`.cancel()` on Swift tasks, `job.cancel()` for + Kotlin coroutines, `subscription.cancel()` for callbacks, or + `sub.cancel()` for Flutter). Missing cleanup causes connection leaks and + stale data. +- **Offline sync (Flutter/Swift/Android):** DataStore is a separate API + from direct API operations. Do not mix `DataStore.query()` with + `Amplify.API.query()` in the same model workflow. + +## Links + +- [Data Overview (Android)](https://docs.amplify.aws/android/build-a-backend/data/) +- [Set Up Data (Android)](https://docs.amplify.aws/android/build-a-backend/data/set-up-data/) +- [Connect to Existing Data Sources (Android)](https://docs.amplify.aws/android/build-a-backend/data/connect-to-existing-data-sources/) +- [Data Client (Android)](https://docs.amplify.aws/android/frontend/data/) +- [Data Overview (Swift)](https://docs.amplify.aws/swift/build-a-backend/data/) +- [Set Up Data (Swift)](https://docs.amplify.aws/swift/build-a-backend/data/set-up-data/) +- [Connect to Existing Data Sources (Swift)](https://docs.amplify.aws/swift/build-a-backend/data/connect-to-existing-data-sources/) +- [Data Client (Swift)](https://docs.amplify.aws/swift/frontend/data/) +- [Data Overview (Flutter)](https://docs.amplify.aws/flutter/build-a-backend/data/) +- [Set Up Data (Flutter)](https://docs.amplify.aws/flutter/build-a-backend/data/set-up-data/) +- [Connect to Existing Data Sources (Flutter)](https://docs.amplify.aws/flutter/build-a-backend/data/connect-to-existing-data-sources/) +- [Data Client (Flutter)](https://docs.amplify.aws/flutter/frontend/data/) diff --git a/aws-amplify/steering/data-web.md b/aws-amplify/steering/data-web.md new file mode 100644 index 0000000..f3442a5 --- /dev/null +++ b/aws-amplify/steering/data-web.md @@ -0,0 +1,106 @@ +# Data — Web + +> **Backend required:** Data must be defined in `amplify/data/resource.ts` +> using `defineData` — see [data-backend.md](data-backend.md). + +## Client Setup + +**`generateClient()` MUST be called at module scope** (outside +any React component). Calling it inside a component creates a new client +per render, breaking subscriptions and caching. + +```typescript +import { generateClient } from 'aws-amplify/data'; +import type { Schema } from '../amplify/data/resource'; + +// Module scope — called once +const client = generateClient(); +``` + +The `` generic gives full type inference on all model operations. + +## CRUD Operations + +All operations return `{ data, errors }`. You **SHOULD** check `errors` before using `data`. + +```typescript +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: + +```tsx +try { + const { data, errors } = await client.models.Todo.create({ content: 'New todo' }); + if (errors) { /* handle GraphQL field/validation errors */ } +} catch (err) { + /* handle network or unexpected errors */ +} +``` + +## Real-Time + +- **`observeQuery()`** — auto-updating list, returns `{ items }` snapshots. Recommended default. +- **`onCreate()` / `onUpdate()` / `onDelete()`** — per-event subscriptions. + +Both return an observable; call `.subscribe({ next })` and **MUST** call `sub.unsubscribe()` in cleanup. + +```tsx +useEffect(() => { + const sub = client.models.Todo.observeQuery().subscribe({ + next: ({ items }) => setTodos(items), + }); + return () => sub.unsubscribe(); +}, []); +``` + +## Server-Side (Next.js) + +```typescript +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({ 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()` from `aws-amplify/data`. +All CRUD, `observeQuery()`, and subscription APIs (`onCreate`, `onUpdate`, `onDelete`) are the same. + +## Pitfalls + +- **Subscription memory leaks:** `useEffect` **MUST** 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 `` generic:** `generateClient()` without `` + returns an untyped client — all operations lose autocomplete and type checking. +- **Server client without cookies:** Using `generateClient()` in Next.js + server components fails (no browser session) — you **MUST** use + `generateServerClientUsingCookies`. + +## Links + +- [Data Overview (React)](https://docs.amplify.aws/react/build-a-backend/data/) +- [Set Up Data (React)](https://docs.amplify.aws/react/build-a-backend/data/set-up-data/) +- [Connect to API (React)](https://docs.amplify.aws/react/frontend/data/connect-to-API/) +- [Data Client (React)](https://docs.amplify.aws/react/frontend/data/) +- [Data Overview (Next.js)](https://docs.amplify.aws/nextjs/build-a-backend/data/) +- [Set Up Data (Next.js)](https://docs.amplify.aws/nextjs/build-a-backend/data/set-up-data/) +- [Data Client (Next.js)](https://docs.amplify.aws/nextjs/frontend/data/) +- [Data Overview (React Native)](https://docs.amplify.aws/react-native/build-a-backend/data/) +- [Set Up Data (React Native)](https://docs.amplify.aws/react-native/build-a-backend/data/set-up-data/) +- [Data Client (React Native)](https://docs.amplify.aws/react-native/frontend/data/) diff --git a/aws-amplify/steering/deployment.md b/aws-amplify/steering/deployment.md new file mode 100644 index 0000000..13a82ca --- /dev/null +++ b/aws-amplify/steering/deployment.md @@ -0,0 +1,270 @@ +# Deployment + +## Prerequisites + +Before deploying, verify: + +- `npx ampx --version` returns a valid version +- `aws sts get-caller-identity` succeeds +- Node.js ≥ 18.x installed +- `.gitignore` includes `node_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 sandbox` generates it automatically +- **CI/CD:** `npx ampx pipeline-deploy` generates it during the build phase +- **Other frontend apps in a monorepo:** Use + `npx ampx generate outputs --app-id ` to generate it +- Project is a Gen2 project — see + [core-web.md](core-web.md) or + [core-mobile.md](core-mobile.md) for detection + logic (Gen2 uses `amplify/backend.ts` + `defineBackend()`) + +## Sandbox Deployment + +Deploy a personal development environment: + +```bash +AWS_REGION=us-east-1 npx ampx sandbox --once +``` + +You **MUST** use the `--once` flag in agent and CI environments — without +it, the command starts a file watcher that never exits. If prompted to +bootstrap, run `npx ampx sandbox --once` again after bootstrapping +completes. + +Verify `amplify_outputs.json` was generated in the project root. + +## CI/CD Setup + +### Create the Amplify App + +```bash +REPO="github.com//" +APP_ID=$(aws amplify create-app \ + --name my-app \ + --repository "$REPO" \ + --access-token "$(gh auth token)" \ + --query 'app.appId' --output text) +``` + +You **MUST** use `github.com/user/repo` format — **not** `https://`. + +### IAM Service Role + +Create a dedicated role for Amplify backend deployments: + +```bash +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 + +```bash +aws amplify create-branch --app-id "$APP_ID" --branch-name main +``` + +### amplify.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//browser` | + +**Wrong `baseDirectory` = blank page in production** (silent failure). +Always match the framework table above. + +```yaml +version: 1 +backend: + phases: + build: + commands: + - npm ci --cache .npm --prefer-offline + - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID +frontend: + phases: + build: + commands: + - npm run build + artifacts: + baseDirectory: dist # Change per framework (see table above) + files: + - '**/*' + cache: + paths: + - .npm/**/* + - node_modules/**/* +``` + +### Monorepo Configuration + +For monorepos, set `appRoot` in `amplify.yml` to the subdirectory +containing the Amplify app: + +```yaml +appRoot: packages/web +``` + +**WARNING:** `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 ` to get their + `amplify_outputs.json`. +- Run `npm ci` at the **repo root**, NOT inside `appRoot`. + +### Trigger Deployment + +```bash +aws amplify start-job --app-id "$APP_ID" --branch-name main --job-type RELEASE +``` + +## Secrets Management +**Sandbox:** Set secrets via CLI: + +```bash +echo "" | npx ampx sandbox secret set MY_API_KEY +``` + +You **MUST** pipe the value via stdin — without the pipe, the command +prompts interactively. + +This stores the secret for your personal sandbox environment. +**Branch environments (production):** Set secrets via the `ampx` CLI: + +```bash +npx ampx secret set MY_API_KEY --branch main --app-id $APP_ID +``` + +Or via the Amplify console under App settings → Environment variables, or +via the AWS CLI: + +```bash +aws amplify update-app --app-id "$APP_ID" \ + --environment-variables MY_API_KEY= +``` + +Reference secrets in functions using `secret()` — see +[functions-and-api.md](functions-and-api.md) for the pattern. + +## Multi-Environment + +Use branch-based environments — each Git branch deploys independently: + +```bash +# 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 RELEASE +``` + +Each 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: + +```bash +aws amplify create-domain-association \ + --app-id "$APP_ID" \ + --domain-name example.com \ + --sub-domain-settings '[ + {"prefix": "", "branchName": "main"}, + {"prefix": "staging", "branchName": "staging"} + ]' +``` + +Amplify auto-provisions an SSL certificate. You **MUST** add the +provided CNAME records to your DNS for verification. Check status: + +```bash +aws amplify get-domain-association --app-id "$APP_ID" --domain-name example.com +``` + +## Amplify 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://..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: + +```bash +git revert HEAD --no-edit +git push origin main +# Amplify auto-triggers a new build from the push +``` + +For CI/CD, manually trigger: `aws amplify start-job --app-id "$APP_ID" +--branch-name main --job-type RELEASE`. + +## Pitfalls + +- **Missing `--once` flag:** Without `--once`, sandbox starts a file + watcher that never exits — agent sessions and CI pipelines hang + indefinitely. **MUST** use `npx ampx sandbox --once` in any + non-interactive environment. +- **Repo format:** You **MUST** use `github.com/user/repo` — the + `https://` prefix causes `create-app` to fail silently. +- **Missing IAM service role:** Skipping role creation causes + `AccessDeniedException` on every backend deployment. +- **Wrong `baseDirectory`:** Using `build` for a Vite app (which outputs + to `dist`) causes a blank page in production — match the framework table + above. This is a silent failure with no error message. +- **Monorepo `appRoot` leading slash:** `appRoot: packages/web` vs + `appRoot: /packages/web` — leading slash breaks path resolution. +- **`amplify_outputs.json` not committed:** This file is gitignored and + generated at build time. CI uses `pipeline-deploy` to generate it; + local dev uses `sandbox`. +- **Not bootstrapping:** First sandbox run in a new account/region + requires CDK bootstrapping — follow prompts or run + `npx ampx sandbox --once` again after bootstrap. + +## Links + +- [Fullstack Branching](https://docs.amplify.aws/react/deploy-and-host/fullstack-branching/) +- [Secrets and Variables](https://docs.amplify.aws/react/deploy-and-host/fullstack-branching/secrets-and-vars/) +- [Mono and Multi-Repos](https://docs.amplify.aws/react/deploy-and-host/fullstack-branching/mono-and-multi-repos/) +- [Custom Pipelines](https://docs.amplify.aws/react/deploy-and-host/fullstack-branching/custom-pipelines/) +- [Sandbox Environments](https://docs.amplify.aws/react/deploy-and-host/sandbox-environments/) +- [Sandbox Setup](https://docs.amplify.aws/react/deploy-and-host/sandbox-environments/setup/) diff --git a/aws-amplify/steering/functions-and-api.md b/aws-amplify/steering/functions-and-api.md new file mode 100644 index 0000000..0e078a2 --- /dev/null +++ b/aws-amplify/steering/functions-and-api.md @@ -0,0 +1,243 @@ +# Functions & API + +## Lambda Functions + +Define a function in `amplify/functions//resource.ts`: + +```typescript +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//handler.ts`: + +```typescript +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`: + +```typescript +import { defineBackend } from '@aws-amplify/backend'; +import { auth } from './auth/resource'; +import { myFunc } from './functions/my-func/resource'; +defineBackend({ auth, myFunc }); +``` + +## Environment Variables & Secrets + +You **SHOULD** import environment variables from `$amplify/env/` +— 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()`: + +```typescript +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 "" | npx ampx sandbox secret set MY_API_KEY`. + +> **IMPORTANT:** The `ampx sandbox secret set` command is for **local/sandbox development only**. For apps deployed to **Amplify Hosting**, secrets **MUST** be created via the Hosting console or CLI — sandbox secrets are NOT available in hosted environments. See: https://docs.amplify.aws/react/deploy-and-host/fullstack-branching/secrets-and-vars/#set-secrets + +## Scheduled Functions + +Use `schedule` to invoke a function on a cron or natural-language schedule: + +```typescript +import { defineFunction } from '@aws-amplify/backend'; + +export const cronJob = defineFunction({ + name: 'cron-job', + entry: './handler.ts', + schedule: 'every 1h', // natural-language shorthand + // Valid shorthands: 'every 5m', 'every 1h', 'every 6h', 'every 1d' + // OR: schedule: '0 */1 * * ? *', // cron expression — same property +}); +``` + +The handler **MUST** use `EventBridgeHandler` type: + +```typescript +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: + +```typescript +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: + +```typescript +const schema = a.schema({ + Todo: a.model({ + content: a.string(), + }).authorization(allow => [allow.resource(myFunc)]), +}); +``` + +## Custom Queries and Mutations + +Use `a.query()` and `a.mutation()` with `.handler()` to add custom server-side logic through AppSync (no API Gateway needed): + +```typescript +// amplify/data/resource.ts +const schema = a.schema({ + // Custom query with Lambda handler + summarize: a.query() + .arguments({ text: a.string().required() }) + .returns(a.string()) + .handler(a.handler.function(summarizeHandler)) + .authorization(allow => [allow.authenticated()]), + + // Custom mutation with Lambda handler + processOrder: a.mutation() + .arguments({ orderId: a.string().required() }) + .returns(a.json()) + .handler(a.handler.function(processOrderHandler)) + .authorization(allow => [allow.authenticated()]), +}); +``` + +> **When to use which:** +> - `a.query()` / `a.mutation()` with `.handler()` — AppSync-native, type-safe, uses the data schema. **Preferred for most custom logic.** +> - API Gateway + Lambda — Use when you need REST endpoints, webhooks, or third-party integrations that require a specific URL. + +## REST API (API Gateway) + +Create a REST API using CDK in `amplify/backend.ts`: + +```typescript +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): + +```typescript +import type { APIGatewayProxyHandler } from 'aws-lambda'; +``` + +## HTTP API (API Gateway v2) + +For a lightweight HTTP API: + +```typescript +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`: + +```typescript +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: + +```typescript +const { data } = await client.queries.myCustomQuery({ input: 'value' }); +``` + +For REST/HTTP API outputs added via `backend.addOutput()`, read the endpoint URL from `amplify_outputs.json` and use standard HTTP clients. + +## Pitfalls + +- **`runtime` must be an integer:** Use `runtime: 22`, NOT + `runtime: "nodejs22.x"`. String format causes build errors. +- **Wrong handler type:** REST API (v1) requires `APIGatewayProxyHandler` + with `event.httpMethod`; HTTP API (v2) requires `APIGatewayProxyHandlerV2` + with `event.requestContext.http.method`. Mixing them causes malformed + responses. Both return `{ statusCode, body }`. +- **Missing resource access:** A function without explicit grants cannot + access auth, data, or storage resources — add grants in `backend.ts`. +- **Secrets in plain `environment`:** Sensitive values **MUST** use + `secret()`, not string literals. +- **`createStack` name collision:** Stack names passed to + `backend.createStack()` **MUST** be unique across the backend. + Duplicate names cause deployment failures. + +## Links + +- [Functions Overview](https://docs.amplify.aws/react/build-a-backend/functions/) +- [Set Up Function](https://docs.amplify.aws/react/build-a-backend/functions/set-up-function/) +- [Environment Variables and Secrets](https://docs.amplify.aws/react/build-a-backend/functions/environment-variables-and-secrets/) +- [Grant Access to Other Resources](https://docs.amplify.aws/react/build-a-backend/functions/grant-access-to-other-resources/) +- [Add custom queries and mutations](https://docs.amplify.aws/react/build-a-backend/data/custom-business-logic/) +- [Connect to Existing Data Sources](https://docs.amplify.aws/react/build-a-backend/data/connect-to-existing-data-sources/) diff --git a/aws-amplify/steering/phase1-backend.md b/aws-amplify/steering/phase1-backend.md deleted file mode 100644 index 72c21f0..0000000 --- a/aws-amplify/steering/phase1-backend.md +++ /dev/null @@ -1,49 +0,0 @@ -# Phase 1: Backend - -Create or modify Amplify Gen 2 backend resources. - ---- - -## Prerequisites Confirmed - -Prerequisites (Node.js, npm, AWS credentials) were already validated by the orchestrator workflow. Do not re-validate. - ---- - -## Critical Constraints - -- **Do NOT create frontend scaffolding or templates during this phase.** Do not run `create-next-app`, `create-react-app`, `create-vite`, `npm create`, or any frontend project generators. This phase is strictly for Amplify backend resources (the `amplify/` directory). If a frontend project already exists, leave it untouched. If no frontend project exists and the user only asked for backend work, do NOT create one. - -- Before creating any files, ensure `.gitignore` exists in the project root and includes: - `node_modules/`, `.env*`, `amplify_outputs.json`, `.amplify/`, `dist/`, `build/`. - Create or update it if these entries are missing. - ---- - -## Retrieve and Follow the SOP - -**Do NOT write any code until you have retrieved and read the SOP.** - -Use the SOP retrieval tool to get **"amplify-backend-implementation"** and follow it completely. - -### SOP Overrides - -- **Skip the SOP's Step 1** ("Verify Dependencies") — prerequisites were already validated by the orchestrator. -- **Skip the SOP's Step 12** ("Determine Next SOP Requirements") — phase sequencing is controlled by the orchestrator workflow, not the SOP. - -Follow all other SOP steps (2 through 11) completely. Do not improvise or skip them. - -### Error Handling - -1. If you encounter an error, fix the immediate issue -2. Return to the SOP and continue from where you left off -3. Do NOT abandon the SOP or start improvising -4. If you lose track, retrieve the SOP again, identify your last completed step, and continue - ---- - -## Phase Complete - -After the SOP is fully executed, summarize what was created (which resources, files, configurations). - -**STOP HERE.** Do NOT read any other steering files. Do NOT proceed to the next phase. The orchestrator workflow will handle what comes next. diff --git a/aws-amplify/steering/phase2-sandbox.md b/aws-amplify/steering/phase2-sandbox.md deleted file mode 100644 index 46f16a2..0000000 --- a/aws-amplify/steering/phase2-sandbox.md +++ /dev/null @@ -1,47 +0,0 @@ -# Phase 2: Sandbox Deployment - -Deploy the Amplify Gen 2 backend to a sandbox environment for testing. - ---- - -## Prerequisites Confirmed - -Prerequisites (Node.js, npm, AWS credentials) were already validated by the orchestrator workflow. Do not re-validate. - ---- - -## Retrieve and Follow the SOP - -Use the SOP retrieval tool to get **"amplify-deployment-guide"** and follow it completely. - -### SOP Overrides - -- **Skip the SOP's Step 1** ("Verify Dependencies") — prerequisites were already validated by the orchestrator. -- **deployment_type is `sandbox`** — do not ask the user for the deployment type. This phase is always a sandbox deployment. -- **app_name** — infer from the project's `package.json` or existing Amplify configuration. Only ask the user if it cannot be determined. - -### SOP Parameter Mapping - -The SOP uses `deployment_type` with values `sandbox` or `cicd`. For this phase: -- deployment_type: **sandbox** - -Follow all applicable SOP steps for sandbox deployment. Do not improvise or skip them. - -### Error Handling - -1. If you encounter an error, fix the immediate issue -2. Return to the SOP and continue from where you left off -3. Do NOT abandon the SOP or start improvising -4. If you lose track, retrieve the SOP again, identify your last completed step, and continue - ---- - -## Phase Complete - -After the SOP is fully executed: - -1. Confirm deployment succeeded -2. Verify `amplify_outputs.json` exists in the project root -3. Summarize the deployment results - -**STOP HERE.** Do NOT read any other steering files. Do NOT proceed to the next phase. The orchestrator workflow will handle what comes next. diff --git a/aws-amplify/steering/phase3-frontend.md b/aws-amplify/steering/phase3-frontend.md deleted file mode 100644 index 57da093..0000000 --- a/aws-amplify/steering/phase3-frontend.md +++ /dev/null @@ -1,63 +0,0 @@ -# Phase 3: Frontend Integration & Testing - -Connect the frontend application to the Amplify Gen 2 backend and verify everything works. - ---- - -## Prerequisites Confirmed - -Prerequisites (Node.js, npm, AWS credentials) were already validated by the orchestrator workflow. Do not re-validate. - -**Required:** `amplify_outputs.json` must exist in the project root. If it does not exist, inform the user that sandbox deployment (Phase 2) must be completed first. Do NOT proceed without it. - ---- - -## Retrieve and Follow the SOP - -**Do NOT write any code until you have retrieved and read the SOP.** - -Use the SOP retrieval tool to get **"amplify-frontend-integration"** and follow it completely. - -### SOP Overrides - -- **Skip the SOP's Step 12** ("Determine Next SOP Requirements") — phase sequencing is controlled by the orchestrator workflow, not the SOP. - -Follow all other SOP steps completely. Do not improvise or skip them. - -### Error Handling - -1. If you encounter an error, fix the immediate issue -2. Return to the SOP and continue from where you left off -3. Do NOT abandon the SOP or start improvising -4. If you lose track, retrieve the SOP again, identify your last completed step, and continue - ---- - -## Local Testing - -After the SOP is fully executed, present the testing instructions to the user: - -``` -## Time to test! - -### Start your dev server -[framework-specific command, e.g., npm run dev, npx next dev, etc.] - -### Try these features -[list all features that were implemented in this session] - -Let me know how it goes — or if anything needs changes! -``` - -**Wait for the user to test and respond.** - -- If the user reports issues, fix them within this phase. Use the SOP's troubleshooting section and documentation tools as needed. After fixing, ask the user to test again. -- If the user confirms everything works (or has no further changes), proceed to phase completion below. - ---- - -## Phase Complete - -Once the user confirms testing is successful (or has no changes needed), summarize the frontend integration and testing results. - -**STOP HERE.** Do NOT read any other steering files. Do NOT proceed to the next phase. The orchestrator workflow will handle what comes next. diff --git a/aws-amplify/steering/phase4-production.md b/aws-amplify/steering/phase4-production.md deleted file mode 100644 index a483d75..0000000 --- a/aws-amplify/steering/phase4-production.md +++ /dev/null @@ -1,55 +0,0 @@ -# Phase 4: Production Deployment - -Deploy the Amplify Gen 2 application to production. - ---- - -## Prerequisites Confirmed - -Prerequisites (Node.js, npm, AWS credentials) were already validated by the orchestrator workflow. Do not re-validate. - ---- - -## Retrieve and Follow the SOP - -Use the SOP retrieval tool to get **"amplify-deployment-guide"** and follow it completely. - -### SOP Overrides - -- **Skip the SOP's Step 1** ("Verify Dependencies") — prerequisites were already validated by the orchestrator. -- **deployment_type is `cicd`** — do not ask the user for the deployment type. This phase is always a production deployment. -- **app_name** — infer from the project's `package.json` or existing Amplify configuration. Only ask the user if it cannot be determined. - -### SOP Parameter Mapping - -The SOP uses `deployment_type` with values `sandbox` or `cicd`. For this phase: -- deployment_type: **cicd** - -Follow all applicable SOP steps for CI/CD deployment. Do not improvise or skip them. - -### Error Handling - -1. If you encounter an error, fix the immediate issue -2. Return to the SOP and continue from where you left off -3. Do NOT abandon the SOP or start improvising -4. If you lose track, retrieve the SOP again, identify your last completed step, and continue - ---- - -## Phase Complete - -After the SOP is fully executed, present to the user: - -``` -## You're live! - -### Production URL -[url from deployment output] - -### Amplify Console -https://console.aws.amazon.com/amplify/home - -Your app is now deployed! Future updates: just push to your repo and it auto-deploys. -``` - -This is the final phase. The workflow is complete. diff --git a/aws-amplify/steering/scaffolding.md b/aws-amplify/steering/scaffolding.md new file mode 100644 index 0000000..9f7e861 --- /dev/null +++ b/aws-amplify/steering/scaffolding.md @@ -0,0 +1,198 @@ +# Scaffolding + +## Web — Greenfield + +You **MUST** use official starter templates. You **MUST NOT** manually +scaffold the project structure — hand-crafted structures **MAY** break +Amplify Hosting deployment detection. + +### React (Vite) + +```bash +git clone https://github.com/aws-samples/amplify-vite-react-template.git my-app +cd my-app && rm -rf .git && git init +npm install +``` + +### Next.js + +App Router (default): + +```bash +git clone https://github.com/aws-samples/amplify-next-template.git my-app +cd my-app && rm -rf .git && git init +npm install +``` + +Pages Router: + +```bash +git clone https://github.com/aws-samples/amplify-next-pages-template.git my-app +cd my-app && rm -rf .git && git init +npm install +``` + +### Vue + +```bash +git clone https://github.com/aws-samples/amplify-vue-template.git my-app +cd my-app && rm -rf .git && git init +npm install +``` + +### Angular + +```bash +git clone https://github.com/aws-samples/amplify-angular-template.git my-app +cd my-app && rm -rf .git && git init +npm install +``` + +## Web — Brownfield + +For existing web projects, add Amplify Gen2 without overwriting application +code. You **SHOULD** use the create command for automatic setup: + +```bash +npm create amplify@latest -y +``` + +You **MUST** use the `-y` flag for non-interactive execution. This +scaffolds the `amplify/` directory and installs backend dependencies. + +For monorepos or custom build pipelines where the create command conflicts, +install manually: + +```bash +npm install --save-dev @aws-amplify/backend@latest @aws-amplify/backend-cli@latest typescript +``` + +Then create `amplify/backend.ts`: + +```typescript +import { defineBackend } from '@aws-amplify/backend'; +defineBackend({}); +``` + +Install the frontend library: + +```bash +npm install aws-amplify +``` + +## Web — React Native + +### Expo + +```bash +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 +``` + +### Bare CLI + +```bash +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 +npx --yes pod-install # iOS only +``` + +You **MUST** use the `-y` flag with `npm create amplify@latest` for +non-interactive execution. + +## Mobile — Flutter + +```bash +flutter create --platforms ios,android my_app +cd my_app +npm create amplify@latest -y +``` + +Add dependencies to `pubspec.yaml`: + +```yaml +dependencies: + amplify_flutter: ^2.0.0 + amplify_auth_cognito: ^2.0.0 +``` + +Then run `flutter pub get`. + +## Mobile — Swift (Apple platforms) + +You **MUST NOT** create the Xcode project from the CLI — assume an existing +Xcode project is open in Xcode. + +1. In the project root (where `.xcodeproj` lives), run: + `npm create amplify@latest -y` +2. Add the Swift package via Xcode: File → Add Package Dependencies → + `https://github.com/aws-amplify/amplify-swift` (Up to Next Major Version). +3. Add `amplify_outputs.json` to the Xcode project (drag into navigator, + check "Copy items if needed"). + +## Mobile — Android + +You **MUST NOT** create the Android project from the CLI — assume an +existing Android Studio project. + +1. In the project root, run: `npm create amplify@latest -y` +2. Add dependencies to `app/build.gradle.kts`: + +```kotlin +dependencies { + implementation("com.amplifyframework:core:2.+") + implementation("com.amplifyframework:aws-auth-cognito:2.+") +} +``` + +3. Copy `amplify_outputs.json` into `app/src/main/res/raw/`. + +## Generate amplify_outputs + +> For mobile projects, this step must be completed before the app can build. +> Run the sandbox before opening the mobile project. + +**WARNING:** After scaffolding, you **MUST** run `npx ampx sandbox --once` +(or `npx ampx sandbox` for local dev) **before** `npm run dev`. This +generates `amplify_outputs.json`, which the frontend imports at build time. +Without it, the app fails to compile because +`import outputs from '../amplify_outputs.json'` resolves to nothing. + +```bash +# After npm install: +npx ampx sandbox --once # generates amplify_outputs.json +npm run dev # NOW the app can compile +``` + +`amplify_outputs.json` is gitignored — see [deployment.md](deployment.md) for generation details. + +## Pitfalls + +- Using the wrong template for a web framework causes broken build configs. + Always match template to framework exactly. +- Forgetting `npm create amplify@latest -y` after the framework scaffold + is the most common mistake — without it, there is no `amplify/` directory. +- **Running `npm run dev` before `npx ampx sandbox`:** The app cannot + compile without `amplify_outputs.json` — always run sandbox first. +- React Native requires `@react-native-async-storage/async-storage` — the + Amplify SDK uses it for token persistence and will fail at runtime without it. +- For Android, `amplify_outputs.json` goes in `app/src/main/res/raw/` — see [core-mobile.md](core-mobile.md). + +## Links + +- [React Quickstart](https://docs.amplify.aws/react/start/quickstart/) +- [Next.js Quickstart](https://docs.amplify.aws/nextjs/start/quickstart/) +- [Vue Quickstart](https://docs.amplify.aws/vue/start/quickstart/) +- [Angular Quickstart](https://docs.amplify.aws/angular/start/quickstart/) +- [React Native Quickstart](https://docs.amplify.aws/react-native/start/quickstart/) +- [Flutter Quickstart](https://docs.amplify.aws/flutter/start/quickstart/) +- [Swift Quickstart](https://docs.amplify.aws/swift/start/quickstart/) +- [Android Quickstart](https://docs.amplify.aws/android/start/quickstart/) +- [Manual Installation](https://docs.amplify.aws/react/start/manual-installation/) +- [Account Setup](https://docs.amplify.aws/react/start/account-setup/) +- [Sandbox Environments](https://docs.amplify.aws/react/deploy-and-host/sandbox-environments/setup/) +- [CLI Commands](https://docs.amplify.aws/react/reference/cli-commands/) diff --git a/aws-amplify/steering/storage-backend.md b/aws-amplify/steering/storage-backend.md new file mode 100644 index 0000000..d48f3f9 --- /dev/null +++ b/aws-amplify/steering/storage-backend.md @@ -0,0 +1,123 @@ +# Storage — Backend + +## Basic Setup + +Define storage in `amplify/storage/resource.ts`: + +```typescript +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`: + +```typescript +import { defineBackend } from '@aws-amplify/backend'; +import { auth } from './auth/resource'; +import { storage } from './storage/resource'; +defineBackend({ auth, storage }); +``` + +## Access Rules + +Path patterns control who can access files. The `{entity_id}` placeholder +resolves to the authenticated user's identity ID at runtime — each user +gets an isolated directory. + +Actions: `'read'`, `'write'`, `'delete'` (granular: `'get'` and `'list'` +instead of `'read'`). Subjects: `allow.guest.to([...])`, +`allow.authenticated.to([...])`, `allow.groups(['Admins']).to([...])`, +`allow.entity('identity').to([...])`. Every rule **MUST** end with `.to()` +specifying the permitted actions — omitting `.to()` means NO permissions +are granted. + +**WARNING:** Storage access rules use `allow.guest` (PROPERTY, no +parentheses) and `allow.authenticated` (PROPERTY). Data authorization +rules use `allow.guest()` (METHOD, with parentheses). Mixing these up +causes TypeScript errors. + +**WARNING:** `{entity_id}` **MUST** be paired with +`allow.entity('identity')`. Using `{entity_id}` in a path without +`allow.entity('identity')` in that path's rules has no effect. + +Paths **MUST** end with `/*` to match all objects under that prefix. +Paths **MUST NOT** start with `/`. + +## Multiple Buckets + +```typescript +export const primaryStorage = defineStorage({ name: 'primaryFiles', isDefault: true, access: (allow) => ({ /* rules */ }) }); +export const secondaryStorage = defineStorage({ name: 'secondaryFiles', access: (allow) => ({ /* rules */ }) }); +``` + +You **MUST** set `isDefault: true` on exactly one bucket when defining +multiple. Each bucket **MUST** have a unique `name` property. The `name` +is what clients reference when targeting a non-default bucket. + +## Event Triggers + +```typescript +import { defineFunction, defineStorage } from '@aws-amplify/backend'; + +const onUploadHandler = defineFunction({ entry: './on-upload-handler.ts' }); + +export const storage = defineStorage({ + name: 'myFiles', + triggers: { onUpload: onUploadHandler, onDelete: onUploadHandler }, + access: (allow) => ({ 'public/*': [allow.authenticated.to(['read', 'write'])] }), +}); +``` + +The trigger handler receives an `S3Handler` event with bucket name and +object key. You **MUST** import the trigger function into `backend.ts`. + +Typed handler example: + +```ts +import type { S3Handler } from 'aws-lambda'; + +export const handler: S3Handler = async (event) => { + const objectKeys = event.Records.map((record) => record.s3.object.key); + console.log(`Upload handler invoked for objects [${objectKeys.join(', ')}]`); +}; +``` + +## Pitfalls + +- **Paths without `/*`:** A path like `'public'` matches nothing — you + **MUST** use `'public/*'` to match files under that prefix. +- **Missing `.to([])`:** Omitting `.to(['read', 'write'])` from an access + rule grants NO permissions — the rule is silently ignored. +- **Missing `{entity_id}`:** Using `'private/*'` instead of + `'private/{entity_id}/*'` exposes every user's private files to all + authenticated users. +- **Leading slash:** Paths **MUST NOT** start with `/` — use `'public/*'`, + not `'/public/*'`. +- **Forgetting `isDefault`:** With multiple buckets and no `isDefault: true`, + client operations fail because no default bucket is resolved. +- **`grantReadWrite()` path argument:** Do NOT pass a path argument to + `grantReadWrite(lambda)` — it operates on the whole bucket. There is no + per-path grant API. + +## Links + +- [Storage Overview](https://docs.amplify.aws/react/build-a-backend/storage/) +- [Set Up Storage](https://docs.amplify.aws/react/build-a-backend/storage/set-up-storage/) +- [Storage Authorization](https://docs.amplify.aws/react/build-a-backend/storage/authorization/) +- [Storage Event Triggers](https://docs.amplify.aws/react/build-a-backend/storage/lambda-triggers/) diff --git a/aws-amplify/steering/storage-mobile.md b/aws-amplify/steering/storage-mobile.md new file mode 100644 index 0000000..cfbe2e3 --- /dev/null +++ b/aws-amplify/steering/storage-mobile.md @@ -0,0 +1,167 @@ +# Storage — Mobile + +> **Backend required:** Storage must be defined in `amplify/storage/resource.ts` +> using `defineStorage` — see [storage-backend.md](storage-backend.md). + +## Flutter + +Imports: `amplify_flutter` + `amplify_storage_s3`. All paths wrapped with `StoragePath.fromString()`. + +| Operation | Call | +|---|---| +| Upload file | `Amplify.Storage.uploadFile(localFile: AWSFile.fromPath(path), path: const StoragePath.fromString('public/photo.jpg'))` | +| Download file | `Amplify.Storage.downloadFile(path: const StoragePath.fromString('public/photo.jpg'), localFile: localFile)` | +| List | `Amplify.Storage.list(path: const StoragePath.fromString('public/'))` → `.result.items` | +| Presigned URL | `Amplify.Storage.getUrl(path: const StoragePath.fromString('public/file.jpg'))` | +| Remove | `Amplify.Storage.remove(path: const StoragePath.fromString('public/file.jpg'))` | + +Upload progress — use the `onProgress` callback parameter: + +```dart +final op = Amplify.Storage.uploadFile( + localFile: AWSFile.fromPath('/path/to/file'), + path: const StoragePath.fromString('public/photos/photo.jpg'), + onProgress: (p) => print('fraction: ${p.fractionCompleted}'), +); +final result = await op.result; +``` + +**MUST** use `const` with `StoragePath.fromString()` for compile-time constant paths. + +## Swift (Apple platforms) + +> Supported: iOS 13+, macOS 12+, tvOS 13+, watchOS 9+, visionOS 1+ (preview). + +Uses `Amplify.Storage` with async/await. Import: `Amplify`. + +| Operation | Call | +|---|---| +| Upload data | `Amplify.Storage.uploadData(path: .fromString("public/file.txt"), data: data)` → `try await task.value` | +| Upload file | `Amplify.Storage.uploadFile(path: .fromString("public/file.txt"), local: fileUrl)` → `try await task.value` | +| Download data | `Amplify.Storage.downloadData(path: .fromString("public/file.txt"))` → `.value` returns `Data` | +| Download file | `Amplify.Storage.downloadFile(path: .fromString("public/path"), local: fileUrl)` → `try await task.value` | +| List | `try await Amplify.Storage.list(path: .fromString("public/"))` → `.items` | +| Presigned URL | `try await Amplify.Storage.getURL(path: .fromString("public/file.jpg"))` | +| Remove | `try await Amplify.Storage.remove(path: .fromString("public/file.jpg"))` | + +**Download with progress tracking:** + +```swift +let downloadTask = Amplify.Storage.downloadData( + path: .fromString("public/example.jpg") +) +Task { + for await progress in await downloadTask.progress { + print("Progress: \(progress.fractionCompleted)") + } +} +let data = try await downloadTask.value +``` + +**Upload with progress tracking:** + +```swift +let uploadTask = Amplify.Storage.uploadData( + path: .fromString("public/photo.jpg"), + data: imageData +) +Task { + for await progress in await uploadTask.progress { + print("Progress: \(progress)") + } +} +let result = try await uploadTask.value +``` + +Use SwiftUI's `PhotosPicker` (from `import PhotosUI`) to obtain image data, +then pass to `uploadData`. + +## Android (Kotlin) + +Android supports both callback-based and coroutine-based APIs. +Import: `com.amplifyframework.core.Amplify`, `com.amplifyframework.storage.StoragePath`. + +**Coroutine example (recommended):** + +```kotlin +private suspend fun uploadFile() { + val exampleFile = File(applicationContext.filesDir, "example") + exampleFile.writeText("Example file contents") + val upload = Amplify.Storage.uploadFile( + StoragePath.fromString("public/example"), exampleFile + ) + try { + val result = upload.result() + Log.i("MyAmplifyApp", "Successfully uploaded: ${result.path}") + } catch (error: StorageException) { + Log.e("MyAmplifyApp", "Upload failed", error) + } +} +``` + +```kotlin +private suspend fun downloadFile() { + val download = Amplify.Storage.downloadFile( + StoragePath.fromString("public/example"), localFile + ) + try { + val result = download.result() + Log.i("MyAmplifyApp", "Successfully downloaded: ${result.file.name}") + } catch (error: StorageException) { + Log.e("MyAmplifyApp", "Download failed", error) + } +} +``` + +| Operation (coroutine) | Call | +|---|---| +| Upload file | `Amplify.Storage.uploadFile(StoragePath.fromString("public/photo.jpg"), file)` → `.result()` | +| Upload stream | `Amplify.Storage.uploadInputStream(StoragePath.fromString("public/example"), stream)` → `.result()` | +| Download file | `Amplify.Storage.downloadFile(StoragePath.fromString("public/photo.jpg"), localFile)` → `.result()` | +| List | `Amplify.Storage.list(StoragePath.fromString("public/"))` → `.items` | +| Presigned URL | `Amplify.Storage.getUrl(StoragePath.fromString("public/file.jpg"))` → `.url` | +| Remove | `Amplify.Storage.remove(StoragePath.fromString("public/file.jpg"))` | + +**Callback alternative:** all operations also accept `onSuccess`/`onError` lambdas — e.g. +`Amplify.Storage.uploadFile(StoragePath.fromString("public/photo.jpg"), file, { result -> ... }, { error -> ... })`. + +## Permissions + +For authenticated user paths, use `protected/{entity_id}/` or `private/{entity_id}/` — the `{entity_id}` resolves to the user's Cognito identity ID at runtime. + +- **Android:** Verify `INTERNET` permission is declared in `AndroidManifest.xml` (usually present by default). If the app accesses the camera, add `CAMERA`; for gallery access, add `READ_MEDIA_IMAGES` (API 33+) or `READ_EXTERNAL_STORAGE` (older). +- **Apple (iOS/macOS):** No special permissions for S3 storage operations. If the app accesses the camera, add `NSCameraUsageDescription` in `Info.plist`. If the app accesses the photo library, add `NSPhotoLibraryUsageDescription`. +- **Flutter:** Follows Android/iOS rules above — add permissions in `AndroidManifest.xml` and `Info.plist` respectively. + +## Pitfalls + +- **Swift SDK uses `getURL` (capital URL), not `getUrl`:** Using the + wrong casing (lowercase `l`) causes compile errors. JS/web uses + `getUrl` (lowercase), but Swift uses `getURL`. +- **Wrong file wrapper per platform:** Flutter requires + `AWSFile.fromPath()`, Swift uses `Data` (for `uploadData`) or a file + URL (for `uploadFile`), Android uses `File`. Using the wrong type + causes compile errors — check the platform's expected input. +- **Missing `StoragePath.fromString()`:** Flutter and Android require + `StoragePath.fromString('path')` to wrap path strings. Passing a raw + string literal does not compile. +- **Large file uploads on mobile:** For files over 5 MB, the SDK + automatically uses multipart upload. You **SHOULD** implement + progress tracking (`onProgress` in Flutter, `for await progress in ...` + in Swift, `transferObserver` or progress callback in Android) to show + upload progress to the user. + +## Links + +- [Storage Overview (Android)](https://docs.amplify.aws/android/build-a-backend/storage/) +- [Set Up Storage (Android)](https://docs.amplify.aws/android/build-a-backend/storage/set-up-storage/) +- [Upload Files (Android)](https://docs.amplify.aws/android/frontend/storage/upload-files/) +- [Download Files (Android)](https://docs.amplify.aws/android/frontend/storage/download-files/) +- [Storage Overview (Swift)](https://docs.amplify.aws/swift/build-a-backend/storage/) +- [Set Up Storage (Swift)](https://docs.amplify.aws/swift/build-a-backend/storage/set-up-storage/) +- [Upload Files (Swift)](https://docs.amplify.aws/swift/frontend/storage/upload-files/) +- [Download Files (Swift)](https://docs.amplify.aws/swift/frontend/storage/download-files/) +- [Storage Overview (Flutter)](https://docs.amplify.aws/flutter/build-a-backend/storage/) +- [Set Up Storage (Flutter)](https://docs.amplify.aws/flutter/build-a-backend/storage/set-up-storage/) +- [Upload Files (Flutter)](https://docs.amplify.aws/flutter/frontend/storage/upload-files/) +- [Download Files (Flutter)](https://docs.amplify.aws/flutter/frontend/storage/download-files/) diff --git a/aws-amplify/steering/storage-web.md b/aws-amplify/steering/storage-web.md new file mode 100644 index 0000000..c2560dd --- /dev/null +++ b/aws-amplify/steering/storage-web.md @@ -0,0 +1,65 @@ +# Storage — Web + +> **Backend required:** Storage must be defined in `amplify/storage/resource.ts` +> using `defineStorage` — see [storage-backend.md](storage-backend.md). + +## API Reference + +All imports from `'aws-amplify/storage'`. + +| Operation | Call | +|---|---| +| Upload | `uploadData({ path: 'public/file.txt', data })` | +| Download blob | `(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 } })` | + +`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` — you **MUST** import **BOTH** CSS files or components render unstyled: + +```typescript +import '@aws-amplify/ui-react/styles.css'; +import '@aws-amplify/ui-react-storage/styles.css'; +``` + +**WARNING:** Missing either CSS import causes unstyled components. +Training data often omits the second import. + +| Component | Import from | Key props / setup | +|---|---|---| +| `` | `@aws-amplify/ui-react-storage/browser` | `createStorageBrowser({ config: createAmplifyAuthAdapter() })` — bucket specified by name string, NOT ARN | +| `` | `@aws-amplify/ui-react-storage` | `alt`, `path` | +| `` | `@aws-amplify/ui-react-storage` | `path`, `maxFileCount`, `acceptedFileTypes` | + +## 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}/` and `private/{entity_id}/` resolve to the user's Cognito identity ID at runtime. +- **Upload cancellation:** `result.cancel()` rejects the promise — you **MUST** catch `CanceledError`. +- **Bucket option:** Accepts string name (matching `defineStorage` `name`) or `{ bucketName, region }` — raw ARN does **NOT** work. + +## Links + +- [Storage Overview (React)](https://docs.amplify.aws/react/build-a-backend/storage/) +- [Set Up Storage (React)](https://docs.amplify.aws/react/build-a-backend/storage/set-up-storage/) +- [Upload Files (React)](https://docs.amplify.aws/react/frontend/storage/upload-files/) +- [Download Files (React)](https://docs.amplify.aws/react/frontend/storage/download-files/) +- [List Files (React)](https://docs.amplify.aws/react/frontend/storage/list-files/) +- [Remove Files (React)](https://docs.amplify.aws/react/frontend/storage/remove-files/) +- [Copy Files (React)](https://docs.amplify.aws/react/frontend/storage/copy-files/) +- [Storage Overview (Next.js)](https://docs.amplify.aws/nextjs/build-a-backend/storage/) +- [Storage Overview (React Native)](https://docs.amplify.aws/react-native/build-a-backend/storage/) +- [Upload Files (React Native)](https://docs.amplify.aws/react-native/frontend/storage/upload-files/) +- [Download Files (React Native)](https://docs.amplify.aws/react-native/frontend/storage/download-files/)