Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ jobs:
- name: Check formatting
run: pnpm format:check

- name: Generate Prisma Client
run: pnpm --filter @devradar/server db:generate
env:
DATABASE_URL: 'postgresql://user:pass@localhost:5432/db'
SHADOW_DATABASE_URL: 'postgresql://user:pass@localhost:5432/shadow'

- name: Lint
run: pnpm lint

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ coverage/

# Docker
docker-compose.override.yml
apps/server/src/generated/

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider moving this rule to a more appropriate section.

The apps/server/src/generated/ ignore rule is placed under the "Docker" section, but it's for Prisma-generated code artifacts. Consider creating a dedicated "Generated files" section or placing it near other build/generated output rules (lines 5-10).

🤖 Prompt for AI Agents
In .gitignore around line 48, the apps/server/src/generated/ entry is currently
grouped under the "Docker" section but it is a Prisma/generated artifact; move
this line out of the Docker group and place it under a new or existing
"Generated files" or "Build artifacts" section (near lines 5-10 where other
generated/build outputs live) so generated code rules are organized together.

8 changes: 8 additions & 0 deletions apps/server/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import baseConfig from '@devradar/eslint-config';

export default [
...baseConfig,
{
ignores: ['prisma.config.ts', 'prisma/generated/**'],
},
];
51 changes: 51 additions & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "@devradar/server",
"version": "0.0.0",
"private": true,
"license": "MIT",
"type": "module",
"main": "./dist/server.js",
Comment on lines +1 to +7

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 | 🟠 Major

Add Node.js version constraint in engines field.

The package uses modern features (ESM, top-level await implied by Prisma/Fastify setup, TypeScript 5.9) but lacks an explicit Node.js version requirement. Add an engines field to enforce compatibility:

"engines": {
  "node": ">=20.0.0"
}

This prevents runtime issues when the codebase uses features unavailable in older Node.js versions.

🤖 Prompt for AI Agents
In apps/server/package.json around lines 1 to 7, there is no Node.js version
constraint; add an "engines" field to require a compatible Node runtime (e.g.
"node": ">=20.0.0"). Update the package.json by inserting the engines object at
the top level (near name/version/private) to enforce Node >=20 so
ESM/top-level-await/TS5.9 features run correctly.

"scripts": {
"dev": "tsx watch --env-file=.env src/server.ts",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Redundant environment loading with tsx --env-file and dotenv dependency.

The dev script uses tsx watch --env-file=.env (line 9), which loads environment variables natively. However, dotenv is also listed as a dependency (line 31). If your application code explicitly calls dotenv.config(), this creates redundant environment loading.

Consider removing the dotenv dependency if tsx --env-file is sufficient for development, or document why both are needed (e.g., production runtime requires dotenv).

🤖 Prompt for AI Agents
In apps/server/package.json around line 9, the dev script uses "tsx watch
--env-file=.env" which duplicates environment loading if the project also has
dotenv as a dependency (line ~31); either remove the redundant dotenv dependency
from package.json or keep the dependency but remove any explicit dotenv.config()
calls in your code, or add a comment in package.json (or README) explaining why
both are needed (e.g., dotenv required at production runtime). Make the change
consistent: if you remove dotenv from package.json, also remove any dotenv
imports/usages; if you keep it, document the rationale.

"build": "tsc",
"start": "node dist/server.js",
"check-types": "tsc --noEmit",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"clean": "rimraf dist",
"db:generate": "prisma generate",
"db:push": "prisma db push",
"db:migrate": "prisma migrate dev",
"db:studio": "prisma studio"
Comment on lines +8 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Test script is a placeholder.

Line 35 in the test script outputs "No tests yet" and exits with code 0. For a backend foundation handling authentication, WebSockets, and database operations, comprehensive test coverage is essential.

Would you like me to open an issue to track setting up a test framework (e.g., Vitest, Jest, or Node's built-in test runner) and writing tests for critical paths (auth flows, WebSocket handlers, database operations)?

🤖 Prompt for AI Agents
In apps/server/package.json around lines 8 to 19, the "test" script is currently
a placeholder that prints "No tests yet"; replace it with a real test setup:
choose a test runner (e.g., Vitest or Jest), add the appropriate
devDependencies, and update package.json scripts to run the test runner (e.g.,
"test": "vitest" or "test": "jest --runInBand") plus a coverage script. Add
commands for running tests in watch/CI modes if needed. Create a tests/ folder
and add focused tests for critical paths: auth flows (login, token refresh,
error cases), WebSocket handlers (connect, message routing, disconnect, error
handling), and database operations (CRUD, migrations, and transactional
behavior) and ensure tests run against a test database (use sqlite in-memory or
a test container) and add a CI step to run the test script.

},
"dependencies": {
"@devradar/shared": "workspace:*",
"@fastify/cookie": "^11.0.2",
"@fastify/cors": "^11.2.0",
"@fastify/helmet": "^13.0.2",
"@fastify/jwt": "^10.0.0",
"@fastify/rate-limit": "^10.3.0",
"@fastify/websocket": "^11.2.0",
"@prisma/adapter-pg": "^7.2.0",
"@prisma/client": "^7.2.0",
Comment on lines +29 to +30

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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd apps/server && head -50 package.json | cat -n

Repository: senutpal/devRadar

Length of output: 1778


🌐 Web query:

Prisma 7.2.0 latest stable version 2025

💡 Result:

Yes — Prisma ORM 7.2.0 is the latest stable release as of January 2, 2026. It was published December 17, 2025. [1][2]

Sources:

  • GitHub release: prisma/prisma — 7.2.0 (Dec 17, 2025). [1]
  • Prisma blog announcement: "Announcing Prisma ORM 7.2.0" (Dec 17, 2025). [2]

🌐 Web query:

@prisma/adapter-pg version 7.2.0 known issues

💡 Result:

Short summary of known issues affecting @prisma/adapter-pg (v7.x, including 7.2.0):

  • It can modify node-postgres (pg) global type parsers and break direct pg Pool/Client usage. [1]
  • Using the adapter with SSL connection params (e.g., ?sslmode=require) has produced misleading P2010 errors originating from node-postgres. [2]
  • Some users report Prisma Migrate failures when the adapter is configured via prisma.config.ts (migrate/column type errors). [3]
  • In some setups the adapter’s dependency handling for pg/packaging can cause runtime/module resolution problems (missing or peer-declared pg causing runtime errors in certain bundlers/monorepos). [4]
  • There are additional repo issues about schema/search_path handling and other edge cases after migrating to the new Prisma v7 adapter model. [5]

If you want, I can:

  • check the exact changelog for v7.2.0 and list which of these were fixed or remain open, or
  • search for workarounds/fixes for any specific issue above.

Sources:
[1] GitHub: “@prisma/adapter-pg modifies node-postgres global type parsers.” (github.com)
[2] GitHub: “node-postgres (pg) errors with misleading P2010 … with SSL” (github.com)
[3] GitHub: “Prisma Migrate fails when using @prisma/adapter-pg through prisma.config.ts.” (github.com)
[4] GitHub issue: “pg is a peer-dependency of @prisma/adapter-pg when it should be a regular dependency.” (github.com)
[5] GitHub: “Inconsistent behavior when setting Postgres schema … with @prisma/adapter-pg” and related threads. (github.com)

Citations:


Versions are synchronized correctly, but be aware of known issues with @prisma/adapter-pg.

All three Prisma packages are correctly pinned to version 7.2.0, which is the latest stable release (published December 17, 2025). The synchronization across @prisma/adapter-pg (line 29), @prisma/client (line 30), and prisma CLI (line 46) is correct.

However, there are several known issues with @prisma/adapter-pg in the v7.x line that the team should monitor:

  • Global type parser modifications affecting direct pg usage
  • SSL connection parameter handling can produce misleading P2010 errors
  • Prisma Migrate failures in some configurations
  • Peer dependency handling for pg causing potential runtime issues

Keep these in mind during development and testing, and watch for updates addressing these edge cases.

"dotenv": "^16.5.0",
"fastify": "^5.6.2",
"ioredis": "^5.8.2",
"pg": "^8.16.3",
"pino": "^10.1.0",
"pino-pretty": "^13.1.3",
"zod": "^3.24.0"
},
"devDependencies": {
"@devradar/eslint-config": "workspace:*",
"@devradar/tsconfig": "workspace:*",
"@types/node": "^25.0.3",
"@types/pg": "^8.16.0",
"@types/ws": "^8.18.1",
"eslint": "^9.39.2",
"prisma": "^7.2.0",
"rimraf": "^6.1.2",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
}
}
27 changes: 27 additions & 0 deletions apps/server/prisma.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Prisma Configuration
*
* Prisma 7 configuration file for CLI commands (migrate, db push, etc.)
* Database URL for migrations is configured here.
*
* IMPORTANT: In Prisma 7, env vars must be explicitly loaded with dotenv.
*/

import 'dotenv/config';
import { defineConfig, env } from 'prisma/config';

