From opensaas-migration
Set up the OpenSaaS Stack Admin UI in an existing Next.js App Router project. Invoke as a forked subagent after migration is complete, passing the project root, desired admin path, and whether auth is enabled.
npx claudepluginhub opensaasau/stack --plugin opensaas-migrationThis skill uses the workspace's default tool permissions.
Set up the OpenSaaS Stack Admin UI in the Next.js project described below.
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.
Set up the OpenSaaS Stack Admin UI in the Next.js project described below.
$ARGUMENTS
@opensaas/stack-ui if not already a dependencyapp/{adminPath}/[[...{segmentName}]]/page.tsx@opensaas/stack-ui Is Already InstalledRead package.json in the project root. If @opensaas/stack-ui is not in dependencies, install it:
# Detect package manager
# - If pnpm-lock.yaml exists → pnpm add @opensaas/stack-ui
# - If yarn.lock exists → yarn add @opensaas/stack-ui
# - Otherwise → npm install @opensaas/stack-ui
Check existing versions of @opensaas/stack-core in package.json and install @opensaas/stack-ui at the same version to avoid mismatches.
The admin path comes from $ARGUMENTS (e.g. /admin, /dashboard/admin).
app/{adminPath}/[[...{segmentName}]]/page.tsx
/admin → app/admin/[[...admin]]/page.tsx/dashboard/admin → app/dashboard/admin/[[...admin]]/page.tsx/cms → app/cms/[[...cms]]/page.tsxparams.{segmentName} reference): the last path segment (e.g. admin, cms)basePath prop on <AdminUI>: the full admin path (e.g. /admin, /dashboard/admin)Create all intermediate directories as needed.
Check whether auth is configured:
opensaas.config.ts for authPlugin usagelib/auth.ts or lib/auth/index.ts that exports getSessionThis determines which page template to use.
Use this when auth is NOT configured:
import { AdminUI } from '@opensaas/stack-ui'
import type { ServerActionInput } from '@opensaas/stack-ui/server'
import { getContext, config } from '@/.opensaas/context'
async function serverAction(props: ServerActionInput) {
'use server'
const context = await getContext()
return await context.serverAction(props)
}
interface AdminPageProps {
params: Promise<{ {SEGMENT_NAME}?: string[] }>
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}
export default async function AdminPage({ params, searchParams }: AdminPageProps) {
const resolvedParams = await params
const resolvedSearchParams = await searchParams
return (
<AdminUI
context={await getContext()}
config={await config}
params={resolvedParams.{SEGMENT_NAME}}
searchParams={resolvedSearchParams}
basePath="{ADMIN_PATH}"
serverAction={serverAction}
/>
)
}
Use this when authPlugin is detected and getSession is available:
import { AdminUI } from '@opensaas/stack-ui'
import type { ServerActionInput } from '@opensaas/stack-ui/server'
import { getContext, config } from '@/.opensaas/context'
import { getSession } from '@/lib/auth'
async function serverAction(props: ServerActionInput) {
'use server'
const context = await getContext({ session: await getSession() })
return await context.serverAction(props)
}
interface AdminPageProps {
params: Promise<{ {SEGMENT_NAME}?: string[] }>
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}
export default async function AdminPage({ params, searchParams }: AdminPageProps) {
const resolvedParams = await params
const resolvedSearchParams = await searchParams
const session = await getSession()
if (!session) {
return (
<div className="p-8">
<div className="bg-destructive/10 border border-destructive text-destructive rounded-lg p-6">
<h2 className="text-lg font-semibold mb-2">Access Denied</h2>
<p>You must be logged in to access the admin interface.</p>
</div>
</div>
)
}
return (
<AdminUI
context={await getContext(session)}
config={await config}
params={resolvedParams.{SEGMENT_NAME}}
searchParams={resolvedSearchParams}
basePath="{ADMIN_PATH}"
serverAction={serverAction}
/>
)
}
Replace {SEGMENT_NAME} with the last segment of the admin path (e.g. admin) and {ADMIN_PATH} with the full path (e.g. /admin).
Important: If getSession is not at @/lib/auth, check for it at its actual location (e.g. @/lib/auth/index, @/app/lib/auth) and use the correct import path.
.opensaas/contextThe admin page imports from @/.opensaas/context. This file is generated by pnpm opensaas generate (or pnpm generate). If it doesn't exist yet:
opensaas.config.tspnpm generate (or pnpm opensaas generate) before starting the dev serverReport to the user:
✓ Admin UI set up at: {adminPath}
✓ File created: app/{adminPath}/[[...{segmentName}]]/page.tsx
✓ Auth-aware: yes/no
✓ @opensaas/stack-ui: already installed / installed at version X.Y.Z
Next steps:
1. Run `pnpm generate` to generate the .opensaas/context.ts file (if not already done)
2. Run `pnpm dev` to start the dev server
3. Visit http://localhost:3000{adminPath} to access the admin UI
Docs: https://stack.opensaas.au/admin-ui
[[...{segmentName}]] catch-all segment handles all admin routes: the list view, item detail, create, and edit pages — all from one file.basePath prop must match exactly the URL path where the admin is mounted.serverAction wrapper function is required — it provides the server action bridge between the client-side admin UI components and the database context.getSession approach (not from @opensaas/stack-auth), the auth template still works — just update the import path and the session shape passed to getContext.@opensaas/stack-ui uses Tailwind for styling and link to the docs for setup: https://stack.opensaas.au/admin-ui#tailwind