import { type ClientSchema, a, defineData } from '@aws-amplify/backend';const schema = a.schema({ Todo: a.model({ content: a.string().required(), priority: a.enum(['low', 'medium', 'high']), done: a.boolean().default(false), dueDate: a.date(), owner: a.string(), }).authorization(allow => [allow.owner()]),});export type Schema = ClientSchema<typeof schema>;export const data = defineData({ schema, authorizationModes: { defaultAuthorizationMode: 'userPool', },});
Import into amplify/backend.ts:
typescript
import { defineBackend } from '@aws-amplify/backend';import { auth } from './auth/resource';import { data } from './data/resource';defineBackend({ auth, data });
Export Schema as ClientSchema<typeof schema> — without this export,
frontend clients lose all type inference.
Field types:a.string(), a.integer(), a.float(), a.boolean(),
a.date(), a.datetime(), a.timestamp(), a.time(), a.email(),
a.url(), a.phone(), a.ipAddress(), a.json(), a.id(),
a.enum([...]). Chain .required() or .array() on any field;
.default(value) on scalar fields only (not enums — see Pitfalls).
a.phone(): Only accepts E.164 format (+15551234567). Hyphens (+1-555-0101) and short formats are rejected.
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])
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.
When multiple rules are applied, the most permissive wins. You cannot use deny rules — if allow.authenticated() grants full CRUD, you cannot selectively deny delete for non-owners. Structure rules from most restrictive:
Pitfall:groupsDefinedIn('fieldName') automatically creates an implicit field on the model. Do NOT also declare that field explicitly — this causes: "Implicit field conflicts with explicit field definition."
Type system gap: The implicit field from groupsDefinedIn('fieldName') is NOT exposed in generated TypeScript client types. To set the field programmatically, use an untyped approach:
The second argument to hasMany/belongsTo/hasOne is the foreign key
field name. That field must be declared explicitly on the child model.
Declare both sides of every relationship — the parent model
needs a.hasMany('Child', 'fkField') AND the child model needs
a.belongsTo('Parent', 'fkField'). Omitting either side causes silent
query failures (e.g., lazy-loading the relation returns undefined).
Deleting a parent record does NOT cascade to children and does NOT fail. Child records become orphaned silently — manually delete children first or implement a soft-delete pattern.
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.
⚠️ 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.
.required() on enums:a.enum(['A','B']).required() does NOT work — .required() doesn’t exist on EnumType. Define the enum separately and use a.ref():
The defaultAuthorizationMode must match at least one strategy used in
your model authorization() rules (e.g., userPool ↔ owner() /
authenticated() / group(); apiKey ↔ publicApiKey(); iam ↔ guest()).
Guest access is enabled by default in Amplify Gen2 — see auth-backend.md for details and how to disable it.
Guest access configuration: see auth-backend.md § Guest Access.
Missing ClientSchema export: Without export type Schema = ClientSchema<typeof schema>, frontend generateClient<Schema>() has no
type information and all operations are untyped.
Auth mode conflict: Using allow.publicApiKey() in model rules but
setting defaultAuthorizationMode: 'userPool' without adding
apiKeyAuthorizationMode causes API key requests to be rejected.
Per-field auth + .required(): Fields with owner-only authorization (allow.owner()) cannot be .required() — other users can’t provide a value on create. Make private fields optional.