Fields

Lucent supports the following field types for defining your collection schemas.

Field Types

text / textarea

{ name: "title",   type: "text",     required: true }
{ name: "content", type: "textarea", required: false }

number / integer

{ name: "price",    type: "number",  required: true }
{ name: "quantity", type: "integer", required: true }

boolean

{ name: "published", type: "boolean", defaultValue: false }

email

{ name: "email", type: "email", required: true, unique: true }

password

{ name: "password", type: "password", required: true }

date

Stored as TIMESTAMPTZ. Pass and receive ISO 8601 strings.

{ name: "publishedAt", type: "date" }

select

{
  name: "status",
  type: "select",
  options: ["draft", "published", "archived"],
  defaultValue: "draft",
}

multiselect

{
  name: "tags",
  type: "multiselect",
  options: ["news", "tech", "design", "culture"],
}

json / richtext

{ name: "metadata", type: "json" }
{ name: "body",     type: "richtext" }

relationship

// Single
{ name: "author", type: "relationship", relationTo: "users" }

// Many
{ name: "categories", type: "relationship", relationTo: "categories", hasMany: true }

upload

{ name: "cover", type: "upload" }

// Restrict to images, max 5 MB
{ name: "avatar", type: "upload", allowedTypes: ["image/jpeg", "image/png"], maxSize: 5 * 1024 * 1024 }

See Storage for upload endpoint configuration.

array

An array field stores a list of structured objects. Each item is validated against the fields sub-schema.

{
  name: "variants",
  type: "array",
  fields: [
    { name: "color", type: "text",    required: true },
    { name: "size",  type: "select",  options: ["S", "M", "L", "XL"], required: true },
    { name: "stock", type: "integer", required: true },
    { name: "price", type: "number",  required: true },
  ],
}

Array items are stored as JSONB (Postgres) or native arrays (SurrealDB / MongoDB). Each item must be a plain object whose keys match the sub-field names.

minItems / maxItems constraints:

{
  name: "variants",
  type: "array",
  minItems: 1,   // at least one variant required
  maxItems: 10,  // no more than 10 allowed
  fields: [
    { name: "color", type: "text",    required: true },
    { name: "size",  type: "select",  options: ["S", "M", "L", "XL"], required: true },
    { name: "stock", type: "integer", required: true },
    { name: "price", type: "number",  required: true },
  ],
}

Validation rules:

  • The top-level value must be an array.
  • If minItems is set and the array is shorter, the request is rejected.
  • If maxItems is set and the array is longer, the request is rejected.
  • Each element must be a plain object (not null, not another array).
  • Every sub-field runs its own type validation — including required, select option checks, email format, etc.
  • Error messages include the failing item index: Item 1 in 'variants': Field 'size' must be one of: S, M, L, XL.

Field Options

OptionTypeDescription
namestringField identifier (alphanumeric + underscore)
typeFieldTypeOne of the types listed above
requiredbooleanReject null / undefined values
uniquebooleanAdd a unique constraint (not supported on JSON types)
defaultValueunknownValue used when the field is omitted on create
readonlybooleanPrevent updates to this field after creation
hiddenbooleanExclude field from API responses
descriptionstringHuman-readable hint shown in the admin UI
validate(value) => true | stringCustom synchronous validation function

Field-Level Access Control

Every field accepts an access object with read, create, and update callbacks. Each callback receives the same AccessContext as collection-level access functions and must return a boolean (or a Promise<boolean>).

CallbackWhen it runsEffect when false
readGET list / GET by ID / any read responseField is omitted from the response
createPOST (create document)Field is silently stripped from input
updatePATCH / PUT (update document)Field is silently stripped from input

When no access is defined the field is readable and writable by anyone who can access the collection.

import { defineCollection } from "@codesordinatestudio/lucent";

export const Users = defineCollection({
  slug: "users",
  auth: true,
  fields: [
    { name: "name", type: "text", required: true },
    { name: "email", type: "email", required: true, unique: true },

    {
      name: "role",
      type: "select",
      options: ["admin", "editor", "viewer"],
      defaultValue: "viewer",
      // Only admins can set or change the role field
      access: {
        create: ({ user }) => user?.role === "admin",
        update: ({ user }) => user?.role === "admin",
      },
    },

    {
      name: "date_of_birth",
      type: "date",
      // The user themselves or an admin can read/write; everyone else gets the field omitted
      access: {
        read: ({ user, doc }) => user?.role === "admin" || user?.id === doc?.id,
        create: ({ user }) => user?.role === "admin" || user?.collection === "users",
        update: ({ user, doc }) => user?.role === "admin" || user?.id === doc?.id,
      },
    },

    {
      name: "internalNotes",
      type: "textarea",
      // Only admins see or write this field — never exposed to regular users
      access: {
        read: ({ user }) => user?.role === "admin",
        create: ({ user }) => user?.role === "admin",
        update: ({ user }) => user?.role === "admin",
      },
    },

    {
      name: "password",
      type: "password",
      hidden: true, // hidden: true is a shorthand to always omit from reads (no access callback needed)
    },
  ],
});

How it works

  • Reads: filterReadableFields() is called on every document before it leaves the API. Fields where access.read returns false are omitted. Fields marked hidden: true are always omitted without calling the callback.
  • Writes: filterWritableFields() strips any fields where access.create / access.update returns false before the document reaches the database. readonly and compute fields are also always stripped on writes.
  • Fast path: If no field in the collection defines an access callback, Lucent skips async evaluation entirely and uses the pre-computed visible-field list.

Custom Validation

{
  name: "username",
  type: "text",
  required: true,
  validate: (value) => {
    if (typeof value === "string" && value.length < 3) {
      return "Username must be at least 3 characters";
    }
    return true;
  },
}

Computed Fields

Use compute to derive a runtime value that is not stored in the database:

{
  name: "fullName",
  type: "text",
  compute: (doc) => `${doc.firstName} ${doc.lastName}`,
}