Skip to content

(SP: 1) [Database] Optimize Neon usage to reduce CU-hours#103

Merged
ViktorSvertoka merged 4 commits into
developfrom
perf/db
Jan 2, 2026
Merged

(SP: 1) [Database] Optimize Neon usage to reduce CU-hours#103
ViktorSvertoka merged 4 commits into
developfrom
perf/db

Conversation

@ViktorSvertoka
Copy link
Copy Markdown
Member

@ViktorSvertoka ViktorSvertoka commented Jan 2, 2026

What was done

This PR focuses on reducing Neon CU-hours usage by optimizing database access
patterns without changing application behavior or business logic.

Key improvements:

  • Replaced persistent DB connections with neon-http for serverless safety
  • Optimized heavy queries (explicit field selection, limits, indexed filters)
  • Added missing indexes to prevent CPU-intensive full table scans
  • Introduced caching via Next.js ISR and React cache to reduce DB hits
  • Removed unnecessary DB work from dev and preview environments
  • Enforced read-only access for preview deployments
  • Added Neon cost safety recommendations (spending limits and alerts)

Why

Neon billing is primarily driven by:

  • Active connection time
  • CPU-heavy queries
  • Uncached repeated reads

These changes significantly reduce unnecessary database work, leading to:

  • Lower CU-hours consumption
  • More predictable costs
  • Better scalability as user count grows

Expected impact

  • 2–3x reduction in Neon CU-hours
  • Fewer active connections and idle usage
  • Lower CPU usage per query
  • Safer preview and CI environments

Out of scope

  • No business logic changes
  • No schema redesign
  • No feature or UI changes

Summary by CodeRabbit

  • Performance Improvements

    • Pages and key APIs now use 5-minute revalidation and caching; several DB indexes added to speed queries.
  • Bug Fixes

    • More reliable avatar/profile image selection with deterministic fallbacks and normalized usernames; improved null-image handling.
  • Chores

    • Switched to a serverless-friendly DB client, added preview/dev env support, added a safety guard for non-preview DB scripts, and allowed additional remote image hosts.

✏️ Tip: You can customize this high-level summary in your review settings.

@ViktorSvertoka ViktorSvertoka requested a review from AM1007 as a code owner January 2, 2026 17:25
@netlify
Copy link
Copy Markdown

netlify Bot commented Jan 2, 2026

Deploy Preview for develop-devlovers ready!

Name Link
🔨 Latest commit c264efe
🔍 Latest deploy log https://app.netlify.com/projects/develop-devlovers/deploys/695811fc56d974000893bd3c
😎 Deploy Preview https://deploy-preview-103--develop-devlovers.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 2, 2026

📝 Walkthrough

Walkthrough

Replace Postgres Pool with Neon serverless, add environment-aware DB URLs and a Netlify preview guard, introduce 5-minute ISR/revalidation and React/Next caching for several queries/routes, add DB indexes, tighten OAuth selects and image fallbacks, and update seed/migrate scripts to use the guard.

Changes

Cohort / File(s) Summary
Database Client Migration
frontend/db/index.ts
Replace pg Pool + node-postgres with @neondatabase/serverless + drizzle-orm/neon-http; add resolveDatabaseUrl() (preview/dev handling); remove closeDb; initialize drizzle with neon sql client.
Environment Configuration
frontend/lib/env/index.ts
Add optional DATABASE_URL_PREVIEW and DATABASE_URL_DEV to serverEnvSchema (zod).
Caching & ISR Strategy
frontend/app/[locale]/leaderboard/page.tsx, frontend/db/queries/leaderboard.ts, frontend/db/queries/quiz.ts
Add export const revalidate = 300 to pages/routes; wrap leaderboard/quiz queries with react cache + next/unstable_cache keys (300s); deterministic Dicebear avatar fallback and username defaults.
Database Schema Indexing
frontend/db/schema/questions.ts, frontend/db/schema/quiz.ts, frontend/db/schema/shop.ts, frontend/db/schema/users.ts
Add indexes: questions(categoryId, sortOrder), quizzes(slug), order_items(order_id), product_prices(product_id), and unique composite users(provider, providerId).
OAuth & API Route Changes
frontend/app/api/auth/github/callback/route.ts, frontend/app/api/auth/google/callback/route.ts, frontend/app/api/questions/[category]/route.ts
Replace broad .select() with explicit field lists; compute safe image fallback (handle "null" literal); update user update to use computed image; questions route adds revalidate = 300 and sets Cache-Control headers.
Image Domains
frontend/next.config.ts
Add remote image patterns: lh3.googleusercontent.com, avatars.githubusercontent.com, api.dicebear.com.
Build Automation & Preview Guard
frontend/package.json, frontend/scripts/guard-non-preview.ts, frontend/db/seed-users.ts
Add scripts/guard-non-preview.ts to skip DB actions in Netlify previews; prepend guard to seed/migrate scripts; remove closeDb usage from seeds and exit process directly.

