Skip to content

managed db - #33

Open
Lftobs wants to merge 9 commits into
feat/managed-db-and-composefrom
managed-db
Open

managed db#33
Lftobs wants to merge 9 commits into
feat/managed-db-and-composefrom
managed-db

Conversation

@Lftobs

@Lftobs Lftobs commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Description

Please provide a summary of the changes and the motivation behind them. What problem does this PR solve?

Fixes #(issue)

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactor (no functional changes)
  • CI / Build / Tooling
  • Other (please describe):

How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce.

  • Existing tests pass (bun test in apps/api/)
  • New tests added (if applicable)
  • Manual testing performed (describe steps)

Checklist

  • My code follows the project's code style (no comments, named exports, functional components, etc.)
  • I have read the contributing guidelines
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the documentation (if applicable)
  • My changes generate no new warnings or lint errors
  • I have run bun test in apps/api/ and all tests pass
  • I have synced the VERSION file if needed (bun run sync-versions)

Screenshots (if applicable)

Before After
(insert here) (insert here)

Additional Context

Add any other context about the PR here (e.g., migration notes, deployment considerations, rollback strategy).

Summary by CodeRabbit

  • New Features
    • Added a dedicated Databases page for creating and managing standalone or project-associated databases.
    • Added support for PostgreSQL, MySQL, Redis, and MongoDB.
    • Added configuration for resources, storage limits, public access, and CIDR restrictions.
    • Added connection details, credential retrieval, storage usage, and start/stop/restart/retry controls.
    • Added lifecycle status indicators and safer deletion handling.
  • Improvements
    • Projects can now be deleted without removing their associated databases.
    • Simplified project creation by removing database provisioning from the workflow.
  • Documentation
    • Updated deployment messaging and average deployment-time statistics.

management

- Migrate databases to a standalone resource model
  with project-optional attachment
- Implement database lifecycle management: start,
  stop, restart, and retry
- Add public access controls with CIDR
  allowlisting
- Introduce Redis and MongoDB support
- Add granular storage monitoring and credential
  management
- Replace legacy project-scoped database tab with
  global dashboard view
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d51744d6-d9b8-4aa7-a247-cf6a2f7b7726

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change introduces standalone managed databases with PostgreSQL, MySQL, Redis, and MongoDB support. It adds validation, persistence, Docker provisioning, lifecycle APIs, monitoring, and a top-level web management interface.

Changes

Managed database lifecycle

