Custom Tables and Model Extension

The pre-built ERP tables cover general business processes, but every industry has its own data — gyms track body measurements, travel agencies track tour packages, tutoring centers track class attendance. This chapter covers how to extend the model.


Three extension paths

PathSuited toQueryableType-constrainedSupports relations
custom_data JSONBA few unstructured extrasLimitedNoNo
Extension fieldsNew columns on existing ERP tablesYesYesYes
Custom tablesEntirely new entitiesYesYesYes

The decision is simple: is this an attribute of an existing entity, or a new entity? A "membership card number" on a customer is the former (extension field); "class attendance records" is the latter (custom table).


Custom tables are organization-level

This is the most important — and most often misunderstood — property:

Custom tables belong to the organization, not to an application.

Every Custom App under the same organization, plus the organization's own Data Center interface, sees the same tables and the same rows.

That imposes one discipline: check for a reusable existing table before creating one. A single "customers" table should not fragment into two disconnected copies because two applications each created their own.


Dual naming: display name and physical name

Every table and every column has two names:

Display namePhysical name
Chosen byYouGenerated from the display name, or specified explicitly
MutableAny timeNever, once created
Character setAnythingASCII only
Used forInterface labelsAPI identity, relation targets

All APIs and SDKs address existing tables and columns by physical name. Display names are just labels — renaming one breaks nothing.

Physical-name generation avoids three namespaces: existing custom tables in the same organization, SQL reserved words, and the platform's built-in ERP table names. A display name of "Customers" in an empty environment yields customers_2, not customers, because the latter belongs to the ERP.

If you specify a physical name that collides with any of those, the system reports the conflict and names which category it hit — it does not silently rename.


Column types

TypeNotesExtra contract
textText
numberNumeric
booleanBoolean
dateDate
datetimeTimestamp
selectSingle choiceRequires an option set; values are constrained at the database level
relationRelationSee below
jsonStructured data
imageImageSee below

Every table automatically carries system columns (identifier, created time, updated time). These cannot be deleted or retyped, and do not count toward the column quota.

relation columns

A relation can point at two kinds of target: another custom table, or a built-in ERP table.

That means an "attendance record" custom table can have a "student" column pointing directly at the ERP customers table — no duplicated customer data, no hand-maintained mapping.

image columns

Image columns provide the full pipeline: uploads route through the platform (rather than the browser writing straight to storage), reads return short-lived signed URLs, and cross-organization access control applies. Images count toward the organization's storage quota.


Who can change structure

Structural operations are administrators only (users holding system.admin): creating, altering, and dropping tables and columns, plus ERP extension field definitions.

Data operations (record CRUD) and structure reads remain at ordinary development permission (builder.access).

What's governed is the shape of the schema, not its use. This check runs at the entry layer, and all three entry points (REST API, SDK, AI tooling) enforce it identically — the AI channel cannot route around the REST gate.


Deletion is two-stage

Dropping a table or column takes two steps, enforced server-side rather than by a front-end confirmation dialog:

  1. Impact preview — the system reports what the deletion affects: how many rows, which relations point at it
  2. Physical-name confirmation — you must type the physical name to proceed

Deletion is irreversible. The design deliberately makes accidents hard.


Three entry points, one service layer

Every operation on custom tables — structural and data — funnels into a single service layer, shared by three entry points:

Entry pointServingIdentity source
REST APIOrganization users (Data Center UI)The signed-in user's session
SDK (ctx.db)Custom App runtimeThe application's runtime identity + scope
Builder toolingThe AI development assistantThe Builder session's organization identity

That service layer is the sole home of the guardrails. Entry points do not reimplement validation, and they do not bypass it:

  • Tenant isolation — every operation is anchored to an organization; cross-organization access reports "not found" without revealing whether the resource exists
  • Serialized structural changes — DDL takes an organization-level lock, so concurrent table creation cannot tear
  • Quotas — per-organization table limits and per-table column limits, checked inside the lock so concurrency cannot slip past
  • Type and value validation — column types, option sets, defaults, relation targets
  • Error translation — database-level failures (unique violations, not-null violations, foreign key violations, dependent objects) are translated into structured codes and actionable messages rather than raw technical errors

Any new entry point should call this layer rather than assembling its own SQL.


Reaching custom tables from the SDK

def execute(ctx):
    # List the organization's custom tables
    tables = ctx.db.list_tables()

    # Query (by physical name)
    rows = ctx.db.query_table('attendance', filters=[
        {'column': 'class_date', 'op': 'gte', 'value': '2026-08-01'}
    ])

    # Insert
    ctx.db.insert_row('attendance', {
        'student_id': ctx.params['customer_id'],   # relation to ERP customers
        'class_date': '2026-08-10',
        'status': 'present',
    })

    # Update / delete
    ctx.db.update_row('attendance', row_id, {'status': 'late'})
    ctx.db.delete_row('attendance', row_id)

The front end uses the built-in src/db.ts SDK with equivalent semantics. See Chapter 8.


ERP extension fields

Extension fields add real columns to existing ERP tables. Unlike custom_data, they are queryable, filterable, type-constrained, and can be authorized through the reference system.

Use them when the data is an intrinsic attribute of an existing entity and you need to filter or sort by it — for example, adding "membership expiry" to customers so you can query "members expiring this month".

Dropping an extension field follows the same two-stage flow with an impact preview.