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
- Log in to the Keystone Admin UI at
https://your-site.com/admin - Click Users in the left sidebar navigation
- Click the Create User button in the top-right corner
- Fill in the required fields (name, email, password)
- Select the appropriate role from the dropdown
- Ensure Is Active is checked
- 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
Deactivation (Recommended)
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
lastLoginAtfield - Remove or deactivate departed team members immediately
- Audit role assignments: run
query { users { email role } }and review - Check that
accessrules on all lists match your security requirements - Verify seed scripts do not contain production credentials