Managed Tables API
Endpoints for managing company-owned PostgreSQL tables. Each company has a dedicated managed database, provisioned on demand. Supports manual table creation, file upload (CSV/Excel/ZIP) with schema inference, and row-level CRUD.
Difference vs. Data API:
- Data API = consumes saved visualizations as a public endpoint (read-only via API key).
- Managed Tables = the user storage layer (CRUD via console JWT).
Visualizations published via the Data API can read from managed tables, but the two modules are independent.
All endpoints require the standard JWT auth (Authorization: Bearer <token>) and are prefixed with /v1/managed-tables.
Provisioningβ
Provision the managed databaseβ
/v1/managed-tables/provisionCreates a dedicated PostgreSQL database for the company (idempotent β re-calling returns the existing connection).
Required Permission: managed-tables:create
Response: 200 OK
{
"provisioned": true,
"connection_id": "507f1f77bcf86cd799439011"
}
Provisioning statusβ
/v1/managed-tables/statusRequired Permission: managed-tables:list
Response: 200 OK
{
"provisioned": true,
"connection_id": "507f1f77bcf86cd799439011"
}
provisioned: false indicates that POST /provision has not run yet.
Tables CRUDβ
List tablesβ
/v1/managed-tablesRequired Permission: managed-tables:list
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
per_page | integer | 20 | Items per page (max 100) |
search | string | - | Filter by display_name (case-insensitive) |
Response: 200 OK
{
"total": 12,
"quantity": 12,
"records": [
{
"_id": "507f1f77bcf86cd799439011",
"company_id": "507f1f77bcf86cd799439012",
"connection_id": "507f1f77bcf86cd799439013",
"display_name": "vendas_mensais",
"columns": [
{ "name": "produto", "display_name": "Product", "type": "text", "nullable": false },
{ "name": "valor", "display_name": "Value", "type": "decimal", "nullable": true }
],
"source": "manual",
"status": "active",
"row_count": 1500,
"created_by_user_id": "507f1f77bcf86cd799439014",
"created_at": "2026-01-15T10:30:00.000Z",
"updated_at": "2026-01-15T10:30:00.000Z"
}
]
}
Get tableβ
/v1/managed-tables/:idRequired Permission: managed-tables:list
Response: 200 OK β same shape as one item from the list response.
Errors: 404 { "error": "Tabela nΓ£o encontrada" }
Create table (manual)β
/v1/managed-tablesRequired Permission: managed-tables:create
Prerequisite: database provisioned (POST /provision). Otherwise returns 400 Base de dados nΓ£o provisionada. Ative "Meus Dados" primeiro.
Body:
| Field | Type | Required | Description |
|---|---|---|---|
display_name | string | Yes | Table name. Must match ^[a-z_][a-z0-9_]*$ (snake_case, no leading digit). |
columns | array | Yes | Column list (>=1). |
columns[].name | string | Yes | PG name (snake_case). |
columns[].display_name | string | Yes | Friendly name. |
columns[].type | enum | Yes | text | integer | decimal | boolean | date | datetime. |
columns[].nullable | boolean | Yes | Whether NULL is allowed. |
{
"display_name": "vendas_mensais",
"columns": [
{ "name": "produto", "display_name": "Product", "type": "text", "nullable": false },
{ "name": "valor", "display_name": "Value", "type": "decimal", "nullable": true }
]
}
Response: 201 Created β same shape as GET /:id.
Errors:
| HTTP | Code | Description |
|---|---|---|
| 400 | MANAGED_TABLE_DUPLICATE_NAME | Active name already exists |
| 400 | - | Database not provisioned |
| 422 | - | Invalid schema (display_name or columns) |
Update tableβ
/v1/managed-tables/:idRequired Permission: managed-tables:update
Body (optional fields):
{
"display_name": "novo_nome",
"columns": [...],
"status": "active"
}
Response: 200 OK
Delete tableβ
/v1/managed-tables/:idRequired Permission: managed-tables:delete
Soft-delete: the table disappears from listings. Restoration is not exposed by the API.
Response: 204 No Content
Empty tableβ
/v1/managed-tables/:id/truncateRequired Permission: managed-tables:data-write
Removes all rows from the table while keeping the schema, indexes, and metadata.
Response: 200 OK
{ "row_count": 0 }
Errors: 404 { "error": "Tabela nao encontrada" }, 409 { "error_code": "MANAGED_TABLE_TRUNCATE_LOCKED" }
File uploadβ
3-step flow:
- Upload β creates a job in
queuedstatus. Worker processes the file and infers the schema (statusanalisandoβwaiting_confirm). - Confirm β user reviews the inferred schema and confirms; job goes to
insertingβdone. - Polling β frontend tracks state via
GET /upload-jobs/:jobId.
Accepts .csv, .xlsx, .xls, .zip. Up to 10 files per upload, 100MB total.
Upload (create new table)β
/v1/managed-tables/uploadRequired Permission: managed-tables:upload
Content-Type: multipart/form-data
Form fields:
| Field | Type | Description |
|---|---|---|
files | file[] | 1-10 files (repeated field). |
Response: 201 Created
{
"job_id": "507f1f77bcf86cd799439011",
"status": "queued"
}
Upload (append to existing table)β
/v1/managed-tables/:id/uploadRequired Permission: managed-tables:upload
Content-Type: multipart/form-data
Form fields:
| Field | Type | Default | Description |
|---|---|---|---|
files | file[] | - | 1-10 files. |
mode | string | append | append (adds rows) or replace (truncate + insert). |
Response: same shape as the new-table upload.
List active jobsβ
/v1/managed-tables/upload-jobs/activeRequired Permission: managed-tables:list
Returns jobs that are not yet terminal (queued, analisando, waiting_confirm, inserting).
Response: 200 OK
{
"records": [
{
"_id": "...",
"company_id": "...",
"user_table_id": null,
"files": [
{ "original_name": "vendas.csv", "storage_path": "...", "size_bytes": 12345, "mime_type": "text/csv" }
],
"mode": "create",
"status": "waiting_confirm",
"inferred_schema": {
"suggested_table_name": "vendas",
"columns": [{ "name": "produto", "display_name": "Product", "type": "text", "nullable": false }],
"sample_rows": [{ "produto": "X" }],
"total_row_count_estimate": 1500
},
"confirmed_schema": null,
"progress": null,
"error": null,
"created_at": "2026-01-15T10:30:00.000Z",
"updated_at": "2026-01-15T10:30:00.000Z"
}
]
}
Get jobβ
/v1/managed-tables/upload-jobs/:jobIdRequired Permission: managed-tables:list
Response: 200 OK β same shape as the active list item.
Possible statuses: queued, analisando, waiting_confirm, inserting, done, failed.
Errors: 404 { "error": "Job nΓ£o encontrado" }
Confirm job schemaβ
/v1/managed-tables/upload-jobs/:jobId/confirmRequired Permission: managed-tables:upload
Applies the user-confirmed schema and enqueues the inserting phase.
Body:
{
"display_name": "vendas",
"columns": [
{ "name": "produto", "display_name": "Product", "type": "text", "nullable": false }
]
}
For an upload into an existing table (user_table_id set on the job), display_name reuses the legacy name and is not validated against the regex.
Response: 200 OK
{ "status": "inserting" }
Errors:
| HTTP | Code | Description |
|---|---|---|
| 400 | - | Job is not in waiting_confirm |
| 400 | MANAGED_TABLE_DUPLICATE_NAME | Name already exists (only on creation) |
| 422 | - | Invalid display_name (regex), invalid column (name/type) |
Data CRUDβ
List rowsβ
/v1/managed-tables/:id/dataRequired Permission: managed-tables:data-read
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
per_page | integer | 50 | Items per page |
sort_by | string | - | field ASC or -field DESC |
Response: 200 OK
{
"total": 1500,
"quantity": 50,
"records": [
{ "id": "uuid", "produto": "X", "valor": 12.5 }
]
}
Insert rowsβ
/v1/managed-tables/:id/dataRequired Permission: managed-tables:data-write
Body:
{
"rows": [
{ "produto": "X", "valor": 12.5 },
{ "produto": "Y", "valor": 30 }
]
}
Response: 201 Created
{ "inserted": 2 }
Errors: 422 if rows is missing or empty.
Update rowβ
/v1/managed-tables/:id/data/:rowIdRequired Permission: managed-tables:data-write
Body: object with the fields to update.
Response: 200 OK β updated row.
Errors: 404 { "error": "Linha nΓ£o encontrada" }
Delete rowβ
/v1/managed-tables/:id/data/:rowIdRequired Permission: managed-tables:data-write
Response: 204 No Content
Errors: 404 { "error": "Linha nΓ£o encontrada" }
Column typesβ
| Type | PostgreSQL DDL | Description |
|---|---|---|
text | TEXT | Free-form string |
integer | BIGINT | 64-bit integer |
decimal | NUMERIC | Arbitrary-precision number |
boolean | BOOLEAN | true/false |
date | DATE | Date (no time) |
datetime | TIMESTAMPTZ | Timestamp with timezone |
Limits and constraintsβ
- Upload: max 100MB total, max 10 files per call.
- Table / column name: regex
^[a-z_][a-z0-9_]*$. - Table
display_nameis unique per company among active tables. DELETEis a soft-delete: the table disappears from listings. Restoration is not exposed by the API.POST /:id/truncaterequiresmanaged-tables:data-writeand returns409if an upload/import job is active.
Error codesβ
| HTTP | Code | Description |
|---|---|---|
| 400 | MANAGED_TABLE_DUPLICATE_NAME | display_name conflict in the company |
| 400 | - | Database not provisioned / job not in waiting_confirm |
| 401 | - | Missing or invalid JWT |
| 403 | - | Missing IAM permission (managed-tables:*) |
| 404 | - | Table, row, or job does not exist |
| 409 | MANAGED_TABLE_TRUNCATE_LOCKED | An upload/import job is active for the table |
| 422 | - | Invalid schema (identifier regex / column type) |
| 500 | - | Internal error |