Internal Documentation internal
TalkIDE internal documentation

Allows an authenticated user to inspect the per-project PostgreSQL database from within the Workspace “Under-the-hood” mode: list all tables, view the schema of a selected table (columns, types, nullable, default), and browse paginated row data.

  • Each project has its own PostgreSQL database whose name equals the project slug (e.g. slug wildwood-bakery → database wildwood-bakery).
  • The projects table has a dedicated slug VARCHAR(100) UNIQUE NOT NULL column (same migration as UC-07001; all three DB Explorer endpoints look up the project by the slug column directly — see UC-07001 for slug column details and migration notes).
  • Database credentials (host, port, username, password, database name) are stored in CLAUDE.md inside the project directory. BE parses them from that file using a structured regex at request time (no persistent storage of credentials).
  • If CLAUDE.md does not exist (project never had a conversation) or does not contain a ## Database section, BE returns a specific error response so FE can display an appropriate empty state in the Viewer.
  • All DB queries are executed with read-only intent (SELECT only). No schema mutations or DML operations are exposed.
  • Row data is paginated using ?page=0&size=50 (offset-based). Maximum page size is 200.
  • Column ordering in the schema endpoint follows the ordinal position in the PostgreSQL catalog (pg_attribute.attnum).
  • Tables are listed in alphabetical order (ORDER BY table_name ASC).
  • SQL NULL values are serialized in the API response as JSON null. FE renders them in italic to distinguish null from an empty string.
  • BE returns JSON null literal for SQL NULL values.

Credentials Parsing from CLAUDE.md

GetProjectDbCredentialsUseCase reads <talkide.output-dir>/<slug>/CLAUDE.md and extracts the ## Database section. The generated format (from ClaudeCliService.generateClaudeMd) is:

## Database
- PostgreSQL 17, database: `<slug>`
- Username: root, Password: 30+Dodge+78
- Port: 5432

