AI agents: fetch the documentation index at llms.txt. Markdown versions are available by appending .md to any page URL, including this page's markdown.
Model relationships
Define references, collections, and back-references between models with lazy and eager loading.
Define foreign keys, inverse collections, and ordered lists with six relationship decorators.
Relationship decorators at a glance
Decorator
Direction
Cardinality
Creates foreign key?
@Reference
Model A -> Model B
One-to-one
Yes (on A)
@ManyToOne
Model A -> Model B
Many-to-one
Yes (on A)
@OneToMany
Model B -> Model A[]
One-to-many
No (uses A's foreign key)
@BackReference
Computed inverse
Varies
No
@ReferenceArray
Model A -> Model B[]
Ordered list
Yes (array field on A)
@ReferenceCollection
Alias for @OneToMany
One-to-many
No
@Reference: belongs to
One-to-one relationship where the current model holds the foreign key.
import { Model, ClientModel, Property, Reference } from "@stratasync/core";@ClientModel("Task", { loadStrategy: "instant" })export class Task extends Model { @Property() declare id: string; @Property() declare title: string; // Creates an `assigneeId` foreign key property automatically @Reference(() => User, "assignedTasks") declare assignee: User | null; // The foreign key is available as a regular property @Property() declare assigneeId: string | null;}
You can override the default foreign key name with the foreignKey option:
Alias for @OneToMany. Use when the collection is unordered.
import { Model, ClientModel, Property, ReferenceCollection,} from "@stratasync/core";@ClientModel("Team", { loadStrategy: "instant" })export class Team extends Model { @Property() declare id: string; @Property() declare name: string; // Unordered set of team members @ReferenceCollection({ foreignKey: "teamId" }) declare members: unknown[];}
By default, references resolve eagerly from the identity map. Use lazy to defer hydration.
// Eager (default): resolved from identity map on access@Reference(() => User, "assignedTasks")declare assignee: User | null;// Lazy: not hydrated until explicitly accessed@Reference(() => User, "assignedTasks", { lazy: true })declare assignee: User | null;
Lazy loading helps with large reference chains, partial-load models (where the referenced data may not be available yet), and performance-sensitive views where you load models but don't display all their references. See Load Strategies for more on controlling when relationship targets load.
Circular references
The identity map and the factory function pattern handle circular references naturally. You can model self-referencing trees (such as subtask hierarchies) without extra configuration.
The factory function () => Task works because TypeScript hoists class declarations, so the class is available when the factory runs. The identity map guarantees that task.parent.children[0] resolves to the same object reference as task. When rendering recursive trees in React, use depth limits or lazy loading to avoid infinite loops.
Navigating the relationship graph
Once you define relationships, you can traverse them through the identity map without extra queries.
// Get a project and traverse relationshipsconst project = client.getCached("Project", projectId);// Navigate to teamconst team = project.team; // Resolves via identity map// Get all tasks in the projectconst tasks = project.tasks; // LazyCollection// Get the assignee of a taskconst task = tasks[0];const assignee = task.assignee; // Resolves via identity map// Navigate back to the user's teamconst userTeam = assignee.team; // Same team object (referential equality)
See Decorators for a complete domain model example.