export default defineConfig({
// Path to the Prisma schema file
schema: 'prisma/schema.prisma',

// Migration settings
migrations: {
path: 'prisma/migrations',
},

// Database connection for Prisma CLI (migrate, db push, studio)
datasource: {
url: env('DATABASE_URL'),
shadowDatabaseUrl: env('SHADOW_DATABASE_URL'),
},
});
Comment on lines +10 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Configuration looks good, but consider aligning environment validation.

The Prisma 7 configuration is correctly set up with the adapter pattern. However, SHADOW_DATABASE_URL is referenced here but not validated in apps/server/src/config.ts. This means:

  1. The app won't fail fast if SHADOW_DATABASE_URL is missing/invalid
  2. Prisma CLI commands like migrate dev may fail at runtime with unclear errors
🔎 Suggested addition to config.ts
 // Database
 DATABASE_URL: z
   .string()
   .url()
   .refine((url: string) => url.startsWith('postgresql://') || url.startsWith('postgres://'), {
     message: 'DATABASE_URL must be a valid PostgreSQL connection string',
   }),
+
+ // Shadow database (optional, used for Prisma migrations)
+ SHADOW_DATABASE_URL: z
+   .string()
+   .url()
+   .refine((url: string) => url.startsWith('postgresql://') || url.startsWith('postgres://'), {
+     message: 'SHADOW_DATABASE_URL must be a valid PostgreSQL connection string',
+   })
+   .optional(),

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

🤖 Prompt for AI Agents
In apps/server/prisma.config.ts around lines 10 to 27, the Prisma config
references SHADOW_DATABASE_URL but apps/server/src/config.ts doesn't validate
it; update apps/server/src/config.ts to read and validate SHADOW_DATABASE_URL
alongside DATABASE_URL (e.g., using same env helper or schema validation you
already use), throw a clear error or exit when it's missing/invalid (or
conditionally require it only in dev/migrate mode), and export the validated
value so prisma.config.ts can consume a guaranteed-good SHADOW_DATABASE_URL;
ensure the validation message references the variable name and suggests how to
set it.

110 changes: 110 additions & 0 deletions apps/server/prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// DevRadar Database Schema
// Using Prisma 7 with PostgreSQL

generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
moduleFormat = "esm"
}

datasource db {
provider = "postgresql"
// Note: URL configured in prisma.config.ts for Prisma 7
}
Comment on lines +10 to +13

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 | 🔴 Critical

Add datasource URL configuration.

The datasource is missing the url property. Prisma requires either a direct URL or an environment variable reference for database connection.

🔎 Proposed fix
 datasource db {
   provider = "postgresql"
+  url      = env("DATABASE_URL")
 }
