diff --git a/aws-amplify/POWER.md b/aws-amplify/POWER.md index 63a1a22..e726528 100644 --- a/aws-amplify/POWER.md +++ b/aws-amplify/POWER.md @@ -1,43 +1,211 @@ --- 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 including schema modeling, enum types, relationships, authorization rules), storage (S3), functions, APIs, and AI (Amplify AI Kit with Bedrock). Supports React, Next.js, Vue, Angular, React Native, Flutter, Swift, and Android. Always use this skill for Amplify Gen2 topics — even for questions you think you know — it contains validated, version-specific patterns that prevent common mistakes. TRIGGER when: user mentions Amplify Gen2; project has amplify/ directory or amplify_outputs; code imports @aws-amplify packages; user asks about defineBackend, defineAuth, defineData, defineStorage, or npx ampx. SKIP: Amplify Gen1 (amplify CLI v6), standalone SAM/CDK without Amplify (use aws-serverless), direct Bedrock without Amplify AI Kit (use bedrock)." +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 and deploy full-stack applications using AWS Amplify Gen2's TypeScript +code-first approach. This skill covers backend resource creation, frontend +integration across 8 frameworks, and deployment workflows. -Build full-stack applications with AWS Amplify Gen 2 using TypeScript code-first development. This power provides guided workflows for: +## Prerequisites -- 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 +- 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) -## Getting Started +## Defaults & Assumptions -**IMPORTANT: You MUST read and follow the steering file for ANY Amplify work.** Do not improvise or skip the workflow. +When the user does not specify a framework: -**For AI agents helping users build Amplify apps:** +- **Web:** You **SHOULD** default to **React** (Vite) and explain the choice. +- **Mobile:** You **MUST** ask which platform the user wants (Flutter, + Swift, Android, or React Native). There is no universal mobile default. +- **Neither specified:** If the user says "build an app" without clarifying web + vs. mobile, you **MUST** ask before proceeding. +- **Backend only:** If only backend changes are requested and no frontend + framework is mentioned, skip the frontend integration step entirely. -ALWAYS read the workflow steering file first: +When the user does not specify tooling or strategy: + +- **Package manager:** You **SHOULD** default to **npm** unless the user + specifies yarn or pnpm. +- **Language:** You **SHOULD** default to **TypeScript**. Gen2 backends are + TypeScript-only; frontends **SHOULD** follow the project's existing language. +- **Next.js:** You **SHOULD** default to **App Router** unless the user + specifies Pages Router. +- **React Native:** Ask the user whether they use **Expo** or **bare + React Native CLI**. +- **Auth:** You **MUST** ask which login method the user wants + (email/password, social login, SAML, passwordless, etc.). Do not assume a default. +- **Data authorization:** default to **`publicApiKey`** + (`allow.publicApiKey()`) — this is the starter template default. When + auth is added, switch to **owner-based** (`allow.owner()`) with + `defaultAuthorizationMode: 'userPool'`. + +## Quick Start — Route to the Right Reference + +### Step 0: Read Core Reference (ALWAYS) + +You **MUST** read the core reference for your target platform **before +reading any other reference file**. These contain Gen2 detection, +`Amplify.configure()` placement per framework, sandbox commands, required +packages, and directory structure rules — patterns needed for **all** tasks, +not just new projects. + +- **Web** (React, Next.js, Vue, Angular, React Native): You **MUST** read + [core-web.md](references/core-web.md) +- **Mobile** (Flutter, Swift, Android): You **MUST** read + [core-mobile.md](references/core-mobile.md) +- **Backend only** (no frontend work): Skip to Step 1. + +### Step 1: Identify the Task Type + +| Task | Go To | +| ---------------------------------------- | ------------------------------------------------------------------------ | +| **Create a new project** | → [scaffolding.md](references/scaffolding.md), then Step 2 and/or Step 3 | +| **Add or modify a backend feature** | → Step 2 (Backend Features) | +| **Connect frontend to existing backend** | → Step 3 (Frontend Integration) | +| **Deploy the application** | → [deployment.md](references/deployment.md) | + +### Step 2: Backend Features + +You **MUST** read the corresponding reference for each backend feature: + +| Feature | Reference | When to Use | +| ---------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| Authentication | [auth-backend.md](references/auth-backend.md) | Email/password, social login, MFA, SAML/OIDC | +| Data Models | [data-backend.md](references/data-backend.md) | GraphQL schema, DynamoDB, relationships, auth rules | +| File Storage | [storage-backend.md](references/storage-backend.md) | S3 uploads/downloads, access rules | +| Functions & API | [functions-and-api.md](references/functions-and-api.md) | Lambda, custom resolvers, REST/HTTP APIs, calling from client | +| AI Features | [ai.md](references/ai.md) | Conversation, generation, AI tools via Bedrock _(backend config + React/Next.js frontend)_ | +| Geo, PubSub, CDK | [advanced-features.md](references/advanced-features.md) | Backend-only: custom CDK stacks, overrides, custom outputs. Backend + frontend: Geo, PubSub, Face Liveness | + +Each backend feature file is self-contained. Load only what you need. + +> **Routing note:** These files apply for both **adding** and **modifying** +> features. Route to the same file whether the user says "add auth" or +> "change auth config" — each reference covers the full define surface. + +### Step 3: Frontend Integration + +After configuring backend resources, connect the frontend. Choose by +platform and feature: + +**Web** (React, Next.js, Vue, Angular, React Native): + +| Feature | Reference | +| ------------------------- | ------------------------------------------- | +| Auth UI & flows | [auth-web.md](references/auth-web.md) | +| Data CRUD & subscriptions | [data-web.md](references/data-web.md) | +| Storage upload/download | [storage-web.md](references/storage-web.md) | + +**Mobile** (Flutter, Swift, Android): + +| Feature | Reference | +| ------------------------- | ------------------------------------------------- | +| Auth UI & flows | [auth-mobile.md](references/auth-mobile.md) | +| Data CRUD & subscriptions | [data-mobile.md](references/data-mobile.md) | +| Storage upload/download | [storage-mobile.md](references/storage-mobile.md) | + +> **Note:** AI and Functions frontend patterns are included in +> [ai.md](references/ai.md) and +> [functions-and-api.md](references/functions-and-api.md) respectively — +> they are **not** split into separate web/mobile files. + +## Core Concepts + +### Amplify Gen2 Architecture + +- **Code-first:** All backend resources defined in TypeScript under `amplify/` +- **Main config:** `amplify/backend.ts` imports and combines all resources via + `defineBackend()` +- **Resource files:** `amplify/auth/resource.ts`, `amplify/data/resource.ts`, + `amplify/storage/resource.ts`, `amplify/functions//resource.ts` +- **Generated output:** `amplify_outputs.json` — consumed by frontend + `Amplify.configure()`. **Gitignored** — generated by `npx ampx sandbox` + (local dev) or `npx ampx pipeline-deploy` (CI/CD), never committed. + +### Directory Structure ``` -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 (gitignored) +└── 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 APIs + +| Package | Purpose | +| -------------------------- | ------------------------------------------------------------------------------ | +| `@aws-amplify/backend` | `defineAuth`, `defineData`, `defineStorage`, `defineFunction`, `defineBackend` | +| `aws-amplify` | Frontend: `Amplify.configure()`, `generateClient()`, auth/data/storage APIs | +| `@aws-amplify/ui-react` | Pre-built UI: ``, `` | +| `@aws-amplify/ui-react-ai` | AI UI: ``, `useAIConversation` | + +## Documentation & Resource Verification + +When you need AWS documentation (advanced CDK constructs, service limits, +provider-specific auth config): + +1. **If AWS documentation tools are available (e.g., via AWS MCP)**, you **SHOULD** + use them to search and retrieve relevant documentation pages. +2. **If AWS documentation tools are unavailable**, you **MUST** fall back to web + search or the `aws` CLI for resource verification. + +> **Why conditional:** Amplify Gen2 is code-first — the primary workflow is +> editing TypeScript files and running `npx ampx` commands. AWS MCP tools +> are useful for post-deployment verification but are **not** required. + +## Security Considerations + +- Use `secret()` for all credentials and API keys — never hardcode or use plain environment variables for sensitive values +- Review `allow.guest()` exposure carefully — guest access is enabled by default and grants unauthenticated users access to IAM-authorized resources +- Scope IAM policies to specific resource ARNs — avoid `resources: ['*']` in production +- Never log secrets or include them in error messages + +## Links -## When to Load Steering Files +> All documentation links use `react` as the default platform slug. Replace `/react/` in any URL with your target framework: -- Any Amplify Gen 2 work -> `amplify-workflow.md` +| Framework | Slug | +| ------------ | -------------- | +| React | `react` | +| Next.js | `nextjs` | +| Vue | `vue` | +| Angular | `angular` | +| React Native | `react-native` | +| Flutter | `flutter` | +| Swift | `swift` | +| Android | `android` | -**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. +- [Amplify Docs for LLMs](https://docs.amplify.aws/ai/llms.txt) +- [Amplify Docs](https://docs.amplify.aws/) +- [Gen2 Docs](https://docs.amplify.aws/react/) +- [Getting Started](https://docs.amplify.aws/react/start/) +- [Quickstart](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/) +- [Core Concepts](https://docs.amplify.aws/react/how-amplify-works/concepts/) +- [Build a Backend](https://docs.amplify.aws/react/build-a-backend/) +- [Deploy and Host](https://docs.amplify.aws/react/deploy-and-host/) +- [Troubleshooting](https://docs.amplify.aws/react/build-a-backend/troubleshooting/) +- [CLI Commands](https://docs.amplify.aws/react/reference/cli-commands/) +- [Amplify Outputs](https://docs.amplify.aws/react/reference/amplify_outputs/) +- [Project Structure](https://docs.amplify.aws/react/reference/project-structure/) +- [Amplify UI](https://ui.docs.amplify.aws/) diff --git a/aws-amplify/steering/advanced-features.md b/aws-amplify/steering/advanced-features.md new file mode 100644 index 0000000..33d8814 --- /dev/null +++ b/aws-amplify/steering/advanced-features.md @@ -0,0 +1,277 @@ +# 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 iam from 'aws-cdk-lib/aws-iam'; + +const pubsubStack = backend.createStack('PubSubStack'); + +backend.auth.resources.authenticatedUserIamRole.addToPrincipalPolicy( + new iam.PolicyStatement({ + actions: ['iot:Connect', 'iot:Publish', 'iot:Subscribe', 'iot:Receive'], + resources: [ + `arn:aws:iot:*:*:client/\${cognito-identity.amazonaws.com:sub}`, + `arn:aws:iot:*:*:topic/amplify/*`, + `arn:aws:iot:*:*:topicfilter/amplify/*`, + ], + }) +); + +backend.addOutput({ + custom: { iotEndpoint: 'your-iot-endpoint.iot.region.amazonaws.com' }, +}); +``` + +**Frontend** — subscribe and publish: + +```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: ['*'], // Rekognition session ARNs are generated at runtime — scope with conditions if needed + }) +); +``` + +**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 causes deployment failures. +- **Missing IAM permissions:** Geo, PubSub, and Face Liveness all require + explicit IAM policies — Amplify does not auto-grant access to these + services. +- **Geo CDK setup:** Geo (maps, place search, geofencing) requires CDK + constructs — there is no `defineGeo()` in Amplify Gen2. Use + `aws-cdk-lib/aws-location` directly as shown above. +- **PubSub endpoint:** You **MUST** configure the correct IoT endpoint for + your region; using the wrong endpoint type causes silent connection + failures. + +## Links + +- [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..98fbac4 --- /dev/null +++ b/aws-amplify/steering/ai.md @@ -0,0 +1,215 @@ +# 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. + +> **Security:** Conversation history sent to Amazon Bedrock may contain PII. Do not log full request/response payloads in production. Enable CloudWatch Logs encryption (KMS) and set appropriate retention policies for any logs that may capture inference data. + +### Backend Integration + +AI conversation and generation routes are part of your data schema. Import into `amplify/backend.ts`: + +```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/data'; +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..2bc9f46 --- /dev/null +++ b/aws-amplify/steering/auth-backend.md @@ -0,0 +1,236 @@ +# 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', + }, +}); +``` + +> **Security:** Guest access grants unauthenticated users IAM-authorized access. For production, explicitly evaluate whether guest access is needed and prefer `allow.authenticated()` as the default. If guest access is required, scope it to read-only on non-sensitive models only. + +To **disable** guest access, use a CDK override in `backend.ts`: + +```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..dd8d412 --- /dev/null +++ b/aws-amplify/steering/auth-mobile.md @@ -0,0 +1,502 @@ +# 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. To default to passkeys, see the platform-specific "Passwordless / user-choice flow" examples below. Custom OTP/passkey flows require additional challenge handling. + +### Flutter + +**Dependencies** — add to `pubspec.yaml`: + +```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 'amplify_outputs.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(amplifyOutputs); + } on Exception catch (e) { + safePrint('Error configuring Amplify: $e'); + } + } + + @override + Widget build(BuildContext context) { + return Authenticator( + child: MaterialApp( + builder: Authenticator.builder(), + home: const Scaffold( + body: Center(child: Text('You are logged in!')), + ), + ), + ); + } +} +``` + +### Swift (Apple platforms) + +> Supports iOS 13+, macOS 12+, tvOS 13+, watchOS 9+, visionOS 1+ (preview). +> Passkeys require iOS 17.4+, macOS 13.5+, or visionOS 1.0+. + +**Dependencies** — add both SPM packages in Xcode (**File > Add Packages…**): + +| Package | URL | Libraries | +| ------------------------------ | --------------------------------------------------------------- | --------------------------------- | +| Amplify Library for Swift | `https://github.com/aws-amplify/amplify-swift` | `Amplify`, `AWSCognitoAuthPlugin` | +| Amplify UI Swift Authenticator | `https://github.com/aws-amplify/amplify-ui-swift-authenticator` | `Authenticator` | + +> **SPM versioning:** For both packages, select **"Up to Next Major Version"** in Xcode's dependency rule. Do NOT pin to a specific branch (e.g., `main`) — use "Up to Next Major Version" to get compatible updates automatically. + +**Usage** — SwiftUI entry point: + +```swift +import Amplify +import Authenticator +import AWSCognitoAuthPlugin +import SwiftUI + +@main +struct MyApp: App { + init() { + do { + try Amplify.add(plugin: AWSCognitoAuthPlugin()) + try Amplify.configure(with: .amplifyOutputs) + } catch { + print("Unable to configure Amplify \(error)") + } + } + + var body: some Scene { + WindowGroup { + Authenticator { state in + VStack { + Text("Hello, \(state.user.username)") + Button("Sign out") { + Task { await state.signOut() } + } + } + } + } + } +} +``` + +**Passwordless / user-choice flow:** + +```swift +Authenticator(authenticationFlow: .userChoice( + preferredAuthFactor: .webAuthn +)) { state in + Text("Welcome \(state.user.username)!") +} +``` + +### Android (Kotlin) + +**Dependencies** — add to your app's `build.gradle.kts`: + +```kotlin +// Enable Jetpack Compose +android { + compileOptions { + isCoreLibraryDesugaringEnabled = true + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + buildFeatures { compose = true } + composeOptions { kotlinCompilerExtensionVersion = "1.5.3" } +} + +dependencies { + implementation("com.amplifyframework.ui:authenticator:1.4.0") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") +} +``` + +`INTERNET` permission is required in `AndroidManifest.xml`: + +```xml + +``` + +**Configure** — in your `Application.onCreate()`: + +```kotlin +try { + Amplify.addPlugin(AWSCognitoAuthPlugin()) + Amplify.configure(AmplifyOutputs(R.raw.amplify_outputs), applicationContext) +} catch (error: AmplifyException) { + Log.e("MyApp", "Could not initialize Amplify", error) +} +``` + +**Usage** — Jetpack Compose: + +```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.kotlin.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 +import com.amplifyframework.core.Amplify // Java facade for callback style + +Amplify.Auth.signIn("username", "password", + { result -> Log.i("Auth", "Signed in: ${result.isSignedIn}") }, + { error -> Log.e("Auth", "Sign in failed", error) } +) +``` + +**Confirm sign-in (coroutines):** + +```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..c21d8ba --- /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 outputs from '@/amplify_outputs.json'; + +const client = generateServerClientUsingCookies({ config: outputs, 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 outputs from '@/amplify_outputs.json'; + +export const { runWithAmplifyServerContext } = createServerRunner({ config: outputs }); +``` + +## React Native + +React Native uses the same `aws-amplify` auth APIs as web. All manual auth +flows (`signIn`, `signUp`, `confirmSignIn`, etc.) and session management +APIs work identically. + +### Setup + +**Import order matters:** `react-native-get-random-values` **MUST** be +the FIRST import in the entry file, `@aws-amplify/react-native` **MUST** +come before `aws-amplify`. See [core-web.md](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..34dacec --- /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(amplifyOutputs); +``` + +**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(amplifyOutputs); + 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..99dd0b2 --- /dev/null +++ b/aws-amplify/steering/core-web.md @@ -0,0 +1,155 @@ +# 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 '@aws-amplify/react-native'; // MUST come before aws-amplify +import { Amplify } from 'aws-amplify'; +import outputs from './amplify_outputs.json'; +Amplify.configure(outputs); +``` + +**React Native (Bare CLI)** — `index.js` (before `AppRegistry.registerComponent`): + +```typescript +import 'react-native-get-random-values'; // MUST be first +import '@aws-amplify/react-native'; // MUST come before aws-amplify +import { Amplify } from 'aws-amplify'; +import outputs from './amplify_outputs.json'; +Amplify.configure(outputs); +``` + +### Gen2 Detection (React Native) + +Same as web — check for `amplify/` directory with `backend.ts` and +`@aws-amplify/backend` in `package.json` devDependencies. + +### React Native Pitfalls + +- **Import order:** `react-native-get-random-values` **MUST** be the FIRST + import in the entry file, `@aws-amplify/react-native` **MUST** come before + `aws-amplify`. Reversing the order causes cryptographic failures at runtime. +- **Missing AsyncStorage:** Without + `@react-native-async-storage/async-storage`, auth tokens are not persisted + and users must re-authenticate on every app restart. + +## Pitfalls + +- Forgetting to import `amplify_outputs.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..1239a54 --- /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: 'userPool', + }, +}); +``` + +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()` or `.array()` on any field; +`.default(value)` on scalar fields only (not enums — see Pitfalls). + +## Authorization Rules + +Six strategies, applied per-model or per-field: + +**WARNING:** In data authorization rules, `allow.guest()` is a **method +call** (with parentheses). In storage access rules, `allow.guest` is a +**property** (no parentheses). Mixing these up causes TypeScript errors. + +```typescript +a.model({ /* fields */ }).authorization(allow => [ + allow.publicApiKey().to(['read']), // API key: public read + allow.guest().to(['read']), // Requires defaultAuthorizationMode: 'iam' + allow.owner(), // Creator has full CRUD + allow.authenticated().to(['read']), // Any signed-in user can read + allow.group('Admins'), // Named Cognito group + allow.custom(), // Lambda authorizer +]) +``` + +> **Security note:** `allow.guest()` and `allow.publicApiKey()` both permit unauthenticated access. Only use for intentionally public, non-sensitive data. Prefer `allow.authenticated()` or `allow.owner()` for sensitive resources. See [Amplify authorization best practices](https://docs.amplify.aws/react/build-a-backend/data/customize-authz/) and [Amazon Cognito Identity Pool security](https://docs.aws.amazon.com/cognito/latest/developerguide/identity-pools.html) for guidance on choosing the right authorization strategy. + +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..57e6b21 --- /dev/null +++ b/aws-amplify/steering/deployment.md @@ -0,0 +1,280 @@ +# 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 +npx ampx sandbox secret set MY_API_KEY +``` + +> **Security:** Avoid passing secret values as CLI arguments or via `echo` — these appear in shell history and `/proc`. Instead, use `npx ampx sandbox secret set MY_SECRET` which prompts for input interactively, or pipe from a secure source: `aws ssm get-parameter --name /path/to/secret --with-decryption --query Parameter.Value --output text | npx ampx sandbox secret set MY_SECRET --from-stdin` + +This stores the secret for your personal sandbox environment. +**Branch environments (production):** Set secrets via the `ampx` CLI: + +```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= +``` + +> **Important:** `--environment-variables` stores values as **plain text**. +> For sensitive values (API keys, tokens), use `npx ampx sandbox secret set` +> (sandbox) or `npx ampx secret set --branch` (production) which stores in +> SSM SecureString. +> +> **Note:** Under the hood, Amplify Gen2 `secret()` references are backed by AWS Systems Manager Parameter Store (SecureString parameters). Review access policies on the `/amplify/` parameter path in your account to ensure only authorized roles can read production secrets. + +Reference secrets in functions using `secret()` — see +[functions-and-api.md](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..6b1c855 --- /dev/null +++ b/aws-amplify/steering/functions-and-api.md @@ -0,0 +1,244 @@ +# 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.summarize({ text: '...' }); +``` + +For REST/HTTP API outputs added via `backend.addOutput()`, read the endpoint URL from `amplify_outputs.json` and use standard HTTP clients. + +## Pitfalls + +- **`runtime` must be an integer:** Use `runtime: 22`, NOT + `runtime: "nodejs22.x"`. String format causes build errors. +- **Wrong handler type:** REST API (v1) requires `APIGatewayProxyHandler` + with `event.httpMethod`; HTTP API (v2) requires `APIGatewayProxyHandlerV2` + with `event.requestContext.http.method`. Mixing them causes malformed + responses. Both return `{ statusCode, body }`. +- **Missing resource access:** A function without explicit grants cannot + access auth, data, or storage resources — add grants in `backend.ts`. +- **Secrets in plain `environment`:** Sensitive values **MUST** use + `secret()`, not string literals. +- **`createStack` name collision:** Stack names passed to + `backend.createStack()` **MUST** be unique across the backend. + Duplicate names cause deployment failures. + +## Links + +- [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..d8ad3b9 --- /dev/null +++ b/aws-amplify/steering/scaffolding.md @@ -0,0 +1,201 @@ +# 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 react-native-get-random-values +``` + +### 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 react-native-get-random-values +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 + +# Flutter requires the Dart output format (see core-mobile.md): +npx ampx sandbox --once --outputs-format dart --outputs-out-dir lib +``` + +`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..c432b1d --- /dev/null +++ b/aws-amplify/steering/storage-mobile.md @@ -0,0 +1,169 @@ +# 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'))` | + +> **Security:** Amplify Gen2 enables S3 server-side encryption (SSE-S3) by default. All transfers use HTTPS (TLS in transit). For sensitive data, configure SSE-KMS with a customer-managed key via CDK overrides. + +Upload progress — use the `onProgress` callback parameter: + +```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..baf3e02 --- /dev/null +++ b/aws-amplify/steering/storage-web.md @@ -0,0 +1,67 @@ +# 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 } })` | + +> **Security:** Amplify Gen2 enables S3 server-side encryption (SSE-S3) by default. For sensitive data, consider configuring SSE-KMS with a customer-managed key via CDK overrides. Amplify also enforces HTTPS-only access to S3 buckets by default; if using custom bucket configurations, add a bucket policy with `"aws:SecureTransport": "false"` → Deny to ensure encryption in transit. + +`uploadData` returns a control object: `.pause()`, `.resume()`, `.cancel()`, `.result` (Promise). Progress: `options.onProgress: ({ transferredBytes, totalBytes }) => …`. + +Custom bucket: `options: { bucket: 'nameFromDefineStorage' }` or `{ bucket: { bucketName, region } }`. Raw ARN does **NOT** work. + +## React UI Components + +`npm add @aws-amplify/ui-react-storage` — 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:** `uploadData` returns a task with `.cancel()` — call `task.cancel()`, not `result.cancel()`. Await `task.result` for the final outcome and 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/)