Entities

The heart: tables → schema → CRUD API

If the rest of the config wires up infrastructure, entities is where you describe your actual domain. It is a required array of NucleusTable definitions, and it's the single most leveraged field in the framework — everything downstream is generated from it.

Declare a table with its columns, constraints and access rules, and Nucleus produces the Drizzle schema, pushes it to Postgres, mounts a complete REST surface, seeds the authorization claims that protect it, and emits a fully-typed client endpoint. Add a column to the array and, after regenerating, it exists end to end.

This page covers the table-level options first, then drills into columns, validation, and indexes/constraints.

The entity → API pipeline#

entities is the only required array in the whole config, and it's the most powerful. Each NucleusTable you declare is run through a generator that produces a Drizzle schema; at boot that schema is pushed to Postgres, REST routes are mounted, claims are seeded, and a fully-typed client is generated. One declaration, the entire stack.

A minimal entity
1{2  "entities": [3    {4      "table_name": "products",5      "add_base_columns": true,6      "bulk_endpoints_enabled": true,7      "columns": [8        { "name": "title", "type": "varchar", "length": 200, "notNull": true },9        { "name": "price", "type": "numeric", "precision": 10, "scale": 2 },10        { "name": "in_stock", "type": "boolean", "default": true }11      ]12    }13  ]14}

Table definition#

The top-level fields of a NucleusTable describe the table as a whole — its name, whether it gets the standard system columns, and which optional route behaviours it exposes.

table_namestringRequired

The snake_case table name (e.g. blog_posts). This is the source of truth — backend route paths are derived as camelCase (/blogPosts) and the FE endpoint key as UPPER_SNAKE (GET_BLOG_POSTS) automatically.

add_base_columnsbooleanOptional

When true, Nucleus prepends six standard system columns so you don't repeat them: a uuid id primary key (gen_random_uuid()), created_at / updated_at timestamptz timestamps (now()), an is_active boolean flag (defaults true) you can use as a soft-delete/active marker, and created_by / updated_by uuid audit columns that the write routes populate from the request's x-user-id. Note: the generic DELETE is a HARD delete — the framework does not auto-filter or soft-delete on is_active.

group_namestringOptional

A logical grouping label used to organise entities in admin UIs and generated docs. Purely organisational.

bulk_endpoints_enabledbooleanOptional

Expose batch create/update/delete routes for this entity. Bulk-operation claims are only seeded when this is true, keeping the claims table clean for entities that don't need them.

is_form_databooleanOptional

Mark the entity as accepting multipart/form-data (file uploads) rather than JSON — relevant for entities that carry binary payloads.

feature_setSystemTableFeatureSets[]Optional

Tags an entity as belonging to a framework feature (authentication, authorization, audit, payment, storage…). Used internally for the built-in system tables; you rarely set this on your own entities.

serviceIdstringOptional

Names the backend service that physically owns this entity. The FE endpoint generator uses it to route calls to the right service in a multi-service deployment. Omit for the default/IDP service.

Example: "blog-api"

requiresNucleusTableRequirement[]Optional

Runtime dependencies that must be configured for this entity's routes to mount (currently 'email'). Lets system entities stay dormant until their provider exists.

extendsbooleanOptional

Inherit a built-in system table instead of replacing it. A config entity that shares a system table's table_name normally REPLACES the built-in definition entirely (dropping its columns, indexes and route policy — resolveEntities warns when you do this). With extends: true the built-in is the base and your entry may only ADD columns and indexes; only table_name, columns and indexes are read (every other field is inherited, and supplying one logs a warning). Invalid extends entries fail boot with typed errors: a column name that collides with a built-in/base column (E1), a notNull column with no DB default (E2 — the fixed-column framework inserts can't supply it), extending an auth table in consumer mode (E3), extends: true naming a table that is not a system table (E4), or extending a table whose feature is disabled (E5).

Example: { "table_name": "users", "extends": true, "columns": [ ... ] }

anonymous_createboolean | { column: string }Optional

Per-table opt-in that suppresses author attribution on CREATE only. It stops created_by from being stamped and detaches the CREATE audit entry (actor + IP + user agent) from the new row — for channels that promise the writer anonymity like a suggestion box, whistleblowing form, or unsigned survey. true applies to every insert; { column: 'is_anonymous' } applies only when that payload boolean is truthy, so users who choose to sign stay attributable. Only CREATE is affected — later updates (e.g. an admin changing status) are attributed as normal. This is a deliberate trade: an anonymous submission cannot be traced back to its author afterwards.

Example: { "column": "is_anonymous" }

Access & route scoping#

These fields decide where an entity lives and which parts of its API are exposed or public. They're how you keep one entity out of certain tenants, or open a read route to the world while protecting writes.

is_publicPartial<Record<GenericNucleusMethods, boolean>>Optional

Per-method public access. { "GET": true } makes reads reachable without auth while writes stay protected — exactly what public-facing list/detail pages need.

Example: { "GET": true }

excluded_methodsGenericNucleusMethods[]Optional

