Table data operations
Table definitions describe the shape of a table; the TablesData service operates on its rows. Every read and write is authorized against the table’s permission object, validated, and — for writes — run together with everything that depends on the row in one transaction.
The operations
Section titled “The operations”| Call | Action | Grant |
|---|---|---|
PostTableData |
Insert one row | insert on the table |
PostTableDataBulk |
Insert up to 10,000 rows | insert on the table |
PutTableData |
Update one row | update on the table |
DeleteTableData |
Soft-delete one row | delete on the table |
GetTableData |
Read one current row | read on the table |
GetTableDataVersion |
Read one exact archived or current row version | read on the table |
GetTableDataCount |
Count rows | read on the table |
GetTableDataByPosition |
Read a page of rows by offset | read on the table |
GetTableUpdateImpact |
List profiles using a row’s current version | update on the table |
ArchiveTableData |
Snapshot the current row version and advance it | update on the table |
All calls name the profile and the table. The table resolves to its family root for permission purposes, so one grant covers a header and its line-item children.
System columns on every row
Section titled “System columns on every row”Every managed table carries the same server-owned columns, named the same in Postgres and in the API:
| Column | Meaning |
|---|---|
id |
Row identity, assigned by the server |
deleted |
Soft-delete flag; false for live rows |
row_revision |
Optimistic-concurrency revision, incremented on every update |
version |
Business-data generation; advances only when the row is archived |
created_at |
When the row was created |
account_id |
Present on accounting-enabled tables; resolved through the virtual account field |
Client-supplied columns are stored under ordinals and exposed under their aliases; the system columns above keep their public names. See Profiles and catalog for how the mapping works.
Deleted rows are excluded from reads and searches. Deletion never removes the row — history stays intact and can be inspected through the database.
Writing a row
Section titled “Writing a row”An insert validates the request against the table definition, then runs, in order:
- column existence and type conversion, including money, temporal, and specialized value types (phone, IBAN, email, GTIN, credit card);
- validation rules attached to the table;
- Steel scripts for computed columns;
- quantity-ledger effects declared by scripts, with balances projected onto target rows;
- accounting posting, when the table is accounting-enabled — one journal line per row, committed with the row;
- a search-index job so the row becomes searchable.
The whole write is one serializable transaction. If any step fails, the row is not committed and no effect is applied. On a retryable serialization failure the server retries the transaction; on success the row is returned with its id and revision.
Inserts are idempotent
Section titled “Inserts are idempotent”PostTableData requires two metadata headers:
| Header | Value |
|---|---|
idempotency-key |
A UUID chosen by the client |
operation-created-at |
An RFC 3339 timestamp with an offset |
The server records the key, the acting user, and a hash of the request. Replaying the same key within 24 hours returns the original result instead of inserting again — a client that times out can retry safely. An operation older than 24 hours, or more than one hour in the future, is rejected.
The key is scoped to the acting user, so two users never collide. Old idempotency records are cleaned up by a background task.
Updating and deleting
Section titled “Updating and deleting”Updates and deletes are optimistic: the request carries the row_revision the client loaded, and the server refuses the write if the row changed or was deleted since — the response says the row is stale and nothing was changed. The row is locked for the duration of the write so concurrent updates serialize.
Deleting a row:
- sets
deleted = true(soft delete); - reverses the row’s quantity-ledger contributions;
- soft-deletes its accounting journal lines, when accounting-enabled;
- removes the row from the search index.
An accounting row that would alter a closed journal or a protected period is refused, and the row change is rolled back with the accounting change.
Links preserve the target row’s version, not only its id. Updating a version that is already referenced requires explicit confirmation of the affected profiles. Deleting a currently referenced version is refused until that version has been archived. See Row versions and archiving for the complete workflow.
System tables stay protected
Section titled “System tables stay protected”System tables are modified only through their dedicated backend APIs. The row API refuses writes to them, with one exception: the user-maintained system tables — accounts and custom_exchange_rates — are ordinary data to their owning feature.
Reading rows
Section titled “Reading rows”GetTableData returns rows with values converted for the caller: temporal values are rendered in the user’s timezone, money values in their stored currency, and physical columns are remapped to display aliases. GetTableDataByPosition pages through rows by offset, and GetTableDataCount reports how many rows match.
Continue
Section titled “Continue”- Quantity ledger explains how projected balances stay consistent with the row write.
- Row versions and archiving explains immutable linked snapshots and confirmed updates.
- Accounting explains how a business row becomes a journal line.
- Bulk import covers loading prepared rows in volume.