Relate

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 in schema holding the source's record key.
  • schema: An ordinary z.object. Mapped fields must be plain z.string(), z.number(), or z.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 as z.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

HelperDescriptionExample
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 under objects: { Customer } in defineGraph is 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).
  • label and pluralLabel: Display names for documentation, UIs, and agents. label defaults to the humanized registry key (AccountReview → Account Review); pluralLabel defaults 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 Customer to its invoices with objects.Customer.traverse.invoices(id).
  • Callers can traverse in reverse from an Invoice to its customer with objects.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:

  1. Verifies property IDs, source fields, reference targets, field groups, and policy dependencies, throwing a CompileError that lists every issue.
  2. Produces an immutable, serializable manifest (format 4).
  3. 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.

On this page