Sequence Diagram(s)

(Skipped — changes are broad but do not introduce a single new multi-component control flow that benefits from a sequence diagram.)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • AM1007

"🐰
Neon hums beneath my paws,
Caches planted in the grass,
Indexes carve tidy trails,
Preview guards let seeds pass,
I hop along — the build grows fast!"

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: optimizing Neon database usage to reduce compute unit hours through connection pooling, query optimization, caching, and indexing.

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2d42fe8 and c264efe.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (1)
  • frontend/package.json
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-30T16:01:02.523Z
Learnt from: kryvosheyin
Repo: DevLoversTeam/devlovers.net PR: 88
File: frontend/drizzle/schema.ts:153-153
Timestamp: 2025-12-30T16:01:02.523Z
Learning: The file frontend/drizzle/schema.ts is a legacy file not used by Drizzle or the runtime. The canonical schemas live in frontend/db/schema/* as configured in drizzle-config.ts, and that's what migrations and runtime use.

Applied to files:

  • frontend/package.json
🔇 Additional comments (3)
frontend/package.json (3)

15-15: LGTM! Migration script now gated for preview safety.

Wrapping drizzle-kit migrate with the guard prevents schema modifications in preview deployments, which is critical for database safety and cost control.


10-12: LGTM! Seed and migrate scripts now properly gated for preview safety.

The guard-non-preview.ts implementation is solid: it correctly uses spawnSync with proper error handling, checks for Netlify preview contexts, and allows commands to execute only in non-preview environments. All three seed scripts and the migrate script follow the consistent pattern, preventing accidental database mutations in preview/CI environments and aligning with the PR's cost-reduction objectives.


34-34: No action needed. Version 0.45.1 is valid, compatible with drizzle-kit 0.31.8 (released together), and contains only a bug fix for pg-native Pool detection with no breaking changes.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (7)
frontend/next.config.ts (1)

34-38: Consider caching strategy for Dicebear avatars.

The Dicebear API pattern introduces an external service dependency. While this is fine for generated avatars, consider implementing a caching strategy (e.g., storing generated avatars in your CDN or serving them through Next.js Image optimization) to reduce external API calls and improve performance. This would align well with the PR's goal of reducing infrastructure costs.

frontend/app/api/auth/google/callback/route.ts (1)

156-156: Remove trailing whitespace.

Line 156 has trailing whitespace after the closing brace.

🔎 Proposed fix
-}   
+}
frontend/app/api/questions/[category]/route.ts (1)

112-125: Consider brief caching for error responses.

The error response path (line 115-124) does not set Cache-Control headers. While this may be intentional to avoid caching transient errors, adding a short-lived cache (e.g., public, s-maxage=10) could reduce load during temporary database issues or incident scenarios.

🔎 Optional: Add short-lived caching to error response
     return NextResponse.json(
       {
         items: [],
         total: 0,
         page: 1,
         totalPages: 0,
         locale: DEFAULT_LOCALE,
       },
-      { status: 500 }
+      { 
+        status: 500,
+        headers: {
+          'Cache-Control': 'public, s-maxage=10, stale-while-revalidate=20'
+        }
+      }
     );
frontend/app/api/auth/github/callback/route.ts (2)

176-176: Remove trailing whitespace.

Line 176 has trailing whitespace (same issue as in the Google callback route).

🔎 Proposed fix
-}
+}

133-136: Pattern is consistent across both OAuth callbacks and properly handles legacy data.

The defensive emailUser.image !== "null" check in the GitHub callback (lines 133-136) matches the identical pattern in the Google callback. Both routes correctly handle legacy data where the literal string "null" was stored instead of a NULL value.

If there is historical data with image = 'null', consider a one-time cleanup:

SELECT COUNT(*) FROM users WHERE image = 'null';
UPDATE users SET image = NULL WHERE image = 'null';
frontend/db/queries/leaderboard.ts (2)

34-34: Redundant fallback for points field.

The expression u.points || 0 is redundant because the points column in the database schema has .notNull().default(0), guaranteeing it will never be NULL. While this doesn't cause issues, it's unnecessary defensive code.

🔎 Simplification
-        points: u.points || 0,
+        points: u.points,

26-27: Consider pinning or documenting Dicebear API version.

The avatar URL uses Dicebear API version 9.x, which may receive breaking changes. While the floating version provides automatic updates, it could cause avatar inconsistencies if the API changes.

Consider either:

  1. Pinning to a specific version (e.g., 9.4) for stability
  2. Adding a comment documenting the version choice

This is a low-priority optimization and can be addressed in future work if avatar rendering issues arise.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95a58d9 and 2a48a8d.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (15)
  • frontend/app/[locale]/leaderboard/page.tsx
  • frontend/app/api/auth/github/callback/route.ts
  • frontend/app/api/auth/google/callback/route.ts
  • frontend/app/api/questions/[category]/route.ts
  • frontend/db/index.ts
  • frontend/db/queries/leaderboard.ts
  • frontend/db/queries/quiz.ts
  • frontend/db/schema/questions.ts
  • frontend/db/schema/quiz.ts
  • frontend/db/schema/shop.ts
  • frontend/db/schema/users.ts
  • frontend/lib/env/index.ts
  • frontend/next.config.ts
  • frontend/package.json
  • frontend/scripts/guard-non-preview.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-30T16:01:02.523Z
Learnt from: kryvosheyin
Repo: DevLoversTeam/devlovers.net PR: 88
File: frontend/drizzle/schema.ts:153-153
Timestamp: 2025-12-30T16:01:02.523Z
Learning: The file frontend/drizzle/schema.ts is a legacy file not used by Drizzle or the runtime. The canonical schemas live in frontend/db/schema/* as configured in drizzle-config.ts, and that's what migrations and runtime use.

Applied to files:

  • frontend/db/index.ts
  • frontend/db/schema/users.ts
  • frontend/package.json
  • frontend/db/schema/shop.ts
🧬 Code graph analysis (8)
frontend/app/[locale]/leaderboard/page.tsx (1)
frontend/app/api/questions/[category]/route.ts (1)
  • revalidate (6-6)
frontend/db/schema/users.ts (3)
frontend/drizzle/schema.ts (2)
  • users (11-26)
  • quizAttempts (224-245)
frontend/drizzle/relations.ts (1)
  • usersRelations (13-17)
frontend/db/schema/quiz.ts (1)
  • quizAttempts (129-163)
frontend/db/schema/questions.ts (2)
frontend/drizzle/schema.ts (2)
  • questions (166-179)
  • categories (58-65)
frontend/db/schema/categories.ts (1)
  • categories (12-19)
frontend/app/api/auth/github/callback/route.ts (3)
frontend/db/schema/users.ts (1)
  • users (11-45)
frontend/drizzle/schema.ts (1)
  • users (11-26)
frontend/db/index.ts (1)
  • db (33-33)
frontend/app/api/auth/google/callback/route.ts (3)
frontend/db/schema/users.ts (1)
  • users (11-45)
frontend/drizzle/schema.ts (1)
  • users (11-26)
frontend/db/index.ts (1)
  • db (33-33)
frontend/db/queries/leaderboard.ts (3)
frontend/components/leaderboard/types.ts (1)
  • User (1-8)
frontend/db/index.ts (1)
  • db (33-33)
frontend/db/schema/users.ts (1)
  • users (11-45)
frontend/app/api/questions/[category]/route.ts (3)
frontend/app/[locale]/leaderboard/page.tsx (1)
  • revalidate (4-4)
frontend/drizzle/schema.ts (1)
  • categories (58-65)
frontend/db/schema/categories.ts (1)
  • categories (12-19)
frontend/db/queries/quiz.ts (3)
frontend/db/index.ts (1)
  • db (33-33)
frontend/db/schema/quiz.ts (3)
  • quizzes (19-42)
  • quizTranslations (44-57)
  • quizAttempts (129-163)
frontend/db/schema/categories.ts (2)
  • categories (12-19)
  • categoryTranslations (21-33)
🔇 Additional comments (25)
frontend/next.config.ts (1)

24-38: Implementation is correct and remote patterns are necessary.

The three remote image patterns are properly configured and actively used in the application:

  • api.dicebear.com: Directly used throughout the codebase for generated avatar images
  • avatars.githubusercontent.com and lh3.googleusercontent.com: Required for OAuth avatar handling (GitHub and Google authentication routes are present)

All patterns follow Next.js best practices with HTTPS protocol and appropriate pathname matching.

Consider implementing caching strategies for Dicebear API responses to further reduce external API calls and align with the PR's cost-reduction objectives.

frontend/scripts/guard-non-preview.ts (4)

6-9: LGTM! Preview guard logic is correct.

The guard correctly exits with code 0 in preview contexts, allowing the npm script chain to continue without triggering errors while safely skipping DB operations. This aligns perfectly with the PR objective to reduce Neon costs in preview environments.


11-16: LGTM! Command validation is proper.

The command extraction from process.argv.slice(2) correctly skips the node executable and script path. The validation with an informative error message and exit code 1 provides good developer feedback when the guard is misused.


18-19: LGTM! Command execution with proper error handling.

The use of spawnSync with stdio: 'inherit' correctly pipes output to the parent process. The defensive ?? 1 fallback properly handles cases where result.status is null or undefined, ensuring the script exits with a failure code if the spawned command status is unavailable.

Note: While the script executes arbitrary commands, this is acceptable since it's only invoked from controlled npm scripts defined in package.json.


1-4: The environment detection logic is correct. CONTEXT is Netlify's standard read-only environment variable, and deploy-preview and branch-deploy are the exact context values Netlify uses. The fallback to NETLIFY_CONTEXT provides additional robustness, though CONTEXT should be available in all Netlify builds.

frontend/package.json (2)

15-15: LGTM! Migration script correctly integrates the guard.

The syntax for wrapping the drizzle-kit migrate command is correct. The guard will receive drizzle-kit as the command with the migrate subcommand and config flag as arguments, properly executing the migration in non-preview contexts. The drizzle config file and guard script both exist in the expected locations.


10-12: LGTM! Seed scripts correctly integrate the guard.

All referenced seed files exist (seed-categories.ts, seed-questions.ts, seed-users.ts) and the guard script is in place. The syntax for wrapping seed commands through the guard script is correct—the guard will receive tsx as the command and the seed file paths as arguments, properly executing them in non-preview contexts.

frontend/db/schema/shop.ts (1)

7-7: LGTM! Foreign key indexes improve join performance.

The added indexes on order_items.order_id and product_prices.product_id are well-placed. These foreign key columns are frequently used in joins and lookups (e.g., fetching all items for an order, or prices for a product), and the indexes will significantly reduce query costs by avoiding table scans.

The naming convention is consistent and the implementation is correct.

Also applies to: 184-184, 253-253

frontend/db/schema/questions.ts (1)

10-10: LGTM! Composite index optimizes category-based question queries.

The composite index on (categoryId, sortOrder) is well-designed for queries that fetch questions within a category, ordered by their sort position. The column order is optimal: filtering by categoryId first, then ordering by sortOrder.

This aligns with the PR's objective to reduce CU-hours by avoiding full table scans on filtered queries.

Also applies to: 15-37

frontend/lib/env/index.ts (1)

9-10: Implementation correctly handles optional database URLs.

The new optional environment variables DATABASE_URL_PREVIEW and DATABASE_URL_DEV are properly consumed in resolveDatabaseUrl() at frontend/db/index.ts with appropriate fallback logic:

  • Preview deploys require DATABASE_URL_PREVIEW and explicitly throw an error if missing
  • Development environments optionally use DATABASE_URL_DEV if available, then fall back to DATABASE_URL
  • Production and other environments use DATABASE_URL with error handling

No runtime errors will occur from accessing undefined optional variables.

frontend/db/schema/quiz.ts (1)

40-40: The slug-only index is justified and necessary.

Verification confirms that the application performs slug-only lookups on the quizzes table:

  1. getQuizBySlugCached (frontend/db/queries/quiz.ts, line 98) queries by slug without filtering by categoryId: .where(eq(quizzes.slug, slug))
  2. Public API endpoint (frontend/app/api/quiz/[slug]/route.ts) uses this cached query to expose quizzes publicly by slug alone
  3. Database seed scripts also rely on slug-only lookups across multiple files

The composite unique index (categoryId, slug) cannot efficiently serve these slug-only queries since categoryId is the leading column. The single-column index on slug is therefore necessary for query performance.

frontend/app/api/auth/google/callback/route.ts (2)

86-105: Explicit field selections optimize data transfer.

The explicit field selections for both googleUser and emailUser queries are well-aligned with the PR's goal to reduce Neon CU-hours by minimizing data transfer. This ensures only necessary fields are retrieved, reducing bandwidth and query cost.


111-114: Defensive "null" string check is appropriate and appears intentional across the codebase.

The check for emailUser.image !== "null" is confirmed in three separate locations (Google OAuth, GitHub OAuth, and leaderboard queries), all using identical defensive patterns. This suggests the check addresses a real or anticipated data quality issue rather than being unnecessary paranoia.

The pattern indicates that the image field may contain the literal string "null" rather than a proper NULL value. While the exact source of these strings cannot be determined from code inspection (likely legacy data, external API responses, or historical imports), the consistent implementation across multiple features confirms this is a known concern.

The code correctly handles this edge case. To confirm whether cleanup is needed, query the production database for records where image = 'null' on the users table.

frontend/app/api/questions/[category]/route.ts (3)

6-6: ISR configuration aligns with PR caching strategy.

The 5-minute revalidation interval (revalidate = 300) is consistent with the caching strategy introduced across the PR (e.g., leaderboard page, unstable_cache in queries) and will reduce database hits for this API route.


36-36: Optimized category lookup reduces data transfer.

Selecting only categories.id instead of all fields reduces unnecessary data transfer, which directly supports the PR's goal to minimize Neon CU-hours.


42-53: Cache-Control headers properly configured.

The Cache-Control headers (public, s-maxage=300, stale-while-revalidate=600) are correctly applied to both the not-found and success response paths. The s-maxage=300 aligns with the ISR revalidation interval, and stale-while-revalidate=600 provides a reasonable grace period for background revalidation.

Also applies to: 100-111

frontend/app/[locale]/leaderboard/page.tsx (1)

4-4: ISR configuration effectively reduces database load.

Replacing dynamic = 'force-dynamic' with revalidate = 300 is an excellent optimization. This shifts the page from rendering on every request to incremental static regeneration with a 5-minute revalidation interval, significantly reducing database hits while maintaining reasonably fresh data. This change directly supports the PR's goal to minimize Neon CU-hours.

The 5-minute interval is consistent with the caching strategy in getLeaderboardData() and other routes in this PR.

frontend/app/api/auth/github/callback/route.ts (1)

109-127: Explicit field selections optimize data transfer.

The explicit field selections for both githubUser and emailUser queries are well-aligned with the PR's goal to reduce Neon CU-hours. This pattern is consistently applied across OAuth providers (see identical changes in frontend/app/api/auth/google/callback/route.ts), ensuring minimal data transfer.

frontend/db/queries/leaderboard.ts (3)

1-2: Double caching pattern correctly implemented.

The combination of unstable_cache (Next.js server-side cache with 5-minute revalidation) and React cache (request-level deduplication) is a correct and beneficial pattern:

  • unstable_cache: Caches the database query result across requests with a 300-second TTL
  • React cache: Deduplicates calls within a single request/render cycle

This two-tier caching strategy will significantly reduce database load:

  • Multiple components calling getLeaderboardData() in the same request will share one result (React cache)
  • Subsequent requests within 5 minutes will use the cached data (unstable_cache)

The implementation correctly defines getLeaderboardDataCached once (outside), then wraps its invocation with React cache. This aligns perfectly with the PR's goal to minimize Neon CU-hours.

Also applies to: 8-46


11-16: Explicit field selection optimizes query performance.

Selecting only the required fields (id, username, points, avatar) reduces data transfer and query cost, directly supporting the PR's database optimization goals.


23-28: Avatar fallback with "null" check is well-implemented.

The avatar generation logic properly handles missing or invalid avatar data:

  • Checks for both falsy values and the literal string "null" (consistent with OAuth callback changes)
  • Generates deterministic Dicebear avatars using username and ID as seed
  • Ensures all users have a valid avatar URL
frontend/db/index.ts (3)

9-29: LGTM! Environment-based database URL resolution is well-structured.

The logic correctly routes preview, development, and production environments to their respective databases with clear error messages.

One clarification: In development (lines 20-22), if DATABASE_URL_DEV is not set, the function falls through to DATABASE_URL. Is this fallback intentional to allow developers to optionally use a production replica?


31-33: LGTM! Neon serverless initialization is correct.

The drizzle instance is properly initialized with the neon HTTP client and schema. This eliminates persistent connections and aligns with the PR's goal to reduce CU-hours.


1-2: Compatibility verified — no changes needed.

The migration to neon-http is compatible with drizzle-orm 0.44.7 and Next.js 16.0.7. The imports follow the documented Neon and Drizzle patterns, and the HTTP API usage is appropriate for the serverless environment.

frontend/db/queries/quiz.ts (1)

310-314: LGTM! Excellent query optimization.

Selecting only the required fields (score, percentage, completedAt) instead of all columns reduces data transfer and aligns with the PR's performance goals.

Comment thread frontend/db/queries/quiz.ts
Comment thread frontend/db/queries/quiz.ts Outdated
Comment thread frontend/db/queries/quiz.ts Outdated
Comment thread frontend/db/schema/users.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
frontend/db/queries/quiz.ts (2)

68-113: Cache key now correctly includes parameters—critical issue resolved.

The cache key at line 107 now includes both slug and locale: ['quiz-by-slug', slug, locale]. This resolves the critical cache collision issue flagged in previous reviews where different quizzes/locales would return the same cached result.

The layered caching approach is sound:

  • cache() deduplicates calls within a single request
  • unstable_cache() caches across requests with 5-minute revalidation

115-158: Cache key now correctly includes locale—critical issue resolved.

The cache key at line 152 now includes the locale parameter: ['active-quizzes', locale]. This resolves the critical internationalization bug flagged in previous reviews where all locales would return the same cached quiz list.

🧹 Nitpick comments (1)
frontend/db/schema/users.ts (1)

6-6: Consider using unique constraint instead of uniqueIndex for OAuth data integrity.

The critical security issue has been addressed—duplicate (provider, providerId) pairs are now prevented. However, the legacy schema used a unique constraint, and for logical data integrity requirements (preventing duplicate OAuth associations), a UNIQUE constraint is semantically more appropriate than a unique index:

  • unique(): Declares a data integrity rule as part of the table's logical schema
  • uniqueIndex(): Creates an index that happens to enforce uniqueness (typically for query optimization)

Both enforce uniqueness at the database level, but for OAuth authentication—where preventing duplicate provider associations is a business rule rather than a performance optimization—a constraint better expresses the intent.

🔎 Suggested refinement to align with legacy schema semantics
 import {
   pgTable,
   text,
   integer,
   timestamp,
-  uniqueIndex,
+  unique,
 } from "drizzle-orm/pg-core";
   table => ({
-    providerProviderIdUq: uniqueIndex(
+    providerProviderIdUq: unique(
       "users_provider_provider_id_unique"
     ).on(table.provider, table.providerId),
   })

Also applies to: 40-42

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2a48a8d and e58c1e5.

📒 Files selected for processing (3)
  • frontend/db/queries/quiz.ts
  • frontend/db/schema/users.ts
  • frontend/db/seed-users.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-30T16:01:02.523Z
Learnt from: kryvosheyin
Repo: DevLoversTeam/devlovers.net PR: 88
File: frontend/drizzle/schema.ts:153-153
Timestamp: 2025-12-30T16:01:02.523Z
Learning: The file frontend/drizzle/schema.ts is a legacy file not used by Drizzle or the runtime. The canonical schemas live in frontend/db/schema/* as configured in drizzle-config.ts, and that's what migrations and runtime use.

Applied to files:

  • frontend/db/schema/users.ts
🧬 Code graph analysis (2)
frontend/db/schema/users.ts (1)
frontend/drizzle/relations.ts (1)
  • usersRelations (13-17)
frontend/db/queries/quiz.ts (3)
frontend/db/index.ts (1)
  • db (33-33)
frontend/db/schema/quiz.ts (3)
  • quizzes (19-42)
  • quizTranslations (44-57)
  • quizAttempts (129-163)
frontend/db/schema/categories.ts (2)
  • categories (12-19)
  • categoryTranslations (21-33)
🔇 Additional comments (6)
frontend/db/schema/users.ts (1)

11-44: LGTM! Table structure is well-defined.

The table restructuring to the 3-argument form with a table callback is correct, all field definitions match the legacy schema, and the constraints are properly configured. The unique constraint on email (line 21) and the table-level uniqueness enforcement on (provider, providerId) both work as intended.

frontend/db/seed-users.ts (3)

4-4: LGTM — aligns with serverless architecture.

Removing closeDb from imports is consistent with the migration to neon-http serverless connections. HTTP-based database connections are stateless and don't require explicit closure.


60-65: LGTM — appropriate exit handling for serverless context.

Removing closeDb() calls and using explicit process.exit() is correct for the neon-http serverless architecture. The process termination handles all cleanup automatically.


42-46: Index on users.email already exists via unique constraint.

The email field in the users schema has the .unique() modifier, which creates a unique index. This provides the optimization needed to avoid full table scans when filtering by email in the seed query.

frontend/db/queries/quiz.ts (2)

1-2: Cache imports are in place; critical key issues from past reviews resolved.

The imports for React's cache and Next.js's unstable_cache are correctly added. Past reviews flagged concerns about unstable_cache being experimental—that guidance remains valid. The critical cache key issues (missing slug/locale parameters) identified in prior reviews have now been addressed in lines 107 and 152.


310-314: Optimization verified—all selected fields are used safely.

The three fields selected (score, percentage, completedAt) are exactly those accessed by the only consumer in frontend/app/[locale]/dashboard/page.tsx: the dashboard calculates realPoints from score, averageScore from percentage, and lastActiveDate from completedAt. No breaking changes will occur.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
frontend/db/index.ts (1)

7-7: Consider conditional dotenv loading.

In serverless environments like Netlify, environment variables are pre-populated and dotenv.config() is redundant. Consider loading dotenv only in development to avoid potential issues if .env files are accidentally deployed.

🔎 Proposed refactor
-dotenv.config();
+if (process.env.NODE_ENV === 'development') {
+  dotenv.config();
+}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e58c1e5 and 2d42fe8.

📒 Files selected for processing (1)
  • frontend/db/index.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-30T16:01:02.523Z
Learnt from: kryvosheyin
Repo: DevLoversTeam/devlovers.net PR: 88
File: frontend/drizzle/schema.ts:153-153
Timestamp: 2025-12-30T16:01:02.523Z
Learning: The file frontend/drizzle/schema.ts is a legacy file not used by Drizzle or the runtime. The canonical schemas live in frontend/db/schema/* as configured in drizzle-config.ts, and that's what migrations and runtime use.

Applied to files:

  • frontend/db/index.ts
🔇 Additional comments (3)
frontend/db/index.ts (3)

9-35: Well-structured environment-based URL resolution.

The function correctly prioritizes preview deployments, then development-specific URLs, and finally the default production URL. The error handling is appropriate, and the fallback warning for preview builds (lines 18-21) provides visibility while preventing complete failure.


37-37: LGTM: Neon serverless client initialization.

The use of neon(resolveDatabaseUrl()) correctly creates a serverless HTTP-based database client. The URL is resolved at module initialization, which is appropriate for serverless environments where process lifecycles are short.


39-39: LGTM: Drizzle initialization with Neon serverless.

The migration from Pool-based connections to drizzle(sql, { schema }) with the Neon HTTP client correctly implements serverless-safe database access, directly supporting the PR's goal of reducing CU-hours through connection pooling elimination.

Comment thread frontend/db/index.ts
@ViktorSvertoka ViktorSvertoka merged commit b0858b3 into develop Jan 2, 2026
8 checks passed
@ViktorSvertoka ViktorSvertoka deleted the perf/db branch January 2, 2026 18:48
liudmylasovetovs pushed a commit that referenced this pull request Jan 9, 2026
 (SP: 1) [Database] Optimize Neon usage to reduce CU-hours
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant