Skip to content

Properties

Properties define the columns in your database table and how they are rendered in the admin UI. Each property has a type that determines:

  • The database column type (via Drizzle schema generation)
  • The form field component
  • The table cell renderer
  • The validation rules
Type Description PostgreSQL Column
string Text, select, markdown, file upload, URL, email text by default; varchar, char or uuid with columnType
number Integer, decimal, currency integer, numeric, bigint, serial
boolean True/false toggle boolean
date Date, datetime, timestamp timestamp, date
array Ordered list of values A native array (text[], integer[], numeric[], boolean[]) when of is one string, number or boolean property; jsonb otherwise
map Key-value object jsonb
geopoint Latitude/longitude pair jsonb
reference Embedded reference to another entity The target’s primary-key type (stores its ID)
relation SQL foreign key relation Uses the relations array
vector Embedding, for nearest-neighbour search VECTOR(n) (pgvector)
binary Raw bytes, read and written as base64 bytea

All property types share these options:

Property Type Description
type string Required. Data type (see above)
name string Display label. Defaults to the property key, prettified — publishDate → “Publish Date”. Set it when that derivation is wrong: "URL" rather than “Url”.
description string Help text shown below the field
defaultValue any Default value for new entities. A literal one — a string, number, boolean, date, enum id, or a map/array document — also becomes the column’s DEFAULT, so a row inserted by anything that is not the admin panel (the REST API, a seed, psql) gets it too
databaseDefault boolean | SqlExpression Since 0.25 The database fills the column when a create leaves it empty: a NOT NULL DEFAULT <expression> column such as gen_random_uuid() or nextval(…). A required field with it may be left out of a create, by the admin form and by the API, and a null sent on create is left to the database; it stays required on edit, and the generated Insert type makes it optional. Give the expression in the template form isId takes, databaseDefault: "sql`gen_random_uuid()`", and the schema planner writes it as the column’s DEFAULT, so rebase db push keeps it. true declares a DEFAULT written elsewhere and writes none, so a push plans dropping it. “Import from table” and rebase schema introspect set the expression they read
validation object Validation rules
propertyConfig string Registered property config key
columnName string Explicit database column name (bypasses snake_case conversion)
callbacks PropertyCallbacks Hooks for afterRead and beforeSave transforms
excludeFromApi boolean Never mention this column on the API surface, in either direction — for a password hash or a verification token. The value is still written and queryable server-side; it is stripped from every row the API serves, for every caller including admins and service keys, and it is absent from the generated Row, Insert and Update types and from the OpenAPI schema. Sugar for access: { read: [], write: [] } — the two are one mechanism, and setting both on one property is refused at boot
access { read?: string[]; write?: string[] } Who may read and who may write this one field, by role. An omitted list delegates to the row’s security rules; [] is nobody through the API; a role list is those roles plus admin. An unreadable field is absent from responses rather than null, and a write naming an unwritable one is a 400. See Field access
metadata Record<string, unknown> Anything your own code needs to hang off a property. Rebase does not read it; it survives serialization unchanged
dynamicProps function Dynamic property builder (see Conditional fields). Admin form only — not enforced by the API or the database.
conditions PropertyConditions Declarative JSON Logic conditions. Admin form only — not enforced by the API or the database.

UI-related options are nested under the admin sub-object:

price: {
type: "number",
name: "Price",
admin: {
readOnly: true,
columnWidth: 120,
hideFromCollection: false
}
}
Property Type Description
admin.readOnly boolean Prevent editing
admin.disabled boolean | PropertyDisabledConfig Disable with optional tooltip
admin.filledByServer boolean The server fills the field when a new record leaves it empty (a beforeSave hook). The form lets a create through without it; it stays required on edit, and the server’s own checks still apply
admin.hideFromCollection boolean Hide from table view. Display only: a hidden relation property’s related rows are still embedded in the list read (see Relations)
admin.columnWidth number Column width in pixels (table view)
admin.span 1 | 2 | 3 | 4 Field width over the four-column form grid
admin.Field React.ComponentType Custom field component
admin.Preview React.ComponentType Custom table cell component
admin.Filter React.ComponentType Custom filter field for this property. Wins over the collection-level Collection.FilterField override and the built-in per-type field
admin.filterOperators WhereFilterOp[] Narrow the operators offered when filtering on this property. The offered set is the intersection of the engine’s capabilities, the type’s defaults and this list — you can only restrict, never add an operator the engine cannot run. [] disables filtering on the property
admin.customProps unknown Anything your own Field or Preview needs. Rebase passes it through untouched

The object form of admin.disabled. The control stays visible and greyed out, which is the difference from admin.readOnly — a reader can see the shape of what they are not allowed to fill in.

Property Type Description
disabledMessage string Why this property is disabled, shown to the reader. Without it a greyed-out control explains nothing
clearOnDisabled boolean Set the value to null when the property becomes disabled, so a stale answer is not saved with the entity
hidden boolean Hide the field entirely while it is disabled, rather than greying it out

The string type is the most versatile — depending on the options you set, it renders as different widgets.

A basic single-line text input.

name: {
type: "string",
name: "Name",
validation: { required: true, min: 2, max: 200 }
}

Set admin.multiline to render as a textarea. It changes how the field is drawn, not what the column holds, so it lives in the admin block with the rest of the presentation options.

description: {
type: "string",
name: "Description",
admin: { multiline: true }
}

Set admin.markdown to render a full markdown editor with toolbar.

body: {
type: "string",
name: "Blog text",
admin: { markdown: true }
}

Set email: true to add email format validation and render with an email icon.

email: {
type: "string",
name: "User email",
email: true,
validation: { required: true }
}

Set url: true to add URL format validation and render with a link icon.

website: {
type: "string",
name: "Amazon link",
url: true
}

Set storage to render a file upload dropzone. The column stays a string — what is written to it is the object’s key in the bucket:

avatar: {
type: "string",
name: "Main image",
storage: { storagePath: "avatars", acceptedFiles: ["image/*"] }
}

Every option inside storage, and the multi-file form of the same field, are on File uploads.

Set enum to render a select dropdown. See Enum Values for details.

category: {
type: "string",
name: "Category",
enum: [
{ id: "electronics", label: "Electronics", color: "blueDark" },
{ id: "clothing", label: "Clothing", color: "pink" },
]
}

Several values out of a fixed set is an array of an enum string, not a flag on one: the column holds many values, so the property that describes it is the array. It is one Postgres column, a text[], rather than a join: an array whose of is a single string property gets a native array by default, and the columnType below only says so explicitly.

locales: {
type: "array",
name: "Available locales",
columnType: "text[]",
of: {
type: "string",
name: "Locale",
enum: [
{ id: "es", label: "Spanish", color: "pink" },
{ id: "en", label: "English", color: "blueLight" },
{ id: "fr", label: "French", color: "purpleLight" },
]
}
}
Property Type Description
admin.multiline boolean Render as textarea
admin.markdown boolean Render as markdown editor
email boolean Email format validation
url boolean URL format validation
storage StorageConfig Enable file upload — see File uploads
enum EnumValues Render as select dropdown
columnType "varchar" | "text" | "char" | "uuid" The database column. Defaults to text, or uuid when isId says so. text for unbounded strings, char for fixed width
isId boolean | string Marks the property as the primary key, and says who fills it: "manual" (you do), "uuid" (gen_random_uuid()), or any other string, used verbatim as the column’s SQL default expression — bare (nanoid()) or in template form ("sql`nanoid()`"). The function has to exist; nothing here creates it. "increment" is a number strategy and does nothing here, and "cuid" is refused at config load
autoValue "user_on_create" | "user_on_update" Stamp the column with the uid of the acting user — on creation only, or on every write. See Audit columns
userSelect boolean Render as a user picker. The value is a plain string column holding the user id — nothing resolves or checks it on the way in. Admin form only — not enforced by the API or the database. The picker lists users’ names, emails and avatars to anyone who may write the collection, through GET /api/admin/users/pick, without the users:read scope. A collection whose write rules are raw SQL does not count, since they cannot be judged ahead: there only users:read holders can pick.
admin.previewAsTag boolean Render this string as a tag in previews
admin.clearable boolean Add an icon to clear the value (set to null)
admin.urlPreview PreviewType How to render a string that holds a URL: a link, or an inline image, audio or video preview. Presentation only — whether the value is a URL is the url flag on the property, which is what the OpenAPI contract is generated from

autoValue on a string is the created_by / updated_by twin of the same option on a date:

createdBy: {
type: "string",
name: "Created by",
autoValue: "user_on_create",
admin: { readOnly: true }
},
updatedBy: {
type: "string",
name: "Updated by",
autoValue: "user_on_update"
}

The value comes from the call context, never from the request body — the driver overwrites whatever arrived, so a caller cannot claim to be somebody else by sending the field. A column whose value the caller supplies is not an audit column.

With no acting user — an anonymous request, a service token, a seed script — the column is set to null. Pair it with validation: { required: true } and that becomes a 400 instead: a collection that insists on knowing who wrote a row is a collection that cannot accept an anonymous write.

There is no foreign key. The uid is a string, the user store may be another database entirely, and a deleted user must not take the audit trail with them — declare a relation if you want the join.

price: {
type: "number",
name: "Price",
validation: { required: true, min: 0 }
}
quantity: {
type: "number",
name: "Quantity",
columnType: "integer" // Store as integer
}

Number fields render as a standard text input with numeric validation.

Property Type Description
enum EnumValues Render as select with numeric values. Unlike a string enum this creates no Postgres enum type — the column stays NUMERIC or INTEGER, so the panel offers the values and the database does not enforce them
columnType string "integer", "real", "double precision", "numeric", "bigint", "serial", "bigserial". Defaults to integer where validation.integer or isId says the value is whole, numeric otherwise
precision / scale number Total significant digits and digits after the point on a numeric column — NUMERIC(10, 2). Money without them is an unbounded NUMERIC that keeps whatever the caller sent, and cannot be tightened later without rewriting the column
isId boolean | string Marks the property as the primary key: "manual", "increment", or any other string used verbatim as the column’s SQL default expression. "increment" is always INTEGER GENERATED BY DEFAULT AS IDENTITY — columnType beside it is ignored, with a warning, because every foreign key pointing at a numeric key is INTEGER and a wider key would be referenced by narrower columns
admin.clearable boolean Add an icon to clear the value (set to null)
admin.format NumberFormatOptions Write the number out as currency, a percentage, or with fixed decimals. Omitted, the raw value renders — a number in the database is shown as the number in the database

Presentation only. The stored value never changes — a rounded display is still the full number in the column, and in the API.

Property Type Description
style "decimal" | "currency" | "percent" Defaults to "decimal". "currency" requires currency
currency string ISO 4217 code — "EUR", "USD". Setting it implies style: "currency", so the common case is one key
locale string BCP 47 tag. Defaults to the panel’s locale, which is what makes the same amount read 1,234.50 for one reader and 1.234,50 for another
minimumFractionDigits number Pad to at least this many decimals — 2 writes 5 as 5.00
maximumFractionDigits number Round to at most this many decimals. Does not change the stored value
notation "standard" | "compact" "compact" renders 12000 as 12K. Useful in a narrow table column
active: {
type: "boolean",
name: "Selectable",
defaultValue: true
}

Booleans render as a toggle switch.

Set mode: "date" to show a date picker without time.

event_date: {
type: "date",
name: "Expiry date",
mode: "date"
}

The default mode "date_time" includes both date and time.

arrival_time: {
type: "date",
name: "Arrival time",
mode: "date_time"
}

Use autoValue to automatically set timestamps on create or update.

createdAt: {
type: "date",
name: "Created At",
autoValue: "on_create",
admin: { readOnly: true }
}
updatedAt: {
type: "date",
name: "Updated At",
autoValue: "on_update"
}

on_update is a database trigger, not only an app-layer stamp. Rebase writes the column on every save it makes — so a beforeSave hook sees the new value — and installs a BEFORE UPDATE trigger that writes it for everyone else. Without the trigger a seed script, a migration backfill or one row fixed in psql left the column claiming the row had not changed, and anything reading it to decide what to re-index or re-send skipped that row.

