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.
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_namestringRequiredThe 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_columnsbooleanOptionalWhen true, Nucleus prepends the standard system columns so you don't repeat them: a uuid id primary key, created_at / updated_at timestamps, an is_active soft-delete flag and a version counter for optimistic concurrency.
group_namestringOptionalA logical grouping label used to organise entities in admin UIs and generated docs. Purely organisational.
bulk_endpoints_enabledbooleanOptionalExpose 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_databooleanOptionalMark the entity as accepting multipart/form-data (file uploads) rather than JSON — relevant for entities that carry binary payloads.
feature_setSystemTableFeatureSets[]OptionalTags 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.
serviceIdstringOptionalNames 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[]OptionalRuntime dependencies that must be configured for this entity's routes to mount (currently 'email'). Lets system entities stay dormant until their provider exists.
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<Method, boolean>>OptionalPer-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[]OptionalMethods to NOT generate for this entity. Use ["DELETE"] for append-only tables, or omit POST for read-only reference data.
available_schemasstring[]OptionalRestrict the entity to specific Postgres schemas. In multi-tenant setups this controls which tenants receive the table.
excluded_schemasstring[]OptionalThe inverse — schemas that should NOT receive this entity.
available_app_idsstring[]OptionalLimit the entity to specific appIds when several services share one schema source, so each service only mounts the tables it owns.
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 + SwaggerOptionalEach 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 guardOptionalBefore 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 drop unknown/forbidden keys — so malformed or over-posted bodies are rejected, not silently stored.
system tablessystem.tables.jsonOptionalYour 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.
table-key resolutionsnakeToCamel / resolveSchemaTableOptionalInternally 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[]OptionalThe field definitions for the table.
name / typestring / NucleusColumnTypeOptionalColumn name (snake_case) and one of 50+ Postgres column types (integer, varchar, uuid, jsonb, timestamptz, vector, geometry, …).
length / precision / scale / dimensionsnumberOptionalType sizing: length for varchar/char, precision + scale for numeric/decimal, dimensions for vector embeddings.
notNull / nullablebooleanOptionalNullability. notNull adds a NOT NULL constraint.
unique / primaryKeybooleanOptionalMark the column UNIQUE or as the table's primary key.
default / defaultRawunknown / stringOptionaldefault sets a literal default value; defaultRaw injects raw SQL (e.g. now(), gen_random_uuid()) for expression defaults.
referencesNucleusColumnReferenceOptionalDeclare a foreign key inline: { table, column?, onDelete?, onUpdate? } where the actions are cascade / restrict / no action / set null / set default.
array / arrayDimensionsboolean / numberOptionalMake the column an array of its base type, optionally multi-dimensional.
enumValues / enumstring[] / NucleusColumnEnumOptionalConstrain to a fixed value set. enum creates a named Postgres enum type; enumValues is the inline shorthand.
generatedAlwaysAs / …Identitystring / booleanOptionalComputed and identity columns: a stored generated expression, or GENERATED ALWAYS / BY DEFAULT AS IDENTITY auto-increment.
mode / withTimezoneNucleusColumnMode / booleanOptionalmode controls how Drizzle marshals the value in JS (string | date | json | number); withTimezone selects timestamptz semantics.
check / commentstringOptionalcheck adds a column-level CHECK expression; comment attaches a SQL COMMENT for documentation.
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.
validationNucleusColumnValidationOptionalDeclarative input rules enforced on create/update.
minLength / maxLengthnumberOptionalString length bounds.
min / maxnumberOptionalNumeric bounds.
patternstringOptionalRegular expression the value must match.
formatenumOptionalBuilt-in semantic formats: email, url, uuid, date, datetime, time, uri, ipv4, ipv6.
customMessagestringOptionalOverride the error message shown when validation fails.
sanitizeNucleusSanitizeOption[]OptionalTransforms 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 and applied during the schema push.
indexesNucleusIndex[]OptionalEach 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" }
constraintsNucleusConstraintsOptionalTable-level constraints: a composite primaryKey (string[]), named unique constraints over multiple columns, and check constraints with arbitrary SQL expressions.
Related sections