Skip to content

Database Viewer functionality#2423

Closed
princeaden1 wants to merge 4 commits intodyad-sh:mainfrom
princeaden1:new-db-viewer
Closed

Database Viewer functionality#2423
princeaden1 wants to merge 4 commits intodyad-sh:mainfrom
princeaden1:new-db-viewer

Conversation

@princeaden1
Copy link
Collaborator

@princeaden1 princeaden1 commented Feb 2, 2026


Open with Devin

Summary by cubic

Adds a Supabase-backed Database Viewer and Manager in the Preview panel. Browse tables, run SQL, edit rows, manage storage/auth/users/secrets, and view project logs.

  • New Features

    • New “Database” mode with Supabase Manager: Database, Storage, Auth, Users, Secrets, Logs.
    • Database: table list, schema view, rows with pagination; SQL editor + results; insert/edit/delete rows.
    • Hooks: useSupabaseTables/schema/rows, useSqlQuery, useRowMutations, useStorageBuckets/objects, useAuthConfig/users, useSecrets, useEdgeLogs, useProjectLogs; new query keys.
    • IPC: listTables/getTableSchema/queryTableRows/executeSql; secrets CRUD; auth users; logs endpoints with test-mode fake responses.
    • Tests: e2e coverage; unit tests for formatting, validation, and pagination.
  • Bug Fixes

    • Hardened IPC: escape table names, validate limit/offset, safe JSON parsing.
    • Stable pagination and totals: ORDER BY ctid, null totals handled, empty tables show 0-0.
    • UX: reset selected table on project change; stable row keys.

Written for commit 209204b. Summary will update on new commits.

@wwwillchen
Copy link
Collaborator

@BugBot run

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @princeaden1, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a database viewer feature, enabling users to directly interact with their Supabase database within the application. It includes UI components for browsing tables, viewing schemas, and displaying paginated data, along with necessary backend integrations and e2e tests.

Highlights

  • Database Panel UI: Introduces a new 'Database' panel in the application's preview mode, allowing users to view and interact with their Supabase database.
  • Table Data Display: Implements functionality to list tables, display table schemas (columns), and show table rows with pagination controls.
  • Supabase Integration: Integrates with Supabase to fetch table data, schema information, and row counts, using Supabase project ID and organization slug.
  • E2E Testing: Adds an end-to-end test to verify the database panel's functionality, including connecting to Supabase and displaying table data.
  • Mock Data for Tests: Adds mock SQL results for test builds, enabling e2e tests to run without a real database connection.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional flags.

Open in Devin Review

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 2, 2026

Greptile Overview

Greptile Summary

This PR adds a comprehensive database viewer feature for Supabase projects, allowing users to browse tables, inspect schemas, query rows with pagination, and execute custom SQL queries.

Key Changes

  • Database viewer IPC handlers - Added handlers for listing tables, fetching schemas, querying rows with pagination, and executing arbitrary SQL
  • SQL security hardening - Implemented table name validation (isValidTableName), SQL escaping for values using single-quote doubling, and numeric parameter sanitization for limit/offset
  • React Query integration - Created hooks (useSupabaseTables, useSupabaseSchema, useSupabaseRows, useSqlQuery, useRowMutations) following the established pattern with proper query keys from the centralized factory
  • Database Panel UI - Built a tabbed interface with table browser, schema viewer, row pagination, and SQL editor with Monaco
  • Test coverage - Added comprehensive E2E tests using deterministic fake data in test mode, plus unit tests for formatting and validation utilities
  • Row mutations - Implemented insert, update, and delete operations with proper escaping and query invalidation
  • Supabase Manager expansion - Extended the preview panel with sections for Storage, Authentication, Logs, Secrets, and Users alongside the Database viewer

Architecture Highlights

The implementation follows Electron IPC best practices with validation at the IPC boundary. Table names are validated with a restrictive regex before being used in queries. The escapeValue helper properly escapes single quotes for string literals. Numeric parameters (limit/offset) are sanitized via Math.floor(Number(...)) with bounds checking. The handlers correctly use throw new Error(...) for failures per the IPC guidelines.

Previous thread concerns about SQL injection in getTableSchema and queryTableRows have been addressed - the code now uses proper escaping (single-quote doubling for table_name = '${escapedTable}') in addition to validation. The total type handling was also fixed to properly return null when the count is unavailable.

Confidence Score: 4/5

  • Safe to merge with minor notes on SQL escaping patterns
  • The implementation is solid with proper validation, escaping, comprehensive tests, and follows established patterns. The previous SQL injection and type mismatch concerns have been addressed. Score is 4/5 due to the use of string interpolation for SQL construction (though properly escaped) rather than parameterized queries, which is an acceptable trade-off given the validation and escaping layers in place.
  • No files require special attention - the code quality is consistent throughout

Important Files Changed

Filename Overview
src/ipc/handlers/supabase_handlers.ts IPC handlers for database operations with SQL escaping and validation; previous SQL injection concerns addressed
src/lib/supabase_utils.ts Utility functions for table name validation, cell formatting, and pagination logic
src/hooks/useSupabaseTables.ts React Query hooks following established patterns with proper query key usage
src/components/preview_panel/DatabasePanel.tsx Main database panel with tabs for tables and SQL editor; properly resets state on project changes
e2e-tests/database_viewer.spec.ts Comprehensive E2E tests with deterministic fake data covering tables, SQL editor, storage, auth, and logs

