Skip to content

Add PostgreSQL database foundation with config, connection, and health check#46

Open
promisszn wants to merge 2 commits into
Fundable-Protocol:devfrom
promisszn:dev
Open

Add PostgreSQL database foundation with config, connection, and health check#46
promisszn wants to merge 2 commits into
Fundable-Protocol:devfrom
promisszn:dev

Conversation

@promisszn

@promisszn promisszn commented Jun 26, 2026

Copy link
Copy Markdown

Summary

Adds the PostgreSQL-backed persistence foundation for the indexer in the @fundable-indexer/common package. This is tooling + infrastructure only — no domain tables are defined yet (that's deferred to later scoped issues).

Implements issues #20, #21, #22, #23.

#20 — Database tooling

  • Adds exact-pinned deps: drizzle-orm@0.45.2, postgres@3.4.9, zod@3.25.67, drizzle-kit@0.31.10 (dev); bun.lock updated
  • indexer/common/drizzle.config.ts (postgresql dialect, src/db/schema.ts entrypoint, ./migrations out dir)
  • Initial migrations/ directory with Drizzle journal
  • Scripts: db:generate / db:migrate (in common) and indexer:db:generate / indexer:db:migrate (root)
  • Migration workflow documented in indexer/README.md

#21 — Config loader + validation

  • loadConfig() in common/src/config — zod-validated, accepts an env record for testability
  • Requires INDEXER_DATABASE_URL; parses numeric INDEXER_PORT / POLL_INTERVAL_MS / START_LEDGER; defaults INDEXER_LOG_LEVEL
  • Aggregates every problem into a single clear ConfigValidationError; treats blank values as unset

#22 — Connection factory

  • createDbClient() in common/src/db — wraps postgres.js + Drizzle, reads from validated config, lazy (no socket until first query), injectable sql factory for tests, close() for graceful shutdown

#23 — Health check

  • checkDbHealth() runs select 1 and returns a typed healthy / unhealthy result (with latency + error) without throwing

Incidental

  • Root ESLint now ignores indexer/** (the workspace is linted by Biome) — prevents the two linters' differing global assumptions from conflicting
  • Biome ignores generated migrations/ metadata

Verification

All six gate commands pass:

Command Result
bun run type-check
bun run test ✅ 10 pass, coverage met
bun run lint ✅ 0 errors
bun run indexer:type-check
bun run indexer:test ✅ 12 pass (common)
bun run indexer:lint

bun run indexer:db:generate also verified — reports "no schema changes" (expected, no tables yet).

Closes #20
Closes #21
Closes #22
Closes #23

Summary by CodeRabbit

  • New Features

    • Added validated environment configuration for the indexer, including clearer startup errors when required values are missing or invalid.
    • Added PostgreSQL database setup and migration commands, plus a basic health check for connection status.
  • Documentation

    • Expanded indexer documentation to explain current setup, database migration workflow, and which parts are implemented versus planned.

promisszn and others added 2 commits June 26, 2026 23:00
…igrations)

Adds the PostgreSQL-backed persistence foundation for the indexer in the
`@fundable-indexer/common` package, covering issues Fundable-Protocol#20-Fundable-Protocol#23:

Fundable-Protocol#20 Database tooling
- Add exact-pinned deps: drizzle-orm 0.45.2, postgres 3.4.9, zod 3.25.67,
  drizzle-kit 0.31.10 (dev); update bun.lock
- Add drizzle.config.ts (postgresql, schema entrypoint, ./migrations out)
- Add initial migrations/ directory with Drizzle journal
- Add db:generate / db:migrate scripts (package + root indexer:db:* )
- Document the migration workflow in indexer/README.md

Fundable-Protocol#21 Config loader + validation
- Add zod-based loadConfig in common/src/config; validates required
  INDEXER_DATABASE_URL, parses numeric INDEXER_PORT / POLL_INTERVAL_MS /
  START_LEDGER, aggregates problems into a clear ConfigValidationError

Fundable-Protocol#22 Connection factory
- Add createDbClient in common/src/db wrapping postgres.js + Drizzle,
  reading from validated config, with an injectable sql factory for tests

Fundable-Protocol#23 Health check
- Add checkDbHealth running `select 1`, returning typed healthy/unhealthy
  results without throwing

Also scope root ESLint away from the indexer workspace (it is linted by
Biome) and ignore generated migration metadata in Biome.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(indexer): database foundation — config, connection factory, health check, migrations
@drips-wave

drips-wave Bot commented Jun 26, 2026

Copy link
Copy Markdown

@promisszn Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Added validated indexer environment loading, a shared PostgreSQL client and health probe, Drizzle migration configuration and scripts, and workspace linting/documentation updates for the indexer package.

Changes

Indexer workspace foundation

Layer / File(s) Summary
Runtime config validation
indexer/common/src/config/env.ts, indexer/common/src/config/env.test.ts, indexer/common/src/config/index.ts, indexer/common/src/index.ts
loadConfig validates required environment values, parses numeric settings, treats blank strings as unset, throws ConfigValidationError on aggregated failures, and is re-exported from the common entrypoints.
PostgreSQL client and health
indexer/common/src/db/schema.ts, indexer/common/src/db/client.ts, indexer/common/src/db/client.test.ts, indexer/common/src/db/health.ts, indexer/common/src/db/health.test.ts, indexer/common/src/db/index.ts, indexer/common/src/index.ts
createDbClient builds a Drizzle-backed PostgreSQL client with close handling, checkDbHealth runs a select 1 probe and returns typed results, and the DB barrel re-exports the client, health, and schema APIs.
Migration tooling and workspace docs
indexer/common/drizzle.config.ts, indexer/common/package.json, package.json, indexer/common/migrations/README.md, indexer/common/migrations/meta/_journal.json, eslint.config.mjs, indexer/biome.jsonc, indexer/README.md
Drizzle Kit configuration, migration commands, migration journal scaffolding, lint ignores, and indexer README notes are updated for the workspace database setup.

Sequence Diagram(s)

sequenceDiagram
  participant IndexerStartup
  participant loadConfig
  participant createDbClient
  participant checkDbHealth
  participant Sql as postgres.js Sql

  IndexerStartup->>loadConfig: read env vars
  loadConfig-->>IndexerStartup: IndexerConfig
  IndexerStartup->>createDbClient: databaseUrl
  createDbClient->>Sql: postgres(url, maxConnections)
  createDbClient->>createDbClient: drizzle(sql)
  IndexerStartup->>checkDbHealth: sql
  checkDbHealth->>Sql: select 1
  Sql-->>checkDbHealth: success or error
  checkDbHealth-->>IndexerStartup: DbHealth
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I hopped through env vars, neat and bright,
Then select 1 blinked back at night.
Drizzle drums and Bun went thump,
My carrot scripts now leap, not slump 🐰
Hooray for configs, logs, and light!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding PostgreSQL foundation work with config, DB connection, and health checks.
Description check ✅ Passed The description is detailed and covers the summary, issue references, and verification; required template sections are largely addressed.
Linked Issues check ✅ Passed The changes satisfy #20-#23: tooling, config validation, connection factory, and health checks are all added with tests and docs.
Out of Scope Changes check ✅ Passed The incidental ESLint/Biome ignore updates are directly tied to the indexer workspace and generated migrations, so no unrelated scope stands out.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@indexer/common/drizzle.config.ts`:
- Around line 18-20: The Drizzle config is silently defaulting
`dbCredentials.url` to an empty string when `INDEXER_DATABASE_URL` is missing,
which should instead fail immediately. Update the config in `drizzle.config.ts`
to validate `process.env.INDEXER_DATABASE_URL` up front and throw a clear local
error if it is absent, matching the required-URL behavior used by `loadConfig`;
keep the fix centered on the `dbCredentials` setup so `db:generate` and
`db:migrate` fail fast with a clear message.

In `@indexer/common/src/config/env.ts`:
- Around line 24-31: The positive integer parser in positiveIntFromString
currently accepts 0, so tighten the schema to reject zero. Update the zod chain
in env.ts by either adding a value check after the safe-integer refine or
splitting this into a non-negative parser plus a separate strictly-positive
schema, and then use the strictly-positive version for PORT and
POLL_INTERVAL_MS.

In `@indexer/common/src/db/health.ts`:
- Around line 20-24: `checkDbHealth` currently awaits `sql\`select 1\`` without
any deadline, so a stalled database can hang the probe instead of failing fast.
Add a bounded timeout path in `checkDbHealth` (or enforce it via
`createDbClient`/postgres.js config) so the health check returns unhealthy when
the query exceeds the limit, and make sure the timeout behavior is covered by
tests for the `checkDbHealth` and `createDbClient` flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: adc5c05a-7d7c-48f7-8a8e-c27a219f4493

📥 Commits

Reviewing files that changed from the base of the PR and between bcfa08d and 8cecb99.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • eslint.config.mjs
  • indexer/README.md
  • indexer/biome.jsonc
  • indexer/common/drizzle.config.ts
  • indexer/common/migrations/README.md
  • indexer/common/migrations/meta/_journal.json
  • indexer/common/package.json
  • indexer/common/src/config/env.test.ts
  • indexer/common/src/config/env.ts
  • indexer/common/src/config/index.ts
  • indexer/common/src/db/client.test.ts
  • indexer/common/src/db/client.ts
  • indexer/common/src/db/health.test.ts
  • indexer/common/src/db/health.ts
  • indexer/common/src/db/index.ts
  • indexer/common/src/db/schema.ts
  • indexer/common/src/index.ts
  • package.json

Comment on lines +18 to +20
dbCredentials: {
url: process.env.INDEXER_DATABASE_URL ?? "",
},

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Fail fast when INDEXER_DATABASE_URL is missing.

Falling back to "" here bypasses the same required-URL contract enforced by loadConfig and turns a simple misconfiguration into a downstream Drizzle CLI failure. Throwing locally gives db:generate/db:migrate a much clearer error path.

Suggested fix
+const databaseUrl = process.env.INDEXER_DATABASE_URL;
+if (!databaseUrl) {
+  throw new Error("INDEXER_DATABASE_URL is required for Drizzle Kit commands");
+}
+
 export default defineConfig({
   dialect: "postgresql",
   schema: "./src/db/schema.ts",
   out: "./migrations",
   dbCredentials: {
-    url: process.env.INDEXER_DATABASE_URL ?? "",
+    url: databaseUrl,
   },
   strict: true,
   verbose: true,
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@indexer/common/drizzle.config.ts` around lines 18 - 20, The Drizzle config is
silently defaulting `dbCredentials.url` to an empty string when
`INDEXER_DATABASE_URL` is missing, which should instead fail immediately. Update
the config in `drizzle.config.ts` to validate `process.env.INDEXER_DATABASE_URL`
up front and throw a clear local error if it is absent, matching the
required-URL behavior used by `loadConfig`; keep the fix centered on the
`dbCredentials` setup so `db:generate` and `db:migrate` fail fast with a clear
message.

Comment on lines +24 to +31
/** A positive integer parsed from an environment string. */
const positiveIntFromString = z
.string()
.trim()
.min(1, "must not be empty")
.regex(/^\d+$/, "must be a positive integer")
.transform((value) => Number.parseInt(value, 10))
.refine((value) => Number.isSafeInteger(value), "must be a safe integer");

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

positiveIntFromString allows 0, which is not a positive integer.

The regex /^\d+$/ and subsequent parseInt accept "0". Zero is not a positive integer, and it is semantically invalid for PORT (ephemeral bind) and POLL_INTERVAL_MS (busy-loop). Either rename this to nonNegativeIntFromString and add a separate strictly-positive schema for PORT/POLL_INTERVAL_MS, or add .refine((v) => v > 0, "must be a positive integer") after the safe-integer check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@indexer/common/src/config/env.ts` around lines 24 - 31, The positive integer
parser in positiveIntFromString currently accepts 0, so tighten the schema to
reject zero. Update the zod chain in env.ts by either adding a value check after
the safe-integer refine or splitting this into a non-negative parser plus a
separate strictly-positive schema, and then use the strictly-positive version
for PORT and POLL_INTERVAL_MS.

Comment on lines +20 to +24
export async function checkDbHealth(sql: Sql): Promise<DbHealth> {
const start = Date.now();
try {
await sql`select 1`;
return { healthy: true, latencyMs: Date.now() - start };

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does postgres.js v3.4.9 support per-query timeout or AbortSignal cancellation for tagged-template queries, and what is the recommended way to bound a simple sql`select 1` health probe?

💡 Result:

As of version 3.4.9, postgres.js does not provide native support for per-query AbortSignal cancellation or a per-query timeout option for tagged-template queries [1][2][3]. The timeout functionality in the library is primarily designed for connection-level lifecycle management (e.g., idle_timeout, max_lifetime, and connect_timeout) [1][4]. For a simple health probe like sqlselect 1``, the recommended approach is to execute the query within a try-catch block [5]. Since the library manages connections seamlessly, a failed health probe will throw an error if the database is unreachable or the connection cannot be established, which is the standard way to detect health status [5]. If you require a hard timeout for your health check to prevent the process from hanging, you should wrap the query execution in a Promise that races against a standard setTimeout: const healthCheck = async (sql) => { const timeout = new Promise((_, reject) => setTimeout( => reject(new Error('Health check timed out')), 5000)); return Promise.race([ sqlselect 1, timeout ]); }; This pattern ensures that your application does not wait indefinitely if the database becomes unresponsive [5][6]. Note that while the query promise might "time out" from the perspective of your application logic, the underlying database driver may still be managing the connection, so ensure your overall pool configuration (such as idle_timeout) is tuned for your environment's needs [1][4][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the health check and postgres client setup
git ls-files | rg '(^|/)indexer/common/src/db/health\.ts$|(^|/)postgres|(^|/)db/|(^|/)sql|(^|/)health'
echo '--- health.ts ---'
cat -n indexer/common/src/db/health.ts

echo '--- postgres-related files ---'
git ls-files | rg 'postgres|sql' | sed -n '1,120p'

Repository: Fundable-Protocol/Backend

Length of output: 1705


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for any timeout/cancellation handling around postgres.js usage
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  'connect_timeout|idle_timeout|max_lifetime|statement_timeout|AbortSignal|timeout|Promise\.race|select 1|postgres\(' \
  indexer common . 2>/dev/null | sed -n '1,220p'

Repository: Fundable-Protocol/Backend

Length of output: 809


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- client.ts ---'
sed -n '1,120p' indexer/common/src/db/client.ts

echo '--- health.test.ts ---'
sed -n '1,220p' indexer/common/src/db/health.test.ts

echo '--- docker-compose.yml (health-related section) ---'
sed -n '1,80p' docker-compose.yml

Repository: Fundable-Protocol/Backend

Length of output: 4527


Bound the health probe with a timeout. createDbClient() only sets max on postgres.js, and checkDbHealth() awaits sql\select 1`` directly, so a stalled connection can leave the probe stuck instead of returning unhealthy. Add a bounded timeout path here (or enforce one in the client config) and cover the failure mode in tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@indexer/common/src/db/health.ts` around lines 20 - 24, `checkDbHealth`
currently awaits `sql\`select 1\`` without any deadline, so a stalled database
can hang the probe instead of failing fast. Add a bounded timeout path in
`checkDbHealth` (or enforce it via `createDbClient`/postgres.js config) so the
health check returns unhealthy when the query exceeds the limit, and make sure
the timeout behavior is covered by tests for the `checkDbHealth` and
`createDbClient` flow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant