AWS Amplify: Full-Stack Serverless App Development (2026)

AWS Amplify is a complete development platform that lets frontend and mobile developers build full-stack applications backed by AWS services without becoming infrastructure experts. It wraps Cognito, AppSync, S3, Lambda, and API Gateway behind simple CLI commands and TypeScript constructs, so you spend more time on product logic and less time writing CloudFormation. With the arrival of Amplify Gen 2 in 2024 — built on CDK and fully TypeScript-first — the platform matured into something teams deploying serious production workloads can rely on. This guide covers the entire stack: CLI workflows, Gen 2 backend definitions, authentication, GraphQL APIs, file storage, and CI/CD hosting.

Amplify Platform Overview

AWS Amplify is not a single product — it is a suite of tools that work together. Understanding what each piece does prevents confusion when the documentation sends you in five different directions at once.

  • Amplify CLI — a command-line tool that provisions and manages backend resources. You run amplify add auth and it generates CloudFormation stacks for Cognito. You run amplify push and it deploys them. Gen 1 used the CLI heavily; Gen 2 replaces most of this with TypeScript files.
  • Amplify Libraries — JavaScript, iOS, Android, and Flutter client libraries that talk to your backend. They wrap the AWS SDK with simpler APIs: signIn() instead of raw Cognito, uploadData() instead of S3 put-object calls.
  • Amplify Studio — a web-based visual interface for modelling your data schema, managing content, and generating React UI components directly from Figma designs.
  • Amplify Hosting — a fully managed CI/CD and CDN service for deploying web apps. Connect a GitHub repo, configure build settings, and every push to main triggers a deployment through CloudFront.
  • Amplify Gen 2 — the current recommended approach (released GA in 2024). Backend infrastructure is defined as TypeScript code in an amplify/ directory, version-controlled alongside your app, and deployed via CDK under the hood.
Gen 1 vs Gen 2: If you are starting a new project in 2026, use Gen 2. Gen 1 relies on the CLI and JSON configuration files that are hard to reason about in code reviews. Gen 2 puts everything in TypeScript, supports per-developer cloud sandboxes, and integrates cleanly with modern monorepos.

Amplify CLI: Init and Core Commands

Even in the Gen 2 world you still interact with the CLI for certain workflows. Install it globally and configure it against your AWS account once.

# Install Amplify CLI
npm install -g @aws-amplify/cli

# Configure with your AWS credentials (runs an interactive wizard)
amplify configure

# In a new project directory, initialise Amplify
amplify init

# Add authentication (Cognito)
amplify add auth

# Add a GraphQL API (AppSync)
amplify add api

# Add file storage (S3)
amplify add storage

# Deploy all pending changes to the cloud
amplify push

# Open Amplify Studio in your browser
amplify studio

# Pull latest backend config into your local environment
amplify pull --appId <app-id> --envName staging

# Remove a category
amplify remove auth

After amplify push completes, the CLI writes src/aws-exports.js (or amplifyconfiguration.json in Gen 2). This file contains all the endpoint URLs, pool IDs, and bucket names your frontend needs. You import it once at app startup and never hardcode AWS resource IDs again.

amplify.yml — Build Specification

When you use Amplify Hosting, every branch needs a build spec. The default file is amplify.yml in the repo root. Here is a production-ready example for a React app with a Gen 2 backend:

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:
    preBuild:
      commands:
        - npm ci --cache .npm --prefer-offline
    build:
      commands:
        - npm run build
  artifacts:
    baseDirectory: dist
    files:
      - "**/*"
  cache:
    paths:
      - .npm/**/*
      - node_modules/**/*

The npx ampx pipeline-deploy command is the Gen 2 equivalent of amplify push — it deploys your TypeScript backend definition from the CI environment. The $AWS_BRANCH and $AWS_APP_ID variables are injected automatically by Amplify Hosting.

Amplify Gen 2: TypeScript-First Backend

In Gen 2, everything that used to live in JSON configuration files or CLI wizard answers now lives in TypeScript. Your amplify/ directory is just code — reviewable, testable, refactorable.

my-app/
├── amplify/
│   ├── auth/
│   │   └── resource.ts       ← Cognito user pool definition
│   ├── data/
│   │   └── resource.ts       ← AppSync schema + resolvers
│   ├── storage/
│   │   └── resource.ts       ← S3 bucket definition
│   ├── functions/
│   │   └── myLambda/
│   │       ├── handler.ts
│   │       └── resource.ts
│   └── backend.ts            ← Root: wires everything together
├── src/
└── package.json

The root backend.ts file imports and combines all your resources:

// amplify/backend.ts
import { defineBackend } from "@aws-amplify/backend";
import { auth } from "./auth/resource";
import { data } from "./data/resource";
import { storage } from "./storage/resource";
import { myLambda } from "./functions/myLambda/resource";

defineBackend({
  auth,
  data,
  storage,
  myLambda,
});

To run your own cloud sandbox (isolated from staging/production) during development:

# Start a personal cloud sandbox — deploys your amplify/ directory to a temporary stack
npx ampx sandbox

# Watch mode: redeploys on every file save
npx ampx sandbox --watch

Each developer gets their own stack with a unique name. When the sandbox is torn down, all resources are deleted. No more "who changed the shared dev environment" incidents.

Authentication with Cognito

Amplify authentication is backed by Amazon Cognito. You configure the user pool in amplify/auth/resource.ts and the Amplify library handles sign-up, sign-in, MFA prompts, and token refresh on the client side.

// amplify/auth/resource.ts
import { defineAuth } from "@aws-amplify/backend";

export const auth = defineAuth({
  loginWith: {
    email: true,
    // Add phone number login:
    // phone: true,

    // Social login via Cognito hosted UI:
    externalProviders: {
      google: {
        clientId: secret("GOOGLE_CLIENT_ID"),
        clientSecret: secret("GOOGLE_CLIENT_SECRET"),
        scopes: ["email", "profile", "openid"],
        attributeMapping: {
          email: "email",
          fullname: "name",
        },
      },
      facebook: {
        clientId: secret("FACEBOOK_CLIENT_ID"),
        clientSecret: secret("FACEBOOK_CLIENT_SECRET"),
        scopes: ["email", "public_profile"],
      },
      callbackUrls: [
        "http://localhost:3000/",
        "https://yourdomain.com/",
      ],
      logoutUrls: [
        "http://localhost:3000/",
        "https://yourdomain.com/",
      ],
    },
  },
  multifactor: {
    mode: "OPTIONAL",        // "OFF" | "OPTIONAL" | "REQUIRED"
    totp: true,              // TOTP (Google Authenticator)
    sms: true,               // SMS OTP
  },
  userAttributes: {
    preferredUsername: {
      mutable: true,
      required: false,
    },
  },
  groups: ["admin", "editor", "viewer"],
});

The secret() helper stores sensitive values in AWS Secrets Manager and injects them at deploy time — your client IDs never appear in source control.

React Authentication Component

The Amplify UI library ships a pre-built <Authenticator> component that renders the entire sign-up/sign-in/MFA flow. Drop it around your app and authentication is done:

// src/main.tsx
import { Amplify } from "aws-amplify";
import { Authenticator } from "@aws-amplify/ui-react";
import "@aws-amplify/ui-react/styles.css";
import outputs from "../amplify_outputs.json";

Amplify.configure(outputs);

export default function App() {
  return (
    <Authenticator
      loginMechanisms={["email"]}
      socialProviders={["google", "facebook"]}
      signUpAttributes={["name"]}
    >
      {({ signOut, user }) => (
        <main>
          <h1>Hello, {user?.signInDetails?.loginId}</h1>
          <button onClick={signOut}>Sign Out</button>
          <MyProtectedContent />
        </main>
      )}
    </Authenticator>
  );
}

For programmatic flows — custom UI, checking auth state in a hook — use the signIn, signUp, confirmSignUp, and getCurrentUser functions from aws-amplify/auth:

import { signIn, signOut, getCurrentUser, fetchAuthSession } from "aws-amplify/auth";

// Sign in
const { isSignedIn, nextStep } = await signIn({
  username: "user@example.com",
  password: "P@ssw0rd!",
});

// Check if MFA is required
if (nextStep.signInStep === "CONFIRM_SIGN_IN_WITH_TOTP_CODE") {
  const code = await promptUserForTOTP();
  await confirmSignIn({ challengeResponse: code });
}

// Get the current JWT tokens (access + id + refresh)
const session = await fetchAuthSession();
const idToken = session.tokens?.idToken?.toString();

GraphQL API with AppSync

Amplify's GraphQL API is powered by AWS AppSync. In Gen 2 you define your schema in TypeScript using the a schema builder, which generates the AppSync schema, DynamoDB tables, resolvers, and IAM policies from a single file.

// amplify/data/resource.ts
import { type ClientSchema, a, defineData } from "@aws-amplify/backend";

const schema = a.schema({
  Blog: a
    .model({
      title: a.string().required(),
      slug: a.string().required(),
      content: a.string(),
      publishedAt: a.datetime(),
      authorId: a.string(),
      tags: a.string().array(),
    })
    .authorization((allow) => [
      allow.owner(),                      // author can CRUD their own posts
      allow.authenticated().to(["read"]), // logged-in users can read
      allow.guest().to(["read"]),         // unauthenticated users can read
    ]),

  Comment: a
    .model({
      content: a.string().required(),
      blogId: a.id().required(),
      blog: a.belongsTo("Blog", "blogId"),
    })
    .authorization((allow) => [
      allow.owner(),
      allow.authenticated().to(["read"]),
    ]),
});

// Make schema type available to client code
export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: "userPool",
    apiKeyAuthorizationMode: {
      expiresInDays: 30,  // used for guest (unauthenticated) access
    },
  },
});

On the client side, Amplify generates a fully typed API client from your schema. No more manually writing GraphQL query strings:

// src/api.ts
import { generateClient } from "aws-amplify/data";
import type { Schema } from "../amplify/data/resource";

const client = generateClient<Schema>();

// Create a blog post
const { data: post, errors } = await client.models.Blog.create({
  title: "Getting Started with Amplify Gen 2",
  slug: "amplify-gen2-guide",
  content: "Full content here...",
  publishedAt: new Date().toISOString(),
  tags: ["aws", "amplify", "serverless"],
});

// List all posts with real-time subscription
const sub = client.models.Blog.observeQuery({
  filter: { tags: { contains: "aws" } },
}).subscribe({
  next: ({ items, isSynced }) => {
    setPosts(items);
    setLoading(!isSynced);
  },
});
@model directive in Gen 1: If you are maintaining a Gen 1 project, the same capabilities are expressed in a GraphQL SDL file using directives: @model generates DynamoDB + resolvers, @auth controls access, @hasMany/@belongsTo wire up relationships. The Gen 2 TypeScript API is cleaner but the underlying AppSync behaviour is the same.

S3 File Storage

Amplify Storage puts S3 file operations behind an access-level model: guest (public read), protected (owner upload, public read by ID), and private (owner only). Define the bucket in amplify/storage/resource.ts:

// amplify/storage/resource.ts
import { defineStorage } from "@aws-amplify/backend";

export const storage = defineStorage({
  name: "myAppBucket",
  access: (allow) => ({
    // Public profile pictures anyone can read
    "profile-pictures/{entity_id}/*": [
      allow.guest.to(["read"]),
      allow.entity("identity").to(["read", "write", "delete"]),
    ],
    // Private documents only the owner can access
    "private/{entity_id}/*": [
      allow.entity("identity").to(["read", "write", "delete"]),
    ],
    // Shared uploads — authenticated users can read, owner can write
    "shared/*": [
      allow.authenticated.to(["read"]),
      allow.entity("identity").to(["write", "delete"]),
    ],
  }),
});

On the client, use the uploadData, downloadData, list, and remove functions from aws-amplify/storage:

import {
  uploadData,
  downloadData,
  list,
  remove,
  getUrl,
} from "aws-amplify/storage";

// Upload a file from a browser input element
async function handleFileUpload(file: File) {
  const result = await uploadData({
    path: `profile-pictures/${userId}/${file.name}`,
    data: file,
    options: {
      contentType: file.type,
      onProgress: ({ transferredBytes, totalBytes }) => {
        if (totalBytes) {
          const pct = Math.round((transferredBytes / totalBytes) * 100);
          setUploadProgress(pct);
        }
      },
    },
  }).result;
  console.log("Uploaded to:", result.path);
}

// Get a pre-signed URL valid for 1 hour
const { url } = await getUrl({
  path: `profile-pictures/${userId}/avatar.jpg`,
  options: { expiresIn: 3600 },
});

// List all files in a prefix
const { items } = await list({ path: `shared/` });
items.forEach((item) => console.log(item.path, item.size));

// Delete a file
await remove({ path: `private/${userId}/old-doc.pdf` });
Large file uploads: uploadData automatically uses S3 multipart upload for files larger than 5 MB. You do not need to write multipart logic yourself. For resumable uploads on mobile or flaky connections, the Amplify library handles retry and resume transparently.

Amplify Hosting: CI/CD and SSR

Amplify Hosting provides a GitHub/GitLab/Bitbucket-connected pipeline that builds and deploys your app on every push. The CDN layer is CloudFront so your app gets global edge distribution automatically.