Parsing rules (applied line by line within the ## Database section):

FieldRegex patternExample match
databasedatabase:\s*`([^`]+)`wildwood-bakery
usernameUsername:\s*(\S+)root
passwordPassword:\s*(.+)30+Dodge+78
portPort:\s*(\d+)5432
hostHost:\s*(\S+) (optional)localhost (default if absent)

Defaults: host=localhost (BE server localhost where PG runs).

The parsed credentials are used to open a short-lived JDBC connection (not pooled) for the duration of the request and closed immediately after. Credentials are never logged, cached to DB, or included in API responses.

Security Debt

MVP uses root PostgreSQL credentials parsed from the project’s CLAUDE.md for all Database Explorer queries. This is acceptable for MVP because:

  • (a) The Explorer endpoints are GET-only and execute SELECT-only queries.
  • (b) The project DBs contain non-sensitive synthetic data.

Post-MVP requirement: migrate to a dedicated talkide_readonly PostgreSQL role with explicit GRANT SELECT on all tables, provisioned during project creation. Using root credentials for read-only queries is a known security debt.

Decision recorded by Míra (PM) on 2026-05-02.

API Endpoints

GET /api/v1/projects/{slug}/db/tables — List Tables

sequenceDiagram
    actor User

    User->>+FE: selects "Databáze" tab in Explorer

    FE->>+BE: GET /api/v1/projects/{slug}/db/tables <br> Authorization: Bearer {accessToken}

    BE->>BE: validate access token
    alt token missing or invalid
        BE-->>FE: 401 Unauthorized <br> ErrorResponse
    end

    BE->>DB: load project by slug column (TalkIDE main DB)
    alt project not found
        BE-->>FE: 404 Not Found <br> ErrorResponse
    end

    BE->>BE: check project.tenantId == user's tenantId
    alt project belongs to different tenant
        BE-->>FE: 403 Forbidden <br> ErrorResponse
    end

    BE->>BE: parse CLAUDE.md for DB credentials
    alt CLAUDE.md missing or Database section absent
        BE-->>FE: 503 Service Unavailable <br> ErrorResponse (DB_NOT_READY)
    end

    BE->>ProjectDB: open JDBC connection, query pg_tables WHERE schemaname='public' ORDER BY tablename ASC
    alt connection failed (DB not provisioned yet)
        BE-->>FE: 503 Service Unavailable <br> ErrorResponse (DB_NOT_READY)
    end

    BE->>-FE: 200 OK <br> TableListResponse

    FE->>-User: render flat list of table names in Explorer

GET /api/v1/projects/{slug}/db/tables

200 OK TableListResponse:

{
  "database": "wildwood-bakery",
  "tables": [
    { "name": "orders" },
    { "name": "products" },
    { "name": "users" }
  ]
}

200 OK (database exists but has no tables):

{
  "database": "wildwood-bakery",
  "tables": []
}

401 Unauthorized (missing or invalid access token) ErrorResponse:

{
  "status": 401,
  "code": "AUTHENTICATION_FAILED",
  "message": "Access token is missing or invalid"
}

403 Forbidden (project belongs to a different tenant) ErrorResponse:

{
  "status": 403,
  "code": "FORBIDDEN",
  "message": "You do not have access to this project"
}

404 Not Found (project not found) ErrorResponse:

{
  "status": 404,
  "code": "NOT_FOUND_PROJECT",
  "message": "Project not found"
}

503 Service Unavailable (CLAUDE.md absent, Database section missing, or DB connection failed) ErrorResponse:

{
  "status": 503,
  "code": "DB_NOT_READY",
  "message": "Project database is not available yet"
}

TableInfo DTO

FieldTypeNullableDescription
namestringNOT NULLTable name in the public schema

GET /api/v1/projects/{slug}/db/tables/{table}/schema — Table Schema

sequenceDiagram
    actor User

    User->>+FE: clicks table name in Explorer, selects "Schéma" sub-tab in Viewer

    FE->>+BE: GET /api/v1/projects/{slug}/db/tables/{table}/schema <br> Authorization: Bearer {accessToken}

    BE->>BE: validate access token + tenant ownership (same as above)

    BE->>BE: parse CLAUDE.md for DB credentials

    BE->>ProjectDB: query information_schema.columns WHERE table_schema='public' AND table_name=? ORDER BY ordinal_position

    alt table not found in catalog
        BE-->>FE: 404 Not Found <br> ErrorResponse (NOT_FOUND_TABLE)
    end

    BE->>-FE: 200 OK <br> TableSchemaResponse

    FE->>-User: render schema (column list) in Viewer "Schéma" sub-tab

GET /api/v1/projects/{slug}/db/tables/{table}/schema

Path parameters:

ParameterTypeDescription
slugstringProject slug
tablestringTable name (public schema)

200 OK TableSchemaResponse:

{
  "table": "orders",
  "columns": [
    {
      "name": "id",
      "type": "bigint",
      "nullable": false,
      "defaultValue": "nextval('orders_id_seq'::regclass)"
    },
    {
      "name": "user_id",
      "type": "bigint",
      "nullable": false,
      "defaultValue": null
    },
    {
      "name": "status",
      "type": "character varying",
      "nullable": false,
      "defaultValue": "'PENDING'::character varying"
    },
    {
      "name": "total_amount",
      "type": "numeric",
      "nullable": true,
      "defaultValue": null
    },
    {
      "name": "created_at",
      "type": "timestamp with time zone",
      "nullable": false,
      "defaultValue": "now()"
    }
  ]
}

200 OK (table exists but has no columns — unlikely in practice):

{
  "table": "orders",
  "columns": []
}

404 Not Found (table not found in public schema) ErrorResponse:

{
  "status": 404,
  "code": "NOT_FOUND_TABLE",
  "message": "Table not found"
}

(401, 403, 404 project, 503 — same as GET /tables above)

ColumnInfo DTO

FieldTypeNullableDescription
namestringNOT NULLColumn name
typestringNOT NULLPostgreSQL data type as returned by information_schema.columns.data_type (e.g. bigint, character varying, timestamp with time zone)
nullablebooleanNOT NULLtrue if IS_NULLABLE = 'YES'
defaultValuestringNULLColumn default expression, or null if no default is defined

GET /api/v1/projects/{slug}/db/tables/{table}/data — Table Data (Paged)

sequenceDiagram
    actor User

    User->>+FE: clicks table name in Explorer, selects "Data" sub-tab in Viewer

    FE->>+BE: GET /api/v1/projects/{slug}/db/tables/{table}/data?page=0&size=50 <br> Authorization: Bearer {accessToken}

    BE->>BE: validate access token + tenant ownership

    BE->>BE: parse CLAUDE.md for DB credentials

    BE->>ProjectDB: SELECT * FROM "{table}" ORDER BY 1 LIMIT ? OFFSET ?

    alt table not found (relation does not exist)
        BE-->>FE: 404 Not Found <br> ErrorResponse (NOT_FOUND_TABLE)
    end

    BE->>ProjectDB: SELECT COUNT(*) FROM "{table}"

    BE->>-FE: 200 OK <br> TableDataResponse

    FE->>-User: render paginated data table in Viewer "Data" sub-tab

GET /api/v1/projects/{slug}/db/tables/{table}/data?page=0&size=50

Query parameters:

ParameterRequiredDefaultMinMaxDescription
pageno00Zero-based page index
sizeno501200Rows per page

200 OK TableDataResponse:

{
  "table": "orders",
  "columns": ["id", "user_id", "status", "total_amount", "created_at"],
  "rows": [
    [1, 42, "COMPLETED", "199.90", "2026-04-30T10:00:00Z"],
    [2, 43, "PENDING", null, "2026-04-30T11:00:00Z"]
  ],
  "page": 0,
  "size": 50,
  "totalRows": 2
}

Row values are serialized as JSON primitives where possible (number, boolean, null, string). All PostgreSQL-specific types that have no direct JSON equivalent (e.g. uuid, jsonb, array, timestamp) are serialized as strings using their PostgreSQL text representation. SQL NULL values are serialized as JSON null; FE renders them in italic.

404 Not Found (table not found) ErrorResponse:

{
  "status": 404,
  "code": "NOT_FOUND_TABLE",
  "message": "Table not found"
}

(401, 403, 404 project, 503 — same as GET /tables above)

TableDataResponse DTO

FieldTypeNullableDescription
tablestringNOT NULLTable name
columnsstring[]NOT NULLOrdered list of column names matching the row arrays
rowsany[][]NOT NULLArray of rows; each row is an array of values in the same order as columns. SQL NULL is serialized as JSON null.
pageintegerNOT NULLCurrent zero-based page index
sizeintegerNOT NULLRequested page size
totalRowsintegerNOT NULLTotal number of rows in the table (from COUNT(*))

Frontend

UX Guidelines

[Placeholder — Tereza doplní design spec pro Database Explorer panel (flat table list s Table ikonou z lucide), Viewer sub-přepínač “Schéma” / “Data”, tabulkový layout dat (monospace, malé fonty), prázdný stav, skeleton loader, pager.]

  • SQL NULL values in the Data tab are rendered in italic (e.g. null in italic) to visually distinguish them from the string "null" or an empty string.
  • Tables in the Explorer panel are listed in alphabetical order.

Validations

FieldConstraintsSizePatternNote
slug (path)not_blank^[a-z0-9-]+$From project context
table (path)not_blankFrom Explorer selection
page (query)min=0Optional, default 0
size (query)min=1, max=200Optional, default 50

Backend

Security Note on Table Name in SQL

The table path variable is used directly in a SELECT * FROM "<table>" query. Because parameterized queries cannot parametrize table names, BE must validate the table name against the list returned by pg_tables before constructing the query. The name is quoted with double-quotes to prevent SQL injection from special characters. If the provided table name does not exist in public schema → 404 NOT_FOUND_TABLE.

Validations

FieldConstraintsNote
slugnot_blank; project exists in TalkIDE DB (by slug column) and belongs to user’s tenantDirect DB lookup by slug column
tablenot_blank; must exist in public schema of project DBValidated against pg_tables before query execution
pagemin=0Default 0
sizemin=1, max=200Default 50

Test Cases

GIVENWHENTHEN
Authenticated user, project DB exists with 3 tablesGET /api/v1/projects/{slug}/db/tables is called200 OK with list of 3 table names ordered alphabetically (ORDER BY table_name ASC)
Authenticated user, project DB exists but has no tablesGET /api/v1/projects/{slug}/db/tables is called200 OK with empty tables array
Authenticated user, CLAUDE.md exists, DB does not exist yet (never provisioned)GET /api/v1/projects/{slug}/db/tables is called503 DB_NOT_READY
Authenticated user, CLAUDE.md does not exist (project dir was never created)GET /api/v1/projects/{slug}/db/tables is called503 DB_NOT_READY
Authenticated user, table orders has 5 columnsGET /api/v1/projects/{slug}/db/tables/orders/schema is called200 OK with 5 columns in ordinal_position order; nullable and defaultValue populated correctly
Authenticated user, table name nonexistent does not exist in DBGET /api/v1/projects/{slug}/db/tables/nonexistent/schema is called404 NOT_FOUND_TABLE
Authenticated user, table orders has 120 rowsGET /api/v1/projects/{slug}/db/tables/orders/data?page=0&size=50 is called200 OK; 50 rows returned; totalRows=120
Authenticated user, table orders has 120 rows, page=2, size=50GET /api/v1/projects/{slug}/db/tables/orders/data?page=2&size=50 is called200 OK; 20 rows returned (last page); totalRows=120
Authenticated user, table is emptyGET /api/v1/projects/{slug}/db/tables/orders/data is called200 OK; rows=[], totalRows=0
Authenticated user, table orders has a row with NULL value in total_amountGET /api/v1/projects/{slug}/db/tables/orders/data is called200 OK; affected row contains JSON null literal for that column
size=300 (exceeds max)GET /api/v1/projects/{slug}/db/tables/orders/data?size=300 is called400 Bad Request with validation error
SQL injection attempt via table name (path variable orders; DROP TABLE users)GET /api/v1/projects/{slug}/db/tables/orders%3B%20DROP%20TABLE%20users/schema is called404 NOT_FOUND_TABLE (table not in pg_tables whitelist; no SQL executed)
No Authorization headerGET /api/v1/projects/{slug}/db/tables is called401 AUTHENTICATION_FAILED error response
Authenticated user, project belongs to different tenantGET /api/v1/projects/{slug}/db/tables is called403 FORBIDDEN error response
Project slug not found in DBGET /api/v1/projects/{slug}/db/tables is called404 NOT_FOUND_PROJECT error response

Post-MVP / Future Work

  • Read-only DB user — MVP uses root PostgreSQL credentials from CLAUDE.md. Post-MVP: create a dedicated talkide_readonly PostgreSQL role with GRANT SELECT on all tables, provisioned during project creation. See Security Debt section above.
  • JDBC connection pooling — MVP uses a short-lived, non-pooled connection per request. For heavily used Explorer (e.g. auto-refresh), consider a per-project HikariCP mini-pool with cleanup on project deletion or archival.
  • Row ordering for stable pagination — MVP uses ORDER BY 1 (first column). Post-MVP: detect the primary key from pg_constraint and order by it if available, for consistent pagination across pages.
  • totalRows COUNT(*) on large tables — MVP uses COUNT(*). For tables with millions of rows this adds latency. Post-MVP: use pg_class.reltuples as a fast estimate for large tables.
  • Credential caching — MVP parses CLAUDE.md on every request. Post-MVP: cache parsed credentials in a ConcurrentHashMap<slug, ParsedDbCredentials> with TTL, invalidated when the project directory is recreated.
  • Testcontainers-based integration tests — Add Testcontainers-based integration tests for DB Explorer endpoints covering: alphabetical ordering, empty tables, pagination, NULL values, SQL injection prevention via table name validation, nonexistent table → 404.

Was this page helpful?

Thanks for the feedback.