Modeling Entities¶
Why The Model Builder Exists¶
defineEntityModel(...) sits above the raw repository schema interface and lets you describe a model in one place.
That model definition is used to derive:
- local entity validation
- remote payload validation
- encrypted field metadata
- entity-to-remote and remote-to-entity mapping
Basic Field Types¶
Primitive builders validate out of the box.
Typical examples:
field.string()field.boolean()field.number()field.json(z.object(...))
Use field.json(...) for structured values whenever possible so invalid data is rejected before encrypting and after decrypting.
Encrypted Fields¶
Call .encrypted() on any field that should be stored as ciphertext on the remote side.
import { E2eeEncryptionStrategy } from "e2ee-client-backend";
const settingsModel = defineEntityModel({
fields: {
id: field.string(),
displayName: field.string(),
preferences: field
.json(preferencesSchema)
.encrypted({ strategyId: E2eeEncryptionStrategy.Aes256Gcm }),
},
idField: "id",
name: "settings",
});
Important behavior:
- unencrypted fields are passed through directly
- encrypted fields are serialized before encryption and parsed after decryption
- the repository chooses the field strategy from
strategyIdor the schema default strategy
When you use E2eeBackend, the backend can inject that schema default once for all registered models through createE2eeBackend({ defaultStrategyId: ... }).
Remote Field Names¶
Use .remote("remoteFieldName") when the local entity field name should not match the backend field name.
const dashboardModel = defineEntityModel({
fields: {
config: field.json(configSchema).remote("configEnvelope").encrypted(),
id: field.string(),
name: field.string(),
},
idField: "id",
name: "dashboard",
});
This is useful when integrating with an existing backend shape you do not want to rename immediately.
Nullability And Optional Shapes¶
The field builder is explicit about nullability. If a field can be null, mark it with .nullable().
That keeps entity validation, remote validation, and encrypted serialization behavior aligned.
Custom Per-Model Services¶
If a model needs an app-facing surface beyond CRUD, use defineClientModel({ setup(...) { ... } }).
const client = createEntityClient({
contextResolver,
models: {
dashboards: defineClientModel({
adapter,
schema: dashboardModel,
setup({ repository }) {
return {
createNamed(name: string) {
return repository.create({
config: null,
id: crypto.randomUUID(),
name,
});
},
repository,
};
},
}),
},
strategies,
});
This keeps custom app workflows close to the model while reusing the repository implementation underneath.
Built-In Schemas¶
If you prefer package-provided shapes, helpers like createIntegrationSchema() and createDashboardSchema() are still available.
Use them when you want a ready-made starting point instead of defining models from scratch.