> keystonejs
Expert guidance for KeystoneJS, the open-source headless CMS and application platform built on Node.js, GraphQL, and Prisma. Helps developers define content schemas, build admin interfaces, implement access control, and create custom GraphQL APIs for content-driven applications.
curl "https://skillshub.wtf/TerminalSkills/skills/keystonejs?format=md"KeystoneJS — Headless CMS & Application Framework
Overview
KeystoneJS, the open-source headless CMS and application platform built on Node.js, GraphQL, and Prisma. Helps developers define content schemas, build admin interfaces, implement access control, and create custom GraphQL APIs for content-driven applications.
Instructions
Schema Definition
Define your data model with Keystone's list API:
// keystone.ts — Main Keystone configuration
import { config, list } from "@keystone-6/core";
import { allowAll } from "@keystone-6/core/access";
import {
text, relationship, timestamp, select, integer,
image, json, checkbox, password, float,
} from "@keystone-6/core/fields";
import { document } from "@keystone-6/fields-document";
export default config({
db: {
provider: "postgresql", // "postgresql" | "sqlite" | "mysql"
url: process.env.DATABASE_URL!,
},
lists: {
// Blog post content type
Post: list({
access: allowAll, // Will customize below
fields: {
title: text({
validation: { isRequired: true },
isIndexed: "unique",
}),
slug: text({
validation: { isRequired: true },
isIndexed: "unique",
hooks: {
// Auto-generate slug from title if not provided
resolveInput({ resolvedData, item }) {
if (!resolvedData.slug && resolvedData.title) {
return resolvedData.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
}
return resolvedData.slug;
},
},
}),
status: select({
options: [
{ label: "Draft", value: "draft" },
{ label: "Published", value: "published" },
{ label: "Archived", value: "archived" },
],
defaultValue: "draft",
ui: { displayMode: "segmented-control" },
}),
content: document({
// Rich text editor with custom blocks
formatting: true,
links: true,
dividers: true,
layouts: [
[1, 1], // Two-column layout
[1, 1, 1], // Three-column layout
],
componentBlocks: {}, // Custom editor components
}),
heroImage: image({
storage: "localImages", // Configured in storage section
}),
publishedAt: timestamp(),
author: relationship({
ref: "User.posts", // Bidirectional relationship
ui: { displayMode: "cards", cardFields: ["name", "email"] },
}),
categories: relationship({
ref: "Category.posts",
many: true, // Many-to-many relationship
ui: { displayMode: "select" },
}),
readingTime: integer({
ui: { createView: { fieldMode: "hidden" } }, // Auto-calculated
hooks: {
resolveInput({ resolvedData }) {
if (resolvedData.content) {
const wordCount = JSON.stringify(resolvedData.content).split(/\s+/).length;
return Math.ceil(wordCount / 200); // ~200 words per minute
}
return undefined;
},
},
}),
},
}),
// Category taxonomy
Category: list({
access: allowAll,
fields: {
name: text({ validation: { isRequired: true }, isIndexed: "unique" }),
description: text({ ui: { displayMode: "textarea" } }),
posts: relationship({ ref: "Post.categories", many: true }),
},
}),
// User with authentication
User: list({
access: allowAll,
fields: {
name: text({ validation: { isRequired: true } }),
email: text({ validation: { isRequired: true }, isIndexed: "unique" }),
password: password({
validation: { isRequired: true, length: { min: 8 } },
}),
role: select({
options: [
{ label: "Admin", value: "admin" },
{ label: "Editor", value: "editor" },
{ label: "Author", value: "author" },
],
defaultValue: "author",
}),
posts: relationship({ ref: "Post.author", many: true }),
},
}),
},
// File storage configuration
storage: {
localImages: {
kind: "local",
type: "image",
generateUrl: (path) => `/images${path}`,
serverRoute: { path: "/images" },
storagePath: "public/images",
},
},
// Authentication
session: {
maxAge: 60 * 60 * 24 * 30, // 30 days
secret: process.env.SESSION_SECRET!,
},
});
Access Control
Implement fine-grained permissions:
// access.ts — Role-based access control rules
import { ListAccessArgs } from "@keystone-6/core/types";
// Helper to check if user is authenticated
function isSignedIn({ session }: ListAccessArgs) {
return !!session;
}
// Helper to check user role
function hasRole(role: string) {
return ({ session }: ListAccessArgs) => session?.data?.role === role;
}
// Post-specific access rules
export const postAccess = {
operation: {
query: allowAll, // Anyone can read published posts
create: isSignedIn, // Must be logged in to create
update: isSignedIn,
delete: hasRole("admin"), // Only admins can delete
},
filter: {
// Non-admins can only see published posts (in query results)
query: ({ session }: ListAccessArgs) => {
if (session?.data?.role === "admin") return {}; // Admins see everything
return { status: { equals: "published" } };
},
// Authors can only update their own posts
update: ({ session }: ListAccessArgs) => {
if (session?.data?.role === "admin") return {};
return { author: { id: { equals: session?.itemId } } };
},
},
item: {
// Additional per-item checks
update: async ({ session, item }: any) => {
// Editors can update any post, authors only their own
if (session?.data?.role === "editor") return true;
return item.authorId === session?.itemId;
},
},
};
Custom GraphQL Extensions
Add custom queries and mutations:
// schema.extensions.ts — Extend the auto-generated GraphQL API
import { graphql } from "@keystone-6/core";
export const extendGraphqlSchema = graphql.extend((base) => ({
query: {
// Custom query: get featured posts
featuredPosts: graphql.field({
type: graphql.list(graphql.nonNull(base.object("Post"))),
args: { limit: graphql.arg({ type: graphql.Int, defaultValue: 5 }) },
async resolve(source, { limit }, context) {
return context.db.Post.findMany({
where: {
status: { equals: "published" },
},
orderBy: { publishedAt: "desc" },
take: limit,
});
},
}),
// Custom query: site statistics
siteStats: graphql.field({
type: graphql.object("SiteStats")({
fields: {
totalPosts: graphql.field({ type: graphql.Int }),
totalUsers: graphql.field({ type: graphql.Int }),
totalCategories: graphql.field({ type: graphql.Int }),
},
}),
async resolve(source, args, context) {
const [posts, users, categories] = await Promise.all([
context.db.Post.count({ where: { status: { equals: "published" } } }),
context.db.User.count(),
context.db.Category.count(),
]);
return {
totalPosts: posts,
totalUsers: users,
totalCategories: categories,
};
},
}),
},
mutation: {
// Custom mutation: publish a post
publishPost: graphql.field({
type: base.object("Post"),
args: { id: graphql.arg({ type: graphql.nonNull(graphql.ID) }) },
async resolve(source, { id }, context) {
return context.db.Post.updateOne({
where: { id },
data: {
status: "published",
publishedAt: new Date().toISOString(),
},
});
},
}),
},
}));
Webhooks and Hooks
React to data changes with lifecycle hooks:
// hooks/post-hooks.ts — Lifecycle hooks for post content
import { ListHooks } from "@keystone-6/core/types";
export const postHooks: ListHooks<any> = {
// Before saving to the database
resolveInput: {
create: async ({ resolvedData, context }) => {
// Auto-set the author to the current user
if (!resolvedData.author && context.session) {
resolvedData.author = { connect: { id: context.session.itemId } };
}
return resolvedData;
},
},
// After saving — trigger side effects
afterOperation: {
create: async ({ item, context }) => {
// Send notification when a new post is created
console.log(`New post created: ${item.title}`);
},
update: async ({ item, originalItem, context }) => {
// Trigger revalidation when a post is published
if (originalItem.status !== "published" && item.status === "published") {
await fetch(`${process.env.FRONTEND_URL}/api/revalidate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
secret: process.env.REVALIDATION_SECRET,
path: `/posts/${item.slug}`,
}),
});
}
},
},
// Validate before saving
validateInput: async ({ resolvedData, addValidationError }) => {
if (resolvedData.status === "published" && !resolvedData.content) {
addValidationError("Cannot publish a post without content");
}
},
};
Installation
# Create a new Keystone project
npm create keystone-app@latest my-cms
# Or add to existing project
npm install @keystone-6/core @keystone-6/fields-document
# Run in development mode (auto-generates Prisma schema + Admin UI)
npx keystone dev
# Admin UI at http://localhost:3000
# GraphQL playground at http://localhost:3000/api/graphql
Examples
Example 1: Setting up Keystonejs with a custom configuration
User request:
I just installed Keystonejs. Help me configure it for my TypeScript + React workflow with my preferred keybindings.
The agent creates the configuration file with TypeScript-aware settings, configures relevant plugins/extensions for React development, sets up keyboard shortcuts matching the user's preferences, and verifies the setup works correctly.
Example 2: Extending Keystonejs with custom functionality
User request:
I want to add a custom access control to Keystonejs. How do I build one?
The agent scaffolds the extension/plugin project, implements the core functionality following Keystonejs's API patterns, adds configuration options, and provides testing instructions to verify it works end-to-end.
Guidelines
- Schema is code — Define your content model in TypeScript; get auto-generated Admin UI, GraphQL API, and database migrations
- Use relationships over IDs — Let Keystone manage foreign keys; bidirectional relationships (
ref: "Post.author") keep data consistent - Access control from the start — Don't use
allowAllin production; define operation + filter + item level access per list - Document field for rich content — The document field gives editors a structured rich text experience with custom blocks
- Hooks for business logic — Use
afterOperationfor side effects (notifications, cache invalidation, webhooks) - Custom GraphQL for complex queries — Extend the schema for aggregations, full-text search, or multi-step operations
- Prisma migrations — Keystone uses Prisma under the hood; run
keystone prisma migratefor production migrations - Separate API from admin — In production, protect the admin UI behind authentication; expose only the GraphQL API publicly
> related_skills --same-repo
> zustand
You are an expert in Zustand, the small, fast, and scalable state management library for React. You help developers manage global state without boilerplate using Zustand's hook-based stores, selectors for performance, middleware (persist, devtools, immer), computed values, and async actions — replacing Redux complexity with a simple, un-opinionated API in under 1KB.
> zoho
Integrate and automate Zoho products. Use when a user asks to work with Zoho CRM, Zoho Books, Zoho Desk, Zoho Projects, Zoho Mail, or Zoho Creator, build custom integrations via Zoho APIs, automate workflows with Deluge scripting, sync data between Zoho apps and external systems, manage leads and deals, automate invoicing, build custom Zoho Creator apps, set up webhooks, or manage Zoho organization settings. Covers Zoho CRM, Books, Desk, Projects, Creator, and cross-product integrations.
> zod
You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.
> zipkin
Deploy and configure Zipkin for distributed tracing and request flow visualization. Use when a user needs to set up trace collection, instrument Java/Spring or other services with Zipkin, analyze service dependencies, or configure storage backends for trace data.