Configuring Two Cognito User Pools for a Single AppSync GraphQL API: A YAML CloudFormation Template Guide

2022-11-08

#cloud#backend
Configuring Two Cognito User Pools for a Single AppSync GraphQL API: A YAML CloudFormation Template Guide

You want end users and admin users to authenticate against the same GraphQL API, but with different security policies. One Cognito pool for customers — simple sign-up, social login, minimal friction. Another for admins — stricter passwords, MFA, different token claims. Both hitting the same AppSync endpoint.

Here's the full CloudFormation setup and the one gotcha that almost stopped me from shipping it.

Step 1: Two Cognito User Pools

Resources:
  EndUsersPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: end-users-pool
      UsernameAttributes:
        - email
      AutoVerifiedAttributes:
        - email
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: false
          RequireUppercase: true
      Schema:
        - AttributeDataType: String
          Name: email
          Required: true
        - AttributeDataType: String
          Name: name
          Required: false
  EndUsersPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: end-users-pool-client
      UserPoolId: !Ref EndUsersPool
  AdminUsersPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: admin-users-pool
      UsernameAttributes:
        - email
      AutoVerifiedAttributes:
        - email
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: false
          RequireUppercase: true
      Schema:
        - AttributeDataType: String
          Name: email
          Required: true
        - AttributeDataType: String
          Name: name
          Required: false
  AdminUsersPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: admin-users-pool-client
      UserPoolId: !Ref AdminUsersPool

Step 2: AppSync API with Both Pools

Resources:
  AppSyncApi:
    Type: 'AWS::AppSync::GraphQLApi'
    Properties:
      Name: graphql-api
      AuthenticationType: AMAZON_COGNITO_USER_POOLS
      UserPoolConfig:
        AwsRegion: us-east-1
        DefaultAction: ALLOW
        UserPoolIds:
          - !Ref EndUsersPool
          - !Ref AdminUsersPool

Step 3: Schema with Pool-Specific Access

type Query {
  endUserQuery: EndUser
  adminUserQuery: AdminUser
}

type EndUser @aws_auth(cognito_user_pools: [{userPoolId: "EndUsersPool", groups: ["endUsers"]}]) {
  id: ID!
  name: String!
}

type AdminUser @aws_auth(cognito_user_pools: [{userPoolId: "AdminUsersPool", groups: ["adminUsers"]}]) {
  id: ID!
  name:

The Gotcha That Almost Stopped Me

You'll find a different syntax in many examples — @aws_cognito_user_pools with just a cognito_groups argument:

type Query {
    endUserQuery: EndUser
   @aws_cognito_user_pools(cognito_groups: ["endUsers"])
}

When I first saw this, I froze. There's no user pool ID in the directive. How does AppSync know which pool's "endUsers" group to check? If both pools have a group called "endUsers," what happens?

My instinct was to stop and research until I fully understood the mapping. Instead, I tried it. It worked. AppSync resolves the group against whichever pool authenticated the current request.

The lesson: if two pools have identically named groups, results could be unpredictable. But in practice, admin and end-user groups rarely collide. And the syntax is clean enough that it's worth using.

The Real Takeaway

I almost didn't ship this because the directive syntax didn't match my mental model. I wanted to understand why it worked before I let myself use it. That caution has value — but so does the "just try it" approach.

The best developers I've worked with don't wait for complete understanding before building. They build, observe, and adjust. The understanding comes from doing, not from reading documentation until the picture is perfect.

When the docs confuse you, deploy to staging and see what happens. You'll learn more in ten minutes of experimentation than in an hour of reading.