Sequence Diagram

sequenceDiagram
    participant User
    participant DatabasePanel
    participant useSupabaseTables
    participant IpcClient
    participant supabase_handlers
    participant executeSupabaseSql
    participant SupabaseAPI

    User->>DatabasePanel: Select "Database" mode
    DatabasePanel->>useSupabaseTables: Fetch tables list
    useSupabaseTables->>IpcClient: listTables(projectId, orgSlug)
    IpcClient->>supabase_handlers: supabase:list-tables
    
    alt Test Mode
        supabase_handlers->>supabase_handlers: Return fake tables
    else Production
        supabase_handlers->>executeSupabaseSql: Query information_schema
        executeSupabaseSql->>SupabaseAPI: POST /sql (SELECT table_name...)
        SupabaseAPI-->>executeSupabaseSql: JSON result
        executeSupabaseSql-->>supabase_handlers: Parsed table names
    end
    
    supabase_handlers-->>IpcClient: Array of table names
    IpcClient-->>useSupabaseTables: Table list
    useSupabaseTables-->>DatabasePanel: Display tables

    User->>DatabasePanel: Click on "users" table
    DatabasePanel->>useSupabaseTables: Fetch schema + rows
    
    par Parallel Queries
        useSupabaseTables->>IpcClient: getTableSchema(table)
        IpcClient->>supabase_handlers: supabase:get-table-schema
        supabase_handlers->>supabase_handlers: Validate table name
        supabase_handlers->>executeSupabaseSql: Query columns
        executeSupabaseSql-->>supabase_handlers: Column definitions
        supabase_handlers-->>IpcClient: Schema array
    and
        useSupabaseTables->>IpcClient: queryTableRows(table, limit, offset)
        IpcClient->>supabase_handlers: supabase:query-table-rows
        supabase_handlers->>supabase_handlers: Validate + sanitize params
        supabase_handlers->>executeSupabaseSql: SELECT * FROM "table" ORDER BY ctid
        executeSupabaseSql-->>supabase_handlers: Row data + total count
        supabase_handlers-->>IpcClient: {rows, total}
    end
    
    IpcClient-->>DatabasePanel: Schema + Rows
    DatabasePanel-->>User: Display table details

    User->>DatabasePanel: Switch to SQL tab
    User->>DatabasePanel: Type and run custom query
    DatabasePanel->>useSqlQuery: Execute query
    useSqlQuery->>IpcClient: executeSql(query)
    IpcClient->>supabase_handlers: supabase:execute-sql
    supabase_handlers->>executeSupabaseSql: Execute arbitrary SQL
    
    alt Query Success
        executeSupabaseSql-->>supabase_handlers: Query results
        supabase_handlers-->>IpcClient: {columns, rows, rowCount, error: null}
    else Query Error
        executeSupabaseSql-->>supabase_handlers: Error thrown
        supabase_handlers-->>IpcClient: {columns: [], rows: [], rowCount: 0, error: msg}
    end
    
    IpcClient-->>DatabasePanel: Display results
Loading

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant and well-implemented Database Viewer feature. The changes are comprehensive, spanning from frontend components and hooks to backend IPC handlers and tests. The code is generally clean and follows good practices. I've identified a few opportunities for improvement, primarily concerning code duplication in helper functions and the test mock implementation. Addressing these points will enhance the maintainability and testability of this new feature. Overall, great work on adding this valuable functionality.

