Skip to content

Fix: AWS Amplify Not Working — Gen 2 Backend, defineData, Auth, Storage, and Sandbox Deployments

FixDevs ·

Quick Answer

How to fix AWS Amplify Gen 2 errors — backend.ts file structure, defineData schema authorization, defineAuth flow, defineStorage bucket access, sandbox vs branch deploy, generated outputs, and Cognito triggers.

The Error

You scaffold a Gen 2 app and npx ampx sandbox errors:

Error: Cannot find module '@aws-amplify/backend'

Or your data schema doesn’t generate types:

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

const client = generateClient<Schema>();
const result = await client.models.Todo.list();
// Type error: Property 'Todo' does not exist

Or auth flow can’t sign in:

NotAuthorizedException: Incorrect username or password.

Or storage upload fails with CORS:

Access to fetch at 'https://s3...' from origin 'https://app.com' 
has been blocked by CORS policy

Why This Happens

Amplify Gen 2 (released 2024) is a complete rewrite — TypeScript-first, code-as-infrastructure. Key concepts:

  • amplify/backend.ts defines your entire AWS backend. Resources (auth, data, storage) are TypeScript functions that compile to CloudFormation.
  • Sandbox is a per-developer cloud environment for fast iteration. Auto-deploys on save.
  • Branch deploys are persistent environments mapped to Git branches.
  • Generated outputs (amplify_outputs.json) bridges backend → frontend config.
  • Authorization is declarative — each model field declares who can read/write.

Most issues map to: missing dependencies, wrong directory structure, mismatched generated types, or unconfigured auth flows.

Fix 1: Initialize the Project

npm create amplify@latest
# Or:
npm install -D @aws-amplify/backend @aws-amplify/backend-cli
npm install aws-amplify

Project structure:

my-app/
├── amplify/
│   ├── backend.ts           # Entry point
│   ├── auth/
│   │   └── resource.ts       # defineAuth
│   ├── data/
│   │   └── resource.ts       # defineData (schema)
│   ├── storage/
│   │   └── resource.ts       # defineStorage
│   └── functions/
│       └── my-fn/
│           ├── handler.ts
│           └── resource.ts
├── src/
│   └── ... your frontend ...
├── amplify_outputs.json     # Generated — DO NOT COMMIT
└── package.json

amplify/backend.ts:

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

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

Start the sandbox:

npx ampx sandbox

