Properties
Overview
Section titled “Overview”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
Property Types
Section titled “Property Types”| 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 |
Common Properties
Section titled “Common Properties”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 Configuration (admin)
Section titled “UI Configuration (admin)”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 |
PropertyDisabledConfig
Section titled “PropertyDisabledConfig”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 |
String Properties
Section titled “String Properties”The string type is the most versatile — depending on the options you set, it renders as different widgets.
Text field
Section titled “Text field”A basic single-line text input.
name: { type: "string", name: "Name", validation: { required: true, min: 2, max: 200 }}Multiline text
Section titled “Multiline text”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 }}Markdown editor
Section titled “Markdown editor”Set admin.markdown to render a full markdown editor with toolbar.
body: { type: "string", name: "Blog text", admin: { markdown: true }}Email field
Section titled “Email field”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 }}URL field
Section titled “URL field”Set url: true to add URL format validation and render with a link icon.
website: { type: "string", name: "Amazon link", url: true}File upload
Section titled “File upload”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.
Select (enum)
Section titled “Select (enum)”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" }, ]}Multi-select
Section titled “Multi-select”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" }, ] }}String Options
Section titled “String Options”| 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 |
Audit columns
Section titled “Audit columns”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.
Number Properties
Section titled “Number Properties”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.
Number Options
Section titled “Number Options”| 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 |
NumberFormatOptions
Section titled “NumberFormatOptions”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 |
Boolean Properties
Section titled “Boolean Properties”active: { type: "boolean", name: "Selectable", defaultValue: true}Booleans render as a toggle switch.
Date Properties
Section titled “Date Properties”Date only
Section titled “Date only”Set mode: "date" to show a date picker without time.
event_date: { type: "date", name: "Expiry date", mode: "date"}Date and time
Section titled “Date and time”The default mode "date_time" includes both date and time.
arrival_time: { type: "date", name: "Arrival time", mode: "date_time"}Auto timestamps
Section titled “Auto timestamps”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.
Date Options
Section titled “Date Options”| 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) |
Array and Map Properties
Section titled “Array and Map Properties”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.
Geopoint Properties
Section titled “Geopoint Properties”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.
Reference Properties
Section titled “Reference Properties”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.
Reference & Relation Options
Section titled “Reference & Relation Options”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 |
Vector Properties
Section titled “Vector Properties”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.
Binary Properties
Section titled “Binary Properties”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.
Next Steps
Section titled “Next Steps”- Relations — Foreign keys and joins
- Security Rules — Row Level Security
- Custom Fields — Build custom field components