Methods to NOT generate for this entity. Use ["DELETE"] for append-only tables, or omit POST for read-only reference data.

available_schemasstring[]Optional

Declarative metadata only: intended to name the Postgres schemas an entity belongs to, but as of the current release nucleus-core has NO mounting/provisioning logic that reads it. It is declared on the type and carried on the built-in system tables as documentation; tenant provisioning applies every table to every active schema (createAllTablesForSchema) and the schema generator emits all tables unconditionally, so setting this does NOT gate which tenants receive the table. Treat it like available_app_ids — inert in this version.

excluded_schemasstring[]Optional

The declared inverse of available_schemas — schemas that should NOT receive this entity — but it is equally inert as of the current release: no runtime or generation code reads it, and every active tenant schema still receives every table. Do not rely on it for tenant isolation; use it only as documentation until schema-gating is actually implemented.

available_app_idsstring[]Optional

Declarative metadata only: it tags which appIds an entity belongs to, but as of the current release nucleus-core has NO mounting/filtering logic that reads it (unlike serviceId, which the FE endpoint generator actually consumes). Carried on the built-in system tables as documentation; setting it on your own entity does not currently gate anything.

Under the hood — generation & validation#

The 'Nucleus writes the rest' promise is concrete code. Knowing the steps explains why your routes are validated, documented and safe without any extra work.

TypeBox schemasbody + response + SwaggerOptional

Each entity's columns are compiled into Elysia TypeBox schemas — a request-body schema (notNull → required, types mapped to t.Number/t.Boolean/t.String{format} / t.Unknown for json) and matching response, list, bulk, delete and error schemas. These power both runtime request validation and the auto-generated Swagger/OpenAPI page, so the documented contract and the enforced contract are the same object.

validatePayload / sanitizePayloadrequest guardOptional

Before a write hits the database the payload is type-checked per column and format-checked against built-in patterns (email, url, uuid, date, datetime, time, uri, ipv4, ipv6), then sanitised to strip server-managed keys, the hardcoded auth-secret list, and columns flagged readOnly so they can't be mass-assigned. Note: a `sensitive` column is NOT write-protected here — sanitizePayload only checks readOnly, so `sensitive` remains writable. `sensitive` is a READ-side control instead: the column is stripped from list/detail responses and from GET /auth/me, blocked in filter/search/sort/select/distinct, and added to the audit/log redaction set — all unless the caller holds the explicit per-column read claim (get.<table>.<column>). Malformed values (wrong types/formats) are rejected by per-column validation; extra/unknown keys are NOT rejected (the body schema allows additionalProperties) but are ignored by the ORM at insert, so they aren't stored.

system tablessystem.tables.jsonOptional

Your entities don't start from an empty database. Nucleus ships a full set of system tables (users, sessions, roles, claims, role_claims, user_roles, tenants, tenant_features, audit_logs, notifications, verification flows, …) that the auth, RBAC, audit, verification and tenant subsystems own. Your tables are generated alongside them, and feature_set marks an entity as belonging to one of these framework features. Note (0.9.746+): generic entity WRITES (POST/PUT/PATCH/DELETE) to the RBAC tables (roles/claims/user_roles/role_claims) and to the sensitive money/tenant-routing/auth-secret system tables (domain_hostnames, payment/marketplace tables, verifications, api_keys, magic_link_tokens, user_sessions, trusted_devices) are hard-locked to godmin as defense-in-depth — use the dedicated feature routes, not generic CRUD, to mutate them.

table-key resolutionsnakeToCamel / resolveSchemaTableOptional

Internally tables are looked up in the schemaTables map by a camelCase key derived from the snake_case table_name (blog_posts → blogPosts). resolveSchemaTable tries the given key, then the normalised form, and warns if neither resolves — preventing the silent undefined that used to bypass role checks.

Columns#

Each entry in columns is a NucleusColumn — a typed, constrained field. Nucleus supports the full PostgreSQL type spectrum: numerics (integer, bigint, numeric, decimal), text (text, varchar, char), uuid, boolean, temporal (date, time, timestamp, timestamptz), json/jsonb, arrays, ranges, network types, and advanced types like vector, geometry and geography.

columns[]NucleusColumn[]Optional

The field definitions for the table.

name / typestring / NucleusColumnTypeOptional

Column name (snake_case) and one of 50+ Postgres column types (integer, varchar, uuid, jsonb, timestamptz, vector, geometry, …).

length / precision / scale / dimensionsnumberOptional