<tbody>
{rows.map((row, rowIdx) => (
<tr
key={rowIdx}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Using the array index as a key is not recommended as it can lead to unpredictable UI behavior and performance issues if the list is re-ordered. Since these are database rows, they should have a primary key. It would be better to use a unique identifier from the row, like an id column, for the key prop.

Suggested change
key={rowIdx}
key={row.id ? String(row.id) : rowIdx}

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

8 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 2, 2026

Additional Comments (1)

src/ipc/handlers/supabase_handlers.ts
[P1] Test-only handler sends a real Supabase OAuth URL, which can make E2E flaky/offline-dependent.

supabase:fake-connect-and-set-project simulates deep-link-received using url: "https://supabase-oauth.dyad.sh/api/connect-supabase/login". If any renderer logic reacts by opening/navigating to this URL (or validating it), E2E may unexpectedly depend on network availability. Using a clearly fake URL (or a dyad:// deep link) would keep tests hermetic.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ipc/handlers/supabase_handlers.ts
Line: 298:354

Comment:
[P1] Test-only handler sends a real Supabase OAuth URL, which can make E2E flaky/offline-dependent.

`supabase:fake-connect-and-set-project` simulates `deep-link-received` using `url: "https://supabase-oauth.dyad.sh/api/connect-supabase/login"`. If any renderer logic reacts by opening/navigating to this URL (or validating it), E2E may unexpectedly depend on network availability. Using a clearly fake URL (or a `dyad://` deep link) would keep tests hermetic.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 16 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="src/components/preview_panel/PaginationControls.tsx">

<violation number="1" location="src/components/preview_panel/PaginationControls.tsx:22">
P3: Empty tables (total=0) render an invalid range like “Showing 1-0 of 0 rows” because start is always offset+1 and end uses total=0.</violation>
</file>

<file name="src/components/preview_panel/DatabasePanel.tsx">

<violation number="1" location="src/components/preview_panel/DatabasePanel.tsx:15">
P2: selectedTable is never cleared when projectId/app changes, so switching apps can leave a stale table name that TableDetails still queries against the new project.</violation>
</file>

<file name="src/__tests__/supabase_table_helpers.test.ts">

<violation number="1" location="src/__tests__/supabase_table_helpers.test.ts:11">
P2: Test duplicates the production `formatCellValue` helper instead of importing it, so the tests can pass even if the real helper regresses.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ea02480442

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@github-actions
Copy link
Contributor

github-actions bot commented Feb 2, 2026

🔍 Multi-Agent Code Review

Reviewed by 3 independent AI agents with different file orderings and focus areas.

Agents:

  • Agent 1: Code Health Focus (maintainability, clarity, abstractions)
  • Agent 2: Correctness Focus (bugs, security, edge cases)
  • Agent 3: Correctness Focus (bugs, security, edge cases)

Summary

Most significant issues have already been identified by previous reviewers. After consensus voting (issues flagged by 2+ agents where at least one rated it MEDIUM or higher):

Severity Issue Status
🟡 MEDIUM Duplicate validation logic in tests vs handlers Already commented
🟡 MEDIUM Missing ORDER BY in paginated query (non-deterministic results) Already commented
🟡 MEDIUM SQL string interpolation consistency Already commented
🟡 MEDIUM COUNT result type coercion Already commented
🟢 LOW Array index as React key for table rows Already commented
🟢 LOW Pagination shows "1-0" for empty tables Already commented

New Finding

Severity File Issue
🟡 MEDIUM src/ipc/handlers/supabase_handlers.ts JSON.parse calls on SQL results lack try-catch. If Supabase returns malformed JSON or error text, this throws an unhandled exception.

Recommendation: Wrap JSON.parse(result) calls in try-catch blocks with meaningful error messages.

🟢 Low Priority Issues (not commented)
  • Test code organization: The getFakeTestSqlResult function (~190 lines) is embedded in the production management client file. Consider moving to a separate fixtures file.
  • Query key structure: The rows query key includes limit/offset which is good for caching but creates many cache entries. Consider if keepPreviousData option would be beneficial.

Generated by multi-agent consensus review (3 agents)

@github-actions
Copy link
Contributor

github-actions bot commented Feb 2, 2026

🎭 Playwright Test Results

❌ Some tests failed

OS Passed Failed Flaky Skipped
🍎 macOS 314 1 4 105
🪟 Windows 315 3 5 105

Summary: 629 passed, 4 failed, 9 flaky, 210 skipped

Failed Tests

🍎 macOS

  • visual_editing.spec.ts > discard changes
    • Error: expect(locator).toBeVisible() failed

🪟 Windows

  • database_viewer.spec.ts > database panel - shows tables and data when connected
    • TimeoutError: locator.click: Timeout 30000ms exceeded.
  • github.spec.ts > create and sync to new repo
    • TimeoutError: page.waitForSelector: Timeout 5000ms exceeded.
  • github.spec.ts > create and sync to existing repo
    • Error: expect(locator).toMatchAriaSnapshot(expected) failed

📋 Test Commands (macOS)

Copy and paste these commands to run or update snapshots for failed tests:

visual_editing.spec.ts > discard changes

# Run test
npm run e2e e2e-tests/visual_editing.spec.ts -- -g "discard changes"

# Update snapshots
npm run e2e e2e-tests/visual_editing.spec.ts -- -g "discard changes" --update-snapshots

⚠️ Flaky Tests

🍎 macOS

  • select_component.spec.ts > select multiple components (passed after 1 retry)
  • select_component.spec.ts > upgrade app to select component (passed after 2 retries)
  • setup.spec.ts > setup ai provider (passed after 1 retry)
  • visual_editing.spec.ts > edit style of one selected component (passed after 1 retry)

🪟 Windows

  • chat_input.spec.ts > send button disabled during pending proposal (passed after 1 retry)
  • concurrent_chat.spec.ts > concurrent chat (passed after 1 retry)
  • context_manage.spec.ts > manage context - exclude paths with smart context (passed after 2 retries)
  • setup.spec.ts > setup ai provider (passed after 1 retry)
  • template-create-nextjs.spec.ts > create next.js app (passed after 2 retries)

📊 View full report

- Extract shared helpers (formatCellValue, isValidTableName, getPaginationInfo, safeJsonParse) to src/lib/supabase_utils.ts
- Fix table row key prop to use row.id when available instead of array index
- Add SQL injection defense: escape single quotes in table names, validate numeric params
- Fix total type handling to properly return null instead of 0 for unknown counts
- Add ORDER BY ctid for deterministic pagination ordering
- Fix empty table pagination to show "0-0" instead of "1-0"
- Reset selectedTable when projectId changes to prevent stale queries
- Add LIMIT/OFFSET support to fake SQL test results
- Improve Date test assertion to verify exact ISO string output
- Add JSON.parse error handling via safeJsonParse helper

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@wwwillchen
Copy link
Collaborator

@BugBot run

@github-actions
Copy link
Contributor

github-actions bot commented Feb 3, 2026

🔍 Multi-Agent Code Review

3 independent review agents analyzed this PR with different focuses and file orderings. Found 3 new issue(s) via consensus voting.

Summary

Severity Count
🟡 MEDIUM 3

Issues to Address

Severity File Issue
🟡 MEDIUM src/ipc/types/supabase.ts / src/ipc/handlers/supabase_handlers.ts Limit max mismatch: Zod schema caps at 100, handler clamps to 1000
🟡 MEDIUM src/ipc/handlers/supabase_handlers.ts:302 COUNT(*) result could produce NaN total, breaking pagination
🟡 MEDIUM src/lib/supabase_utils.ts:36 isValidTableName rejects valid PostgreSQL table names with hyphens
🟢 Low Priority Issues (1 item)
  • End value misleading when total is null - src/components/preview_panel/PaginationControls.tsx:25 - When total is null and fewer rows are returned than limit, the display shows e.g. "Showing 1-25 rows" even if only 5 rows exist. The component lacks the actual row count to compute a correct end value.

See inline comments for details.

Generated by multi-agent consensus review (3 agents, consensus threshold: 2/3)

@wwwillchen
Copy link
Collaborator

@BugBot run

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 11 additional findings in Devin Review.

Open in Devin Review

throw new Error("supabase:insert-row: No data provided");
}

const columnNames = columns.map((c) => `"${c}"`).join(", ");
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 SQL injection via unescaped double quotes in column names

Column names from user-provided data objects are interpolated into SQL queries with double-quote wrapping but without escaping embedded double quotes. In PostgreSQL, double-quoted identifiers require internal double quotes to be escaped by doubling them (""), but this escaping is not performed.

Root Cause and Impact

The insertRow, updateRow, and deleteRow handlers construct SQL queries by taking column names from Object.keys(data) or Object.keys(primaryKey) and wrapping them in double quotes:

const columnNames = columns.map((c) => `"${c}"`).join(", ");

If a column name contains a double quote character (e.g., name"--), it produces malformed SQL like:

INSERT INTO "users" ("id", "name"--", "email") VALUES (...);

This breaks SQL parsing and could potentially be exploited for SQL injection. While table names are validated with isValidTableName(), column names receive no such validation or escaping.

Impact: A malicious actor with access to the row mutation UI could craft column names to inject arbitrary SQL commands, potentially leading to data exfiltration or database modification.

Prompt for agents
Add column name validation/escaping in the SQL query construction for insertRow, updateRow, and deleteRow handlers. Either:

1. Add a validation function similar to isValidTableName() for column names and reject invalid ones, OR
2. Escape double quotes in column names by replacing " with "" before wrapping in double quotes.

The affected lines are:
- Line 428: `const columnNames = columns.map((c) => `"${c}"`).join(", ");`
- Line 461: `.map((c) => `"${c}" = ${escapeValue(data[c])}`)`
- Line 465: `.map((c) => `"${c}" = ${escapeValue(primaryKey[c])}`)`
- Line 493: `.map((c) => `"${c}" = ${escapeValue(primaryKey[c])}`)`

Recommended fix is to create a helper function like:
const escapeIdentifier = (name: string): string => {
  return `"${name.replace(/"/g, '""')}"`;
};

Then use it: `columns.map((c) => escapeIdentifier(c)).join(", ")`
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

15 issues found across 30 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="src/components/preview_panel/RecordEditorDialog.tsx">

<violation number="1" location="src/components/preview_panel/RecordEditorDialog.tsx:89">
P2: Boolean columns (especially nullable ones) cannot be set to `null` using a simple Checkbox. Once clicked, the value toggles between `true` and `false`, making it impossible to revert to `null`.

Consider using a tri-state control (e.g., a Select with True/False/Null options) or adding a "Clear/Set Null" button next to the checkbox.</violation>
</file>

<file name="src/components/preview_panel/ResultsTable.tsx">

<violation number="1" location="src/components/preview_panel/ResultsTable.tsx:66">
P2: The key generation logic is unstable for rows with `id: 0` because `row.id ? ...` evaluates to false for the number `0`, causing it to fall back to the array index.
Additionally, this assumes the primary key is always named `id`, which may not be true for all tables.

Consider using `row.id != null` (or checking for specific PK columns if available) to ensure stable keys.</violation>
</file>

<file name="src/ipc/handlers/supabase_handlers.ts">

<violation number="1" location="src/ipc/handlers/supabase_handlers.ts:428">
P1: SQL injection vulnerability: Column names from user input are not validated. The `Object.keys(data)` values are used directly in the SQL query without validation or escaping double quotes. A malicious column name containing `"` could escape the identifier quoting. Apply the same validation used for table names (`isValidTableName`) to column names, or escape double quotes by doubling them.</violation>

<violation number="2" location="src/ipc/handlers/supabase_handlers.ts:460">
P1: SQL injection vulnerability: Column names from `dataColumns` and `pkColumns` are not validated before being used in SQL. Add validation for column names before using them in the query.</violation>

<violation number="3" location="src/ipc/handlers/supabase_handlers.ts:464">
P1: SQL injection vulnerability: Column names from `pkColumns` are not validated before being used in the SQL WHERE clause. Add validation for column names.</violation>
</file>

<file name="e2e-tests/database_viewer.spec.ts">

<violation number="1" location="e2e-tests/database_viewer.spec.ts:110">
P2: This setup logic is duplicated from the previous test. Consider extracting it into a `test.beforeEach` block or a helper function (e.g., `setupSupabaseAndConnect(po)`) to improve maintainability and reduce code duplication.</violation>
</file>

<file name="src/components/preview_panel/DatabasePanel.tsx">

<violation number="1" location="src/components/preview_panel/DatabasePanel.tsx:40">
P2: Race condition: If the project changes while a query is executing, the results from the previous project will overwrite the state of the new project.

To fix this, track the current `projectId` using a `useRef` and ensure it matches the `projectId` from the closure before updating state in the `onSuccess`/`onError` callbacks.</violation>
</file>

<file name="src/components/preview_panel/TableDetails.tsx">

<violation number="1" location="src/components/preview_panel/TableDetails.tsx:126">
P1: `RecordEditorDialog` submits empty strings (`""`) for untouched fields. This will cause INSERT operations to fail for non-text columns that have default values (e.g., auto-incrementing `id` columns) because the database expects the column to be omitted to apply the default, but receives an invalid empty string instead.

Filter out empty strings for columns with default values before sending the mutation.</violation>

<violation number="2" location="src/components/preview_panel/TableDetails.tsx:149">
P2: The `AlertDialogAction` automatically closes the dialog immediately when clicked, preventing the loading state (`Loader2`) from being visible and closing the dialog before the operation completes.

Accept the event object and call `e.preventDefault()` to keep the dialog open while the async delete operation is pending. The dialog is already being closed manually via `setDeleteDialogOpen(false)` on success.</violation>
</file>

<file name="src/hooks/useStorageBuckets.ts">

<violation number="1" location="src/hooks/useStorageBuckets.ts:45">
P2: Pagination without `placeholderData: keepPreviousData` causes the UI to flicker or show a loading state when changing pages, which is a poor user experience.

Consistent with other paginated hooks in the repository (e.g., `useSearchApps`), use `keepPreviousData` to maintain the current view while fetching the next page.

**Note:** This fix requires importing `keepPreviousData` from `@tanstack/react-query`.</violation>
</file>

<file name="src/components/preview_panel/SqlEditor.tsx">

<violation number="1" location="src/components/preview_panel/SqlEditor.tsx:45">
P1: The keyboard shortcut handler captures the initial `handleExecute` closure, which uses the initial `value` (defaultValue) and `isExecuting` state.
Because the command is registered only once on mount, subsequent updates to the query or executing state are ignored by the shortcut. Pressing Ctrl/Cmd+Enter will always execute the default query.

**Fix Suggestion**:
Use a `useRef` to hold the latest `handleExecute` function (or the state variables) and access `ref.current` inside the command callback.</violation>
</file>

<file name="src/components/preview_panel/sections/StorageSection.tsx">

<violation number="1" location="src/components/preview_panel/sections/StorageSection.tsx:151">
P2: The "No Files" state is misleading when `offset > 0`. If items are deleted externally, a user on a non-zero offset might see `objects.length === 0` and be told "This bucket is empty", while the bucket actually contains files on previous pages. Checking `total === 0` ensures the bucket is truly empty.</violation>
</file>

<file name="src/components/preview_panel/sections/UsersSection.tsx">

<violation number="1" location="src/components/preview_panel/sections/UsersSection.tsx:29">
P1: When switching between projects (changing `projectId`), the `page` state is not reset. If a user is on page 2 of Project A and switches to Project B (which has fewer users/pages), they may land on an empty page.

In this state, `users` will be empty, triggering the "No Users Yet" empty state, which hides the pagination controls. The user will incorrectly believe Project B has no users and will be unable to navigate back to page 1.

Add a `useEffect` to reset `page` to 1 when `projectId` changes. This requires importing `useEffect` from "react".</violation>
</file>

<file name="src/hooks/useSecrets.ts">

<violation number="1" location="src/hooks/useSecrets.ts:11">
P2: Add `meta: { showErrorToast: true }` to this query. This ensures consistent error reporting (UI toasts) for Supabase operations, matching the pattern in `src/hooks/useSupabase.ts`.</violation>
</file>

<file name="src/components/preview_panel/sections/DatabaseSection.tsx">

<violation number="1" location="src/components/preview_panel/sections/DatabaseSection.tsx:135">
P2: Switching between tabs causes `TabsContent` to unmount its children, leading to state loss.
Specifically:
1. The SQL query in `SqlEditor` is lost because its internal state is destroyed on unmount.
2. Scroll positions in `TableList` and `TableDetails` are lost.

To fix this, use `forceMount` on `TabsContent` to keep the DOM elements alive (hidden) when inactive.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

.map((c) => `"${c}" = ${escapeValue(data[c])}`)
.join(", ");

const whereClause = pkColumns
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 5, 2026

Choose a reason for hiding this comment

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

P1: SQL injection vulnerability: Column names from pkColumns are not validated before being used in the SQL WHERE clause. Add validation for column names.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/ipc/handlers/supabase_handlers.ts, line 464:

<comment>SQL injection vulnerability: Column names from `pkColumns` are not validated before being used in the SQL WHERE clause. Add validation for column names.</comment>

<file context>
@@ -318,6 +337,387 @@ export function registerSupabaseHandlers() {
+      .map((c) => `"${c}" = ${escapeValue(data[c])}`)
+      .join(", ");
+
+    const whereClause = pkColumns
+      .map((c) => `"${c}" = ${escapeValue(primaryKey[c])}`)
+      .join(" AND ");
</file context>
Suggested change
const whereClause = pkColumns
// Validate column names to prevent SQL injection
for (const c of pkColumns) {
if (!isValidTableName(c)) {
throw new Error(`supabase:delete-row: Invalid column name: ${c}`);
}
}
const whereClause = pkColumns
Fix with Cubic

throw new Error("supabase:update-row: No data provided");
}

const setClause = dataColumns
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 5, 2026

Choose a reason for hiding this comment

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

P1: SQL injection vulnerability: Column names from dataColumns and pkColumns are not validated before being used in SQL. Add validation for column names before using them in the query.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/ipc/handlers/supabase_handlers.ts, line 460:

<comment>SQL injection vulnerability: Column names from `dataColumns` and `pkColumns` are not validated before being used in SQL. Add validation for column names before using them in the query.</comment>

<file context>
@@ -318,6 +337,387 @@ export function registerSupabaseHandlers() {
+      throw new Error("supabase:update-row: No data provided");
+    }
+
+    const setClause = dataColumns
+      .map((c) => `"${c}" = ${escapeValue(data[c])}`)
+      .join(", ");
</file context>
Suggested change
const setClause = dataColumns
// Validate column names to prevent SQL injection
for (const c of [...dataColumns, ...pkColumns]) {
if (!isValidTableName(c)) {
throw new Error(`supabase:update-row: Invalid column name: ${c}`);
}
}
const setClause = dataColumns
Fix with Cubic

throw new Error("supabase:insert-row: No data provided");
}

const columnNames = columns.map((c) => `"${c}"`).join(", ");
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 5, 2026

Choose a reason for hiding this comment

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

P1: SQL injection vulnerability: Column names from user input are not validated. The Object.keys(data) values are used directly in the SQL query without validation or escaping double quotes. A malicious column name containing " could escape the identifier quoting. Apply the same validation used for table names (isValidTableName) to column names, or escape double quotes by doubling them.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/ipc/handlers/supabase_handlers.ts, line 428:

<comment>SQL injection vulnerability: Column names from user input are not validated. The `Object.keys(data)` values are used directly in the SQL query without validation or escaping double quotes. A malicious column name containing `"` could escape the identifier quoting. Apply the same validation used for table names (`isValidTableName`) to column names, or escape double quotes by doubling them.</comment>

<file context>
@@ -318,6 +337,387 @@ export function registerSupabaseHandlers() {
+      throw new Error("supabase:insert-row: No data provided");
+    }
+
+    const columnNames = columns.map((c) => `"${c}"`).join(", ");
+    const values = columns.map((c) => escapeValue(data[c])).join(", ");
+
</file context>
Suggested change
const columnNames = columns.map((c) => `"${c}"`).join(", ");
// Validate column names to prevent SQL injection
for (const c of columns) {
if (!isValidTableName(c)) {
throw new Error(`supabase:insert-row: Invalid column name: ${c}`);
}
}
const columnNames = columns.map((c) => `"${c}"`).join(", ");
Fix with Cubic


const handleSave = async (data: Record<string, unknown>) => {
try {
if (editorMode === "insert") {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 5, 2026

Choose a reason for hiding this comment

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

P1: RecordEditorDialog submits empty strings ("") for untouched fields. This will cause INSERT operations to fail for non-text columns that have default values (e.g., auto-incrementing id columns) because the database expects the column to be omitted to apply the default, but receives an invalid empty string instead.

Filter out empty strings for columns with default values before sending the mutation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/preview_panel/TableDetails.tsx, line 126:

<comment>`RecordEditorDialog` submits empty strings (`""`) for untouched fields. This will cause INSERT operations to fail for non-text columns that have default values (e.g., auto-incrementing `id` columns) because the database expects the column to be omitted to apply the default, but receives an invalid empty string instead.

Filter out empty strings for columns with default values before sending the mutation.</comment>

<file context>
@@ -45,6 +75,90 @@ export function TableDetails({
+
+  const handleSave = async (data: Record<string, unknown>) => {
+    try {
+      if (editorMode === "insert") {
+        await insertRow.mutateAsync(data);
+        showSuccess("Row inserted successfully");
</file context>
Fix with Cubic

editorRef.current = editor;

// Add keyboard shortcut for executing query (Ctrl/Cmd + Enter)
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 5, 2026

Choose a reason for hiding this comment

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

P1: The keyboard shortcut handler captures the initial handleExecute closure, which uses the initial value (defaultValue) and isExecuting state.
Because the command is registered only once on mount, subsequent updates to the query or executing state are ignored by the shortcut. Pressing Ctrl/Cmd+Enter will always execute the default query.

Fix Suggestion:
Use a useRef to hold the latest handleExecute function (or the state variables) and access ref.current inside the command callback.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/preview_panel/SqlEditor.tsx, line 45:

<comment>The keyboard shortcut handler captures the initial `handleExecute` closure, which uses the initial `value` (defaultValue) and `isExecuting` state.
Because the command is registered only once on mount, subsequent updates to the query or executing state are ignored by the shortcut. Pressing Ctrl/Cmd+Enter will always execute the default query.

**Fix Suggestion**:
Use a `useRef` to hold the latest `handleExecute` function (or the state variables) and access `ref.current` inside the command callback.</comment>

<file context>
@@ -0,0 +1,119 @@
+    editorRef.current = editor;
+
+    // Add keyboard shortcut for executing query (Ctrl/Cmd + Enter)
+    editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {
+      handleExecute();
+    });
</file context>
Fix with Cubic

}
};

const handleDeleteConfirm = async () => {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 5, 2026

Choose a reason for hiding this comment

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

P2: The AlertDialogAction automatically closes the dialog immediately when clicked, preventing the loading state (Loader2) from being visible and closing the dialog before the operation completes.

Accept the event object and call e.preventDefault() to keep the dialog open while the async delete operation is pending. The dialog is already being closed manually via setDeleteDialogOpen(false) on success.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/preview_panel/TableDetails.tsx, line 149:

<comment>The `AlertDialogAction` automatically closes the dialog immediately when clicked, preventing the loading state (`Loader2`) from being visible and closing the dialog before the operation completes.

Accept the event object and call `e.preventDefault()` to keep the dialog open while the async delete operation is pending. The dialog is already being closed manually via `setDeleteDialogOpen(false)` on success.</comment>

<file context>
@@ -45,6 +75,90 @@ export function TableDetails({
+    }
+  };
+
+  const handleDeleteConfirm = async () => {
+    if (!rowToDelete) return;
+    try {
</file context>
Fix with Cubic

limit: number;
offset: number;
}) {
return useQuery<ListStorageObjectsResult, Error>({
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 5, 2026

Choose a reason for hiding this comment

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

P2: Pagination without placeholderData: keepPreviousData causes the UI to flicker or show a loading state when changing pages, which is a poor user experience.

Consistent with other paginated hooks in the repository (e.g., useSearchApps), use keepPreviousData to maintain the current view while fetching the next page.

Note: This fix requires importing keepPreviousData from @tanstack/react-query.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/hooks/useStorageBuckets.ts, line 45:

<comment>Pagination without `placeholderData: keepPreviousData` causes the UI to flicker or show a loading state when changing pages, which is a poor user experience.

Consistent with other paginated hooks in the repository (e.g., `useSearchApps`), use `keepPreviousData` to maintain the current view while fetching the next page.

**Note:** This fix requires importing `keepPreviousData` from `@tanstack/react-query`.</comment>

<file context>
@@ -0,0 +1,65 @@
+  limit: number;
+  offset: number;
+}) {
+  return useQuery<ListStorageObjectsResult, Error>({
+    queryKey: queryKeys.supabase.storageObjects({
+      projectId: projectId ?? "",
</file context>
Fix with Cubic

<AlertDescription>{objectsError.message}</AlertDescription>
</Alert>
</div>
) : objects.length === 0 ? (
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 5, 2026

Choose a reason for hiding this comment

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

P2: The "No Files" state is misleading when offset > 0. If items are deleted externally, a user on a non-zero offset might see objects.length === 0 and be told "This bucket is empty", while the bucket actually contains files on previous pages. Checking total === 0 ensures the bucket is truly empty.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/preview_panel/sections/StorageSection.tsx, line 151:

<comment>The "No Files" state is misleading when `offset > 0`. If items are deleted externally, a user on a non-zero offset might see `objects.length === 0` and be told "This bucket is empty", while the bucket actually contains files on previous pages. Checking `total === 0` ensures the bucket is truly empty.</comment>

<file context>
@@ -0,0 +1,270 @@
+                <AlertDescription>{objectsError.message}</AlertDescription>
+              </Alert>
+            </div>
+          ) : objects.length === 0 ? (
+            <div className="flex flex-col items-center justify-center h-full gap-4 text-center p-8">
+              <HardDrive className="w-12 h-12 text-muted-foreground" />
</file context>
Fix with Cubic

}

export function useSecrets({ projectId, organizationSlug }: UseSecretsParams) {
return useQuery({
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 5, 2026

Choose a reason for hiding this comment

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

P2: Add meta: { showErrorToast: true } to this query. This ensures consistent error reporting (UI toasts) for Supabase operations, matching the pattern in src/hooks/useSupabase.ts.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/hooks/useSecrets.ts, line 11:

<comment>Add `meta: { showErrorToast: true }` to this query. This ensures consistent error reporting (UI toasts) for Supabase operations, matching the pattern in `src/hooks/useSupabase.ts`.</comment>

<file context>
@@ -0,0 +1,84 @@
+}
+
+export function useSecrets({ projectId, organizationSlug }: UseSecretsParams) {
+  return useQuery({
+    queryKey: queryKeys.supabase.secrets({
+      projectId: projectId ?? "",
</file context>
Fix with Cubic

</TabsContent>

{/* SQL tab content */}
<TabsContent value="sql" className="flex-1 m-0 overflow-hidden">
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 5, 2026

Choose a reason for hiding this comment

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

P2: Switching between tabs causes TabsContent to unmount its children, leading to state loss.
Specifically:

  1. The SQL query in SqlEditor is lost because its internal state is destroyed on unmount.
  2. Scroll positions in TableList and TableDetails are lost.

To fix this, use forceMount on TabsContent to keep the DOM elements alive (hidden) when inactive.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/preview_panel/sections/DatabaseSection.tsx, line 135:

<comment>Switching between tabs causes `TabsContent` to unmount its children, leading to state loss.
Specifically:
1. The SQL query in `SqlEditor` is lost because its internal state is destroyed on unmount.
2. Scroll positions in `TableList` and `TableDetails` are lost.

To fix this, use `forceMount` on `TabsContent` to keep the DOM elements alive (hidden) when inactive.</comment>

<file context>
@@ -0,0 +1,157 @@
+        </TabsContent>
+
+        {/* SQL tab content */}
+        <TabsContent value="sql" className="flex-1 m-0 overflow-hidden">
+          <div className="flex flex-col h-full">
+            {/* SQL Editor - top half */}
</file context>
Fix with Cubic

return "NULL";
}
if (typeof value === "number") {
return String(value);
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 MEDIUM - escapeValue does not handle NaN, Infinity, or -Infinity

String(NaN) produces 'NaN' and String(Infinity) produces 'Infinity', neither of which are valid SQL literals. These values can reach this function via user input (e.g., parseInt("abc") returns NaN from RecordEditorDialog).

Suggested change
return String(value);
if (typeof value === "number") {
if (!Number.isFinite(value)) {
throw new Error(`Invalid numeric value: ${value}`);
}
return String(value);
}

Consensus: identified by 3/3 independent reviewers

Comment on lines +29 to +38
predicate: (query) => {
const key = query.queryKey;
return (
Array.isArray(key) &&
key[0] === "supabase" &&
key[1] === "rows" &&
key[2] === projectId &&
key[4] === table
);
},
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 MEDIUM - Fragile query invalidation using hardcoded array indices

This predicate checks key[0] === "supabase" && key[1] === "rows" && key[2] === projectId && key[4] === table, which is tightly coupled to the key structure in queryKeys.ts. It skips index 3 (organizationSlug), meaning it invalidates rows across all organizations. If the query key structure changes, this will silently break.

Consider using a prefix match instead:

queryClient.invalidateQueries({
  queryKey: ["supabase", "rows", projectId, organizationSlug, table],
});

Consensus: identified by 3/3 independent reviewers

raw_user_meta_data as user_metadata
FROM auth.users
ORDER BY created_at DESC
LIMIT ${perPage} OFFSET ${offset};
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 MEDIUM - perPage and offset interpolated into SQL without numeric validation

Unlike queryTableRows handler which clamps numeric params with Math.max/Math.min/Math.floor, listAuthUsers interpolates perPage and offset directly into the SQL string. While the Zod schema provides min/max constraints at the IPC boundary, defense-in-depth would add validation here too, since this function can be called directly.

Suggested change
LIMIT ${perPage} OFFSET ${offset};
LIMIT ${Math.max(1, Math.min(100, Math.floor(Number(perPage))))} OFFSET ${Math.max(0, Math.floor(Number(offset)))};

Consensus: identified by 3/3 independent reviewers

Comment on lines +620 to +621
const escapedPrefix = prefix.replace(/'/g, "''");
whereClause += ` AND name LIKE '${escapedPrefix}%'`;
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 MEDIUM - LIKE pattern injection via unescaped % and _ in prefix

The prefix parameter is escaped for single quotes but not for LIKE wildcards. A prefix containing % would match all objects in the bucket, and _ matches any single character. This bypasses the intended prefix filtering.

Suggested change
const escapedPrefix = prefix.replace(/'/g, "''");
whereClause += ` AND name LIKE '${escapedPrefix}%'`;
const escapedPrefix = prefix
.replace(/'/g, "''")
.replace(/%/g, "\\%")
.replace(/_/g, "\\_");
whereClause += ` AND name LIKE '${escapedPrefix}%' ESCAPE '\\'`;

Consensus: identified by 2/3 independent reviewers

@github-actions
Copy link
Contributor

github-actions bot commented Feb 5, 2026

🔍 Multi-Agent Code Review

Found 4 new issue(s) flagged by 3 independent reviewers.
(6 issue(s) skipped - already commented by other reviewers)

Summary

Severity Count
🟡 MEDIUM 4

New Issues

Severity File Issue Agents
🟡 MEDIUM src/ipc/handlers/supabase_handlers.ts:402 escapeValue doesn't handle NaN/Infinity 3/3
🟡 MEDIUM src/hooks/useRowMutations.ts:29 Fragile query invalidation using hardcoded array indices 3/3
🟡 MEDIUM src/supabase_admin/supabase_management_client.ts:1070 SQL params interpolated without numeric validation 3/3
🟡 MEDIUM src/ipc/handlers/supabase_handlers.ts:621 LIKE pattern injection via unescaped wildcards in prefix 2/3

Already Commented (skipped)

The following consensus issues were already identified by other reviewers:

  • SQL injection via unvalidated column names in INSERT/UPDATE/DELETE (line 428)
  • Stale closure in Monaco Ctrl+Enter handler (SqlEditor.tsx:45)
  • RecordEditorDialog insert mode sends empty string for columns with defaults (line 49)
  • DatabasePanel appears to be dead code (line 1)
  • getPrimaryKey dangerous fallback using entire row (TableDetails.tsx:85)
  • safeLimit allows 1000 but Zod caps at 100 (line 304)
🟢 Low Priority Issues (3 items - single agent only)
  • Edge logs hardcoded to level: "info" - src/supabase_admin/supabase_management_client.ts:1281 - All edge log entries lose their actual severity level
  • Log level colors use hardcoded dark-theme classes - src/components/preview_panel/sections/LogsSection.tsx:294 - Inconsistent with theme-aware CSS used elsewhere
  • ExecuteSqlResult schema allows both error and rows simultaneously - src/ipc/types/supabase.ts:94 - Consider a discriminated union

See inline comments for details.

Generated by multi-agent consensus review (3 independent Claude reviewers)

@wwwillchen
Copy link
Collaborator

i'll go ahead and close this for now, but feel free to open this later when it's ready

@wwwillchen wwwillchen closed this Mar 5, 2026
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.

2 participants