Opbox

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

CategoryDescriptionExamples
SYSTEMBuilt-in CRM tables (one per org)Individuals, Companies, Line Items, Documents
FORMAuto-created when a form is publishedCustomer Feedback (table)
PIPELINESynced from external pipeline connectorsConnected CRM, Project Management data
USERCustom tables created by usersAny user-defined table
MATTERAuto-managed per board - syncs matter data as rowsIncorporations Board, Renewals Board
INTEGRATIONAuto-created email log tables for email providersEmail Log tables

Table Endpoints

MethodEndpointDescription
GET/api/tablesList all tables
GET/api/tables/treeGet hierarchical table tree
GET/api/tables/statsGet table statistics
POST/api/tables/system/initInitialize system CRM tables
GET/api/tables/:tableIdGet a specific table
POST/api/tablesCreate a new table
PUT/api/tables/:tableIdUpdate a table
DELETE/api/tables/:tableIdDelete a table
POST/api/tables/registersCreate a statutory register table
POST/api/tables/:tableId/aggregatePerform aggregate calculations on table rows
GET/api/tables/:tableId/page-viewsGet page view analytics (views, leaderboard, chart data)
POST/api/tables/:tableId/page-viewsRecord a page view (throttled)
GET/api/tables/:tableId/favouriteCheck if current user has favourited this table/base
POST/api/tables/:tableId/favouriteToggle favourite (star/unstar) a table or base
DELETE/api/tables/:tableId/favouriteRemove favourite
POST/api/tables/:tableId/duplicateDeep-copy a table or base (ADMIN/OWNER only)

Row Endpoints

MethodEndpointDescription
GET/api/tables/:tableId/rowsList rows in a table
POST/api/tables/:tableId/rowsCreate a new row
GET/api/tables/:tableId/rows/:rowIdGet a specific row
PUT/api/tables/:tableId/rows/:rowIdUpdate a row
DELETE/api/tables/:tableId/rows/:rowIdDelete a row
PATCH/api/tables/:tableId/rows/:rowId/cellUpdate a single cell
GET/api/tables/:tableId/rows/:rowId/relatedGet 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.

MethodEndpointDescription
GET/api/tables/:tableId/commentsList comments (threaded, with replies and reactions)
POST/api/tables/:tableId/commentsCreate a comment (supports replies, mentions, attachments)
GET/api/tables/:tableId/reactionsList reactions on comments
POST/api/tables/:tableId/reactionsAdd 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.

Returns related records for a specific row across linked tables.

GET /api/tables/:tableId/rows/:rowId/related

Column Endpoints

MethodEndpointDescription
GET/api/tables/:tableId/columnsList columns for a table
POST/api/tables/:tableId/columnsAdd a column to a table
PATCH/api/tables/:tableId/columns/:columnIdUpdate a column
DELETE/api/tables/:tableId/columns/:columnIdDelete a column

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.

MethodEndpointDescription
GET/api/tables/:tableId/rows/:rowId/links?columnId=xxxGet linked rows for a link column on a row
POST/api/tables/:tableId/rows/:rowId/linksCreate links between rows
DELETE/api/tables/:tableId/rows/:rowId/linksRemove links between rows
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.

MethodEndpointDescription
GET/api/tables/:tableId/viewsList views for a table
POST/api/tables/:tableId/viewsCreate a view
GET/api/views/:viewIdGet view details
PATCH/api/views/:viewIdUpdate a view
DELETE/api/views/:viewIdDelete 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.

ModeDescriptionWho can edit config
collaborativeDefault - visible and editable by all org membersAny org member with editor access
personalOnly visible to the creatorCreator only (404 for others)
lockedVisible to all, config frozen until unlockedNo 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.

MethodEndpointDescription
GET/api/tables/:tableId/page-viewsGet view analytics (total, unique, leaderboard, time-series)
POST/api/tables/:tableId/page-viewsRecord 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

RoleAccess
viewerRead-only access to rows
editorCreate, update, and delete rows
managerEditor + table settings, columns, snapshots, webhooks, permissions
noneExplicitly blocked access