The trigger is generated into .rebase/sql/triggers.sql, applied by rebase db push and at boot by the schema ensure — Atlas cannot manage it, for the same reason it cannot manage the full-text search helpers.

Property Type Description
mode "date" | "date_time" Date only or date + time (default: "date_time"). Picks the widget; the column is the same either way. Admin form only — not enforced by the API or the database.
autoValue "on_create" | "on_update" Auto-set timestamps. on_create is a DEFAULT now(); on_update is a BEFORE UPDATE trigger as well as an app-layer stamp, so a raw UPDATE from a seed or a migration moves the column too
columnType "timestamp" | "date" | "time" The database column. Defaults to timestamp with time zone. A date column is picked and shown as a day, whatever mode says; a time column as a time of day alone, in UTC, with no day (it travels as that time on 1970-01-01)
timezone string Which timezone the picker reads and writes the value in. Admin form only — not enforced by the API or the database.
admin.clearable boolean Add an icon to clear the value (set to null)

The two composite types — a repeatable list and a structured object, both stored as JSONB unless a native Postgres array fits. Repeats, block editors, groups, key-value editors and every option either one takes are on Arrays and maps.

A latitude/longitude pair, stored as jsonb — { latitude, longitude }, which is what the generated TypeScript type, the OpenAPI schema and the form field all describe.

location: {
type: "geopoint",
name: "Location"
}

Writes take the same shape, or a GeoPoint from @rebasepro/types: new GeoPoint(41.4036, 2.1744).

There are no options beyond the ones every property has. The column is not spatially indexed and there is no distance operator to filter on: a geopoint is a value you store and show, not one you search by. For that, model the two numbers as columns of their own.

References link to entities in another collection. They render as a preview card showing the referenced entity’s details.

client: {
type: "reference",
name: "Related client",
path: "clients",
admin: {
previewProperties: ["first_name", "last_name", "email"]
}
}

On Postgres a reference is a foreign key like any other, so it follows the same ON DELETE rule as a belongsTo relation: optional is SET NULL, required is RESTRICT. See Relations.

These apply to both reference and relation properties.

Property Type Description
admin.fixedFilter FilterValues Filter the entities offered in the selection widget
admin.widget "select" | "dialog" Which widget selects the related entity (relations only)
admin.includeId boolean Show the related entity’s id in previews (default: true)
admin.includeEntityLink boolean Show a link that opens the related entity (default: true)
admin.previewProperties string[] Which of the target’s properties appear in the preview (max 3)
admin.renderInForm boolean Render a many-relation as a picker inside the form as well as the tab it already gets. false by default: the entity view already lists the related rows as a tab, so the two surfaces are redundant by construction. No effect on a to-one relation, which gets no tab

An embedding column, for nearest-neighbour search. Postgres only.

embedding: {
type: "vector",
name: "Embedding",
dimensions: 1536
}
Property Type Description
dimensions number Required. Length of the vector. Compiles to VECTOR(n).
index VectorIndexConfig | false How the ANN index is built. Omitted, one HNSW index for cosine distance. false creates none.

An ANN index is created with the column, so vectorSearch is approximate and fast rather than an exact scan. It is built for cosine distance, which is what vectorSearch measures with unless you pass distance — an index serves exactly one operator, so an l2 query against a cosine index quietly goes back to scanning. method (hnsw or ivfflat), distance (one or several), m, efConstruction and lists are all on index. Above 2000 dimensions pgvector can build neither index type, so the column is left unindexed and the boot says so.

Query it with .vectorSearch(), which also covers what pgvector needs to be installed and how to tune the index. For ordinary btree, GIN and BRIN indexes on any collection, see Indexes.

Raw bytes, stored as bytea. Values cross the API as base64 strings — including defaultValue.

signature: {
type: "binary",
name: "Signature"
}

Large files belong in storage, not in a column: a bytea is read and written whole, with the row.

Enum values, validation rules and conditional fields have a page of their own: Validation and conditions.