Skip to content

Dynamic, row-scoped authorization (e.g. “member of club”) does not compose with generated model queries + selection sets #648

@mgreiner79

Description

@mgreiner79

Summary

I’m building an app with club-scoped data using AWS Amplify Gen 2 Data.
A common access rule is:

“A user may read data for a given clubId only if they are a member of that club.”

Today, this rule cannot be expressed in model authorization, and solving it via custom queries or resolvers breaks important features like frontend-controlled selection sets, filters, and reuse of generated model APIs.

Minimal example scenario

Models

User
- id
- sub

Club
- id
- name

ClubMember   // join table
- id
- clubId
- userId
- role
- status

Desired behavior

  1. A user who is a member of a club should be able to:

    • list all members of that club
    • fetch nested data (e.g. member → user display name)
  2. A user who is not a member of that club should receive Unauthorized.

  3. The frontend should be able to:

    • use generated model queries (or equivalent)
    • control selection sets for nested objects
    • avoid N+1 queries

What works today

Case 1: “My memberships”

Using allow.ownerDefinedIn(...) works well for:

listClubMembersByOwner(...)

This supports:

  • generated model queries
  • frontend selectionSet
  • no custom resolvers

What does NOT work

Case 2: “List members of a club I belong to”

This rule is dynamic:

allow if a row exists in ClubMember for (clubId, user)

There is currently no way to express this as:

allow.if( isMemberOfClub(ctx.identity, ctx.args.clubId) )

Attempted solutions & issues

1. Custom query (Lambda or AppSync JS resolver)
✔ Can enforce membership
❌ Cannot use generated client.models.* queries
❌ Cannot pass frontend selectionSet via client.queries.*
❌ Requires duplicating shapes (custom types, mapping, nested selection logic)

2. Lambda authorizer
✔ Can enforce access globally
❌ Replaces (rather than composes with) allow.owner, allow.groups
❌ Forces re-implementation of existing auth semantics

3. Frontend N+1 queries
❌ Inefficient
❌ Complex
❌ Defeats GraphQL’s purpose

Core problem

There is no composable way to say:

“Run the existing model authorization AND also check this custom condition.”

What’s missing is a concept similar to middleware / policy hooks that:

  • run before resolvers
  • have access to args (clubId)
  • can perform a lookup
  • do not replace existing allow.owner, allow.groups, etc.

Desired capability (conceptual)

Something like:

allow.custom((ctx) => {
  return isMemberOfClub(ctx.identity.sub, ctx.args.clubId)
})

Where:

  • This check runs in addition to model auth
  • Generated resolvers remain usable
  • Frontend keeps selection sets, filters, pagination
  • No need to re-implement existing queries

Why this matters

This pattern appears in many real apps:

  • teams / organizations
  • projects
  • workspaces
  • classrooms
  • clubs

Currently, developers must choose between:

  • strong authorization or
  • good GraphQL ergonomics

Questions for the Amplify team

  1. Is there a recommended pattern for dynamic, relationship-based authorization that preserves generated model queries?
  2. Are there plans to support composable authorization hooks (policy/middleware-style)?
  3. Is the lack of selectionSet support in client.queries.* intentional or a current limitation?

Closing

I really like Amplify Gen 2’s direction — especially the data modeling and generated APIs.
This gap is the one place where I consistently feel forced to “drop down a level” and lose the benefits of the system.

Happy to provide a runnable repro if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions