Tables API
Manage tables, columns, rows, views, bases, and notes. Tables are the structured data layer of Opbox - they store CRM data (Individuals, Companies) and form submission data. Tables can be organised into bases (parent containers) for grouping and shared settings.
Visibility rule: GET /api/tables and GET /api/tables/tree exclude entity-shadow records (document, matter workflow, automation, form template, form submission, matter) so they do not appear in "Your Tables".
Route Conventions (Current)
- All routes require authentication and workspace authorization.
- Mutation routes validate request payloads.
- Filtering and sorting are handled server-side where possible.
- Error responses follow a standardized format:
{ success: false, error, code, details? }.
Table Categories
| Category | Description | Examples |
|---|---|---|
SYSTEM | Built-in CRM tables (one per org) | Individuals, Companies, Line Items, Documents |
FORM | Auto-created when a form is published | Customer Feedback (table) |
PIPELINE | Synced from external pipeline connectors | Connected CRM, Project Management data |
USER | Custom tables created by users | Any user-defined table |
MATTER | Auto-managed per board - syncs matter data as rows | Incorporations Board, Renewals Board |
INTEGRATION | Auto-created email log tables for email providers | Email Log tables |
Table Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /api/tables | List all tables |
GET | /api/tables/tree | Get hierarchical table tree |
GET | /api/tables/stats | Get table statistics |
POST | /api/tables/system/init | Initialize system CRM tables |
GET | /api/tables/:tableId | Get a specific table |
POST | /api/tables | Create a new table |
PUT | /api/tables/:tableId | Update a table |
DELETE | /api/tables/:tableId | Delete a table |
POST | /api/tables/registers | Create a statutory register table |
POST | /api/tables/:tableId/aggregate | Perform aggregate calculations on table rows |
GET | /api/tables/:tableId/page-views | Get page view analytics (views, leaderboard, chart data) |
POST | /api/tables/:tableId/page-views | Record a page view (throttled) |
GET | /api/tables/:tableId/favourite | Check if current user has favourited this table/base |
POST | /api/tables/:tableId/favourite | Toggle favourite (star/unstar) a table or base |
DELETE | /api/tables/:tableId/favourite | Remove favourite |
POST | /api/tables/:tableId/duplicate | Deep-copy a table or base (ADMIN/OWNER only) |
Row Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /api/tables/:tableId/rows | List rows in a table |
POST | /api/tables/:tableId/rows | Create a new row |
GET | /api/tables/:tableId/rows/:rowId | Get a specific row |
PUT | /api/tables/:tableId/rows/:rowId | Update a row |
DELETE | /api/tables/:tableId/rows/:rowId | Delete a row |
PATCH | /api/tables/:tableId/rows/:rowId/cell | Update a single cell |
GET | /api/tables/:tableId/rows/:rowId/related | Get related records |
List Rows
Returns paginated rows for a table. Supports multi-column sort and filter expressions.
GET /api/tables/:tableId/rows?page=1&limit=50&sort=createdAt:desc&filter=Status:eq:Active
Response
{
"rows": [
{
"id": "cmlb456def...",
"data": {
"Name": "Acme Corp",
"Email": "contact@acme.com",
"Status": "Active"
},
"createdAt": "2025-01-15T10:00:00.000Z",
"updatedAt": "2025-01-16T12:00:00.000Z"
}
],
"columns": [
{ "id": "col_status", "name": "Status", "type": "SELECT" }
],
"pagination": {
"page": 1,
"limit": 50,
"total": 150,
"totalPages": 3
}
}
Create a Row
Creates a new row in a table. The data object should match the table's column structure.
POST /api/tables/:tableId/rows
Content-Type: application/json
{
"data": {
"Name": "New Client",
"Email": "client@example.com",
"Phone": "+1-555-0123",
"Status": "Lead"
}
}
Update a Single Cell
Updates a single field in a row without replacing the entire data object. Used by inline editing in the table grid.
PATCH /api/tables/:tableId/rows/:rowId/cell
Content-Type: application/json
{
"column": "Status",
"value": "Active"
}
Table Comments
Rich threaded comments on tables and individual rows. Supports @mentions with notifications, threaded replies, emoji reactions, file attachments (up to 10 per comment), and resolve/reopen. Comments can be scoped to a specific row using the rowId parameter, or left table-wide when rowId is omitted.
| Method | Endpoint | Description |
|---|---|---|
GET | /api/tables/:tableId/comments | List comments (threaded, with replies and reactions) |
POST | /api/tables/:tableId/comments | Create a comment (supports replies, mentions, attachments) |
GET | /api/tables/:tableId/reactions | List reactions on comments |
POST | /api/tables/:tableId/reactions | Add an emoji reaction to a comment |
List Comments (with row scoping)
GET /api/tables/:tableId/comments?rowId=:rowId
Omit rowId to get table-level comments only. Returns threaded comments (top-level with nested replies, reactions, and user info).
Create a Comment
POST /api/tables/:tableId/comments
Content-Type: application/json
{
"content": "Reviewed the data and it looks correct",
"rowId": "optional-row-id",
"parentId": "optional-parent-comment-id",
"mentionedUserIds": ["user-id-1", "user-id-2"],
"attachments": [
{ "name": "file.pdf", "url": "/uploads/...", "size": 1024, "mimeType": "application/pdf" }
]
}
Mentioned users receive in-app notifications (type TABLE_COMMENT_MENTION). Attachments are limited to 10 per comment.
Related Records
Returns related records for a specific row across linked tables.
GET /api/tables/:tableId/rows/:rowId/related
Column Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /api/tables/:tableId/columns | List columns for a table |
POST | /api/tables/:tableId/columns | Add a column to a table |
PATCH | /api/tables/:tableId/columns/:columnId | Update a column |
DELETE | /api/tables/:tableId/columns/:columnId | Delete a column |
Link Column Endpoints
Link columns create bidirectional many-to-many relationships between rows in different tables. When you create a LINK column on table A pointing to table B, a reverse column is auto-created on table B. Link cells display a count (e.g. "3 Entities") and open a modal to manage linked records.
| Method | Endpoint | Description |
|---|---|---|
GET | /api/tables/:tableId/rows/:rowId/links?columnId=xxx | Get linked rows for a link column on a row |
POST | /api/tables/:tableId/rows/:rowId/links | Create links between rows |
DELETE | /api/tables/:tableId/rows/:rowId/links | Remove links between rows |
Creating a LINK column
POST /api/tables/:tableId/columns
{
"name": "Related Entities",
"type": "LINK",
"config": {
"targetTableId": "target-table-id",
"relationType": "many_to_many",
"singularLabel": "Entity",
"pluralLabel": "Entities"
}
}
A reverse column is automatically created in the target table. Deleting either column deletes both. Links cascade-delete when rows or columns are removed.
View Endpoints
Views define saved configurations of filters, sorts, and visible columns for a table.
| Method | Endpoint | Description |
|---|---|---|
GET | /api/tables/:tableId/views | List views for a table |
POST | /api/tables/:tableId/views | Create a view |
GET | /api/views/:viewId | Get view details |
PATCH | /api/views/:viewId | Update a view |
DELETE | /api/views/:viewId | Delete a view |
View Modes
Every view has one of three access modes, controlled via the accessMode field on PATCH /api/views/:viewId. The response includes a computed viewMode field on every view object.
| Mode | Description | Who can edit config |
|---|---|---|
collaborative | Default - visible and editable by all org members | Any org member with editor access |
personal | Only visible to the creator | Creator only (404 for others) |
locked | Visible to all, config frozen until unlocked | No one until unlocked by locker or ADMIN/OWNER |
Only the view creator or ADMIN/OWNER can change the view mode. Example:
PATCH /api/views/:viewId
Content-Type: application/json
{ "accessMode": "locked" }
Initialize System Tables
Idempotently creates the four system tables (Individuals, Companies, Line Items, Documents) for your workspace. Safe to call multiple times - existing tables are not modified.
POST /api/tables/system/init
Table Tree
Returns a lightweight hierarchical tree of all tables, grouped by category (System, Form, Pipeline, User). No row data is included.
GET /api/tables/tree
Table Statistics
Returns aggregate counts of tables and total rows across your workspace.
GET /api/tables/stats
Company Registers
Create statutory register views for corporate entities. Register tables are auto-generated with register-specific column layouts (e.g. Share Register, Director Register, Member Register).
POST /api/tables/registers
Content-Type: application/json
{
"entityId": "cm...",
"registerType": "SHARE_REGISTER",
"name": "Share Register - Acme Corp"
}
The register table is created with pre-defined columns appropriate for the register type. Data can then be managed using the standard rows API.
Page View Analytics
Track how often a table is visited and who views it. Views are throttled to 1 per user per 5 minutes to avoid inflating counts. The GET endpoint returns full analytics including a per-user leaderboard and daily view time-series data for chart rendering.
| Method | Endpoint | Description |
|---|---|---|
GET | /api/tables/:tableId/page-views | Get view analytics (total, unique, leaderboard, time-series) |
POST | /api/tables/:tableId/page-views | Record a page view (throttled: 1 per user per 5 min) |
Response (GET)
{
"totalViews": 142,
"uniqueViewers": 8,
"recentViewers": [
{
"viewedAt": "2026-02-22T10:00:00.000Z",
"user": { "id": "cuid...", "name": "Alice", "email": "alice@co.com", "image": null }
}
],
"leaderboard": [
{ "user": { "id": "cuid...", "name": "Alice" }, "views": 34 },
{ "user": { "id": "cuid...", "name": "Bob" }, "views": 21 }
],
"viewsOverTime": [
{ "date": "2026-02-20", "count": 5 },
{ "date": "2026-02-21", "count": 12 }
]
}
leaderboard returns the top 20 users ranked by view count. viewsOverTime returns daily view counts for the last 30 days (date + count pairs for chart rendering).
Bases
A base is a USER table with settings.isBase = true. Bases act as containers for grouping related tables. Child tables reference their base via parentId. The base detail page shows tabs for Overview, Members, Permissions, Data Sources, and Settings.
POST /api/tables
Content-Type: application/json
{
"name": "My Base",
"settings": { "isBase": true }
}
Create a table inside a base
POST /api/tables
Content-Type: application/json
{
"name": "Contacts",
"parentId": "base-id-here",
"autoSetup": true
}
Base Settings
Base settings are stored in the table's settings JSON field. Update them via PUT /api/tables/:baseId.
{
"isBase": true,
"showNullInCells": false,
"showNullEmptyInFilter": false,
"showM2MTables": false,
"mcpEndpointName": null
}
Favourites (Stars)
Star tables or bases to mark them as favourites. The POST endpoint toggles the favourite state - if already favourited it removes the favourite, otherwise it creates one.
GET /api/tables/:tableId/favourite
POST /api/tables/:tableId/favourite
DELETE /api/tables/:tableId/favourite
Responses: { "favourited": true }, { "favourited": true | false }, { "favourited": false }.
Favourites are per-user and per-org. The TableFavourite model uses @@unique([tableId, userId]) to prevent duplicates. Cascade-deletes when the table, user, or workspace is removed.
Duplicate
Deep-copy a table or base with all columns and optionally all row data. When duplicating a base, all child tables are also duplicated. Requires ADMIN or OWNER role.
POST /api/tables/:tableId/duplicate
Content-Type: application/json
{
"includeData": false
}
Response
{
"id": "new-table-cuid...",
"name": "My Base (copy)",
"category": "USER"
}
The duplicate is created with systemType: null to prevent conflicts with system tables. An audit log entry is created with action TABLE_DUPLICATE.
Aggregation
Perform aggregate calculations on table rows (sum, count, average, and grouped aggregates).
POST /api/tables/:tableId/aggregate
Content-Type: application/json
{
"aggregates": [
{ "column": "Amount", "function": "sum" },
{ "column": "Amount", "function": "avg" }
],
"groupBy": "Status"
}
Supported Functions
sum, count, avg, min, max
Table Permissions
Per-table access control with user-specific grants, workspace role grants, and oversight workspace grants. Resolution order (most specific wins): OWNER/ADMIN bypass -> user-specific grant -> workspace_role grant -> oversight_workspace grant -> "everyone" default -> org role fallback.
Get Permissions
GET /api/tables/:tableId/permissions
{
"permissions": [
{ "id": "cm...", "userId": "cm...", "role": "editor", "grantType": "user", "grantRef": null,
"user": { "name": "Jane Doe", "email": "jane@example.com" } },
{ "id": "cm...", "userId": null, "role": "viewer", "grantType": "workspace_role", "grantRef": "MEMBER" },
{ "id": "cm...", "userId": null, "role": "viewer", "grantType": "oversight_workspace", "grantRef": "cm_ws_overseer" },
{ "id": "cm...", "userId": null, "role": "viewer", "grantType": "user", "grantRef": null }
],
"overseerWorkspaces": [
{ "workspaceId": "cm_ws_overseer", "workspaceName": "Regulator", "members": [] }
]
}
Set Permission
POST /api/tables/:tableId/permissions
Content-Type: application/json
User-specific grant:
{ "userId": "cm...", "role": "editor" }
Workspace role grant:
{ "grantType": "workspace_role", "grantRef": "MEMBER", "role": "viewer" }
Oversight workspace grant:
{ "grantType": "oversight_workspace", "grantRef": "cm_ws_overseer", "role": "viewer" }
Requires ADMIN or OWNER role. Oversight workspace grants are validated against active oversight relationships.
Permission Roles
| Role | Access |
|---|---|
viewer | Read-only access to rows |
editor | Create, update, and delete rows |
manager | Editor + table settings, columns, snapshots, webhooks, permissions |
none | Explicitly blocked access |