Graph Modeling
In Relate, your domain model is defined in TypeScript using the relate
package. A graph consists of sources, objects, references, and
relationships, registered together with defineGraph.
1. Defining Sources
Sources declare external systems of record. Each source has a stable identifier, the field holding its record key, and a Zod schema for its records:
import { z } from 'zod';
import { defineSource } from 'relate';
export const crmSource = defineSource({
id: 'crm.customers',
idField: 'id',
schema: z.object({
id: z.string(),
name: z.string(),
region: z.string(),
tier: z.string(),
}),
});
export const billingSource = defineSource({
id: 'billing.invoices',
idField: 'invoice_id',
schema: z.object({
invoice_id: z.string(),
customer_id: z.string(),
amount_cents: z.number().min(0),
status: z.string(),
}),
});id: Stable identifier for the source definition.idField: The field inschemaholding the source's record key.schema: An ordinaryz.object. Mapped fields must be plainz.string(),z.number(), orz.boolean(), optionally wrapped in.optional()or.nullable(). String length bounds (.min,.max,.length) and number range bounds (.min,.max,.gt,.lt) are kept. Enums,.int(), formats such asz.email(), refinements, transforms, coercion, and defaults are rejected at compile time. Unmapped source fields are kept privately and never exposed.
2. Defining Objects
Objects represent business entities in the graph. They define an object ID, presentation labels, source membership, and mapped properties.
import { defineObject, from, objectId, source } from 'relate';
import { crmSource } from './sources.js';
export const Customer = defineObject({
id: 'customer',
label: 'Customer',
pluralLabel: 'Customers',
description: 'Enterprise and commercial customer accounts.',
membership: source(crmSource),
properties: {
id: objectId({ id: 'customer.id' }),
name: from(crmSource.fields.name, { id: 'customer.name' }),
region: from(crmSource.fields.region, { id: 'customer.region' }),
tier: from(crmSource.fields.tier, { id: 'customer.tier' }),
},
});An object has a single membership source, and its from properties must read
fields of that source. Combining fields from a second source is a preview and
not yet supported.
Property Types
| Helper | Description | Example |
|---|---|---|
objectId({ id }) | The object ID, generated by Relate. Exactly one per object. | objectId({ id: 'customer.id' }) |
from(sourceField, { id, access? }) | Maps a property to a field of the membership source, with its type inferred. | from(crm.fields.name, { id: 'customer.name' }) |
reference(TargetObject, { id, from?, access? }) | Typed pointer to another graph object. With from, the target is resolved from a source field; without it, the reference is stored natively by Relate. | reference(Customer, { id: 'invoice.customer', from: billing.fields.customer_id }) |
Machine Names vs Display Labels
- Registry key (
apiName): The key underobjects: { Customer }indefineGraphis the public API name used in SDK calls:objects.Customer. Changing it renames the public API. - Definition
id: The stable definition identity (e.g.'customer'). Stored data is keyed by object and property IDs, so renaming a registry key or property keeps those IDs. A rename still changes the graph's definition revision, which an existing Postgres installation will refuse until it is explicitly migrated (see compilation). labelandpluralLabel: Display names for documentation, UIs, and agents.labeldefaults to the humanized registry key (AccountReview→Account Review);pluralLabeldefaults to the singular label.
3. Defining References and Relationships
To connect objects, declare a reference property on the dependent object,
define a relationship over it, and register both in defineGraph:
import { z } from 'zod';
import {
defineAccess,
defineGraph,
defineObject,
defineRelationship,
from,
objectId,
reference,
source,
} from 'relate';
import { billingSource } from './sources.js';
import { Customer } from './customer.js';
export const Invoice = defineObject({
id: 'invoice',
label: 'Invoice',
pluralLabel: 'Invoices',
membership: source(billingSource),
properties: {
id: objectId({ id: 'invoice.id' }),
customer: reference(Customer, {
id: 'invoice.customer',
from: billingSource.fields.customer_id,
}),
amount: from(billingSource.fields.amount_cents, { id: 'invoice.amount' }),
status: from(billingSource.fields.status, { id: 'invoice.status' }),
},
});
export const CustomerInvoices = defineRelationship({
id: 'customer.invoices',
forward: 'invoices',
reverse: 'customer',
via: Invoice.properties.customer,
});
const access = defineAccess({
roles: ['sales'],
fieldGroups: ['ordinary'],
claims: { region: z.string() },
});
export const graph = defineGraph({
id: 'business',
objects: { Customer, Invoice },
relationships: { CustomerInvoices },
access,
policies: {
Customer: { read: { gate: access.role('sales') } },
Invoice: { read: { gate: access.role('sales') } },
},
});Every registered object needs a read policy; use read: 'deny' to deny it on
purpose. See Access Control.
Once CustomerInvoices is registered under relationships:
- Callers can traverse forward from a
Customerto itsinvoiceswithobjects.Customer.traverse.invoices(id). - Callers can traverse in reverse from an
Invoiceto itscustomerwithobjects.Invoice.traverse.customer(id).
See Reading Data for traversal results.
4. Graph Compilation & Revision Pinning
compile(graph) from relate/compiler checks the graph and produces its
manifest. createRuntime compiles the graph for you, so you only need to call
it directly to inspect or store the result. Compilation:
- Verifies property IDs, source fields, reference targets, field groups, and
policy dependencies, throwing a
CompileErrorthat lists every issue. - Produces an immutable, serializable manifest (format 4).
- Computes a deterministic SHA-256 definition revision of that manifest.
import { compile } from 'relate/compiler';
import { graph } from './graph.js';
const { manifest, definitionRevision } = compile(graph);
console.log(definitionRevision); // 'sha256:…'Stores pin a graph ID to one definition revision. The Postgres store refuses to serve a graph whose revision differs from the one installed for that graph ID. Migrating between revisions is not yet implemented.