Type sizing: length for varchar/char, precision + scale for numeric/decimal, dimensions for vector embeddings. Note: beyond DDL sizing, a set `length` is ALSO enforced at request-validation time as a max-length check on any string value (regardless of the column's Postgres type) — validatePayload rejects with `<name> exceeds max length of <length>`.

notNull / nullablebooleanOptional

Nullability. notNull adds a NOT NULL constraint in DDL. `nullable` emits NO DDL — it only affects request validation: `nullable: true` relaxes the required-field check even when notNull is set (isRequired = notNull && !nullable && !hasDbDefault), so a NOT NULL column can still skip the 'is required' check. Columns are nullable by default when neither is set.

unique / primaryKeybooleanOptional

Mark the column UNIQUE or as the table's primary key.

default / defaultRawunknown / stringOptional

default sets a literal default value — but only SCALAR defaults (string / boolean / number) are emitted as the given literal; an array or object passed to default is silently coerced to an empty `'{}'` SQL literal (its contents discarded), so use defaultRaw for a non-empty array/json default. defaultRaw injects raw SQL (e.g. now(), gen_random_uuid()) for expression defaults.

referencesNucleusColumnReferenceOptional

Declare a foreign key inline: { table, column?, onDelete?, onUpdate? } where the actions are cascade / restrict / no action / set null / set default.

array / arrayDimensionsboolean / numberOptional

array: true makes the column a single-dimension array of its base type (codegen appends .array()). arrayDimensions is accepted by the type but is a NO-OP — generateColumnCode never reads it and no multi-dimensional array DDL is ever emitted (see the callout above).

enumValuesstring[]Optional

Constrain a column to a fixed value set — enforced by runtime validation (allowed-set check on write). Codegen does not emit a native Postgres enum type; the column stays its declared base type (e.g. text). Note: the separate `enum` ({ name, values }) object is accepted by the type but is currently a NO-OP — neither emitted as a native enum nor runtime-validated — so use `enumValues` for actual enforcement.

readOnlybooleanOptional

When true, the column is stripped from generic entity CRUD write bodies by sanitizePayload — for server-managed or security-sensitive fields that must never be mass-assigned by clients.

sensitivebooleanOptional

Marks a column PII/secret-grade. When true it is stripped from generic entity responses and from GET /auth/me (stripUnclaimedSensitive), and it cannot be used in filter/search/sort/select/distinct (sensitiveFieldQueryable) — UNLESS the caller holds the explicit per-column read claim `get.<table>.<column>`, which lifts both restrictions. Its name is also unioned into the audit + log redaction key set. Distinct from the hardcoded auth-secret block in ElysiaPlugin/utils.ts, which no claim opens: `sensitive` is claim-gated, so an integration holding the column claim can still read AND search by the value (e.g. a national-ID lookup) instead of being forced to fetch whole tables.

generatedAlwaysAs / …Identitystring / booleanOptional

Identity columns: only generatedAlwaysAsIdentity / generatedByDefaultAsIdentity emit DDL (GENERATED ALWAYS / BY DEFAULT AS IDENTITY auto-increment). The non-identity generatedAlwaysAs (a stored generated expression) is metadata only — generateColumnCode never turns it into a GENERATED column (see the callout above); it is read only at request-validation time to mark the field as having a DB default, so it is a no-op at codegen and the value is not DB-computed.

mode / withTimezoneNucleusColumnMode / booleanOptional

mode controls how Drizzle marshals the value in JS (string | date | json | number), but codegen only emits it for bigint columns today (e.g. mode: 'number' → bigint('...', { mode: 'number' })); on timestamp/json/numeric columns the field is accepted by the type but ignored by schema generation. Timezone semantics come from choosing type: 'timestamptz' (which emits { withTimezone: true }) — the standalone withTimezone field on a timestamp column is accepted by the type but never read by codegen, so type: 'timestamp' + withTimezone: true still produces a plain, non-tz column.

check / commentstringOptional

Metadata only — codegen emits no DDL for either (see the callout above). check is intended to hold a column-level CHECK expression and comment a SQL COMMENT for documentation, but generateColumnCode never reads them, so neither field currently affects the generated schema.

Validation & sanitization#

Beyond database constraints, each column can carry request-time validation and sanitization. These run in the route layer before a write hits the database, so bad input is rejected with a clear error and good input is normalised consistently.

validationNucleusColumnValidationOptional

Declarative input rules enforced on create/update.

minLength / maxLengthnumberOptional

String length bounds.

min / maxnumberOptional

Numeric bounds.

patternstringOptional

Regular expression the value must match.

formatenumOptional

Built-in semantic formats: email, url, uuid, date, datetime, time, uri, ipv4, ipv6.

customMessagestringOptional

Override the error message shown when validation fails.

sanitizeNucleusSanitizeOption[]Optional

Transforms applied to incoming values before persistence: trim, lowercase, uppercase, escapeHtml, stripTags, normalizeEmail, toNumber, toBoolean, slugify. Chain several to clean input deterministically.

Example: ["trim", "lowercase", "normalizeEmail"]

Indexes & constraints#

Tune performance and integrity at the table level. Indexes and multi-column constraints are declared alongside the columns.

indexesNucleusIndex[]Optional

Each index has columns plus optional name, unique, using (btree | hash | gist | spgist | gin | brin), a partial where clause, and concurrently for non-blocking creation.

Example: { "columns": ["email"], "unique": true, "using": "btree" }

constraintsNucleusConstraintsOptional

Table-level constraints: a composite primaryKey (string[]), named unique constraints over multiple columns, and check constraints with arbitrary SQL expressions.

Related sections