Skip to content
Merged
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
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

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

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

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

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

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

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

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

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

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

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

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