opbox

Not everything a tenant works with belongs on a matter. Client lists, company registers, product catalogues, line items, pipelines: this is list-shaped data, and Opbox holds it in a structured tables store sitting alongside the matter spine. A table is a set of typed columns and rows, but a cell is not a loose value in a spreadsheet blob. Each cell is a canonical Fact keyed to its row and its column, so the table carries the same one-source-of-truth discipline as the rest of the system: one value, written through one front door, checked and recorded every time.

What it does

Typed tables in named categories. A table is authored with a category that decides how it is provisioned and synced, not how it is special-cased: USER tables are the hand-built ones, and SYSTEM tables stand for the well-known registers a tenant keeps - individuals, companies, partners, products, line items, deals, documents. A SYSTEM table names its type and there is at most one of each per workspace, so the “companies register” is a single, unambiguous table rather than three competing copies. Tables can also nest: a table can sit under a parent as a base-and-child relation, so related lists are grouped rather than scattered.

Typed and computed columns. A column has a real storage type - text, number, date, boolean, select, file, reference, JSON - and the type is a closed set, not a free-form label. Four column kinds are computed rather than stored: a formula, a link, a lookup, and a rollup. A computed column is derived at read time and is never written to and never persisted, so a calculated total can never silently drift out of step with the figures it is calculated from. You add columns, change a column’s storage type with a best-effort coercion that never deletes data, hide a column for a role, reorder columns, or drop one.

Bidirectional link columns. A link column relates rows in one table to rows in another, and the relationship is held on both sides. When you create a link, its reverse column is created with it; when you delete the link column, its reverse twin is deleted in the same transaction, so the relation is never left half-defined with one end pointing at nothing.

One canonical value per cell. The data of a row does not live in a single blob on the row. The row is a typed shell - identity, ordering, soft-delete state - and each value is a Fact bound to the column’s stable key. Set a single cell or patch several fields of a row at once; either way the write lands as the one canonical value for that cell. Because Facts bind to the column key rather than the display label, renaming a column never breaks the values underneath it.

Locked cells. A cell that has been checked by hand can be pinned with a lock, so a later bulk write or an agent patch leaves it alone. A patch that touches a locked cell skips that field and reports it rather than overwriting verified work. Setting a lock is a role-gated administrative action, kept deliberately narrow because the ability to set or clear it is the ability to control which values can be changed.

Regulated cells stay encrypted. A column can be marked as holding personal data. Its values are stored encrypted, and when the grid reads rows back, a regulated cell comes through masked - shown as visibly regulated, never as plaintext leaked from the ciphertext. Decrypting a regulated value is a separate, sensitivity-gated read, not something the grid does on its own.

Import with a preview first. Bringing data in from a CSV or a paste is a two-step, human-driven act. You preview the import first: the system dry-runs your column mapping against the target table and classifies every row as a create, an update, or a reject, with the per-cell type coercion shown, and writes nothing canonical while it does so. When the preview looks right, you commit it, and the rows are created or updated per the mapping and the match key. The commit honours cell locks, rejects writes to computed columns, respects per-column permissions, and records the import as a single human-attributed entry in the audit record.

How you use it

Open a table. List the workspace’s tables, then open one to read its column schema and page through its rows in the grid. The grid reads rows in pages, so a large table loads a screen at a time rather than all at once.

Edit a value. Click into a cell and set it, or patch several fields of a row in one go. A locked cell and a regulated cell are both protected: the edit to a locked field is skipped and reported, and a regulated cell is never shown or written in plaintext from the grid.

Shape a table. Add a typed column, change a column’s type when the data outgrows it, hide a column that a role should not see, reorder columns, or drop one. Author a new table in the right category, archive a table you no longer need (reversible, the rows are kept), or tear one down for good.

Relate two tables. Add a link column that points at another table, and its reverse appears on the other side automatically. Drop the link later and both ends go together.

Bring data in. Map your CSV or pasted columns onto the table, preview the result to see what would be created, updated, or rejected and how each value coerces, then commit the import once it reads correctly.

The kernel verbs behind it

Each goes through the one front door - permission-checked and audited before it runs. Most of the editing verbs are administrative, with read access available on demand.

Shaping the table:

  • table.create - author a categorised, typed table (at most one SYSTEM table per type per workspace).
  • table.get - read a table’s metadata and its full column schema.
  • table.list - list the workspace’s tables, filtered by category.
  • table.archive - hide a table, reversibly; its rows are retained.
  • column.add - add a typed or computed column.
  • column.list - read the columns of a table, with the computed and personal-data columns flagged.
  • column.retype - change a column’s storage type with a best-effort coercion that never deletes data.
  • column.hide - hide a column from a role.
  • column.reorder - reorder a table’s columns.
  • column.delete - drop a column; a LINK column deletes its reverse twin in the same transaction.

Reading and writing the data:

  • row.list - list the rows of a table for the grid, paginated and workspace-scoped, with regulated cells masked.
  • row.create - add a row.
  • row.patch - write several fields of one row; honours cell locks and rejects writes to computed columns.
  • row.bulk - create or update many rows in one bounded batch.
  • row.delete - delete one row.
  • cell.set - write one canonical Fact, the single value for one cell.
  • cell.lock - pin or unpin a hand-verified cell so later writes leave it alone (role-gated).

Bringing data in:

  • import.preview - dry-run a CSV or paste column-map against a table: per-row create / update / reject and per-cell coercion, writing nothing canonical.
  • import.commit - commit a previewed import, creating or updating rows per the mapping and match key, as one human-attributed action.

See the full set in the table, column, row, cell, and import verb references.