Adding & Removing Users on Keystone JS | OpsBlu Docs

Adding & Removing Users on Keystone JS

Adding & Removing Users on Keystone JS — setup, configuration, and best practices for KeystoneJS.

Keystone.js manages users as first-class list items in its GraphQL schema. Because Keystone is a headless CMS framework, user management is defined in code through your schema configuration and executed through the Admin UI or GraphQL API.

How Keystone.js User Management Works

Unlike traditional CMS platforms with a fixed user model, Keystone lets you define your own User list with custom fields, hooks, and access control. The Admin UI at /admin provides a visual interface, while the GraphQL API at /api/graphql enables programmatic management.

Default user list setup in keystone.ts:

import { list } from '@keystone-6/core';
import { text, password, checkbox, select, timestamp } from '@keystone-6/core/fields';

export const lists = {
  User: list({
    fields: {
      name: text({ validation: { isRequired: true } }),
      email: text({
        validation: { isRequired: true },
        isIndexed: 'unique',
      }),
      password: password({
        validation: { isRequired: true },
      }),
      role: select({
        type: 'enum',
        options: [
          { label: 'Admin', value: 'admin' },
          { label: 'Editor', value: 'editor' },
          { label: 'Author', value: 'author' },
        ],
        defaultValue: 'author',
        ui: { displayMode: 'segmented-control' },
      }),
      isActive: checkbox({ defaultValue: true }),
      createdAt: timestamp({ defaultValue: { kind: 'now' } }),
    },
    access: {
      operation: {
        query: ({ session }) => !!session,
        create: ({ session }) => session?.data?.role === 'admin',
        update: ({ session }) => session?.data?.role === 'admin',
        delete: ({ session }) => session?.data?.role === 'admin',
      },
    },
  }),
};

Adding Users via Admin UI

  1. Log in to the Keystone Admin UI at https://your-site.com/admin
  2. Click Users in the left sidebar navigation
  3. Click the Create User button in the top-right corner
  4. Fill in the required fields (name, email, password)
  5. Select the appropriate role from the dropdown
  6. Ensure Is Active is checked
  7. Click Create User to save

The Admin UI validates unique email constraints and password requirements before saving. If your schema includes custom hooks, those execute during creation.

Adding Users via GraphQL API

Use the createUser mutation for programmatic user creation:

mutation CreateUser {
  createUser(data: {
    name: "Jane Developer"
    email: "jane@example.com"
    password: "securePassword123!"
    role: "editor"
    isActive: true
  }) {
    id
    name
    email
    role
    createdAt
  }
}

Bulk creation with createUsers:

mutation BulkCreateUsers {
  createUsers(data: [
    {
      name: "Dev One"
      email: "dev1@example.com"
      password: "tempPass123!"
      role: "author"
    },
    {
      name: "Dev Two"
      email: "dev2@example.com"
      password: "tempPass456!"
      role: "editor"
    }
  ]) {
    id
    name
    email
    role
  }
}

Adding Users with Seed Scripts

For development or initial deployment, use Keystone's seed functionality:

// seed-users.ts
import { getContext } from '@keystone-6/core/context';
import config from './keystone';

async function seedUsers() {
  const context = getContext(config, PrismaModule);
  const sudo = context.sudo();

  const users = [
    { name: 'Admin', email: 'admin@company.com', password: 'AdminPass!1', role: 'admin' },
    { name: 'Editor', email: 'editor@company.com', password: 'EditorPass!1', role: 'editor' },
    { name: 'Author', email: 'author@company.com', password: 'AuthorPass!1', role: 'author' },
  ];

  for (const user of users) {
    const existing = await sudo.query.User.findOne({
      where: { email: user.email },
    });
    if (!existing) {
      await sudo.query.User.createOne({ data: user });
      console.log(`Created user: ${user.email}`);
    }
  }
}

seedUsers();

Removing and Deactivating Users

Deactivating preserves content associations and audit trails. Update the isActive flag:

mutation DeactivateUser {
  updateUser(
    where: { email: "jane@example.com" }
    data: { isActive: false }
  ) {
    id
    name
    isActive
  }
}

Add a session check to block deactivated users from logging in:

// keystone.ts
export default config({
  session: statelessSessions({
    secret: process.env.SESSION_SECRET,
  }),
  auth: createAuth({
    listKey: 'User',
    identityField: 'email',
    secretField: 'password',
    sessionData: 'id name email role isActive',
  }),
  server: {
    extendExpressApp: (app, context) => {
      app.use(async (req, res, next) => {
        if (req.session?.data && !req.session.data.isActive) {
          req.session = undefined;
          return res.redirect('/admin');
        }
        next();
      });
    },
  },
});

Permanent Deletion

Via Admin UI: Navigate to the user record, click the three-dot menu, select Delete, and confirm.

Via GraphQL:

mutation DeleteUser {
  deleteUser(where: { email: "jane@example.com" }) {
    id
    name
  }
}

What happens to their content: By default, Keystone does not cascade-delete related records. If a Post list has a relationship field pointing to User, the post remains but the author reference becomes null. To enforce cleanup, add a beforeOperation hook:

Post: list({
  hooks: {
    validateDelete: async ({ item, context }) => {
      // Prevent deletion if user has published posts
      const posts = await context.query.Post.findMany({
        where: { author: { id: { equals: item.id } } },
      });
      if (posts.length > 0) {
        throw new Error(`Cannot delete user with ${posts.length} posts. Reassign first.`);
      }
    },
  },
}),

Bulk User Management

Bulk Delete via GraphQL

mutation BulkDeleteUsers {
  deleteUsers(where: [
    { id: "user-id-1" },
    { id: "user-id-2" },
    { id: "user-id-3" }
  ]) {
    id
    name
  }
}

Bulk Role Updates

// bulk-role-update.ts
async function bulkUpdateRoles(context, emails: string[], newRole: string) {
  const users = await context.query.User.findMany({
    where: { email: { in: emails } },
    query: 'id email role',
  });

  const updates = users.map((user) =>
    context.query.User.updateOne({
      where: { id: user.id },
      data: { role: newRole },
    })
  );

  return Promise.all(updates);
}

SSO and External Authentication

Keystone supports custom authentication providers through its auth configuration. For enterprise SSO:

Auth.js (NextAuth) integration:

// Custom auth provider example with external IdP
import { createAuth } from '@keystone-6/auth';

const { withAuth } = createAuth({
  listKey: 'User',
  identityField: 'email',
  secretField: 'password',
  initFirstItem: {
    fields: ['name', 'email', 'password'],
    itemData: { role: 'admin' },
  },
});

For SAML/OIDC enterprise SSO, Keystone projects typically implement a custom Express middleware or use Passport.js strategies alongside the Keystone session:

import passport from 'passport';
import { Strategy as SAMLStrategy } from 'passport-saml';

// Configure SAML strategy for enterprise IdP
passport.use(new SAMLStrategy({
  entryPoint: 'https://idp.company.com/sso/saml',
  issuer: 'keystone-cms',
  cert: process.env.SAML_CERT,
  callbackUrl: 'https://cms.company.com/auth/saml/callback',
}, async (profile, done) => {
  // Find or create user in Keystone
  const context = getContext(config);
  let user = await context.sudo().query.User.findOne({
    where: { email: profile.email },
  });
  if (!user) {
    user = await context.sudo().query.User.createOne({
      data: {
        name: profile.displayName,
        email: profile.email,
        password: crypto.randomUUID(), // SSO users don't need passwords
        role: mapSAMLRoleToKeystone(profile.role),
      },
    });
  }
  done(null, user);
}));

Access Audit Checklist

  • Review all users in the Admin UI quarterly (filter by isActive: true)
  • Query for users with no recent login using a custom lastLoginAt field
  • Remove or deactivate departed team members immediately
  • Audit role assignments: run query { users { email role } } and review
  • Check that access rules on all lists match your security requirements
  • Verify seed scripts do not contain production credentials