@@ -68,6 +68,14 @@ export interface ImplementationPattern {
6868 example ?: string ;
6969}
7070
71+ export interface ArchitectureGuidance {
72+ fileStructure : string ;
73+ keystoneSetup : string ;
74+ nextAuthSetup : string ;
75+ apiRoutes : string ;
76+ criticalPatterns : string [ ] ;
77+ }
78+
7179export class KeystoneGuidanceProvider {
7280 generateImplementationGuidance ( spec : ParsedSpecification ) : KeystoneGuidance {
7381 return {
@@ -80,6 +88,273 @@ export class KeystoneGuidanceProvider {
8088 } ;
8189 }
8290
91+ generateArchitectureGuidance ( spec : ParsedSpecification , requiresAuth : boolean ) : ArchitectureGuidance {
92+ return {
93+ fileStructure : this . getOpinionatedFileStructure ( ) ,
94+ keystoneSetup : this . getKeystoneContextSetup ( ) ,
95+ nextAuthSetup : requiresAuth ? this . getNextAuthSetup ( ) : '' ,
96+ apiRoutes : this . getAPIRouteSetup ( ) ,
97+ criticalPatterns : this . getCriticalPatterns ( requiresAuth ) ,
98+ } ;
99+ }
100+
101+ private getOpinionatedFileStructure ( ) : string {
102+ return `## 📁 Required File Structure (Based on on-the-hill-drama-club)
103+
104+ \`\`\`
105+ src/
106+ ├── app/ # Next.js 15 App Router
107+ │ ├── api/
108+ │ │ ├── graphql/
109+ │ │ │ └── route.ts # GraphQL API endpoint (uses Yoga + Keystone)
110+ │ │ └── auth/
111+ │ │ └── [...nextauth]/ # NextAuth routes (if auth required)
112+ │ ├── admin/ # Keystone Admin UI pages
113+ │ └── (your feature routes)/
114+ ├── keystone/
115+ │ ├── context/
116+ │ │ └── index.ts # **CRITICAL**: getContext() setup
117+ │ ├── lists/ # Keystone schema definitions
118+ │ │ ├── User.ts
119+ │ │ └── (your list).ts
120+ │ ├── schema.ts # Exports all lists
121+ │ └── helpers.ts # Access control helpers (isAdmin, isLoggedIn)
122+ ├── lib/
123+ │ ├── auth.ts # NextAuth configuration (if auth required)
124+ │ └── utils.ts
125+ └── types/
126+ └── session.ts # NextAuth session types
127+
128+ keystone.ts # Root Keystone config
129+ schema.prisma # Prisma schema
130+ \`\`\`` ;
131+ }
132+
133+ private getKeystoneContextSetup ( ) : string {
134+ return `## 🔧 Keystone Context Setup (CRITICAL)
135+
136+ **Location**: \`src/keystone/context/index.ts\`
137+
138+ \`\`\`typescript
139+ import { getContext } from '@keystone-6/core/context';
140+ import config from '../../../keystone';
141+ import * as PrismaModule from '.prisma/client';
142+ import { Pool, neonConfig } from '@neondatabase/serverless';
143+ import { PrismaNeon } from '@prisma/adapter-neon';
144+ import { Prisma PrismaClient } from '@prisma/client';
145+
146+ // Neon serverless driver for Vercel deployment
147+ neonConfig.webSocketConstructor = ws;
148+ const connectionString = process.env.DATABASE_URL!;
149+
150+ // Custom Prisma Client with Neon adapter
151+ class NeonPrismaClient extends PrismaClient {
152+ constructor() {
153+ const pool = new Pool({ connectionString });
154+ const adapter = new PrismaNeon(pool);
155+ super({ adapter });
156+ }
157+ }
158+
159+ // Global singleton pattern (prevents multiple instances in dev)
160+ export const keystoneContext: Context =
161+ globalThis.keystoneContext ||
162+ getContext(config, { ...PrismaModule, PrismaClient: NeonPrismaClient });
163+
164+ if (process.env.NODE_ENV !== 'production') {
165+ globalThis.keystoneContext = keystoneContext;
166+ }
167+
168+ // Export for use in API routes and server actions
169+ export type Context = typeof keystoneContext;
170+ \`\`\`
171+
172+ **Why This Matters**:
173+ - ❌ **NEVER** start a separate GraphQL server
174+ - ✅ **ALWAYS** use \`getContext()\` to embed Keystone in Next.js
175+ - ✅ Use Neon adapter for serverless PostgreSQL on Vercel
176+ - ✅ Global singleton prevents multiple connections in development` ;
177+ }
178+
179+ private getNextAuthSetup ( ) : string {
180+ return `## 🔐 NextAuth Configuration (CRITICAL for Auth Features)
181+
182+ **Location**: \`src/lib/auth.ts\`
183+
184+ \`\`\`typescript
185+ import NextAuth from 'next-auth';
186+ import GoogleProvider from 'next-auth/providers/google';
187+ import CredentialsProvider from 'next-auth/providers/credentials';
188+ import { keystoneContext } from '@/keystone/context';
189+
190+ export const { handlers, auth, signIn, signOut } = NextAuth({
191+ providers: [
192+ GoogleProvider({
193+ clientId: process.env.GOOGLE_CLIENT_ID!,
194+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
195+ }),
196+ CredentialsProvider({
197+ name: 'Email and Password',
198+ credentials: {
199+ email: { label: 'Email', type: 'email' },
200+ password: { label: 'Password', type: 'password' },
201+ },
202+ async authorize(credentials) {
203+ if (!credentials?.email || !credentials?.password) return null;
204+
205+ // **CRITICAL**: Use keystoneContext.sudo() for auth queries
206+ const user = await keystoneContext.sudo().query.User.findOne({
207+ where: { email: credentials.email },
208+ query: 'id name email password',
209+ });
210+
211+ if (!user || !user.password) return null;
212+
213+ // Verify password (implement your password verification)
214+ const isValid = await verifyPassword(credentials.password, user.password);
215+ if (!isValid) return null;
216+
217+ return { id: user.id, name: user.name, email: user.email };
218+ },
219+ }),
220+ ],
221+
222+ callbacks: {
223+ async signIn({ user, account, profile }) {
224+ // For OAuth providers, create user in Keystone if doesn't exist
225+ if (account?.provider !== 'credentials') {
226+ const existing = await keystoneContext.sudo().query.User.findOne({
227+ where: { email: user.email! },
228+ query: 'id',
229+ });
230+
231+ if (!existing) {
232+ await keystoneContext.sudo().query.User.createOne({
233+ data: {
234+ name: user.name!,
235+ email: user.email!,
236+ // Set default role, etc.
237+ },
238+ });
239+ }
240+ }
241+ return true;
242+ },
243+
244+ async session({ session, token }) {
245+ // **CRITICAL**: Fetch full user data from Keystone
246+ const user = await keystoneContext.sudo().query.User.findOne({
247+ where: { email: session.user?.email! },
248+ query: 'id name email role allowAdminUI',
249+ });
250+
251+ if (user) {
252+ session.user.id = user.id;
253+ session.user.role = user.role;
254+ session.allowAdminUI = user.allowAdminUI || false;
255+ }
256+
257+ return session;
258+ },
259+ },
260+
261+ pages: {
262+ signIn: '/auth/signin',
263+ error: '/auth/error',
264+ },
265+ });
266+ \`\`\`
267+
268+ **Key Patterns**:
269+ - ✅ Use \`keystoneContext.sudo()\` for auth queries (bypasses access control)
270+ - ✅ Sync NextAuth users with Keystone User list
271+ - ✅ Store user roles and permissions in Keystone
272+ - ✅ Extend session with Keystone user data
273+ - ✅ Support both OAuth and credentials providers` ;
274+ }
275+
276+ private getAPIRouteSetup ( ) : string {
277+ return `## 🌐 GraphQL API Route Setup
278+
279+ **Location**: \`src/app/api/graphql/route.ts\`
280+
281+ \`\`\`typescript
282+ import { createYoga } from 'graphql-yoga';
283+ import { keystoneContext } from '@/keystone/context';
284+ import { auth } from '@/lib/auth';
285+
286+ const { handleRequest } = createYoga({
287+ schema: keystoneContext.graphql.schema,
288+ graphqlEndpoint: '/api/graphql',
289+ fetchAPI: { Response },
290+ context: async (req) => {
291+ // **CRITICAL**: Get NextAuth session
292+ const session = await auth();
293+
294+ // Return Keystone context with session
295+ return keystoneContext.withRequest(req, {
296+ session: session ? {
297+ itemId: session.user.id,
298+ data: session.user,
299+ allowAdminUI: session.allowAdminUI,
300+ } : undefined,
301+ });
302+ },
303+ });
304+
305+ // Protected GraphQL endpoint (requires auth)
306+ export async function GET(req: Request) {
307+ const session = await auth();
308+ if (!session?.allowAdminUI) {
309+ return new Response('Unauthorized', { status: 401 });
310+ }
311+ return handleRequest(req, {});
312+ }
313+
314+ export async function POST(req: Request) {
315+ const session = await auth();
316+ if (!session?.allowAdminUI) {
317+ return new Response('Unauthorized', { status: 401 });
318+ }
319+ return handleRequest(req, {});
320+ }
321+
322+ export async function OPTIONS(req: Request) {
323+ return handleRequest(req, {});
324+ }
325+ \`\`\`
326+
327+ **Critical Points**:
328+ - ✅ Use GraphQL Yoga (not Apollo Server)
329+ - ✅ Integrate NextAuth session with Keystone context
330+ - ✅ Protect GraphQL endpoint with auth middleware
331+ - ✅ Use \`keystoneContext.withRequest()\` to pass session` ;
332+ }
333+
334+ private getCriticalPatterns ( requiresAuth : boolean ) : string [ ] {
335+ const patterns = [
336+ '❌ **NEVER** run Keystone as a separate server - use getContext() only' ,
337+ '✅ **ALWAYS** use \`keystoneContext.graphql.run()\` for GraphQL queries in server actions' ,
338+ '✅ **ALWAYS** use \`keystoneContext.sudo()\` for auth-related queries (bypasses access control)' ,
339+ '✅ **ALWAYS** use Next.js Server Actions for mutations (NOT custom GraphQL resolvers)' ,
340+ '✅ **ALWAYS** validate with Zod schemas before calling Keystone mutations' ,
341+ '✅ **ALWAYS** use \`ResultOf<typeof QUERY>\` for GraphQL type inference (never \`any\`)' ,
342+ '✅ **ALWAYS** use Neon serverless adapter for Vercel deployment' ,
343+ '✅ **ALWAYS** define lists in separate files under \`src/keystone/lists/\`' ,
344+ '✅ **ALWAYS** export all lists from \`src/keystone/schema.ts\`' ,
345+ ] ;
346+
347+ if ( requiresAuth ) {
348+ patterns . push (
349+ '✅ **ALWAYS** sync NextAuth users with Keystone User list' ,
350+ '✅ **ALWAYS** extend NextAuth session with Keystone user data' ,
351+ '✅ **ALWAYS** use \`auth()\` from NextAuth to get session in API routes' ,
352+ ) ;
353+ }
354+
355+ return patterns ;
356+ }
357+
83358 private generateSchemaGuidance ( spec : ParsedSpecification ) : SchemaGuidance [ ] {
84359 const featureName = this . pascalCase ( spec . title ) ;
85360
0 commit comments