Layer / File(s) Summary
Database contracts and persistence
apps/api/src/types.ts, apps/web/src/types/index.ts, apps/api/src/db/schema.ts, apps/api/src/db/migrations/*, apps/api/src/db/repo/*, apps/api/src/db/__tests__/*
Database records support nullable projects, storage, access controls, proxy metadata, volume names, expanded statuses, migration backfills, runtime updates, and project detachment.
Creation and API lifecycle routes
apps/api/src/databases/validation.ts, apps/api/src/api/databases/index.ts, apps/api/src/databases/__tests__/validation.test.ts
Creation validates names, engines, versions, resources, CIDRs, and public access. API routes support listing, creation, credentials, lifecycle actions, retries, sanitized responses, and deletion failure states.
Provisioning and runtime reconciliation
apps/api/src/databases/manager.ts, apps/api/src/utils/dequel-labels.ts, apps/api/src/databases/__tests__/manager.test.ts, apps/api/src/index.ts
The manager provisions four database engines, optional HAProxy proxies, lifecycle operations, storage measurement, cleanup retries, and periodic runtime reconciliation.
Database management interface
apps/web/src/api/client.ts, apps/web/src/components/databases/*, apps/web/src/routes/Databases.tsx, apps/web/src/routes/index.tsx, apps/web/src/components/layout/SidebarNav.tsx, apps/web/src/components/StatusBadge.tsx, apps/web/src/routes/ProjectDetail.tsx
The web app adds a top-level databases page with creation, listing, credentials, lifecycle controls, deletion, status display, and project association.
Project creation and presentation updates
apps/web/src/routes/CreateProjectPage.tsx, apps/web/src/components/project/create/*, apps/web/src/index.css, apps/docs/src/components/*
Project creation no longer provisions databases, deployment submission occurs after environment variables are created, engine options include Redis and MongoDB, and documentation presentation styles and statistics are updated.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant DatabasesPage
  participant API
  participant DatabaseManager
  participant Docker
  Operator->>DatabasesPage: Configure and submit database
  DatabasesPage->>API: Create database request
  API->>API: Validate and normalize input
  API->>DatabaseManager: Start asynchronous provisioning
  DatabaseManager->>Docker: Create database resources
  Docker-->>DatabaseManager: Return runtime resources
  DatabaseManager-->>API: Persist status and connection metadata
  API-->>DatabasesPage: Return sanitized database record
  DatabasesPage-->>Operator: Display status and controls
Loading

Possibly related PRs

  • Lftobs/dequel#19: Both changes modify Docker labels used for provisioned database containers.
  • Lftobs/dequel#29: Both changes expand provisionDatabase provisioning and failure handling.
  • Lftobs/dequel#32: Both changes overlap in database provisioning error handling.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is related to the database-management changes but is too vague to identify the main scope of the pull request. Use a specific title such as "Add standalone managed database provisioning and lifecycle management."
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch managed-db

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.

management

- Migrate databases to a standalone resource model
  with project-optional attachment
- Implement database lifecycle management: start,
  stop, restart, and retry
- Add public access controls with CIDR
  allowlisting
- Introduce Redis and MongoDB support
- Add granular storage monitoring and credential
  management
- Replace legacy project-scoped database tab with
  global dashboard view
Lftobs added 2 commits August 4, 2026 01:44
and update landing page

- Ensure environment variables are created before
  initial deployments to
  prevent runtime errors for apps requiring
  specific configuration.
- Update hero copy and stats on the documentation
  site for better
  messaging and accuracy.
@Lftobs
Lftobs marked this pull request as ready for review August 4, 2026 01:01
Lftobs added 2 commits August 4, 2026 03:08
management

- Migrate databases to a standalone resource model
  with project-optional attachment
- Implement database lifecycle management: start,
  stop, restart, and retry
- Add public access controls with CIDR
  allowlisting
- Introduce Redis and MongoDB support
- Add granular storage monitoring and credential
  management
- Replace legacy project-scoped database tab with
  global dashboard view
and update landing page

- Ensure environment variables are created before
  initial deployments to
  prevent runtime errors for apps requiring
  specific configuration.
- Update hero copy and stats on the documentation
  site for better
  messaging and accuracy.
@Lftobs

Lftobs commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (6)
apps/api/src/databases/manager.ts (5)

167-177: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Write runtime data before the status flips to running.

Line 171 sets the status to running, and Line 172 then writes externalPort and proxyContainerName. A concurrent GET /databases/:id between the two writes returns a running database with a null externalPort. Swap the order so the runtime fields are persisted first.

♻️ Proposed change
         if (status.trim() === 'running') {
-          await updateDatabaseStatus(dbRecord.id, 'running', containerName);
-          await updateDatabaseRuntime(dbRecord.id, { externalPort, proxyContainerName });
+          await updateDatabaseRuntime(dbRecord.id, { externalPort, proxyContainerName });
+          await updateDatabaseStatus(dbRecord.id, 'running', containerName);
           return;
         }
🤖 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 `@apps/api/src/databases/manager.ts` around lines 167 - 177, In the container
readiness path around updateDatabaseStatus and updateDatabaseRuntime, persist
the runtime fields externalPort and proxyContainerName before changing the
database status to running. Keep the existing return and retry behavior
unchanged so a running database is only exposed after its runtime data is
available.

296-303: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Runtime imports duplicate the static import.

Line 6 already imports getDatabaseById, updateDatabaseRuntime, and updateDatabaseStatus from ../db/repo. Line 302 re-imports two of those names dynamically and shadows the module-level bindings. reconcileMissingContainer at Line 291 does the same. The dynamic import adds no cycle protection here because the static import to the same module already exists. Use the static bindings.

🤖 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 `@apps/api/src/databases/manager.ts` around lines 296 - 303, Remove the dynamic
../db/repo imports from startDatabaseMonitoring and reconcileMissingContainer,
and reuse the existing module-level bindings for updateDatabaseRuntime and
updateDatabaseStatus (plus any other already statically imported repository
symbols). Keep the existing monitoring and reconciliation behavior unchanged.

277-287: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Measuring storage starts one throwaway container per database per minute.

startDatabaseMonitoring calls measureDatabaseStorage for every running database on each 60 second tick. Each call starts an alpine:3.20 container and runs du -sm over the whole volume. For a host with many databases this adds container churn and disk I/O every minute, and du cost grows with the data size. Consider a longer interval for storage measurement than for status reconciliation, or read the size from docker system df -v.

🤖 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 `@apps/api/src/databases/manager.ts` around lines 277 - 287, Reduce the
frequency of storage measurements in the startDatabaseMonitoring flow instead of
invoking measureDatabaseStorage on every 60-second reconciliation tick. Keep
status reconciliation at its existing interval, and schedule
measureDatabaseStorage using a longer interval while preserving the
updateDatabaseRuntime behavior.

60-65: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider failing loudly for an unknown database type.

Line 64 returns a PostgreSQL image for any unrecognized type. If an unexpected type ever reaches this function, the code provisions PostgreSQL while the record keeps a different port, credentials, and connection string. The mismatch is hard to diagnose. Validation upstream makes this unlikely today, so this is optional.

♻️ Proposed change
-  return 'postgres:18-alpine';
+  throw new Error(`Unsupported database type: ${type}`);
🤖 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 `@apps/api/src/databases/manager.ts` around lines 60 - 65, Update the database
image selection function containing the mongodb branch to reject unknown
database types instead of falling back to the PostgreSQL image. Preserve the
existing MongoDB version handling and PostgreSQL return for the explicitly
supported PostgreSQL type, while throwing a clear error for any other value.

106-136: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a per-engine configuration table.

The initializers at Lines 106-112 hold PostgreSQL values, and the postgresql branch then only overrides volumeTarget. The default values are unused for the other three engines. A lookup keyed by dbRecord.type states each engine's volume target, environment variables, and command arguments in one place and removes the implicit PostgreSQL default.

🤖 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 `@apps/api/src/databases/manager.ts` around lines 106 - 136, Replace the
mutable default initialization of volumeTarget, envVars, and extraCmdArgs with a
per-engine configuration table keyed by dbRecord.type, defining all three values
explicitly for PostgreSQL, MySQL, Redis, and MongoDB. Update the surrounding
manager flow to select the matching configuration and preserve the existing
PostgreSQL version-dependent volume target behavior.
apps/api/src/databases/__tests__/manager.test.ts (1)

35-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add cases for the default version and the empty CIDR list.

The tests cover the main branches. Three gaps remain:

  • resolveDbImage('postgresql') and resolveDbImage('mysql') with no version, which exercise the default tags.
  • resolveDbImage with an unrecognized type, which currently returns a PostgreSQL image.
  • proxyConfig with allowPublicAccessFromAnywhere: false and an empty allowedCidrs, which produces an ACL line with no source.

Run bun test in apps/api/ before you commit these API changes.

As per coding guidelines: "Run 'bun test' in apps/api/ before committing API changes".

🤖 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 `@apps/api/src/databases/__tests__/manager.test.ts` around lines 35 - 63, Add
tests in the existing resolveDbImage suite for default postgresql and mysql
versions, and verify an unrecognized database type returns the current
PostgreSQL fallback image. Extend the proxyConfig tests with
allowPublicAccessFromAnywhere false and an empty allowedCidrs list, asserting
the resulting ACL behavior. Run bun test from apps/api before committing.

Source: Coding guidelines

🤖 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 `@apps/api/src/api/databases/index.ts`:
- Around line 89-139: Serialize the lifecycle handlers startDatabase,
stopDatabase, restartDatabase, retry, and deleteDatabase per database, using a
conditional status transition or per-database operation lock. Reject requests
when the database is provisioning, restarting, deleting, or recovering from
deletion, and ensure the state check and transition are atomic so concurrent
requests cannot execute Docker operations or overwrite status.
- Around line 74-86: Update the external credential construction around
resolveServerIp and buildConnectionString so IPify failures are not exposed as
127.0.0.1: return null external connection fields or a controlled error when the
public host cannot be resolved. Add an abort timeout to the resolveServerIp
lookup, and ensure external credentials are only returned for a successfully
resolved non-loopback host.

In `@apps/api/src/databases/manager.ts`:
- Around line 231-256: Update provisionPublicProxy to attach the HAProxy
container to config.dockerNetwork and publish externalPort with Docker’s -p
option instead of using --network host. Change the proxyConfig backend target to
dbRecord.internalHost rather than a resolved container IP, preserving the
existing port-allocation and health-check flow.
- Around line 87-94: Update abortIfDeleted to also remove the public proxy
container when provisioning is aborted, using the existing proxy-name derivation
and cleanup mechanism used by deprovisionDatabase or provisionPublicProxy. Keep
the cleanup conditional alongside ensureVolumeRemoved and
ensureContainerRemoved, and ensure failures remain non-blocking.
- Around line 190-197: Update the Docker inspect template in resolveBackendHost
to select and return only the first network IPAddress rather than concatenating
addresses from every network. Preserve the existing trim, empty-address
validation, and return behavior.
- Around line 114-116: Update the PostgreSQL handling around dbRecord.version
and the image resolver to reject version "latest" or resolve it to a concrete
major version before selecting volumeTarget; do not let nonnumeric versions fall
back to major 18. Preserve the existing path selection for validated numeric
versions, using the resolved major version to choose between /var/lib/postgresql
and /var/lib/postgresql/data.

In `@apps/api/src/databases/validation.ts`:
- Around line 26-33: Update validateDatabaseCreate to verify name and version
are strings before calling trim or isSafeDatabaseVersion, returning the existing
validation errors for invalid non-string values. Preserve current validation for
valid strings and absent version values, and add regression tests covering
object name and numeric version inputs.

In `@apps/web/src/components/databases/CreateDatabaseDialog.tsx`:
- Around line 68-69: Associate the project and engine labels with their Select
controls in the CreateDatabaseDialog JSX by adding matching unique htmlFor
values to the labels and id values to the corresponding SelectTrigger elements.
Use distinct identifiers for the project and engine fields while preserving the
existing Select behavior.

In `@apps/web/src/components/databases/DatabaseCard.tsx`:
- Around line 33-46: Use the lifecycle action state managed by lifecycle to
serialize deletion: disable or reject the delete control/handler whenever a
start, stop, restart, or retry is in progress, and keep it unavailable until
lifecycle’s finally block clears the busy state. Ensure the existing
provisioning coordination remains intact while preventing deletion from running
concurrently with these actions.

In `@apps/web/src/index.css`:
- Around line 36-39: Update the universal selector rule in the global stylesheet
by inserting an empty line between the `@apply` border-border declaration and the
scrollbar declarations to satisfy Stylelint’s declaration-empty-line-before
rule.

In `@apps/web/src/routes/CreateProjectPage.tsx`:
- Line 875: Remove the obsolete project-scoped database flow: in
apps/web/src/routes/CreateProjectPage.tsx lines 875-875 delete the hidden
database section, and at lines 102-105 remove the stale database state and
source comment; in
apps/web/src/components/project/create/CreateProjectDialog.tsx lines 73-75
delete the permanent false state and no-op setter; in
apps/web/src/components/project/create/StepResources.tsx lines 3-18 remove
unused database props and update all callers accordingly.
- Around line 102-105: Update CreateProjectPage’s local database handling to
import and use the shared DatabaseType contract. Keep the "postgresql" branch,
remove "postgres" and "mariadb" branches, and ensure the remaining handling
matches the allowed values: "postgresql", "mysql", "redis", and "mongodb".

---

Nitpick comments:
In `@apps/api/src/databases/__tests__/manager.test.ts`:
- Around line 35-63: Add tests in the existing resolveDbImage suite for default
postgresql and mysql versions, and verify an unrecognized database type returns
the current PostgreSQL fallback image. Extend the proxyConfig tests with
allowPublicAccessFromAnywhere false and an empty allowedCidrs list, asserting
the resulting ACL behavior. Run bun test from apps/api before committing.

In `@apps/api/src/databases/manager.ts`:
- Around line 167-177: In the container readiness path around
updateDatabaseStatus and updateDatabaseRuntime, persist the runtime fields
externalPort and proxyContainerName before changing the database status to
running. Keep the existing return and retry behavior unchanged so a running
database is only exposed after its runtime data is available.
- Around line 296-303: Remove the dynamic ../db/repo imports from
startDatabaseMonitoring and reconcileMissingContainer, and reuse the existing
module-level bindings for updateDatabaseRuntime and updateDatabaseStatus (plus
any other already statically imported repository symbols). Keep the existing
monitoring and reconciliation behavior unchanged.
- Around line 277-287: Reduce the frequency of storage measurements in the
startDatabaseMonitoring flow instead of invoking measureDatabaseStorage on every
60-second reconciliation tick. Keep status reconciliation at its existing
interval, and schedule measureDatabaseStorage using a longer interval while
preserving the updateDatabaseRuntime behavior.
- Around line 60-65: Update the database image selection function containing the
mongodb branch to reject unknown database types instead of falling back to the
PostgreSQL image. Preserve the existing MongoDB version handling and PostgreSQL
return for the explicitly supported PostgreSQL type, while throwing a clear
error for any other value.
- Around line 106-136: Replace the mutable default initialization of
volumeTarget, envVars, and extraCmdArgs with a per-engine configuration table
keyed by dbRecord.type, defining all three values explicitly for PostgreSQL,
MySQL, Redis, and MongoDB. Update the surrounding manager flow to select the
matching configuration and preserve the existing PostgreSQL version-dependent
volume target behavior.
🪄 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 Plus

Run ID: 96224943-d4b2-4caf-b7e1-85d32f95b7d4

📥 Commits

Reviewing files that changed from the base of the PR and between a3f06a8 and 3c2d519.

📒 Files selected for processing (33)
  • apps/api/src/api/databases/index.ts
  • apps/api/src/databases/__tests__/manager.test.ts
  • apps/api/src/databases/__tests__/validation.test.ts
  • apps/api/src/databases/manager.ts
  • apps/api/src/databases/validation.ts
  • apps/api/src/db/__tests__/databases-repo-runner.ts
  • apps/api/src/db/__tests__/databases.test.ts
  • apps/api/src/db/__tests__/migration-backfill.test.ts
  • apps/api/src/db/migrations/0007_standalone_databases.sql
  • apps/api/src/db/migrations/meta/_journal.json
  • apps/api/src/db/repo/databases.ts
  • apps/api/src/db/repo/index.ts
  • apps/api/src/db/repo/projects.ts
  • apps/api/src/db/schema.ts
  • apps/api/src/index.ts
  • apps/api/src/types.ts
  • apps/api/src/utils/dequel-labels.ts
  • apps/docs/src/components/Hero.astro
  • apps/docs/src/components/Stats.astro
  • apps/web/src/api/client.ts
  • apps/web/src/components/StatusBadge.tsx
  • apps/web/src/components/databases/CreateDatabaseDialog.tsx
  • apps/web/src/components/databases/DatabaseCard.tsx
  • apps/web/src/components/layout/SidebarNav.tsx
  • apps/web/src/components/project/create/CreateProjectDialog.tsx
  • apps/web/src/components/project/create/StepResources.tsx
  • apps/web/src/components/project/databases/DatabasesTab.tsx
  • apps/web/src/index.css
  • apps/web/src/routes/CreateProjectPage.tsx
  • apps/web/src/routes/Databases.tsx
  • apps/web/src/routes/ProjectDetail.tsx
  • apps/web/src/routes/index.tsx
  • apps/web/src/types/index.ts
💤 Files with no reviewable changes (2)
  • apps/web/src/components/project/databases/DatabasesTab.tsx
  • apps/web/src/routes/ProjectDetail.tsx

Comment thread apps/api/src/api/databases/index.ts Outdated
Comment on lines +74 to +86
const externalHost = dbRecord.publicAccess && dbRecord.externalPort
? await resolveServerIp()
: null;
const externalConnectionString = externalHost
? buildConnectionString(dbRecord, externalHost, dbRecord.externalPort!)
: null;
return {
username: dbRecord.username,
password: dbRecord.password,
internalConnectionString: dbRecord.connectionString,
externalConnectionString,
externalHost,
externalPort: dbRecord.externalPort,

Copy link
Copy Markdown

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

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
fd -a 'index.ts|databases' . | sed 's#^\./##' | head -100

echo "== target file context =="
wc -l apps/api/src/api/databases/index.ts
sed -n '1,140p' apps/api/src/api/databases/index.ts

echo "== resolveServerIp references =="
rg -n "resolveServerIp|externalHost|externalConnectionString|127\.0\.0\.1" apps/api/src -S

Repository: Lftobs/dequel

Length of output: 10090


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== apps/api/src/utils/dns.ts =="
wc -l apps/api/src/utils/dns.ts
cat -n apps/api/src/utils/dns.ts

echo "== dns tests resolveServerIp context =="
sed -n '98,135p' apps/api/src/utils/__tests__/dns.test.ts

echo "== domains resolveServerIp usage =="
sed -n '1,120p' apps/api/src/api/domains/index.ts

echo "== buildConnectionString definitions/usages =="
rg -n "function buildConnectionString|const buildConnectionString|buildConnectionString" apps/api/src -S

echo "== dependency fetch availability =="
if command -v bun >/dev/null 2>&1; then bun -v; fi
if command -v node >/dev/null 2>&1; then node -e "console.log(process.version); const http = require('http'); console.log(typeof http.request);" 2>&1 || true; fi

Repository: Lftobs/dequel

Length of output: 6157


Do not return unresolved externalHost for database credentials.

resolveServerIp() falls back to 127.0.0.1 on IPify fetch failures, and this route does not reject that response. Remote clients can receive a loopback externalConnectionString that cannot connect. Return no external credentials or a controlled error when resolution fails, and limit the IPify lookup with an abort timeout.

🤖 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 `@apps/api/src/api/databases/index.ts` around lines 74 - 86, Update the
external credential construction around resolveServerIp and
buildConnectionString so IPify failures are not exposed as 127.0.0.1: return
null external connection fields or a controlled error when the public host
cannot be resolved. Add an abort timeout to the resolveServerIp lookup, and
ensure external credentials are only returned for a successfully resolved
non-loopback host.

Comment thread apps/api/src/api/databases/index.ts Outdated
Comment on lines +89 to +139
.post("/databases/:id/start", async ({ params: { id }, set }) => {
const dbRecord = await findDatabase(id, set);
if (!dbRecord) return { error: "Database not found" };
const { startDatabase } = await import("../../databases/manager");
await startDatabase(dbRecord);
return sanitizeDatabase((await getDatabaseById(id))!);
})
.post("/databases/:id/stop", async ({ params: { id }, set }) => {
const dbRecord = await findDatabase(id, set);
if (!dbRecord) return { error: "Database not found" };
const { stopDatabase } = await import("../../databases/manager");
await stopDatabase(dbRecord);
return sanitizeDatabase((await getDatabaseById(id))!);
})
.post("/databases/:id/restart", async ({ params: { id }, set }) => {
const dbRecord = await findDatabase(id, set);
if (!dbRecord) return { error: "Database not found" };
const { restartDatabase } = await import("../../databases/manager");
await restartDatabase(dbRecord);
return sanitizeDatabase((await getDatabaseById(id))!);
})
.post("/databases/:id/retry", async ({ params: { id }, set }) => {
const dbRecord = await findDatabase(id, set);
if (!dbRecord) return { error: "Database not found" };
if (dbRecord.status !== "failed") {
set.status = 409;
return { error: "Only failed databases can be retried" };
}
await updateDatabaseStatus(id, "provisioning");
const { provisionDatabase } = await import("../../databases/manager");
provisionDatabase((await getDatabaseById(id))!).catch((err: Error) =>
console.error("DB reprovision failed", err),
);
return sanitizeDatabase((await getDatabaseById(id))!);
})
.delete("/databases/:id", async ({ params: { id }, set }) => {
const dbRecord = await findDatabase(id, set);
if (!dbRecord) return { error: "Database not found" };
const { deprovisionDatabase, waitForProvision } = await import("../../databases/manager");
await waitForProvision(id);
await updateDatabaseStatus(id, "deleting");
const latest = (await getDatabaseById(id)) ?? dbRecord;
try {
await deprovisionDatabase(latest);
} catch (error) {
console.error(`Failed to deprovision database ${id}:`, error);
await updateDatabaseStatus(id, "deletion_failed");
set.status = 502;
return { error: "Database resources could not be deleted; cleanup will be retried automatically" };
}
return { ok: await deleteDatabase(id) };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Serialize lifecycle transitions for each database.

These handlers do not reject or atomically transition incompatible states. After deletion sets deleting, a concurrent start, stop, or restart request can still execute Docker commands and overwrite the record status. Use a conditional status transition or a per-database operation lock. Reject lifecycle operations while provisioning, restarting, deleting, or deletion recovery is active.

🤖 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 `@apps/api/src/api/databases/index.ts` around lines 89 - 139, Serialize the
lifecycle handlers startDatabase, stopDatabase, restartDatabase, retry, and
deleteDatabase per database, using a conditional status transition or
per-database operation lock. Reject requests when the database is provisioning,
restarting, deleting, or recovering from deletion, and ensure the state check
and transition are atomic so concurrent requests cannot execute Docker
operations or overwrite status.

Comment on lines +87 to +94
const abortIfDeleted = async () => {
if (!(await stillProvisioning())) {
if (createdVolume) await ensureVolumeRemoved(dbRecord.volumeName).catch(() => {});
await ensureContainerRemoved(containerName).catch(() => {});
return true;
}
return false;
};

Copy link
Copy Markdown

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

Remove the public proxy container in abortIfDeleted.

abortIfDeleted removes only the database container and volume. The check at Line 165 runs after provisionPublicProxy can create the HAProxy container. If the state changes at that point, the proxy container stays running with --restart unless-stopped and keeps a host port bound. The API delete route calls deprovisionDatabase, which derives the proxy name, so the orphan is usually cleaned later. Remove it here to avoid a window with a live public listener.

🛡️ Proposed fix
   const abortIfDeleted = async () => {
     if (!(await stillProvisioning())) {
+      await ensureContainerRemoved(publicProxyName(dbRecord)).catch(() => {});
       if (createdVolume) await ensureVolumeRemoved(dbRecord.volumeName).catch(() => {});
       await ensureContainerRemoved(containerName).catch(() => {});
       return true;
     }
     return false;
   };
📝 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
const abortIfDeleted = async () => {
if (!(await stillProvisioning())) {
if (createdVolume) await ensureVolumeRemoved(dbRecord.volumeName).catch(() => {});
await ensureContainerRemoved(containerName).catch(() => {});
return true;
}
return false;
};
const abortIfDeleted = async () => {
if (!(await stillProvisioning())) {
await ensureContainerRemoved(publicProxyName(dbRecord)).catch(() => {});
if (createdVolume) await ensureVolumeRemoved(dbRecord.volumeName).catch(() => {});
await ensureContainerRemoved(containerName).catch(() => {});
return true;
}
return false;
};
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from 'node:child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 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 `@apps/api/src/databases/manager.ts` around lines 87 - 94, Update
abortIfDeleted to also remove the public proxy container when provisioning is
aborted, using the existing proxy-name derivation and cleanup mechanism used by
deprovisionDatabase or provisionPublicProxy. Keep the cleanup conditional
alongside ensureVolumeRemoved and ensureContainerRemoved, and ensure failures
remain non-blocking.

Comment thread apps/api/src/databases/manager.ts Outdated
Comment on lines +114 to +116
if (dbRecord.type === 'postgresql') {
const major = Number((dbRecord.version ?? '18').match(/^\d+/)?.[0] ?? '18');
volumeTarget = major >= 18 ? '/var/lib/postgresql' : '/var/lib/postgresql/data';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

postgres official docker image 18 PGDATA /var/lib/postgresql volume change

💡 Result:

Starting with PostgreSQL 18, the official PostgreSQL Docker image has updated its directory structure for data storage to better align with the pg_ctlcluster standard and to facilitate easier major-version upgrades [1][2]. Key Changes in PostgreSQL 18 and Later: 1. New VOLUME Location: The Docker image volume has moved from the legacy /var/lib/postgresql/data to /var/lib/postgresql [1][3]. 2. New PGDATA Environment Variable: The default PGDATA environment variable is now version-specific [1][2]. For PostgreSQL 18, it is set to /var/lib/postgresql/18/docker [1][4]. Subsequent versions will follow this pattern (e.g., /var/lib/postgresql/19/docker for version 19) [1][3]. Why This Change Was Made: This new structure enables users to perform in-place major-version upgrades using pg_upgrade with the --link option [3][5]. By mounting a single volume at /var/lib/postgresql, the container can manage subdirectories for different PostgreSQL major versions, which is a requirement for using the efficient hard-linking upgrade method [1][6]. Migration and Usage Guidance: - For New Deployments: You should mount your data volume at /var/lib/postgresql instead of the legacy /var/lib/postgresql/data path [3][6]. - For Existing Deployments: Upgrading from a version prior to 18 (e.g., 17) to 18+ requires careful migration [3]. Because the internal path has changed, a simple image update will not automatically relocate your existing data [6]. To migrate, you must either: - Perform a proper pg_upgrade, which involves structuring your data directory to match the new 18+ subdirectory convention [2][6]. - Use data export/import tools like pg_dumpall to move your data into a fresh container configured with the new mount point [7]. Note: For versions 17 and below, the legacy recommendation remains to mount at /var/lib/postgresql/data [1][3]. Mounting at /var/lib/postgresql for those older versions will not correctly persist data across container re-creations [1][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg '(^|/)manager\.ts$|resolveDbImage|databases' || true

echo
echo "manager.ts section:"
if [ -f apps/api/src/databases/manager.ts ]; then
  nl -ba apps/api/src/databases/manager.ts | sed -n '80,145p'
fi

echo
echo "postgres resolve references:"
rg -n "resolveDbImage|postgres:latest|version.*latest|postgresql" apps/api/src -S || true

echo
echo "Dockerfile / tags / image references:"
rg -n "postgres:latest|postgres:[0-9]|DATABASE|DATABASE_VERSION|version" . --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' || true

Repository: Lftobs/dequel

Length of output: 796


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "manager.ts section:"
sed -n '80,145p' apps/api/src/databases/manager.ts | cat -n -v

echo
echo "image/version references in databases code:"
rg -n "postgres:latest|postgres:|version|resolveDbImage|DATABASE_VERSION|DATABASE_TYPE|postgresql|MySQL|Redis|MongoDB|mongo" apps/api/src/databases apps/api/src/db apps/api/src/api/databases -S || true

echo
echo "validation test relevant lines:"
sed -n '1,220p' apps/api/src/databases/__tests__/validation.test.ts | cat -n -v

echo
echo "manager tests relevant lines:"
sed -n '1,260人' apps/api/src/databases/__tests__/manager.test.ts | cat -n -v

Repository: Lftobs/dequel

Length of output: 14055


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "validation.ts:"
sed -n '1,80p' apps/api/src/databases/validation.ts | cat -n -v

echo
echo "manager resolveDbImage:"
sed -n '40,70p' apps/api/src/databases/manager.ts | cat -n -v

echo
echo "manager tests relevant lines:"
sed -n '1,70p' apps/api/src/databases/__tests__/manager.test.ts | cat -n -v

echo
echo "Node parser probe:"
node - <<'JS'
const versions = ['latest', '17', '18', '16-alpine', '18-alpine', '~latest', 'latest;evil', ''];
for (const version of versions) {
  const major = Number((version ?? '18').match(/^\d+/)?.[0] ?? '18');
  const volumeTarget = major >= 18 ? '/var/lib/postgresql' : '/var/lib/postgresql/data';
  console.log(JSON.stringify({ version, missingLeadingDigits: Number((version ?? '18').match(/^\d+$/)?.[0] ?? '18') !== Number((version ?? '18').match(/^\d+/)?.[0] ?? '18'), leadingDigitsMajor: major, volumeTarget }));
}
JS

Repository: Lftobs/dequel

Length of output: 8968


Reject version: "latest" or resolve it before choosing the PostgreSQL volume path.

The PostgreSQL image resolver maps latest to postgres:latest, but the volume path parser treats any input without leading digits as major version 18 and mounts /var/lib/postgresql. Accepting latest exposes new databases to whichever Docker tag is current; if that tag resolves to any pre-18 image, the data directory is under the default PGDATA path and the mount will not correctly persist the cluster.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from 'node:child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 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 `@apps/api/src/databases/manager.ts` around lines 114 - 116, Update the
PostgreSQL handling around dbRecord.version and the image resolver to reject
version "latest" or resolve it to a concrete major version before selecting
volumeTarget; do not let nonnumeric versions fall back to major 18. Preserve the
existing path selection for validated numeric versions, using the resolved major
version to choose between /var/lib/postgresql and /var/lib/postgresql/data.

Comment thread apps/api/src/databases/manager.ts Outdated
Comment on lines +190 to +197
const resolveBackendHost = async (containerName: string): Promise<string> => {
const output = await run(dockerBin, [
'inspect', '-f', '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}', containerName,
]);
const address = output.trim();
if (!address) throw new Error(`Database container ${containerName} has no network address`);
return address;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Take the first network address instead of concatenating all of them.

The Go template at Line 192 iterates every entry in .NetworkSettings.Networks and prints each IPAddress without a separator. If the container is attached to more than one network, the result is two addresses joined together, for example 172.18.0.5172.19.0.4. The non-empty check at Line 195 passes, and HAProxy then routes to an invalid backend. The provisioning code attaches a single network today, so this only breaks if a network is added later.

🛡️ Proposed fix
   const output = await run(dockerBin, [
-    'inspect', '-f', '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}', containerName,
+    'inspect', '-f', `{{with index .NetworkSettings.Networks "${config.dockerNetwork}"}}{{.IPAddress}}{{end}}`, containerName,
   ]);
📝 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
const resolveBackendHost = async (containerName: string): Promise<string> => {
const output = await run(dockerBin, [
'inspect', '-f', '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}', containerName,
]);
const address = output.trim();
if (!address) throw new Error(`Database container ${containerName} has no network address`);
return address;
};
const resolveBackendHost = async (containerName: string): Promise<string> => {
const output = await run(dockerBin, [
'inspect', '-f', `{{with index .NetworkSettings.Networks "${config.dockerNetwork}"}}{{.IPAddress}}{{end}}`, containerName,
]);
const address = output.trim();
if (!address) throw new Error(`Database container ${containerName} has no network address`);
return address;
};
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from 'node:child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 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 `@apps/api/src/databases/manager.ts` around lines 190 - 197, Update the Docker
inspect template in resolveBackendHost to select and return only the first
network IPAddress rather than concatenating addresses from every network.
Preserve the existing trim, empty-address validation, and return behavior.

Comment on lines +68 to +69
<div className="grid gap-2"><label className="text-xs font-medium text-zinc-400">Attach to project</label><Select value={projectId} onValueChange={setProjectId}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectItem value="standalone">No project</SelectItem>{projects.map((project) => <SelectItem key={project.id} value={project.id}>{project.name}</SelectItem>)}</SelectContent></Select></div>
<div className="grid gap-2"><label className="text-xs font-medium text-zinc-400">Engine</label><Select value={type} onValueChange={(value) => setType(value as DatabaseType)}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent>{["postgresql", "mysql", "redis", "mongodb"].map((engine) => <SelectItem key={engine} value={engine}>{engine}</SelectItem>)}</SelectContent></Select></div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Associate each Select label with its control.

The label elements have no htmlFor value. The SelectTrigger elements have no matching id. Screen readers cannot determine the field name for project and engine selection.

Proposed fix
-<div className="grid gap-2"><label className="text-xs font-medium text-zinc-400">Attach to project</label><Select value={projectId} onValueChange={setProjectId}><SelectTrigger><SelectValue /></SelectTrigger>
+<div className="grid gap-2"><label htmlFor="database-project" className="text-xs font-medium text-zinc-400">Attach to project</label><Select value={projectId} onValueChange={setProjectId}><SelectTrigger id="database-project"><SelectValue /></SelectTrigger>

-<div className="grid gap-2"><label className="text-xs font-medium text-zinc-400">Engine</label><Select value={type} onValueChange={(value) => setType(value as DatabaseType)}><SelectTrigger><SelectValue /></SelectTrigger>
+<div className="grid gap-2"><label htmlFor="database-engine" className="text-xs font-medium text-zinc-400">Engine</label><Select value={type} onValueChange={(value) => setType(value as DatabaseType)}><SelectTrigger id="database-engine"><SelectValue /></SelectTrigger>
📝 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
<div className="grid gap-2"><label className="text-xs font-medium text-zinc-400">Attach to project</label><Select value={projectId} onValueChange={setProjectId}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectItem value="standalone">No project</SelectItem>{projects.map((project) => <SelectItem key={project.id} value={project.id}>{project.name}</SelectItem>)}</SelectContent></Select></div>
<div className="grid gap-2"><label className="text-xs font-medium text-zinc-400">Engine</label><Select value={type} onValueChange={(value) => setType(value as DatabaseType)}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent>{["postgresql", "mysql", "redis", "mongodb"].map((engine) => <SelectItem key={engine} value={engine}>{engine}</SelectItem>)}</SelectContent></Select></div>
<div className="grid gap-2"><label htmlFor="database-project" className="text-xs font-medium text-zinc-400">Attach to project</label><Select value={projectId} onValueChange={setProjectId}><SelectTrigger id="database-project"><SelectValue /></SelectTrigger><SelectContent><SelectItem value="standalone">No project</SelectItem>{projects.map((project) => <SelectItem key={project.id} value={project.id}>{project.name}</SelectItem>)}</SelectContent></Select></div>
<div className="grid gap-2"><label htmlFor="database-engine" className="text-xs font-medium text-zinc-400">Engine</label><Select value={type} onValueChange={(value) => setType(value as DatabaseType)}><SelectTrigger id="database-engine"><SelectValue /></SelectTrigger><SelectContent>{["postgresql", "mysql", "redis", "mongodb"].map((engine) => <SelectItem key={engine} value={engine}>{engine}</SelectItem>)}</SelectContent></Select></div>
🤖 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 `@apps/web/src/components/databases/CreateDatabaseDialog.tsx` around lines 68 -
69, Associate the project and engine labels with their Select controls in the
CreateDatabaseDialog JSX by adding matching unique htmlFor values to the labels
and id values to the corresponding SelectTrigger elements. Use distinct
identifiers for the project and engine fields while preserving the existing
Select behavior.

Comment on lines +33 to +46
const lifecycle = async (action: "start" | "stop" | "restart" | "retry") => {
setIsBusy(true);
setActionError(null);
try {
if (action === "start") await api.startDatabase(database.id);
else if (action === "stop") await api.stopDatabase(database.id);
else if (action === "retry") await api.retryDatabase(database.id);
else await api.restartDatabase(database.id);
onChanged();
} catch (err) {
setActionError(err instanceof Error ? err.message : "Action failed");
} finally {
setIsBusy(false);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline apps/api/src/databases/manager.ts --items all --match 'startDatabase|stopDatabase|restartDatabase|deprovisionDatabase|waitForProvision'

rg -n -C 5 \
  'startDatabase|stopDatabase|restartDatabase|deprovisionDatabase|waitForProvision|provisionInFlight' \
  apps/api/src/databases/manager.ts \
  apps/api/src/api/databases/index.ts

Repository: Lftobs/dequel

Length of output: 12238


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== DatabaseCard relevant lines =="
sed -n '1,120p' apps/web/src/components/databases/DatabaseCard.tsx

echo
echo "== Database API route relevant lines =="
sed -n '60,150p' apps/api/src/api/databases/index.ts

echo
echo "== Manager relevant lines =="
sed -n '190,285p' apps/api/src/databases/manager.ts

echo
echo "== DB status/repository status helpers search =="
rg -n -C 3 "deleteDatabase|deletion_failed|deleting|status.*=.*deleting|findDatabase|getDatabaseById|updateDatabaseStatus" apps/api/src apps/web/src | head -n 220

echo
echo "== Programmatic control-flow check for lifecycle/delete serialization =="
python3 - <<'PY'
from pathlib import Path
import re

api = Path('apps/api/src/api/databases/index.ts').read_text()
manager = Path('apps/api/src/databases/manager.ts').read_text()

routes = {
    'start': re.search(r'/databases/:id/start[\s\S]*?await startDatabase\(dbRecord\)', api),
    'stop': re.search(r'/databases/:id/stop[\s\S]*?await stopDatabase\(dbRecord\)', api),
    'restart': re.search(r'/databases/:id/restart[\s\S]*?await restartDatabase\(dbRecord\)', api),
    'delete': re.search(r'/databases/:id[\s\S]*?await(deprovision|updateDatabaseStatus|getDatabaseById|findDatabase)', api),
}

for name, m in routes.items():
    print(f"{name}: {bool(m)}")

delete = routes['delete'].group(0) if routes['delete'] else ''
lifecycle_managers = ['startDatabase(', 'stopDatabase(', 'restartDatabase(', 'retryDatabase(']
delete_checks = ['waitForProvision(', 'updateDatabaseStatus("deleting")', 'waitForDel']
for token in lifecycle_managers:
    print(f"lifecycle manager has '{token}': {token in manager}")
for token in delete_checks:
    print(f"delete route has '{token}': {token in delete or token in routes.get('delete', [] or '')}")

# Static-ish check for the same in-flight map used by deprovision/delete.
print("provisionInFlight used by non-provision endpoints:", bool(re.search(r'provisionInFlight\.get|provisionInFlight\.has|provisionInFlight\.set', manager)))
print("deprovisionDatabase calls waitForProvision or deletes from provisionInFlight:", bool(re.search(r'waitForProvision|provisionInFlight\.delete|provisionInFlight\.get', delete)))

# Find the delete route body via line numbers as a small second check.
api_lines = api.splitlines()
for i, line in enumerate(api_lines, 1):
    if '.delete("/databases/:id"' in line:
        print(f"delete route from line {i-1}")
        for j in range(max(1, i-1), min(len(api_lines), i+18)+1):
            print(f"{j}: {api_lines[j-1]}")
        break
PY

Repository: Lftobs/dequel

Length of output: 31663


Disable deletion while lifecycle actions run.

onStart, stop, restart, and retry can run while the delete route still leaves the containers/volume on disk. provisionInFlight only coordinates provisioning/retry; it does not serialize delete against these lifecycle operations. Disable or reject deletion until active lifecycle actions complete.

🤖 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 `@apps/web/src/components/databases/DatabaseCard.tsx` around lines 33 - 46, Use
the lifecycle action state managed by lifecycle to serialize deletion: disable
or reject the delete control/handler whenever a start, stop, restart, or retry
is in progress, and keep it unavailable until lifecycle’s finally block clears
the busy state. Ensure the existing provisioning coordination remains intact
while preventing deletion from running concurrently with these actions.

Comment thread apps/web/src/index.css
Comment on lines 36 to +39
* {
@apply border-border;
scrollbar-width: thin;
scrollbar-color: #27272a transparent;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the Stylelint declaration spacing error.

Add an empty line after @apply border-border;. Stylelint reports declaration-empty-line-before for Line 38.

Proposed fix
  * {
    `@apply` border-border;
+
    scrollbar-width: thin;
    scrollbar-color: `#27272a` transparent;
  }
📝 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
* {
@apply border-border;
scrollbar-width: thin;
scrollbar-color: #27272a transparent;
* {
`@apply` border-border;
scrollbar-width: thin;
scrollbar-color: `#27272a` transparent;
🧰 Tools
🪛 Stylelint (17.14.1)

[error] 38-38: Expected empty line before declaration (declaration-empty-line-before)

(declaration-empty-line-before)

🤖 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 `@apps/web/src/index.css` around lines 36 - 39, Update the universal selector
rule in the global stylesheet by inserting an empty line between the `@apply`
border-border declaration and the scrollbar declarations to satisfy Stylelint’s
declaration-empty-line-before rule.

Source: Linters/SAST tools

Comment on lines +102 to +105
// Legacy project-scoped database controls are hidden in favor of /databases.
const provisionDb = false;
const setProvisionDb = (_value: boolean) => {};
const [dbType, setDbType] = useState<DatabaseType>('postgresql');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 2 \
  'import type|DatabaseType|mariadb|dbType' \
  apps/web/src/routes/CreateProjectPage.tsx \
  apps/web/src/types/index.ts

Repository: Lftobs/dequel

Length of output: 8012


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
route = Path('apps/web/src/routes/CreateProjectPage.tsx')
types = Path('apps/web/src/types/index.ts')

route_text = route.read_text()
types_text = types.read_text()

print('CreateProjectPage.tsx first 20 import lines:')
print('\n'.join(f'{i+1}: {line}' for i, line in enumerate(route_text.splitlines()[:20])))

print('\nDatabaseType reference context:')
for i, line in enumerate(route_text.splitlines(), 1):
    if 'DatabaseType' in line or ("mariadb" in line and 'dbType' in line):
        s = max(1, i-2); e = min(len(route_text.splitlines()), i+2)
        for j in range(s, e+1):
            print(f'{j}: {route_text.splitlines()[j-1]}')
        print()

print('CreateProjectPage.tsx has explicit DatabaseType import?')
print(any('DatabaseType' in line and 'import' in line for line in route_text.splitlines()))

print('Exported DatabaseType branch values:')
for line in types_text.splitlines():
    if line.strip().startswith('export type DatabaseType'):
        print(line.strip())
PY

Repository: Lftobs/dequel

Length of output: 1738


Restore the DatabaseType contract.

This route uses DatabaseType without importing it, so type checking fails. Align the local database handling with the shared type: DatabaseType is 'postgresql' | 'mysql' | 'redis' | 'mongodb', so keep the "postgresql" branch and remove "postgres" and "mariadb".

🤖 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 `@apps/web/src/routes/CreateProjectPage.tsx` around lines 102 - 105, Update
CreateProjectPage’s local database handling to import and use the shared
DatabaseType contract. Keep the "postgresql" branch, remove "postgres" and
"mariadb" branches, and ensure the remaining handling matches the allowed
values: "postgresql", "mysql", "redis", and "mongodb".


{/* Managed Database Selection Cards */}
<div className="pt-4 border-t border-[#1c1c21] space-y-4">
<div className="hidden pt-4 border-t border-[#1c1c21] space-y-4">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Remove the obsolete project-scoped database flow.

The database flow now belongs to the Databases page. These files retain disabled state, no-op setters, hidden markup, and unused props. This also leaves apps/web/src/routes/CreateProjectPage.tsx at 1,160 lines and apps/web/src/components/project/create/CreateProjectDialog.tsx at 555 lines.

  • apps/web/src/routes/CreateProjectPage.tsx#L875-L875: delete the hidden database section.
  • apps/web/src/routes/CreateProjectPage.tsx#L102-L105: delete the stale database state and the unnecessary source comment.
  • apps/web/src/components/project/create/CreateProjectDialog.tsx#L73-L75: delete the permanent false state and no-op setter.
  • apps/web/src/components/project/create/StepResources.tsx#L3-L18: delete the unused database props and update callers.

As per coding guidelines, “No comments in source code unless absolutely necessary” and “No file should be above 500 lines of code; if it is, refactor and split into smaller files with proper feature grouping in a folder rather than scattered across the codebase.”

📍 Affects 3 files
  • apps/web/src/routes/CreateProjectPage.tsx#L875-L875 (this comment)
  • apps/web/src/routes/CreateProjectPage.tsx#L102-L105
  • apps/web/src/components/project/create/CreateProjectDialog.tsx#L73-L75
  • apps/web/src/components/project/create/StepResources.tsx#L3-L18
🤖 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 `@apps/web/src/routes/CreateProjectPage.tsx` at line 875, Remove the obsolete
project-scoped database flow: in apps/web/src/routes/CreateProjectPage.tsx lines
875-875 delete the hidden database section, and at lines 102-105 remove the
stale database state and source comment; in
apps/web/src/components/project/create/CreateProjectDialog.tsx lines 73-75
delete the permanent false state and no-op setter; in
apps/web/src/components/project/create/StepResources.tsx lines 3-18 remove
unused database props and update all callers accordingly.

Source: Coding guidelines

Lftobs added 3 commits August 4, 2026 03:52
projects

Add `installCommand` and `outputDir` fields to the
project schema and
repository to allow custom build configurations.
Introduce an error
summarizer utility to extract actionable build
failure messages from
noisy logs.
robust validation

- Add operation locking to prevent concurrent
  database state changes
- Enforce type checking for database inputs and
  names
- Add default image versions for supported
  database engines
- Include cleanup of proxy containers during
  provision failures
- Add timeout and error handling to IP resolution
- Disable delete buttons in UI during ongoing
  operations
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