-
Notifications
You must be signed in to change notification settings - Fork 0
feat(docs): add AI Coding Agent instructions for universal starter se… #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| # AI Coding Agent instructions – Universal Starter (Server) | ||
|
|
||
| ## 📌 Tech Stack | ||
| - **Framework**: [Elysia](https://elysiajs.com/) | ||
| - **Database ORM**: [Prisma](https://www.prisma.io/) | ||
| - **RPC Layer**: [oRPC](https://orpc.dev/) – lightweight, type-safe RPC between server and client | ||
| - **Authentication**: [better-auth](https://better-auth.vercel.app/) | ||
|
|
||
| --- | ||
|
|
||
| ## 1. General Rules | ||
| - Always **use TypeScript** with strict typing. | ||
| - Keep imports **absolute** from `src/` using path aliases. | ||
| - Always return **typed responses** from RPC handlers and API routes. | ||
|
|
||
| --- | ||
|
|
||
| ## 2. Elysia Guidelines | ||
| - Group related routes in a single Elysia plugin file inside `src/routes/`. | ||
| - When adding middleware: | ||
| - Use `app.use()` for global middleware. | ||
| - Use plugin-scoped middleware for route-specific logic. | ||
|
|
||
| ## 3. Prisma Guidelines | ||
| - Never write raw SQL unless necessary; use Prisma Client for all DB operations. | ||
| - Use select or include to fetch only required fields — avoid findMany() without filters unless needed. | ||
|
|
||
| Example: | ||
|
|
||
| ```typescript | ||
| import { prisma } from '@/src/lib/db'; | ||
|
|
||
| export const userService = { | ||
| create: (data: { name: string; email: string }) => | ||
| prisma.user.create({ data }), | ||
|
|
||
| findByEmail: (email: string) => | ||
| prisma.user.findUnique({ | ||
| where: { email }, | ||
| select: { id: true, name: true, email: true } | ||
| }), | ||
| }; | ||
| ``` | ||
|
|
||
| ## 4. oRPC Guidelines | ||
| - Use typed inputs/outputs for every RPC method. | ||
| - Group related RPCs into a single file and export from index.ts |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,14 +10,18 @@ | |||||||||||||||||||||||||
| "@better-auth/expo": "^1.3.4", | ||||||||||||||||||||||||||
| "@elysiajs/cors": "^1.3.3", | ||||||||||||||||||||||||||
| "@elysiajs/swagger": "^1.3.1", | ||||||||||||||||||||||||||
| "@orpc/openapi": "^1.7.10", | ||||||||||||||||||||||||||
| "@orpc/server": "^1.7.10", | ||||||||||||||||||||||||||
| "@orpc/zod": "^1.7.10", | ||||||||||||||||||||||||||
|
Comment on lines
+13
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainAdd missing peer dependency "zod" and keep ORPC packages aligned @orpc/zod typically peers with zod. I don't see "zod" in dependencies; without it, runtime/type generation can break. Also, keeping all @orpc/* packages on the same version is wise to avoid subtle mismatches. Proposed additions: "dependencies": {
"@elysiajs/swagger": "^1.3.1",
"@orpc/openapi": "^1.7.10",
"@orpc/server": "^1.7.10",
"@orpc/zod": "^1.7.10",
+ "zod": "^3.23.8",
"@plunk/node": "^3.0.3",Run this to verify usage and presence: 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Check if zod is declared in package.json:"
rg -n '"zod"\s*:' package.json || echo "MISSING: zod not declared in package.json"
echo
echo "Scan for zod usage in source:"
rg -n 'from\s+[\'"]zod[\'"]|import\s+\*\s+as\s+z\s+from\s+[\'"]zod[\'"]|@orpc/zod|ZodToJsonSchemaConverter' -S src || echo "No explicit zod-related imports found in src/"Length of output: 432 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Verifying 'zod' in package.json dependencies:"
rg -n '"zod"\s*:' package.json || echo "MISSING: zod not declared in package.json"
echo
echo "Scanning source for @orpc/zod imports:"
if ! rg -n "@orpc/zod" -S src; then
echo "No @orpc/zod imports found"
fi
echo
echo "Scanning source for direct zod imports:"
if ! rg -n "import .*from ['\"]zod['\"]" -S src && ! rg -n "require(['\"]zod['\"])" -S src; then
echo "No direct ES Module or CommonJS imports of zod found"
fiLength of output: 652 Add missing “zod” dependency We’re importing • File: package.json "dependencies": {
"@elysiajs/swagger": "^1.3.1",
"@orpc/openapi": "^1.7.10",
"@orpc/server": "^1.7.10",
"@orpc/zod": "^1.7.10",
+ "zod": "^3.23.8",
"@plunk/node": "^3.0.3",📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| "@plunk/node": "^3.0.3", | ||||||||||||||||||||||||||
| "@prisma/client": "^6.11.1", | ||||||||||||||||||||||||||
| "@prisma/extension-accelerate": "^2.0.1", | ||||||||||||||||||||||||||
| "better-auth": "^1.3.4", | ||||||||||||||||||||||||||
| "elysia": "latest", | ||||||||||||||||||||||||||
| "elysia-rate-limit": "^4.4.0", | ||||||||||||||||||||||||||
| "nodemailer": "^7.0.5", | ||||||||||||||||||||||||||
| "resend": "^4.8.0" | ||||||||||||||||||||||||||
| "resend": "^4.8.0", | ||||||||||||||||||||||||||
| "zod": "^4.0.17" | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| "devDependencies": { | ||||||||||||||||||||||||||
| "@biomejs/biome": "2.0.6", | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import { Elysia } from "elysia"; | |
| import { auth } from "@/src/lib/auth"; | ||
| import { isOriginAllowed } from "./cors"; | ||
| import { OpenAPI } from "./lib/open-api"; | ||
| import { orpcHandler } from "./orpc"; | ||
|
|
||
| const app = new Elysia() | ||
| .use( | ||
|
|
@@ -55,6 +56,23 @@ const app = new Elysia() | |
| }, | ||
| }, | ||
| ) | ||
| // Orpc | ||
| .all( | ||
| "/orpc/*", | ||
| async ({ request }: { request: Request }) => { | ||
| const { response } = await orpcHandler.handle(request, { | ||
| prefix: "/orpc", | ||
| context: { | ||
| headers: request.headers, | ||
| }, | ||
| }); | ||
|
|
||
| return response ?? new Response("Not Found", { status: 404 }); | ||
| }, | ||
| { | ||
| parse: "none", | ||
| }, | ||
| ) | ||
|
Comment on lines
+59
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CORS duplication/mismatch: Elysia CORS vs ORPC CORSPlugin Global CORS is already applied via @elysiajs/cors, while orpcHandler also installs a CORSPlugin. This can set duplicate and potentially conflicting CORS headers. Additionally, @elysiajs/cors methods exclude HEAD/PATCH but ORPC allows them, risking failed preflight for PATCH. Action:
I’ll propose the CORSPlugin removal in src/orpc/index.ts. For this file, adjust Elysia CORS config to include methods: methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"]🤖 Prompt for AI Agents
Comment on lines
+59
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Avoid double CORS; centralize policy and ensure OPTIONS is handled.
Also ensure OPTIONS is allowed for preflight on ORPC (missing in allowMethods in src/orpc/index.ts). See diff there. |
||
| .listen(3000); | ||
|
|
||
| console.log( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { OpenAPIHandler } from "@orpc/openapi/fetch"; | ||
| import { OpenAPIReferencePlugin } from "@orpc/openapi/plugins"; | ||
| // import { CORSPlugin } from "@orpc/server/plugins"; | ||
| import { ZodToJsonSchemaConverter } from "@orpc/zod"; | ||
| import { appRouter } from "./routers"; | ||
|
|
||
| export const orpcHandler = new OpenAPIHandler(appRouter, { | ||
| plugins: [ | ||
| // new CORSPlugin({ | ||
| // origin: (origin, options) => origin, | ||
| // allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"], | ||
| // }), | ||
| new OpenAPIReferencePlugin({ | ||
| schemaConverters: [new ZodToJsonSchemaConverter()], | ||
| specGenerateOptions: { | ||
| info: { | ||
| title: "ORPC Playground", | ||
| version: "1.0.0", | ||
| }, | ||
| components: { | ||
| securitySchemes: { | ||
| bearerAuth: { | ||
| type: "http", | ||
| scheme: "bearer", | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }), | ||
| ], | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import { ORPCError, os } from "@orpc/server"; | ||
| import { auth } from "@/src/lib/auth"; | ||
|
|
||
| type BAUser = typeof auth.$Infer.Session.user | null; | ||
| type BASession = typeof auth.$Infer.Session.session | null; | ||
|
|
||
|
Comment on lines
+4
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Type extraction for BASession/BAUser is brittle Using typeof on auth.$Infer.Session parts is fragile. Derive types from getSession to stay in sync with the auth API. -type BAUser = typeof auth.$Infer.Session.user | null;
-type BASession = typeof auth.$Infer.Session.session | null;
+type SessionResponse = Awaited<ReturnType<typeof auth.api.getSession>>;
+type BAUser = SessionResponse["user"] | null;
+type BASession = SessionResponse["session"] | null;🤖 Prompt for AI Agents
Comment on lines
+4
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove sensitive logging and harden session typing and retrieval.
Suggested changes: - type BAUser = typeof auth.$Infer.Session.user | null;
- type BASession = typeof auth.$Infer.Session.session | null;
+ type GetSessionResult = Awaited<ReturnType<typeof auth.api.getSession>>;
+ type BAUser = GetSessionResult["user"] | null;
+ type BASession = GetSessionResult["session"] | null;- console.log("withBetterAuth context headers", headers);
-
- // Server-side Better Auth API (no client in middlewares)
- const sessionData = await auth.api.getSession({ headers }); // server API call
- console.log("withBetterAuth sessionData", sessionData);
+ // Server-side Better Auth API (no client in middlewares)
+ const sessionData = await auth.api
+ .getSession({ headers })
+ .catch(() => null); // fail closed without leaking- console.log({ context });Also applies to: 26-31, 45-45 |
||
| export type InputCtx = { | ||
| // initial context must provide request headers | ||
| headers: Headers | Record<string, string>; | ||
| }; | ||
|
|
||
| export type AuthCtx = InputCtx & { | ||
| // injected by middleware | ||
| user: BAUser; | ||
| session: BASession; | ||
| }; | ||
|
|
||
| export const withBetterAuth = os | ||
| .$context<InputCtx>() | ||
| .middleware(async ({ context, next }) => { | ||
| const headers = | ||
| context.headers instanceof Headers | ||
| ? context.headers | ||
| : new Headers(context.headers); | ||
|
|
||
| // Server-side Better Auth API (no client in middlewares) | ||
| const sessionData = await auth.api.getSession({ headers }); // server API call | ||
|
|
||
| return next({ | ||
| context: { | ||
| // preserve existing context and inject auth data | ||
| ...context, | ||
| session: sessionData?.session || null, | ||
| user: sessionData?.user || null, | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| export const requireAuth = os | ||
| .$context<AuthCtx>() | ||
| .middleware(async ({ context, next }) => { | ||
| if (!context.session || !context.user) { | ||
| throw new ORPCError("UNAUTHORIZED", { message: "Unauthenticated" }); | ||
| } | ||
| return next({ context }); | ||
| }); | ||
|
|
||
| export const requireRole = (roles: string[]) => | ||
| os.$context<AuthCtx>().middleware(async ({ context, next }) => { | ||
| const rolesValue = (context.user as unknown as { roles?: unknown })?.roles; | ||
| const userRoles: string[] = Array.isArray(rolesValue) | ||
| ? (rolesValue as string[]) | ||
| : []; | ||
| if (!roles.some((r) => userRoles.includes(r))) { | ||
| throw new ORPCError("FORBIDDEN", { message: "Forbidden" }); | ||
| } | ||
|
Comment on lines
+50
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Stricter role extraction and validation Avoid double type assertions; ensure roles is an array of strings before using includes. - const rolesValue = (context.user as unknown as { roles?: unknown })?.roles;
- const userRoles: string[] = Array.isArray(rolesValue)
- ? (rolesValue as string[])
- : [];
+ const rolesValue = context.user && (context.user as any).roles;
+ const userRoles: string[] =
+ Array.isArray(rolesValue) && rolesValue.every((r) => typeof r === "string")
+ ? (rolesValue as string[])
+ : [];🤖 Prompt for AI Agents |
||
| return next({ context }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { os } from "@orpc/server"; | ||
| import { prisma } from "@/src/lib/db"; | ||
|
|
||
| export const dbProviderMiddleware = os | ||
| .$context<{ | ||
| db?: typeof prisma; | ||
| }>() | ||
| .middleware(async ({ context, next }) => { | ||
| const db = context.db ?? prisma; | ||
|
|
||
| return next({ | ||
| context: { | ||
| // preserve existing context while injecting db | ||
| ...context, | ||
| db, | ||
| }, | ||
| }); | ||
| }); | ||
|
Comment on lines
+5
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Make db required post-middleware and align types for stronger safety.
Option 1 (recommended): make db required after this middleware -export const dbProviderMiddleware = os
- .$context<{
- db?: typeof prisma;
- }>()
- .middleware(async ({ context, next }) => {
- const db = context.db ?? prisma;
- return next({
- context: {
- // preserve existing context while injecting db
- ...context,
- db,
- },
- });
- });
+export const dbProviderMiddleware = os
+ .$context<{}>()
+ .middleware(async ({ context, next }) => {
+ // If you still want to honor an upstream-injected db (eg tests), read it
+ const maybeDb = (context as { db?: typeof prisma }).db;
+ const db = maybeDb ?? prisma;
+ return next({
+ context: {
+ ...context,
+ db, // now guaranteed present for downstream handlers
+ },
+ });
+ });Option 2: keep current shape but export a shared type alias to avoid value-coupled types
Either option improves type clarity; Option 1 also improves ergonomics in downstream code. 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { os } from "@orpc/server"; | ||
| import { withBetterAuth } from "./auth.middleware"; | ||
| import { dbProviderMiddleware } from "./db.middleware"; | ||
|
|
||
| export const baseMiddleware = os | ||
| .$context<{ headers: Headers | Record<string, string> }>() | ||
| .use(dbProviderMiddleware) | ||
| .use(withBetterAuth); | ||
|
Comment on lines
+5
to
+8
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove PII-heavy logging in withBetterAuth. The composed withBetterAuth (see src/orpc/middleware/auth.middleware.ts) logs headers and session data. This can leak PII and secrets to logs. To fix in src/orpc/middleware/auth.middleware.ts, remove console logs: - console.log("withBetterAuth context headers", headers);
...
- console.log("withBetterAuth sessionData", sessionData);
🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { ORPCError } from "@orpc/server"; | ||
| import { z } from "zod"; | ||
| import { baseMiddleware } from "../../middleware"; | ||
| import { requireAuth } from "../../middleware/auth.middleware"; | ||
|
|
||
| export const meProcedure = baseMiddleware | ||
| .use(requireAuth) | ||
| .route({ method: "GET", path: "/user/me" }) | ||
| .output( | ||
| z.object({ | ||
| id: z.string(), | ||
| email: z.string(), | ||
| name: z.string().nullable(), | ||
| }), | ||
| ) | ||
| .handler(({ context }) => { | ||
| const { user } = context; | ||
| if (!user) { | ||
| // Should be unreachable because of requireAuth, but keeps types narrow | ||
| throw new ORPCError("UNAUTHORIZED", { message: "Unauthenticated" }); | ||
| } | ||
| return { | ||
| id: user.id, | ||
| email: user.email, | ||
| name: user.name ?? null, | ||
| }; | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add zod dependency to satisfy @orpc/zod peer requirement
@orpc/zod typically expects zod as a peer dependency. Without zod installed you'll get peer warnings or runtime import errors when using @orpc/zod validators.
Suggested addition:
"dependencies": { "@elysiajs/swagger": "^1.3.1", "@orpc/openapi": "^1.7.10", "@orpc/server": "^1.7.10", "@orpc/zod": "^1.7.10", + "zod": "^3.23.8", "@plunk/node": "^3.0.3",📝 Committable suggestion
🤖 Prompt for AI Agents