Extensibility
Let users add their own attributes to objects at runtime without touching the developer-defined base.
standards lets your users add their own attributes to any object at runtime, without touching the developer-defined base. The schema you ship is a floor, not a ceiling.
// @noverify
import { object, text } from "@stndrds/schema";
export const contact = object({ name: "contact", label: "Contact" })
// Declared in code → system: true → protected from user modification
.attribute(text({ name: "email", label: "Email" }).required())
.attribute(text({ name: "phone", label: "Phone" }));
// Users can add their own attributes via the runtime API (see below).Protected vs custom
Anything declared in code defaults to system: true. The runtime rejects user attempts to delete or rename system attributes and objects. Attributes added at runtime through the admin UI are system: false — their creator can rename, reorder, or delete them at will.
The same default applies to the object itself. A code-declared object cannot be renamed or deleted by users, but its custom runtime attributes can still be managed freely.
Breaking in vNEXT: .system() has been removed from builders. Default is now system: true. Use .runtime() to opt out — but only in tests, seeds, and fixtures.
Adding attributes at runtime
Use the useAddAttribute hook to add a new attribute to an existing object. Pass the object's id (from the runtime registry) and an AddAttributeInput payload:
// @noverify
import { useAddAttribute } from "@stndrds/react";
const { mutate: addAttribute } = useAddAttribute();
addAttribute({
objectId: "obj-123",
attribute: {
name: "internalNotes",
label: "Internal notes",
type: "text",
multiline: true,
},
});useAddAttribute returns a TanStack Query mutation. On success it invalidates the object detail, attribute list, and view caches automatically.
To remove a user-created attribute, use the useDeleteAttribute hook with { objectId, attributeId }.
Sealed objects
By default any system object accepts user-added attributes — the schema you ship is a floor, not a ceiling. If a particular object must stay locked to exactly the attributes you declared (think invoice or payment rows where extra columns would break downstream automations), mark it .sealed():
// @noverify
import { object, number, text } from "@stndrds/schema";
export const invoice = object({ name: "invoice", label: "Invoice" })
.sealed()
.tolerate(["legacy_ref"])
.attribute(text({ name: "number", label: "Number" }).required())
.attribute(number({ name: "amount", label: "Amount" }).required());Sealing does not block the runtime API on its own — it surfaces as drift in standards schema diff (and the GET /schema/drift HTTP endpoint). Any custom attribute on a sealed object is reported as unexpected drift unless its name appears in .tolerate([...]). The CLI exits non-zero when unexpected drift is present, so you can gate CI on a clean diff.
Types preserved
User-added attributes are recorded in the runtime registry alongside developer-defined ones. Hooks and UI components see them identically — no extra wiring required. Their values appear in record.values keyed by attribute name, with the type inferred from the type field supplied at creation time.
Supported type values include text, number, checkbox, date, status, select, and relation, among others. The full list is exported as AttributeType from @stndrds/schema.
System attributes can only be removed in code — remove the .attribute() line and rely on the boot-time schema sync to demote or delete the attribute. Users see a "protected" indicator in the UI next to any system attribute.