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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions