From opensaas-migration
Expert knowledge for migrating projects to OpenSaaS Stack. Invoke whenever the user mentions migrating from KeystoneJS, Prisma, or an existing Next.js project; asks about access control patterns or opensaas.config.ts; or is troubleshooting any aspect of an OpenSaaS Stack migration. Don't wait for the user to say "migration" — trigger whenever the conversation touches these areas.
npx claudepluginhub opensaasau/stack --plugin opensaas-migrationThis skill uses the workspace's default tool permissions.
Expert guidance for migrating existing projects to OpenSaaS Stack.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
Expert guidance for migrating existing projects to OpenSaaS Stack.
IMPORTANT: Always install packages before starting migration
Detect the user's package manager (check for package-lock.json, pnpm-lock.yaml, yarn.lock, or bun.lockb) and use their preferred package manager.
Required packages:
# Using npm
npm install --save-dev @opensaas/stack-cli
npm install @opensaas/stack-core
# Using pnpm
pnpm add -D @opensaas/stack-cli
pnpm add @opensaas/stack-core
# Using yarn
yarn add -D @opensaas/stack-cli
yarn add @opensaas/stack-core
# Using bun
bun add -D @opensaas/stack-cli
bun add @opensaas/stack-core
Optional packages (based on user needs):
@opensaas/stack-auth - If the project needs authentication@opensaas/stack-ui - If the project needs the admin UI@opensaas/stack-tiptap - If the project needs rich text editing@opensaas/stack-storage - If the project needs file storage@opensaas/stack-rag - If the project needs semantic search/RAGDatabase adapters (required for Prisma 7):
SQLite:
npm install better-sqlite3 @prisma/adapter-better-sqlite3
PostgreSQL:
npm install pg @prisma/adapter-pg
Neon (serverless PostgreSQL):
npm install @neondatabase/serverless @prisma/adapter-neon ws
IMPORTANT: For KeystoneJS projects, uninstall KeystoneJS packages before installing OpenSaaS
KeystoneJS migrations should preserve the existing file structure and just swap packages. Do NOT create a new project structure.
# Detect package manager and uninstall KeystoneJS packages
npm uninstall @keystone-6/core @keystone-6/auth @keystone-6/fields-document
# Or with pnpm
pnpm remove @keystone-6/core @keystone-6/auth @keystone-6/fields-document
Remove all @keystone-6/* packages from package.json.
Prisma Projects:
schema.prismaKeystoneJS Projects:
keystone.config.ts or keystone.tsCommon Patterns:
// Public read, authenticated write
operation: {
query: () => true,
create: ({ session }) => !!session?.userId,
update: ({ session }) => !!session?.userId,
delete: ({ session }) => !!session?.userId,
}
// Author-only access
operation: {
query: () => true,
update: ({ session, item }) => item.authorId === session?.userId,
delete: ({ session, item }) => item.authorId === session?.userId,
}
// Admin-only
operation: {
query: ({ session }) => session?.role === 'admin',
create: ({ session }) => session?.role === 'admin',
update: ({ session }) => session?.role === 'admin',
delete: ({ session }) => session?.role === 'admin',
}
// Filter-based access
operation: {
query: ({ session }) => ({
where: { authorId: { equals: session?.userId } }
}),
}
Prisma to OpenSaaS:
| Prisma Type | OpenSaaS Field |
|---|---|
String | text() |
Int | integer() |
Boolean | checkbox() |
DateTime | timestamp() |
Enum | select({ options: [...] }) |
Relation | relationship({ ref: '...' }) |
KeystoneJS to OpenSaaS:
| KeystoneJS Field | OpenSaaS Field |
|---|---|
text | text() |
integer | integer() |
checkbox | checkbox() |
timestamp | timestamp() |
select | select() |
relationship | relationship() |
password | password() |
virtual | virtual() — requires changes (no GraphQL, use hooks.resolveOutput) |
SQLite (Development):
import { PrismaBetterSQLite3 } from '@prisma/adapter-better-sqlite3'
import Database from 'better-sqlite3'
export default config({
db: {
provider: 'sqlite',
url: process.env.DATABASE_URL || 'file:./dev.db',
prismaClientConstructor: (PrismaClient) => {
const db = new Database(process.env.DATABASE_URL || './dev.db')
const adapter = new PrismaBetterSQLite3(db)
return new PrismaClient({ adapter })
},
},
})
PostgreSQL (Production):
import { PrismaPg } from '@prisma/adapter-pg'
import pg from 'pg'
export default config({
db: {
provider: 'postgresql',
url: process.env.DATABASE_URL,
prismaClientConstructor: (PrismaClient) => {
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })
const adapter = new PrismaPg(pool)
return new PrismaClient({ adapter })
},
},
})
CRITICAL: KeystoneJS projects should be migrated IN PLACE
Do NOT create a new project structure. Instead:
Keep existing files and update them:
Rename config file:
keystone.config.ts → opensaas.config.tskeystone.ts → opensaas.config.tsUpdate imports in ALL files:
// Before (KeystoneJS)
import { config, list } from '@keystone-6/core'
import { text, relationship, timestamp } from '@keystone-6/core/fields'
// After (OpenSaaS)
import { config, list } from '@opensaas/stack-core'
import { text, relationship, timestamp } from '@opensaas/stack-core/fields'
Rename KeystoneJS concepts to OpenSaaS:
keystone.config.ts → opensaas.config.tsKeystone references → OpenSaaS or remove entirelyUpdate schema/list definitions:
@keystone-6/core/fields to @opensaas/stack-core/fieldsPreserve API routes and pages:
| KeystoneJS Import | OpenSaaS Import |
|---|---|
@keystone-6/core | @opensaas/stack-core |
@keystone-6/core/fields | @opensaas/stack-core/fields |
@keystone-6/auth | @opensaas/stack-auth |
@keystone-6/fields-document | @opensaas/stack-tiptap |
Before (keystone.config.ts):
import { config, list } from '@keystone-6/core'
import { text, relationship, timestamp } from '@keystone-6/core/fields'
export default config({
db: {
provider: 'postgresql',
url: process.env.DATABASE_URL,
},
lists: {
Post: list({
fields: {
title: text({ validation: { isRequired: true } }),
content: text({ ui: { displayMode: 'textarea' } }),
author: relationship({ ref: 'User.posts' }),
publishedAt: timestamp(),
},
}),
},
})
After (opensaas.config.ts):
import { config, list } from '@opensaas/stack-core'
import { text, relationship, timestamp } from '@opensaas/stack-core/fields'
import { PrismaPg } from '@prisma/adapter-pg'
import pg from 'pg'
export default config({
db: {
provider: 'postgresql',
url: process.env.DATABASE_URL,
prismaClientConstructor: (PrismaClient) => {
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })
const adapter = new PrismaPg(pool)
return new PrismaClient({ adapter })
},
},
lists: {
Post: list({
fields: {
title: text({ validation: { isRequired: true } }),
content: text(), // Note: OpenSaaS text() doesn't have ui.displayMode
author: relationship({ ref: 'User.posts' }),
publishedAt: timestamp(),
},
}),
},
})
keystone.config.ts to opensaas.config.ts@keystone-6/core → @opensaas/stack-core@keystone-6/core/fields → @opensaas/stack-core/fields@keystone-6/auth → @opensaas/stack-authvirtual() fields exist, invoke the keystone-virtual-fields-context skillcontext.graphql.run(, context.graphql.raw(, context.query.; for simple reads replace with context.db.*; for nested/joined data use defineFragment + context.db.{list}.findMany({ query: fragment }); invoke the migrate-context-calls skill for detailed patternsDO NOT:
DO:
Solution:
opensaas generate to create Prisma schemaprisma db push instead of migrations for existing databasesprisma migrate dev with existing dataSolution:
Solution:
BaseFieldConfiggetZodSchema, getPrismaType, getTypeScriptTypeSolution:
@opensaas/stack-tiptap rich text fieldKeystone virtual fields use graphql.field({ type: graphql.String, resolve }). OpenSaaS Stack has no GraphQL — virtual fields use hooks.resolveOutput with a type property instead.
Quick example:
// Keystone
fullName: virtual({
field: graphql.field({
type: graphql.String,
resolve: (item) => `${item.firstName} ${item.lastName}`,
}),
})
// OpenSaaS Stack
fullName: virtual({
type: 'string',
hooks: {
resolveOutput: ({ item }) => `${item.firstName} ${item.lastName}`,
},
})
Field arguments are not supported in OpenSaaS Stack. For detailed patterns including context queries and custom types, invoke the keystone-virtual-fields-context skill.
Keystone apps often use context.graphql.run() for type-safe data access from routes, server actions, and hooks. OpenSaaS Stack has no GraphQL — use context.db.{listName}.{method}() directly, or the new fragment-based query utilities for nested/joined data.
Simple queries (no nesting):
// Keystone
const { posts } = await context.graphql.run({
query: `query { posts(where: { status: { equals: published } }) { id title } }`,
})
// OpenSaaS Stack
const posts = await context.db.post.findMany({
where: { status: { equals: 'published' } },
})
Queries with nested/related data (fragments — recommended for Keystone migrations):
OpenSaaS Stack provides defineFragment for composable, fully typed queries — the closest equivalent to Keystone GraphQL fragments and codegen types. Pass the fragment directly to context.db operations using the query parameter.
// Keystone — GraphQL fragment + codegen types
import type { PostFragment } from './__generated__/graphql'
const { posts } = await context.graphql.run({
query: `
fragment AuthorFields on User { id name }
query { posts { id title author { ...AuthorFields } } }
`,
})
// OpenSaaS Stack — defineFragment + context.db (no codegen, no GraphQL)
import type { User, Post } from '.prisma/client'
import { defineFragment, type ResultOf } from '@opensaas/stack-core'
const authorFragment = defineFragment<User>()({ id: true, name: true } as const)
const postFragment = defineFragment<Post>()({
id: true,
title: true,
author: authorFragment,
} as const)
type PostData = ResultOf<typeof postFragment>
// → { id: string; title: string; author: { id: string; name: string } | null }
// Primary API: pass query to context.db operations
const posts = await context.db.post.findMany({ query: postFragment })
// posts: PostData[]
// With filter, orderBy, pagination
const filtered = await context.db.post.findMany({
query: postFragment,
where: { published: true },
orderBy: { createdAt: 'desc' },
take: 10,
})
// Single record
const post = await context.db.post.findUnique({ where: { id }, query: postFragment })
// Nested relationship filtering with RelationSelector
const commentFrag = defineFragment<Comment>()({ id: true, body: true } as const)
const postWithComments = defineFragment<Post>()({
id: true,
comments: { query: commentFrag, where: { approved: true }, take: 5 },
} as const)
const postsWithComments = await context.db.post.findMany({ query: postWithComments })
List names are camelCase: Post → context.db.post, BlogPost → context.db.blogPost. Access control is enforced automatically. For detailed patterns including sudo access, invoke the migrate-context-calls skill.
opensaas.config.tsopensaas generate (or npx opensaas generate)prisma generate (or npx prisma generate)prisma db push (or npx prisma db push)keystone.config.ts to opensaas.config.tsgraphql.field() + resolve() with hooks.resolveOutput; invoke keystone-virtual-fields-context skillcontext.db.*; for nested/related data use defineFragment + context.db.{list}.findMany({ query: fragment }) from @opensaas/stack-core; invoke migrate-context-calls skill for detailed patternsopensaas generateprisma generateprisma db pushcontext.db@opensaas/stack-auth for authenticationopensaas.config.ts to gitWhen you encounter bugs or missing features in OpenSaaS Stack:
If during migration you discover:
Use the github-issue-creator agent to create a GitHub issue on the OpenSaasAU/stack repository:
Invoke the github-issue-creator agent with:
- Clear description of the bug or missing feature
- Steps to reproduce (if applicable)
- Expected vs actual behavior
- Affected files and line numbers
- Your suggested solution (if you have one)
This ensures bugs and feature requests are properly tracked and addressed by the OpenSaaS Stack team, improving the experience for future users.
Example:
If you notice that the migration command doesn't properly handle Prisma enums, invoke the github-issue-creator agent:
"Found a bug: The migration generator doesn't convert Prisma enums to OpenSaaS select fields. Enums are being ignored during schema analysis in packages/cli/src/migration/introspectors/prisma-introspector.ts"
The agent will create a detailed GitHub issue with reproduction steps and proposed solution.