📝 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
datasource db {
provider = "postgresql"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
🤖 Prompt for AI Agents
In apps/server/prisma/schema.prisma around lines 10 to 12, the datasource block
is missing the required url property; add a url field that references your
environment variable (for example url = env("DATABASE_URL")) so Prisma can
connect to the PostgreSQL database, and optionally include a shadowDatabaseUrl =
env("SHADOW_DATABASE_URL") if you use migrations with a separate shadow DB.


// ===================
// User Management
// ===================

model User {
id String @id @default(cuid())
githubId String @unique
username String
displayName String?
avatarUrl String?
email String?
tier Tier @default(FREE)
privacyMode Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

// Relations
following Follow[] @relation("Following")
followers Follow[] @relation("Followers")
teams TeamMember[]
ownedTeams Team[] @relation("TeamOwner")

@@index([username])
@@index([githubId])
}

// ===================
// Social Graph
// ===================

model Follow {
followerId String
followingId String
createdAt DateTime @default(now())

// Relations
follower User @relation("Following", fields: [followerId], references: [id], onDelete: Cascade)
following User @relation("Followers", fields: [followingId], references: [id], onDelete: Cascade)

@@id([followerId, followingId])
@@index([followerId])
@@index([followingId])
}
Comment on lines +45 to +57

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider using a composite primary key instead of a separate ID.

The Follow model has a unique constraint on (followerId, followingId) but also defines a separate id field. Since this is a junction table, a composite primary key would be more idiomatic and efficient.

🔎 Proposed refactor
 model Follow {
-  id          String   @id @default(cuid())
   followerId  String
   followingId String
   createdAt   DateTime @default(now())

   // Relations
   follower  User @relation("Following", fields: [followerId], references: [id], onDelete: Cascade)
   following User @relation("Followers", fields: [followingId], references: [id], onDelete: Cascade)

-  @@unique([followerId, followingId])
+  @@id([followerId, followingId])
   @@index([followerId])
   @@index([followingId])
 }

Note: This would require updating routes/friends.ts to use the composite key directly instead of follow.id in the delete operation.

📝 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
model Follow {
id String @id @default(cuid())
followerId String
followingId String
createdAt DateTime @default(now())
// Relations
follower User @relation("Following", fields: [followerId], references: [id], onDelete: Cascade)
following User @relation("Followers", fields: [followingId], references: [id], onDelete: Cascade)
@@unique([followerId, followingId])
@@index([followerId])
@@index([followingId])
}
model Follow {
followerId String
followingId String
createdAt DateTime @default(now())
// Relations
follower User @relation("Following", fields: [followerId], references: [id], onDelete: Cascade)
following User @relation("Followers", fields: [followingId], references: [id], onDelete: Cascade)
@@id([followerId, followingId])
@@index([followerId])
@@index([followingId])
}
🤖 Prompt for AI Agents
In apps/server/prisma/schema.prisma lines ~43-56, replace the surrogate id with
a composite primary key on (followerId, followingId): remove the id field, keep
followerId and followingId as required String fields, add @@id([followerId,
followingId]) and keep the existing relations and indexes as needed; then update
routes/friends.ts where deletes reference follow.id to delete by the composite
key instead (use the Prisma delete/find unique call that targets the composite
key using both followerId and followingId).


// ===================
// Team Management
// ===================

model Team {
id String @id @default(cuid())
name String
slug String @unique
ownerId String
tier Tier @default(TEAM)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

// Relations
owner User @relation("TeamOwner", fields: [ownerId], references: [id], onDelete: Cascade)
members TeamMember[]

@@index([slug])
@@index([ownerId])
}
Comment on lines +63 to +78

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding a relation from Team to User for the owner.

The ownerId field lacks a corresponding relation to User, which means:

  1. No referential integrity enforcement at the database level
  2. No cascading behavior when a user is deleted
  3. Potential for orphaned teams
🔎 Proposed fix
 model Team {
   id        String   @id @default(cuid())
   name      String
   slug      String   @unique
   ownerId   String
   tier      Tier     @default(TEAM)
   createdAt DateTime @default(now())
   updatedAt DateTime @updatedAt

   // Relations
+  owner   User         @relation("OwnedTeams", fields: [ownerId], references: [id], onDelete: Cascade)
   members TeamMember[]

   @@index([slug])
   @@index([ownerId])
 }

Also add the inverse relation on User:

 model User {
   // ... existing fields ...

   // Relations
   following Follow[] @relation("Following")
   followers Follow[] @relation("Followers")
   teams     TeamMember[]
+  ownedTeams Team[] @relation("OwnedTeams")
   // ...
 }
📝 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
model Team {
id String @id @default(cuid())
name String
slug String @unique
ownerId String
tier Tier @default(TEAM)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
members TeamMember[]
@@index([slug])
@@index([ownerId])
}
model Team {
id String @id @default(cuid())
name String
slug String @unique
ownerId String
tier Tier @default(TEAM)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
owner User @relation("OwnedTeams", fields: [ownerId], references: [id], onDelete: Cascade)
members TeamMember[]
@@index([slug])
@@index([ownerId])
}
🤖 Prompt for AI Agents
In apps/server/prisma/schema.prisma around lines 62-76, the Team model has
ownerId but no relation to User; add a relation field on Team (e.g., owner User)
that uses @relation(fields: [ownerId], references: [id], onDelete: Cascade) to
enforce referential integrity and cascade deletes, and add the inverse relation
field on the User model (e.g., teams or ownedTeams of type Team[]) with the same
relation name so Prisma understands the bidirectional relationship.


model TeamMember {
id String @id @default(cuid())
userId String
teamId String
role Role @default(MEMBER)
joinedAt DateTime @default(now())

// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)

@@unique([userId, teamId])
@@index([userId])
@@index([teamId])
}
Comment on lines +80 to +94

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider using composite primary key for TeamMember.

Similar to the Follow model refactor, TeamMember could use @@id([userId, teamId]) instead of a separate id field since the combination is already unique. This would be more consistent with the junction table pattern used for Follow.

🔎 Suggested refactor
 model TeamMember {
-  id        String   @id @default(cuid())
   userId    String
   teamId    String
   role      Role     @default(MEMBER)
   joinedAt  DateTime @default(now())

   // Relations
   user User @relation(fields: [userId], references: [id], onDelete: Cascade)
   team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)

-  @@unique([userId, teamId])
+  @@id([userId, teamId])
   @@index([userId])
   @@index([teamId])
 }
