Skip to content

Migrate to Supabase#54

Merged
Nusab19 merged 5 commits intoContest-Hive:mainfrom
Nusab19:main
Feb 14, 2026
Merged

Migrate to Supabase#54
Nusab19 merged 5 commits intoContest-Hive:mainfrom
Nusab19:main

Conversation

@Nusab19
Copy link
Copy Markdown
Member

@Nusab19 Nusab19 commented Feb 14, 2026

Supabase doesn't have the fixed connection limit like mongodb.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added statistics tracking for API and page usage metrics, including 24-hour data trends.
  • Refactor

    • Migrated database backend from MongoDB to PostgreSQL for improved reliability and performance.
    • Enhanced error handling in statistics queries with automatic fallback caching.
    • Optimized code structure for message sending and editor components.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 14, 2026

📝 Walkthrough

Walkthrough

This pull request migrates the application's data persistence layer from MongoDB to PostgreSQL with Drizzle ORM. It adds Drizzle configuration, database schema definitions, migrations, and replaces MongoDB-based queries in API routes and utility functions with Drizzle-based equivalents.

Changes

Cohort / File(s) Summary
Database Configuration & Setup
drizzle.config.ts, src/db/drizzle.ts, src/db/schema.ts
Adds Drizzle configuration and PostgreSQL connection initialization. Defines the "stats" table schema with id, api, page, total, past24, past24api, and past24page integer columns.
Database Migrations & Metadata
drizzle/0000_shocking_calypso.sql, drizzle/meta/0000_snapshot.json, drizzle/meta/_journal.json, scripts/db-migrate.ts
Introduces SQL migration creating the stats table, migration metadata snapshots, and a migration execution script using drizzle-orm's migrate function.
Database Operations
src/db/updateStats.ts, src/app/api/others/stats/route.ts
Replaces MongoDB queries with Drizzle ORM queries. Updates GET endpoint to select from stats table via Drizzle, and refactors update logic to use Drizzle's increment syntax with conditional per-key field updates.
Server Message Handling
src/server/sendMessage.ts, src/server/sendAPImessage.ts, src/app/api/others/send/route.ts
Extracts inline sendToAuthor logic into a dedicated server module. Creates a wrapper function sendAPImessage for client-side message sending via the /api/others/send endpoint.
Client-side Components
src/components/sub/MdEditor.tsx, src/components/ui/theme-toggle.tsx
Updates MdEditor to use the new sendAPImessage function instead of direct fetch. Removes debug console.log from theme-toggle hotkey handler.
Dependencies
package.json
Removes mongoose dependency. Adds drizzle-orm, pg, @types/pg, dotenv, drizzle-kit, and tsx packages.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Add Keyboard Shortcuts #53: Modifies src/app/api/others/send/route.ts request body formatting; related because this PR refactors the same route to import sendToAuthor from a new module.
  • Update things #37: Changes theme-toggle component structure; related because this PR modifies the same component to remove a console.log statement.

Poem

🐰 Hopping from Mongo's deep mongoose warren,
To PostgreSQL fields we now tread upon,
Drizzle ORM guides our data migration,
Stats table blooms with fresh definition,
Messages flow through new channels true! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (7 files):

⚔️ package-lock.json (content)
⚔️ package.json (content)
⚔️ src/app/api/others/send/route.ts (content)
⚔️ src/app/api/others/stats/route.ts (content)
⚔️ src/components/sub/MdEditor.tsx (content)
⚔️ src/components/ui/theme-toggle.tsx (content)
⚔️ src/db/updateStats.ts (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ 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 'Migrate to Supabase' accurately reflects the main objective of the pull request, which involves migrating from MongoDB to a Drizzle ORM with PostgreSQL (Supabase).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch main
  • Post resolved changes as copyable diffs in a comment

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
src/server/sendMessage.ts (2)

8-13: HTML-entity escaping is unnecessary for a URL-based messaging endpoint.

The sanitization replaces &, <, >, ", ' with HTML entities, but the content is sent via a URL (likely a Telegram Bot API call), not rendered as HTML. This means the recipient sees &amp; literally instead of &. If the downstream API expects HTML, this is fine; otherwise, strip these replacements.


24-49: Error handling silently swallows all failures — consider logging.

The catch block (Line 42) discards the error entirely. At minimum, log the error server-side so failed sends can be diagnosed in production.

Proposed fix
   } catch (error) {
+    console.error("sendToAuthor failed:", error);
     return {
       ok: false,
       status_code: 500,

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: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/app/api/others/send/route.ts (1)

10-10: ⚠️ Potential issue | 🟡 Minor

req.json() can throw on malformed request bodies.

If the client sends a non-JSON body, this will throw an unhandled error. Consider wrapping in try-catch.

src/app/api/others/stats/route.ts (1)

10-18: ⚠️ Potential issue | 🟡 Minor

POST handler lacks runtime validation of path and ignores updateData result.

Two issues:

  1. path comes directly from user input with no runtime validation. If it's not "api" or "page" (or missing entirely), updateData silently falls through to fetchStats and the handler still returns { ok: true }.
  2. The return value of updateData is discarded, so the caller never knows if the update actually succeeded.
Proposed fix
 export async function POST(req: NextRequest) {
   try {
     const jsonData = await req.json();
     const { path } = jsonData;
+    if (path !== "api" && path !== "page") {
+      return JsonResponse({ ok: false, error: "Invalid path" }, 400);
+    }
     await updateData(path);
     return JsonResponse({ ok: true });
   } catch {
     return JsonResponse({ ok: false }, 500);
   }
 }
🤖 Fix all issues with AI agents
In `@package.json`:
- Around line 38-44: The package.json currently lists "mongoose" in production
dependencies even though the app has migrated to Drizzle ORM; remove "mongoose"
from the regular dependencies and either delete it entirely or move it to
devDependencies if you want to keep the one-off migration script (referenced in
scripts/migrate-data.ts) for future use; update package.json accordingly and run
npm/yarn install to refresh the lockfile so production installs no longer
include mongoose.

In `@scripts/migrate-data.ts`:
- Line 90: The top-level call migrate() can reject before its internal try/catch
(e.g., if mongoose.connect fails); wrap the invocation in a top-level async
wrapper or attach a .catch handler so rejections are handled: call await
migrate() inside an immediately-invoked async function with a surrounding
try/catch (or use migrate().catch(...)) and in the catch log the error (include
exception details) and exit non‑zero; reference the migrate() invocation in
scripts/migrate-data.ts and ensure any process termination happens after
logging.
- Around line 51-58: Remove the stream-of-consciousness comments surrounding the
"Pushing schema to Supabase..." console.log in scripts/migrate-data.ts and
replace them with a single concise comment that states the intended behavior
(e.g., whether the script assumes drizzle-kit push is run beforehand or that the
script will create the table if missing). Specifically, delete the multiple
question-style/brainstorm lines and leave one clear comment describing the
chosen approach so the migration script is readable and production-ready.
- Around line 82-87: The finally block currently calls process.exit(0) and masks
errors; change control flow so failures produce a non-zero exit code: remove or
stop calling process.exit(0) inside the finally block (leave await
mongoose.connection.close()), and instead set process.exitCode = 1 or call
process.exit(1) inside the catch(error) branch after logging the error, and call
process.exit(0) (or set process.exitCode = 0) only on success after the try
completes; reference the catch(error) block, the finally block that awaits
mongoose.connection.close(), and the process.exit calls to locate and update the
logic.

In `@src/app/api/others/stats/route.ts`:
- Around line 23-33: The code uses result[0] from the query into data without
guarding for an empty result set, so when no row with id=1 exists you silently
return a response missing stats fields; update the handler to check the query
result (e.g., inspect result.length or result[0]) after calling
db.select().from(stats) and before spreading into data, and if no row is found
return an appropriate response (e.g., JsonResponse({ ok: false, error: "stats
not found" }, 404) or populate defaults) instead of spreading undefined; ensure
you still remove id (data.id) only when data exists and keep using JsonResponse
for the final reply.

In `@src/db/schema.ts`:
- Around line 4-12: The integer columns on the stats table are currently
nullable (producing number | null) but downstream code (e.g., getStatsData)
expects plain numbers; update the schema by adding .notNull() to each integer
column on the exported stats table (fields: api, page, total, past24, past24api,
past24page) while keeping their .default(0), and then generate/apply a migration
(or alter SQL) to add NOT NULL constraints in the DB so TypeScript types and
runtime DB constraints match.

In `@src/server/sendAPImessage.ts`:
- Around line 6-18: sendAPImessage currently issues an HTTP POST back to your
own app, introducing latency, LOCAL branching and hardcoded URLs; replace the
fetch-based implementation by making a direct server-side call to sendToAuthor
(import sendToAuthor from your server sendMessage module and invoke
sendToAuthor(message, ipOrPlaceholder)) and remove the LOCAL/URL logic and
fetch; also mark the file as a server action (add "use server") and add a type
annotation to the parameter (message: string) so sendAPImessage(message: string)
calls sendToAuthor directly.
- Line 2: The import sendToAuthor from "@/server/sendMessage" in
sendAPImessage.ts is unused; remove the unused import line (or if the intention
was to call sendToAuthor, add the appropriate invocation where messages are
dispatched). Locate the import statement referencing sendToAuthor and either
delete it or implement the missing call to the sendToAuthor function so the
import is actually used.

In `@src/server/sendMessage.ts`:
- Around line 6-14: In sendToAuthor, user content and ip are concatenated into
the URL (variable URL) with only newlines encoded, which allows URL injection
and can produce "undefined..." if process.env.URL is missing; fix by reading and
validating the base URL (process.env.URL) before use (throw or handle when
missing), build the full content string as currently done, then apply
encodeURIComponent to the entire content when appending to URL (i.e., use URL +
encodeURIComponent(content)) so characters like &, #, ?, and spaces are properly
escaped; ensure sendToAuthor returns/handles the case of a missing base URL
rather than concatenating "undefined".
🧹 Nitpick comments (9)
src/server/sendMessage.ts (2)

3-3: Variable URL shadows the global URL constructor.

Renaming to something like BASE_URL or SEND_URL avoids shadowing the built-in URL and improves clarity.


16-18: No timeout on the outbound fetch — could hang indefinitely.

If the external service is unresponsive, this server action will block without a timeout. Consider using AbortSignal.timeout() (available in Node 18+).

Example
-    const response = await fetch(url);
+    const response = await fetch(url, { signal: AbortSignal.timeout(10_000) });
src/app/api/others/send/route.ts (1)

6-6: URL is declared but no longer used after extracting sendToAuthor.

This is dead code — the URL is now consumed inside src/server/sendMessage.ts.

Proposed fix
-const URL = process.env.URL;
-
src/db/drizzle.ts (2)

6-8: Consider using Supabase's connection pooler URL or adding pool size limits.

The PR objective mentions migrating to Supabase to avoid MongoDB's fixed connection limits. However, a bare pg.Pool in a serverless environment (Vercel) can still exhaust Supabase's direct connection limit, since each serverless invocation may spin up a new pool. Consider either:

  1. Using Supabase's pooler connection string (port 6543) instead of the direct connection, or
  2. Setting max: 1 on the pool to minimize connection usage per invocation.
Example: constrain pool size for serverless
 const pool = new Pool({
   connectionString: process.env.DATABASE_URL!,
+  max: 1,
 });

7-7: Missing validation for DATABASE_URL environment variable.

The non-null assertion (!) silently passes undefined to the Pool constructor if the env var is unset, producing a confusing connection error at runtime. A guard (like in scripts/migrate-data.ts for MONGO_URI) would fail fast with a clear message.

Proposed fix
+if (!process.env.DATABASE_URL) {
+  throw new Error("DATABASE_URL environment variable must be defined");
+}
+
 const pool = new Pool({
-  connectionString: process.env.DATABASE_URL!,
+  connectionString: process.env.DATABASE_URL,
 });
src/db/updateStats.ts (2)

9-12: Avoid any type — use Drizzle's inferred types for type safety.

Using any for updateValues defeats the purpose of a typed ORM. Drizzle provides utility types you can use here, or you can use a typed record.

Proposed fix
-    const updateValues: any = {
+    const updateValues: Record<string, ReturnType<typeof sql>> = {
       total: sql`${stats.total} + 1`,
       past24: sql`${stats.past24} + 1`,
     };

20-23: Dead code: the else branch is unreachable.

TypeScript enforces key: "api" | "page", so this branch can never execute at compile time. However, since the POST handler in route.ts passes user-controlled path without runtime validation, consider adding runtime validation there instead, or keep this as a defensive guard with a more explicit type narrowing comment.

src/app/api/others/stats/route.ts (1)

37-45: Error response returns HTTP 200 with ok: false — intentional?

Returning a 200 status on error can confuse clients and monitoring tools. If this is intentional for backwards compatibility, consider adding a comment. Otherwise, use an appropriate error status code (e.g., 500).

scripts/migrate-data.ts (1)

62-63: Explicitly setting a serial column's value may desync the sequence.

Inserting id: 1 explicitly doesn't advance the underlying PostgreSQL sequence. If a future insert relies on auto-increment, it could conflict with the existing row. Since this app only uses id=1, this is low risk, but for correctness you can reset the sequence after migration:

SELECT setval(pg_get_serial_sequence('stats', 'id'), (SELECT MAX(id) FROM stats));

Comment on lines +82 to +87
} catch (error) {
console.error("Migration failed:", error);
} finally {
await mongoose.connection.close();
process.exit(0);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

process.exit(0) in finally masks migration failures.

If the catch block on line 82 executes (migration failed), the finally block still exits with code 0, making it impossible to detect failures in CI or scripts.

Proposed fix
+  let success = false;
   try {
     await db.insert(stats).values({
       // ...
     }).onConflictDoUpdate({
       // ...
     });
     console.log("Migration complete!");
+    success = true;
   } catch (error) {
     console.error("Migration failed:", error);
   } finally {
     await mongoose.connection.close();
-    process.exit(0);
+    process.exit(success ? 0 : 1);
   }
🤖 Prompt for AI Agents
In `@scripts/migrate-data.ts` around lines 82 - 87, The finally block currently
calls process.exit(0) and masks errors; change control flow so failures produce
a non-zero exit code: remove or stop calling process.exit(0) inside the finally
block (leave await mongoose.connection.close()), and instead set
process.exitCode = 1 or call process.exit(1) inside the catch(error) branch
after logging the error, and call process.exit(0) (or set process.exitCode = 0)
only on success after the try completes; reference the catch(error) block, the
finally block that awaits mongoose.connection.close(), and the process.exit
calls to locate and update the logic.

}
}

migrate();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unhandled promise rejection if migrate() throws before the internal try/catch.

If an error occurs between the mongoose.connect call and the try block (e.g., connection failure), the promise rejection is unhandled.

Proposed fix
-migrate();
+migrate().catch((err) => {
+  console.error("Migration script failed:", err);
+  process.exit(1);
+});
📝 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
migrate();
migrate().catch((err) => {
console.error("Migration script failed:", err);
process.exit(1);
});
🤖 Prompt for AI Agents
In `@scripts/migrate-data.ts` at line 90, The top-level call migrate() can reject
before its internal try/catch (e.g., if mongoose.connect fails); wrap the
invocation in a top-level async wrapper or attach a .catch handler so rejections
are handled: call await migrate() inside an immediately-invoked async function
with a surrounding try/catch (or use migrate().catch(...)) and in the catch log
the error (include exception details) and exit non‑zero; reference the migrate()
invocation in scripts/migrate-data.ts and ensure any process termination happens
after logging.

Comment on lines +23 to 33
const result = await db.select().from(stats).where(eq(stats.id, 1));
const data: any = {
ok: true,
...stats.toObject(),
...result[0],
href: req.nextUrl.href,
ip: req.headers.get("x-real-ip") || "127.0.0.1",
};
delete data._id;

if (data.id) delete data.id;

return JsonResponse(data);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

No guard for empty result set when id=1 row doesn't exist.

If the stats table is empty (e.g., migration ran but no seed data), result[0] is undefined. Spreading undefined is a no-op in JS, so the response will be { ok: true, href: ..., ip: ... } with no stats fields — silently wrong rather than failing.

Proposed fix
     const result = await db.select().from(stats).where(eq(stats.id, 1));
+    if (!result.length) {
+      const cachedStats = await fetchStats();
+      return JsonResponse({ ok: false, ...cachedStats, href: req.nextUrl.href, ip: req.headers.get("x-real-ip") || "127.0.0.1" });
+    }
     const data: any = {
       ok: true,
       ...result[0],
🤖 Prompt for AI Agents
In `@src/app/api/others/stats/route.ts` around lines 23 - 33, The code uses
result[0] from the query into data without guarding for an empty result set, so
when no row with id=1 exists you silently return a response missing stats
fields; update the handler to check the query result (e.g., inspect
result.length or result[0]) after calling db.select().from(stats) and before
spreading into data, and if no row is found return an appropriate response
(e.g., JsonResponse({ ok: false, error: "stats not found" }, 404) or populate
defaults) instead of spreading undefined; ensure you still remove id (data.id)
only when data exists and keep using JsonResponse for the final reply.

@@ -0,0 +1,19 @@
"use server";
import sendToAuthor from "@/server/sendMessage";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unused import: sendToAuthor is imported but never used.

Proposed fix
 "use server";
-import sendToAuthor from "@/server/sendMessage";
📝 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
import sendToAuthor from "@/server/sendMessage";
"use server";
🤖 Prompt for AI Agents
In `@src/server/sendAPImessage.ts` at line 2, The import sendToAuthor from
"@/server/sendMessage" in sendAPImessage.ts is unused; remove the unused import
line (or if the intention was to call sendToAuthor, add the appropriate
invocation where messages are dispatched). Locate the import statement
referencing sendToAuthor and either delete it or implement the missing call to
the sendToAuthor function so the import is actually used.

@Nusab19 Nusab19 self-assigned this Feb 14, 2026
@Nusab19 Nusab19 merged commit 3fbfb01 into Contest-Hive:main Feb 14, 2026
1 check passed
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