Key Hosting Features

  • Branch-based deployments — every branch gets its own URL (feature-login.abc123.amplifyapp.com). Merge to main deploys to production.
  • Pull request previews — enable in the Amplify console and every PR gets a unique preview URL posted back to GitHub. Reviewers can click through to a live deployment before approving.
  • Custom domains — point your Route 53 domain (or an external registrar) at your Amplify app. SSL is provisioned via ACM automatically.
  • SSR support — Next.js App Router (including server components and server actions), Nuxt, Astro, and Angular SSR are all supported. Amplify provisions Lambda@Edge functions to render server-side pages.
  • Environment variables — set per-branch environment variables in the console or via the CLI. They are injected at build time and (for SSR) at runtime.
  • Redirects and rewrites — configure SPA fallback (/*/index.html with 200), API proxies, and permanent redirects from the console or in amplify.yml.

Connecting a GitHub Repo

From the Amplify console: choose "Host web app" → select GitHub → authorise the Amplify GitHub App → choose your repo and branch → review the auto-detected build settings → deploy. The first build runs immediately and subsequent pushes trigger automatic rebuilds.

Custom Domain Configuration

In the console go to App settings → Custom domains → Add domain. Enter your apex domain. Amplify will show you the CNAME records to add at your registrar. For Route 53 domains, Amplify can create the records automatically. ACM certificate validation happens within a few minutes for DNS-validated certs.

SSR with Next.js

Amplify Hosting detects Next.js automatically from next.config.js. No special config is needed for App Router with server components. For the Amplify data client to work in server components, use the server-side auth utilities:

// app/posts/page.tsx (Next.js App Router server component)
import { cookies } from "next/headers";
import { generateServerClientUsingCookies } from "@aws-amplify/adapter-nextjs/data";
import outputs from "@/amplify_outputs.json";
import type { Schema } from "@/amplify/data/resource";

export default async function PostsPage() {
  const client = generateServerClientUsingCookies<Schema>({
    config: outputs,
    cookies,
  });

  const { data: posts } = await client.models.Blog.list({
    filter: { publishedAt: { attributeExists: true } },
    limit: 20,
  });

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <a href={`/posts/${post.slug}`}>{post.title}</a>
        </li>
      ))}
    </ul>
  );
}

Amplify Studio: Visual Data Modelling and Figma-to-Code

Amplify Studio is a browser-based UI that sits on top of your Amplify backend. You access it via amplify studio or directly from the Amplify console. It has two main features teams find genuinely useful:

Visual Data Modelling

Studio's data modeller lets you create models and relationships by dragging boxes and drawing lines between them — no TypeScript required. This is useful for product managers or designers who need to understand the data structure but are not comfortable editing resource.ts files. Once you click "Save and deploy," Studio generates the TypeScript code and pushes the changes.

Figma-to-Code (UI Kit)

If your design team works in Figma, Studio can import your Figma file and generate React components that match your design. The workflow:

  1. Install the Amplify UI Kit in Figma (available in the Figma Community).
  2. In Studio, go to UI Library → Sync with Figma → paste your Figma file URL.
  3. Studio shows each component with a preview and lets you bind data props to your Amplify data models.
  4. Click "Get component code" and copy the generated React component into your project.

The generated components use the Amplify UI React library primitives, so they inherit your theme tokens and dark/light mode support automatically.

Content Management

Studio's Content tab is a lightweight CMS layer over your DynamoDB tables. Non-developers can create, edit, and delete records without writing code or accessing the AWS console. You can configure which fields are shown and set validation rules.

React and Next.js Integration Examples

Here is a complete pattern for a React app that ties auth, data, and storage together. This covers the setup you need for 90% of CRUD applications.

// src/main.tsx — app entry point
import React from "react";
import ReactDOM from "react-dom/client";
import { Amplify } from "aws-amplify";
import outputs from "../amplify_outputs.json";
import App from "./App";

// Configure Amplify once at startup
Amplify.configure(outputs);

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
// src/hooks/usePosts.ts — data fetching hook with real-time updates
import { useEffect, useState } from "react";
import { generateClient } from "aws-amplify/data";
import type { Schema } from "../../amplify/data/resource";

const client = generateClient<Schema>();

export function usePosts() {
  const [posts, setPosts] = useState<Schema["Blog"]["type"][]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const sub = client.models.Blog.observeQuery().subscribe({
      next: ({ items, isSynced }) => {
        setPosts(items);
        setLoading(!isSynced);
      },
      error: (err) => console.error("Subscription error:", err),
    });
    return () => sub.unsubscribe();
  }, []);

  async function createPost(title: string, content: string) {
    return client.models.Blog.create({
      title,
      slug: title.toLowerCase().replace(/\s+/g, "-"),
      content,
      publishedAt: new Date().toISOString(),
    });
  }

  async function deletePost(id: string) {
    return client.models.Blog.delete({ id });
  }

  return { posts, loading, createPost, deletePost };
}

File Upload Component

// src/components/FileUploader.tsx
import { useState } from "react";
import { uploadData, getUrl } from "aws-amplify/storage";
import { getCurrentUser } from "aws-amplify/auth";

export function FileUploader() {
  const [progress, setProgress] = useState(0);
  const [fileUrl, setFileUrl] = useState("");

  async function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0];
    if (!file) return;

    const { userId } = await getCurrentUser();
    const path = `private/${userId}/${Date.now()}-${file.name}`;

    try {
      await uploadData({
        path,
        data: file,
        options: {
          contentType: file.type,
          onProgress: ({ transferredBytes, totalBytes }) => {
            if (totalBytes) {
              setProgress(Math.round((transferredBytes / totalBytes) * 100));
            }
          },
        },
      }).result;

      const { url } = await getUrl({ path, options: { expiresIn: 3600 } });
      setFileUrl(url.toString());
    } catch (err) {
      console.error("Upload failed:", err);
    }
  }

  return (
    <div>
      <input type="file" onChange={handleChange} />
      {progress > 0 && progress < 100 && (
        <progress value={progress} max={100}>{progress}%</progress>
      )}
      {fileUrl && <a href={fileUrl} target="_blank">View uploaded file</a>}
    </div>
  );
}

amplify_outputs.json (auto-generated)

After npx ampx sandbox or a CI deployment, Amplify writes this file automatically. Never edit it by hand — it is regenerated on every deploy. Your .gitignore should include it in most cases (or commit a production version for SSR deployments).

{
  "version": "1",
  "auth": {
    "user_pool_id": "us-east-1_XXXXXXXXX",
    "aws_region": "us-east-1",
    "user_pool_client_id": "XXXXXXXXXXXXXXXXXXXXXXXX",
    "identity_pool_id": "us-east-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "oauth": {
      "domain": "myapp-dev.auth.us-east-1.amazoncognito.com",
      "redirect_sign_in_uri": ["http://localhost:3000/"],
      "redirect_sign_out_uri": ["http://localhost:3000/"],
      "response_type": "code",
      "scopes": ["email", "openid", "profile"]
    },
    "mfa_configuration": "OPTIONAL",
    "mfa_methods": ["TOTP", "SMS"],
    "standard_required_attributes": ["email"]
  },
  "data": {
    "url": "https://XXXXXXXXXXXXXXXXXXXXXXXXXX.appsync-api.us-east-1.amazonaws.com/graphql",
    "aws_region": "us-east-1",
    "default_authorization_type": "AMAZON_COGNITO_USER_POOLS",
    "authorization_types": ["API_KEY"],
    "api_key": "da2-XXXXXXXXXXXXXXXXXXXXXXXXXX",
    "model_introspection": { "version": 1, "models": {} }
  },
  "storage": {
    "bucket_name": "myappbucket-dev-xxxxxxxxxx",
    "aws_region": "us-east-1"
  }
}

Frequently Asked Questions

When should I use Amplify Gen 2 vs writing CDK directly?

Use Amplify Gen 2 when your team is primarily frontend-focused and you want rapid full-stack prototyping with Cognito, AppSync, and S3. The TypeScript abstractions save hundreds of lines of CDK. Write CDK directly (or use CDK alongside Gen 2 via the escape hatch) when you need non-standard infrastructure — multi-region, custom VPC configurations, complex IAM, or services Amplify does not abstract (Aurora, ElastiCache, MSK). Gen 2 is not a ceiling; you can drop down to CDK for any resource Amplify does not cover.

How do I handle environment-specific configuration in Amplify?

Gen 2 uses Git branches to separate environments. Your main branch becomes production, staging becomes staging, and each developer's sandbox is ephemeral. Amplify Hosting creates a separate backend stack per connected branch. Environment variables are configured per branch in the Amplify console under App settings → Environment variables. Secrets (OAuth client IDs, API keys) go into Secrets Manager via secret() in your resource definitions.

Can I use Amplify with an existing backend?

Yes. Amplify Libraries are independent of the Amplify CLI and Gen 2. If you already have a Cognito user pool, an AppSync endpoint, or an S3 bucket, you can configure the Amplify library to point at them manually without using the CLI or amplify_outputs.json:

Amplify.configure({
  Auth: {
    Cognito: {
      userPoolId: "us-east-1_XXXXXXXXX",
      userPoolClientId: "XXXXXXXXXXXXXXXXXXXXXXXX",
    },
  },
});

How do I add custom Lambda resolvers to my AppSync API in Gen 2?

Define the Lambda function in amplify/functions/, then reference it as a handler in your data schema using a.handler.function(myFunction). For mutations that need custom business logic — payment processing, email sending, calling a third-party API — this pattern keeps the function co-located with the schema that calls it.

What does Amplify Hosting cost for SSR apps?

Amplify Hosting charges for build minutes, data transferred out, and SSR request execution time. For an SSR Next.js app, expect build costs similar to a CI provider plus Lambda@Edge invocation costs per rendered page. Static pages are served from CloudFront at standard CDN transfer rates. The free tier covers 1,000 build minutes and 15 GB of data per month, which is sufficient for development and low-traffic sites.