Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/copilot-instructions.md
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
50 changes: 48 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@orpc/openapi": "^1.7.10",
"@orpc/server": "^1.7.10",
"@orpc/zod": "^1.7.10",
"@orpc/openapi": "^1.7.10",
"@orpc/server": "^1.7.10",
"@orpc/zod": "^1.7.10",
"zod": "^3.23.8",
🤖 Prompt for AI Agents
In package.json around lines 13 to 15, @orpc/zod is listed but zod itself is
missing; add zod as a direct dependency (matching the major version expected by
@orpc/zod, e.g. a ^3.x release) to the dependencies block so the peer
requirement is satisfied and runtime/peer warnings are avoided.

Comment on lines +13 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Add 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"
fi

Length of output: 652


Add missing “zod” dependency

We’re importing ZodToJsonSchemaConverter from @orpc/zod (src/orpc/index.ts) and z from "zod" (src/orpc/procedures/user/me.p.ts), but zod isn’t declared in package.json. To satisfy the peer dependency and prevent runtime/type-generation breaks, add it alongside the aligned ORPC packages (all at ^1.7.10).

• File: package.json
• Section: dependencies

   "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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@orpc/openapi": "^1.7.10",
"@orpc/server": "^1.7.10",
"@orpc/zod": "^1.7.10",
"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",
// ...other dependencies...
}
🤖 Prompt for AI Agents
In package.json around lines 13 to 15, add the missing "zod" dependency to the
dependencies section to satisfy the peer import and align versions; add an entry
"zod": "^1.7.10" alongside "@orpc/openapi", "@orpc/server", and "@orpc/zod" so
the package declares zod at the same ^1.7.10 version.

"@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",
Expand Down
18 changes: 18 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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:

  • Prefer a single CORS source. Remove CORSPlugin from orpcHandler (recommended), or align both to use the same allowlist.
  • Align Elysia CORS to include HEAD and PATCH.

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
In src/index.ts around lines 59 to 75, global CORS applied via @elysiajs/cors
currently omits HEAD and PATCH while the orpcHandler installs its own CORSPlugin
causing duplicate/conflicting headers; remove the duplicate by keeping a single
CORS source and, for this file, update the Elysia CORS config to include the
missing methods so preflight covers ORPC: set methods to
["GET","POST","PUT","DELETE","OPTIONS","HEAD","PATCH"] and ensure no other
ORPC-specific CORSPlugin is applied elsewhere.

Comment on lines +59 to +75

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid double CORS; centralize policy and ensure OPTIONS is handled.

  • The app already applies @elysiajs/cors globally. The ORPC handler also enables CORS via CORSPlugin (see src/orpc/index.ts). Two CORS layers can emit conflicting headers (e.g., Access-Control-Allow-Origin), especially since ORPC currently echoes any origin, while the app uses isOriginAllowed.
  • Recommendation: choose a single source of truth for CORS.
    • Option A (simplest): Remove CORSPlugin from ORPC and rely on app-level CORS.
    • Option B: Reuse the same origin check in ORPC (see suggested diff in src/orpc/index.ts comment) and consider scoping app-level CORS to non-/orpc routes if needed.

Also ensure OPTIONS is allowed for preflight on ORPC (missing in allowMethods in src/orpc/index.ts). See diff there.

.listen(3000);

console.log(
Expand Down
1 change: 0 additions & 1 deletion src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export const auth = betterAuth({
},
emailVerification: {
sendVerificationEmail: async ({ user, url, token }, request) => {
console.log("Sending verification email to:", user.email);
await emailService.sendEmailVerification(user.email, url);
},
sendOnSignUp: true,
Expand Down
1 change: 0 additions & 1 deletion src/lib/email/email-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ class EmailService {
try {
// Validate required fields
this.validateOptions(options);
console.log("Sending email with options:", options);
return await this.provider.sendEmail(options);
} catch (error) {
console.error("Error sending email:", error);
Expand Down
2 changes: 0 additions & 2 deletions src/lib/email/providers/resend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ export class ResendProvider implements EmailProvider {
})),
});

console.log("Email sent successfully:", result.data?.id);

return {
success: true,
messageId: result.data?.id,
Expand Down
31 changes: 31 additions & 0 deletions src/orpc/index.ts
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",
},
},
},
},
}),
],
});
58 changes: 58 additions & 0 deletions src/orpc/middleware/auth.middleware.ts
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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
In src/orpc/middleware/auth.middleware.ts around lines 4 to 6, the current type
extraction using typeof on auth.$Infer.Session is brittle; replace it by
deriving types from the actual getSession function so they stay in sync with the
auth API: import getSession, use Awaited<ReturnType<typeof getSession>> to
obtain the resolved session shape, then declare BASession as that resolved
type's session field (plus null if needed) and BAUser as the resolved type's
user field (plus null if needed); update imports accordingly and remove the
existing typeof-based type aliases.

Comment on lines +4 to +6

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove sensitive logging and harden session typing and retrieval.

  • Avoid logging headers or full context; they can include Authorization tokens, cookies, and PII.
  • Infer types from getSession robustly via Awaited<ReturnType<...>>.
  • Make getSession resilient; treat failures as unauthenticated instead of throwing.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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
In src/orpc/middleware/auth.middleware.ts around lines 54 to 60, the code uses a
double type assertion and assumes roles is already an array of strings; change
it to safely extract and validate roles: read context.user.roles with a single
type-safe check, verify Array.isArray(rolesValue) and that every element is a
string (filter or map to strings only), then assign userRoles to that validated
array (or empty array if invalid), and keep the existing permission check; this
removes the unsafe cast and ensures includes() operates on a true string[].

return next({ context });
});
18 changes: 18 additions & 0 deletions src/orpc/middleware/db.middleware.ts
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.

  • Today db is declared optional in the input context, but you always inject a value. Keeping it optional forces downstream code to treat it as possibly undefined.
  • Prefer: empty input context and inject a required db, or keep optional on input but ensure the output context has db as required.

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

  • In src/lib/db.ts add:
    export type DbClient = typeof prisma
  • Then here use import type { DbClient } from "@/src/lib/db" and declare db?: DbClient.

Either option improves type clarity; Option 1 also improves ergonomics in downstream code.

🤖 Prompt for AI Agents
In src/orpc/middleware/db.middleware.ts around lines 5 to 18, the middleware
currently types the incoming context as having an optional db but always injects
a prisma instance, which forces downstream code to treat db as possibly
undefined; change the types so the middleware produces a context with a required
db: either (recommended) update the middleware generic to accept an empty input
context and return context with db: typeof prisma (make db required for
downstream handlers), or (alternate) add a shared DbClient type in src/lib/db.ts
(export type DbClient = typeof prisma), import it here and change the
middleware/output context to have db: DbClient (non-optional); ensure the
middleware signature and returned context type reflect db as required so
downstream code no longer needs null checks.

8 changes: 8 additions & 0 deletions src/orpc/middleware/index.ts
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/orpc/middleware/auth.middleware.ts (where withBetterAuth is implemented,
the spots that log headers and session data), remove any
console.log/console.debug/console.info calls that print request headers, session
objects, tokens or other PII/secrets; instead either remove the logs entirely or
replace them with a non-PII message (e.g., "auth middleware invoked") or a log
that only records a correlation id or masked fields. Ensure no other Logger
calls in that file emit full header or session contents and, if needed, mask
sensitive fields before logging.

27 changes: 27 additions & 0 deletions src/orpc/procedures/user/me.p.ts
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,
};
});
Loading