This deploys to a personal AWS environment (in your account, isolated by username). Stays in sync as you edit amplify/*.

Pro Tip: Add amplify_outputs.json and .amplify/ to .gitignore. The outputs file is environment-specific and shouldn’t be committed.

Fix 2: Configure the Data Schema

amplify/data/resource.ts:

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

const schema = a.schema({
  Todo: a
    .model({
      content: a.string().required(),
      done: a.boolean().default(false),
      ownerId: a.string(),
    })
    .authorization((allow) => [
      allow.owner(),                       // Only owner can read/write
      allow.authenticated().to(["read"]),  // Authenticated users can read
    ]),
  
  Comment: a
    .model({
      text: a.string().required(),
      todoId: a.id().required(),
      todo: a.belongsTo("Todo", "todoId"),
    })
    .authorization((allow) => [allow.authenticated()]),
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: "userPool",
  },
});

The authorization rules:

  • allow.owner() — only the record’s creator. Requires a Cognito user.
  • allow.authenticated() — any logged-in user.
  • allow.publicApiKey() — public read with an API key.
  • allow.guest() — unauthenticated (must have Cognito identity pool).
  • .to(['read', 'create', 'update', 'delete']) — limit operations.

Multiple rules combine — most permissive wins. allow.owner() + allow.authenticated().to(['read']) = owner does all, anyone reads.

Common Mistake: Forgetting to add authorization. Without it, no one can access the model at all — including the schema’s intended users.

Fix 3: Generated Types and Client

After defining your schema, types are generated automatically by the sandbox. To regenerate manually:

npx ampx generate outputs --branch=sandbox

amplify_outputs.json is generated. Import it in your frontend:

// src/main.tsx
import { Amplify } from "aws-amplify";
import outputs from "../amplify_outputs.json";

Amplify.configure(outputs);

Use the typed client:

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

const client = generateClient<Schema>();

// Type-safe operations:
const { data: todos, errors } = await client.models.Todo.list();
const { data: newTodo } = await client.models.Todo.create({
  content: "Buy milk",
});
const { data: updated } = await client.models.Todo.update({
  id: "abc",
  done: true,
});
await client.models.Todo.delete({ id: "abc" });

// Subscriptions:
const sub = client.models.Todo.observeQuery().subscribe({
  next: ({ items, isSynced }) => {
    console.log("todos:", items);
  },
});
sub.unsubscribe();

For React, the @aws-amplify/ui-react package wraps subscriptions:

import { useEffect, useState } from "react";
import { generateClient } from "aws-amplify/data";
import type { Schema } from "../amplify/data/resource";

const client = generateClient<Schema>();

export function TodoList() {
  const [todos, setTodos] = useState<Array<Schema["Todo"]["type"]>>([]);

  useEffect(() => {
    const sub = client.models.Todo.observeQuery().subscribe({
      next: ({ items }) => setTodos(items),
    });
    return () => sub.unsubscribe();
  }, []);

  return (
    <ul>
      {todos.map((t) => (
        <li key={t.id}>{t.content}</li>
      ))}
    </ul>
  );
}

observeQuery returns live updates — any change to the Todo table reflects in real-time.

Pro Tip: Use Schema["ModelName"]["type"] for the model’s TypeScript type. Saves you redefining shapes.

Fix 4: Auth Setup

amplify/auth/resource.ts:

import { defineAuth } from "@aws-amplify/backend";

export const auth = defineAuth({
  loginWith: {
    email: true,
    externalProviders: {
      google: {
        clientId: secret("GOOGLE_CLIENT_ID"),
        clientSecret: secret("GOOGLE_CLIENT_SECRET"),
      },
      callbackUrls: ["http://localhost:3000/", "https://app.example.com/"],
      logoutUrls: ["http://localhost:3000/", "https://app.example.com/"],
    },
  },
  multifactor: {
    mode: "OPTIONAL",
    sms: true,
    totp: true,
  },
  userAttributes: {
    email: { required: true, mutable: false },
    "custom:role": { dataType: "String" },
  },
});

To use secrets (passwords, OAuth client secrets):

npx ampx sandbox secret set GOOGLE_CLIENT_ID
# Prompts for the value, stores in AWS Parameter Store / Secrets Manager.

Sign-in code:

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

await signUp({
  username: "[email protected]",
  password: "SuperSecret1!",
  options: {
    userAttributes: { email: "[email protected]" },
  },
});

await confirmSignUp({
  username: "[email protected]",
  confirmationCode: "123456",
});

await signIn({
  username: "[email protected]",
  password: "SuperSecret1!",
});

const user = await getCurrentUser();
console.log(user.userId);

For React auth UI:

import { Authenticator } from "@aws-amplify/ui-react";
import "@aws-amplify/ui-react/styles.css";

export default function App() {
  return (
    <Authenticator>
      {({ signOut, user }) => (
        <main>
          <h1>Hello {user?.username}</h1>
          <button onClick={signOut}>Sign out</button>
        </main>
      )}
    </Authenticator>
  );
}

Authenticator handles sign-up / sign-in / forgot password / MFA UI out of the box.

Common Mistake: Adding social providers but not setting redirect URLs. Cognito rejects redirects to URLs not in the allowlist. Add every dev and prod URL to callbackUrls / logoutUrls.

Fix 5: Storage (S3)

amplify/storage/resource.ts:

import { defineStorage } from "@aws-amplify/backend";

export const storage = defineStorage({
  name: "myAppStorage",
  access: (allow) => ({
    "uploads/{entity_id}/*": [
      allow.entity("identity").to(["read", "write", "delete"]),
    ],
    "public/*": [
      allow.guest.to(["read"]),
      allow.authenticated.to(["read", "write"]),
    ],
    "private/{entity_id}/*": [
      allow.entity("identity").to(["read", "write", "delete"]),
    ],
  }),
});

Three path patterns:

  • {entity_id} — owner’s identity ID, scoped per-user.
  • Static pathspublic/*, shared/* — shared across users.

Use:

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

// Upload:
const result = await uploadData({
  path: "uploads/${user.identityId}/avatar.jpg",
  data: file,
}).result;

// Download:
const downloaded = await downloadData({ path: "uploads/abc/avatar.jpg" }).result;
const blob = await downloaded.body.blob();

// List:
const files = await list({ path: "uploads/${user.identityId}/" });

// Delete:
await remove({ path: "uploads/abc/avatar.jpg" });

The ${...} syntax is a template the storage system fills in. Cleaner than manually computing paths.

For getting a presigned URL:

import { getUrl } from "aws-amplify/storage";

const { url, expiresAt } = await getUrl({
  path: "uploads/abc/avatar.jpg",
  options: { expiresIn: 300 },  // 5 minutes
});

console.log(url.toString());

Common Mistake: Forgetting that S3 requires CORS config for browser uploads. Amplify auto-configures CORS for the bucket — if you customize, preserve the patterns Amplify needs.

Fix 6: Sandbox vs Branch Deploy

Sandbox = per-developer cloud env:

npx ampx sandbox
# Deploys to a temporary stack. Outputs at amplify_outputs.json.
# Auto-redeploys on save.

npx ampx sandbox --once
# One-time deploy, then exit (for CI testing).

npx ampx sandbox delete
# Tear down the sandbox.

Branch deploys are persistent — one per Git branch:

# Connect Git in the Amplify Hosting console:
# AWS Console → Amplify → New app → Host web app → connect repo.

When you push to main, Amplify deploys a main environment. Pushing to feature/foo deploys a feature/foo environment.

For pull request preview environments (per-PR):

# amplify.yml
backend:
  phases:
    build:
      commands:
        - npm ci
        - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID

Branch deploys are billable as full CloudFormation stacks. Use sandbox for dev; branch for staging/prod.

Pro Tip: Delete unused sandbox stacks (ampx sandbox delete) to avoid lingering AWS resources. Sandboxes don’t auto-clean.

Fix 7: Lambda Functions

For custom backend logic:

// amplify/functions/my-fn/resource.ts
import { defineFunction } from "@aws-amplify/backend";

export const myFn = defineFunction({
  name: "my-fn",
  entry: "./handler.ts",
  environment: {
    DB_URL: process.env.DB_URL!,
  },
  timeoutSeconds: 30,
});
// amplify/functions/my-fn/handler.ts
export const handler = async (event: any) => {
  console.log("event:", event);
  return { statusCode: 200, body: JSON.stringify({ ok: true }) };
};

Reference in backend.ts:

import { defineBackend } from "@aws-amplify/backend";
import { auth } from "./auth/resource";
import { data } from "./data/resource";
import { myFn } from "./functions/my-fn/resource";

defineBackend({
  auth,
  data,
  myFn,
});

To trigger from a GraphQL operation (custom resolver):

// amplify/data/resource.ts
import { myFn } from "../functions/my-fn/resource";

const schema = a.schema({
  // ...
  callMyFn: a
    .query()
    .arguments({ name: a.string() })
    .returns(a.string())
    .handler(a.handler.function(myFn))
    .authorization((allow) => [allow.authenticated()]),
});

Now client.queries.callMyFn({ name: "Alice" }) invokes the Lambda.

For Cognito triggers (pre-sign-up, post-confirmation):

// amplify/auth/resource.ts
import { postConfirmation } from "../functions/post-confirmation/resource";

export const auth = defineAuth({
  // ...
  triggers: {
    postConfirmation,
  },
});

Fix 8: Frontend Configuration

After Amplify.configure(outputs), the SDK is ready. Common patterns:

// src/main.tsx
import { Amplify } from "aws-amplify";
import outputs from "../amplify_outputs.json";

Amplify.configure(outputs);

import { createRoot } from "react-dom/client";
import App from "./App";

createRoot(document.getElementById("root")!).render(<App />);

For Next.js App Router with SSR:

// app/layout.tsx
import ConfigureAmplifyClientSide from "@/components/ConfigureAmplifyClientSide";

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <ConfigureAmplifyClientSide />
        {children}
      </body>
    </html>
  );
}
// components/ConfigureAmplifyClientSide.tsx
"use client";

import { Amplify } from "aws-amplify";
import outputs from "@/amplify_outputs.json";

Amplify.configure(outputs, { ssr: true });

export default function ConfigureAmplifyClientSide() {
  return null;
}

{ ssr: true } enables server-side rendering compatibility. Required for Next.js / Remix / SvelteKit.

For Server Component data fetching:

// app/page.tsx
import { cookiesClient } from "@/utils/amplify-server";

export default async function Home() {
  const { data: todos } = await cookiesClient.models.Todo.list();
  return <ul>{todos.map((t) => <li key={t.id}>{t.content}</li>)}</ul>;
}

The cookiesClient is a server-side authenticated client created via generateServerClientUsingCookies.

Still Not Working?

A few less-obvious failures:

  • Sandbox deploy fails with User not authorized. Your AWS credentials don’t have permission. Need IAM with sufficient access for CloudFormation, AppSync, Cognito, S3, Lambda.
  • amplify_outputs.json not found. Sandbox isn’t running, or generate outputs hasn’t been called. Run npx ampx sandbox and wait for “Deployment completed.”
  • Real-time subscriptions don’t fire. WebSocket port blocked. Check your network/firewall.
  • Schema types empty. TypeScript hasn’t picked up the generated types. Restart your TS server in VS Code.
  • CORS errors despite proper auth. Custom resolvers / Lambda might need explicit CORS headers in the response.
  • Cognito password policy too strict. Default requires 8+ chars with mixed case, number, symbol. Configure via passwordPolicy in defineAuth for laxer dev rules.
  • Cannot read properties of undefined after Amplify.configure. Configure ran before outputs loaded. Ensure top-level import order (Amplify first, then anything using it).
  • Hosted UI redirect loop. Callback URL not in allowlist or hosted UI domain not set. Check Cognito User Pool → App integration.

For related AWS and full-stack issues, see AWS Lambda timeout, Auth.js not working, GraphQL error handling not working, and Next.js server action not working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles