Skip to main content
Pylon’s data-layer automation is deliberately small and mostly automatic. Two built-in plugins wire themselves from your schema — you never name them in a config block — and the SDK adds a few field- and entity-level helpers. Everything else (cascades, slugs, derived fields, versioning) is a server function or a policy, not a plugin.

tenant_scope

Row-level multi-tenancy. Automatic — the signal is a tenantId field on the entity. No config entry.
Once an entity has a tenantId (or tenant_id) field:
  • Inserts auto-fill tenantId = auth.tenantId when the caller doesn’t provide it, and reject a write whose tenantId doesn’t match the caller’s — you can’t insert into another tenant.
  • Reads / updates / deletes are scoped to the caller’s tenant.
  • Admin contexts bypass scoping.
This makes tenant isolation a default posture rather than a where clause you have to remember on every query. In your own query/mutation handlers you still have auth.tenantId available to scope further.

owner_stamp

Per-row ownership. Automatic — the signal is field.X().owner() on the field.
On insert the runtime overwrites sellerId with auth.userId from the session and rejects any client attempt to set it to a different user. This is what makes optimistic, local-first writes safe for owned data: the client inserts with its own id for an instant local paint, and the server validates the owner from the session. Pair it with a policy like allowUpdate: "data.sellerId == auth.userId".

Schema behaviors

behaviors([...]) on the entity builder run field-injection helpers before the schema is registered. Two ship today:
Behaviors mutate the EntityDefinition’s fields, so the rest of the framework (storage, sync, policies) treats the injected columns as ordinary columns.
Behaviors add the columns. Automatic value-stamping is driven by the defaultNow() marker and is still being wired end-to-end — until it lands, set the values in your function handler (or on the client) and the columns will persist normally. softDelete adds the deletedAt column; the DELETE-sets-deletedAt and list-filtering semantics are app-driven for now (filter deletedAt == null in your query).

Field-level constraints & defaults

The real “validation” surface is on the field builder — it’s recorded in the manifest and enforced by the runtime + codegen:
For richer validation (length limits, regex, cross-field rules), do the check in a mutation handler before writing, and gate access with policies.

What is not a plugin

These were previously documented as data plugins; they don’t exist as configurable plugins. Here’s where the real capability lives: Search is configured per-entity (not as a data plugin) — see Search & AI.