📝 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
model TeamMember {
id String @id @default(cuid())
userId String
teamId String
role Role @default(MEMBER)
joinedAt DateTime @default(now())
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
@@unique([userId, teamId])
@@index([userId])
@@index([teamId])
}
model TeamMember {
userId String
teamId String
role Role @default(MEMBER)
joinedAt DateTime @default(now())
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
@@id([userId, teamId])
@@index([userId])
@@index([teamId])
}
🤖 Prompt for AI Agents
In apps/server/prisma/schema.prisma around lines 80 to 94, replace the
single-column surrogate primary key with a composite primary key on userId and
teamId: remove the id field and add @@id([userId, teamId]); also remove the
now-redundant @@unique([userId, teamId]) and drop any duplicate indexes for
userId/teamId (or keep single-column indexes only if needed for queries). Ensure
the relation definitions remain unchanged and that any code/migrations
referencing TeamMember.id are updated to use the composite key.


// ===================
// Enums
// ===================

enum Tier {
FREE
PRO
TEAM
}

enum Role {
OWNER
ADMIN
MEMBER
}
90 changes: 90 additions & 0 deletions apps/server/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* Environment Configuration
*
* Validates and exports environment variables using Zod.
* Follows 12-factor app principles - all config from environment.
*/

import { z } from 'zod';

/**
* Environment schema with validation rules.
* All required variables must be set or have sensible defaults.
*/
const envSchema = z.object({
// Node environment
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),

// Server
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
HOST: z.string().default('0.0.0.0'),

// Database
DATABASE_URL: z
.string()
.url()
.refine((url: string) => url.startsWith('postgresql://') || url.startsWith('postgres://'), {
message: 'DATABASE_URL must be a valid PostgreSQL connection string',
}),

// Redis
REDIS_URL: z
.string()
.url()
.refine((url: string) => url.startsWith('redis://') || url.startsWith('rediss://'), {
message: 'REDIS_URL must be a valid Redis connection string',
}),

// JWT
JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'),
JWT_EXPIRES_IN: z.string().default('7d'),

// GitHub OAuth
GITHUB_CLIENT_ID: z.string().min(1, 'GITHUB_CLIENT_ID is required'),
GITHUB_CLIENT_SECRET: z.string().min(1, 'GITHUB_CLIENT_SECRET is required'),
GITHUB_CALLBACK_URL: z.string().url(),

// Logging
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
});

/**
* Parsed and validated environment configuration.
*/
export type Env = z.infer<typeof envSchema>;

/**
* Parse and validate environment variables.
* Throws descriptive error if validation fails.
*/
function parseEnv(): Env {
const result = envSchema.safeParse(process.env);

if (!result.success) {
console.error('❌ Invalid environment variables:\n', result.error.format());
throw new Error('Invalid environment configuration');
}

return result.data;
}
Comment on lines +60 to +69

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider more detailed error output for debugging.

When validation fails, the current output shows formatted errors but doesn't indicate which variables are missing vs. invalid. In CI/CD environments, this can slow down debugging.

🔎 Enhanced error output
 function parseEnv(): Env {
   const result = envSchema.safeParse(process.env);

   if (!result.success) {
-    console.error('❌ Invalid environment variables:\n', result.error.format());
+    console.error('❌ Invalid environment variables:');
+    for (const issue of result.error.issues) {
+      console.error(`  - ${issue.path.join('.')}: ${issue.message}`);
+    }
     throw new Error('Invalid environment configuration');
   }

   return result.data;
 }
🤖 Prompt for AI Agents
In apps/server/src/config.ts around lines 60–69, the current error handling just
prints result.error.format() and throws, which doesn't clearly separate missing
vs invalid variables; update the block to produce a more actionable error:
extract result.error.errors (or error.flatten()/error.format() details) and
build a clear summary that lists missing required keys and keys with invalid
values, include their expected types/constraints and the actual provided value
(masking secrets), then log this structured summary (e.g., "Missing vars: [...],
Invalid vars: [{key, issue, expected}]") before throwing so CI logs show exactly
which env entries are absent vs malformed.


/**
* Validated environment configuration.
* Access this throughout the application for type-safe config.
*/
export const env = parseEnv();

/**
* Check if running in production mode.
*/
export const isProduction = env.NODE_ENV === 'production';

/**
* Check if running in development mode.
*/
export const isDevelopment = env.NODE_ENV === 'development';

/**
* Check if running in test mode.
*/
export const isTest = env.NODE_ENV === 'test';
Loading