Users & Teams

Keel provides a built-in User model and a team system for managing application users and team-based access control. Enable user creation in your keelconfig.yaml to automatically link authenticated identities to user records and organize users into teams with role-based permissions.

Enabling user creation

Add userCreation to the auth section of your keelconfig.yaml:

keelconfig.yaml
auth:
  userCreation: auto
ValueDefaultDescription
offYesNo built-in User model. Existing behaviour preserved.
autoNoOn authentication, Keel auto-creates a User if one doesn't exist, or links an existing User by email match. No custom code needed.
requiredNoOn authentication, Keel looks for a pre-existing User with a matching email. If no User is found, authentication is denied. Use this when users must be pre-provisioned before they can log in.

When userCreation is off, the rest of this page does not apply. Your schema behaves as before, and you can continue using the manual Identity-to-User pattern described on the Identity page.

The built-in User model

When userCreation is set to auto or required, Keel injects a User model with the standard fields (id, createdAt, updatedAt) plus an email field. Each Identity gets a user foreign key linking it to the corresponding User.

// Keel provides this automatically — you don't need to declare it
model User {
  fields {
    email Text @unique
  }
}

On authentication (OAuth callback, token exchange, or password login), Keel links the Identity to a User based on the mode:

  • In auto mode, a new User is created if one doesn't already exist with a matching email.
  • In required mode, authentication fails if no User exists with a matching email.

Extending the User model

Add custom fields and actions to the built-in User model using extend model User. Extended fields must be optional or have a @default value, since User records are auto-created by the system.

schema.keel
extend model User {
  fields {
    name Text?
    avatar Text?
    department Text?
  }
 
  actions {
    get getUser(id)
    update updateUser(id) with (name, avatar, department)
  }
 
  @permission(
    expression: ctx.isAuthenticated,
    actions: [get, update]
  )
}

Accessing the User

In expressions

ctx.identity.user is available in schema expressions including @where, @set, and @permission. Use it to filter records by the authenticated user or set ownership fields.

schema.keel
model Order {
  fields {
    customer User
    total Decimal
  }
 
  actions {
    create createOrder() with (total) {
      @set(order.customer = ctx.identity.user)
    }
    list myOrders() {
      @where(order.customer == ctx.identity.user)
    }
  }
 
  @permission(
    expression: order.customer == ctx.identity.user,
    actions: [update, delete]
  )
}

In functions

The linked User is accessible in custom functions via ctx.identity.user:

import { MyFunction, models } from "@teamkeel/sdk";
 
export default MyFunction(async (ctx, inputs) => {
  const user = ctx.identity?.user;
 
  if (!user) {
    throw new Error("No user linked to this identity");
  }
 
  // user.id, user.email, and any extended fields are available
  return user;
});

Teams

Teams let you group users and assign roles to those groups. Define teams in your schema, and Keel automatically adds a teams field to the User model.

⚠️

Teams require userCreation to be set to auto or required. They do not work when userCreation is off.

Defining teams

Declare teams with associated roles using the team keyword:

schema.keel
role Developer {}
role Manager {}
 
team Engineering {
  roles {
    Developer
  }
}
 
team Leadership {
  roles {
    Manager
  }
}
 
team Operations {
}

When any team declarations exist in your schema:

  • Team becomes a built-in field type
  • The User model automatically gets a teams Team[] field (defaults to [])
  • Team names are available as constants in expressions (e.g. Team.Engineering)

Team names must be PascalCase and unique across models, enums, and other teams.

Team-based permissions

Users in a team inherit that team's associated roles. This means role-based @permission rules automatically apply to team members without needing email or domain matching on the role definition.

schema.keel
role Developer {}
 
team Engineering {
  roles {
    Developer
  }
}
 
model Deployment {
  fields {
    version Text
  }
 
  actions {
    create createDeployment() with (version)
  }
 
  // Any user on the Engineering team has the Developer role
  // and can create deployments
  @permission(
    roles: [Developer],
    actions: [create]
  )
}

Team-based roles work alongside email and domain-based roles. A user satisfies a role check if they match any source — team membership, email, or domain.

Using teams in expressions

Team names are available as constants using the Team.Name syntax. Use the in operator to check if a user belongs to a team:

schema.keel
model Report {
  fields {
    title Text
    content Markdown
  }
 
  actions {
    create createReport() with (title, content) {
      @permission(expression: Team.Engineering in ctx.identity.user.teams)
    }
    list listReports() {
      @permission(expression: Team.Engineering in ctx.identity.user.teams || Team.Leadership in ctx.identity.user.teams)
    }
  }
}

This pattern also works in @where expressions and flow permissions.

Using Team as a field type

Team can be used as a field type on your own models. This is useful for associating records with a specific team.

schema.keel
model Project {
  fields {
    name Text
    team Team
  }
 
  actions {
    create createProject() with (name, team)
    list listProjects()
  }
 
  @permission(
    expression: project.team in ctx.identity.user.teams,
    actions: [get, update, delete]
  )
}

Managing team membership

Team membership is managed per environment in the Keel Console under Configure > Teams. From there you can:

  • Assign project members to teams
  • Pre-assign invited members to teams before they authenticate (memberships carry over when the invite is accepted)

Membership changes sync automatically to the runtime. A full reconciliation also runs on each deploy.

Migrating an existing project

If you have an existing Keel project and want to adopt Users & Teams, follow these steps.

Enabling auto mode is safe for existing projects. It won't disrupt existing authentication flows — it only creates User records alongside existing Identity records as users log in.

Step 1: Enable user creation

Add userCreation to your keelconfig.yaml:

keelconfig.yaml
auth:
  userCreation: auto

The auto mode is recommended as a starting point. It transparently creates User records on authentication without requiring any changes to your existing authentication flows.

Step 2: Deploy

Deploy the updated configuration. On the next authentication for each existing identity, Keel will auto-create a User record and link it via email matching. There is no need to manually create User records — they are created as users log in.

Step 3: Extend the User model (optional)

If you need custom fields on the User, add them to your schema:

schema.keel
extend model User {
  fields {
    name Text?
    department Text?
  }
}

Step 4: Define teams (optional)

If you want team-based access control, add team and role declarations:

schema.keel
role Operator {}
role Admin {}
 
team Operations {
  roles {
    Operator
  }
}
 
team Management {
  roles {
    Admin
  }
}

Step 5: Assign team members

In the Console, go to Configure > Teams for each environment and assign project members to the appropriate teams.

Step 6: Update permissions

Replace email or domain-based role rules with team-based permissions where appropriate. Update permission attributes to use roles granted through team membership:

// Before: domain-based role matching
role Staff {
  domains {
    "mycompany.com"
  }
}
 
// After: team-based role assignment
role Staff {}
 
team Operations {
  roles {
    Staff
  }
}

Complete example

keelconfig.yaml
auth:
  userCreation: auto
  providers:
    - type: google
      name: google_client
      clientId: "1234567890.apps.googleusercontent.com"
schema.keel
extend model User {
  fields {
    name Text?
    department Text?
  }
 
  actions {
    get getUser(id)
    update updateUser(id) with (name, department)
  }
 
  @permission(
    expression: ctx.isAuthenticated,
    actions: [get, update]
  )
}
 
role Developer {}
role Operator {}
 
team Engineering {
  roles {
    Developer
  }
}
 
team Operations {
  roles {
    Operator
  }
}
 
model Deployment {
  fields {
    version Text
    notes Text?
    deployedBy User
  }
 
  actions {
    create createDeployment() with (version, notes) {
      @set(deployment.deployedBy = ctx.identity.user)
    }
    list listDeployments()
  }
 
  @permission(
    roles: [Developer, Operator],
    actions: [create]
  )
 
  @permission(
    expression: ctx.isAuthenticated,
    actions: [list]
  )
}

In this example:

  • Users are auto-created on authentication with Google
  • The User model is extended with name and department fields
  • Two teams (Engineering and Operations) grant their members the Developer and Operator roles respectively
  • Only users with the Developer or Operator role (i.e. members of either team) can create deployments
  • Any authenticated user can list deployments