Worked example: User → Post → Comment

This is the clearest illustration of why relationships matter (technical-specification.md §3). A Project defines three data types: User, Post, and Comment. A Post references a User through its userId field, and a Comment references a Post through its postId field:

[
  {
    "sourceTypeId": 2,
    "sourceField": "userId",
    "targetTypeId": 1,
    "targetField": "id"
  },
  {
    "sourceTypeId": 3,
    "sourceField": "postId",
    "targetTypeId": 2,
    "targetField": "id"
  }
]

When the Project's dataset is generated (Epic 8), independent types are generated first and types with relationships are generated afterward, in dependency order: Users are generated first, Posts next (each assigned a real userId), then Comments (each assigned a real postId). This guarantees every generated reference points at a record that actually exists. A separately configured Runtime route can then expose nested collections such as /users/{id}/posts or /posts/{id}/comments, using these same relationships to filter.

GET
/admin/projects/{projectId}/relationships
List relationships

Returns every relationship configured in the Project. Requires any effective Project role (viewer included). Returns 404 if the caller has no access to the Project.

Example response

{
  "relationships": [
    {
      "id": 1,
      "sourceTypeId": 2,
      "sourceField": "userId",
      "targetTypeId": 1,
      "targetField": "id"
    }
  ]
}
POST
/admin/projects/{projectId}/relationships
Create a relationship

Creates a relationship between a field on sourceTypeId and a field on targetTypeId, both of which must already exist as data types in this Project. sourceField must be a key in the source type's schema. targetField must be a key in the target type's schema, must be a scalar field (string, number, boolean, or date - never array/object) of the same type as sourceField, and must be that type's configured identifier or another configured unique field, so the relationship can always be resolved unambiguously. Requires effective Project editor or admin. Creating a relationship changes Project configuration only - it does not regenerate existing template records. It also immediately auto-creates the standard nested CRUD Routes under the target (parent) type's collection - e.g. GET/POST /places/{id}/people and GET/PUT/PATCH/DELETE /places/{id}/people/{id} for a relationship from people to places - so no separate Route definition is required to reach the nesting via the Runtime API.

Request

{
  "sourceTypeId": 2,
  "sourceField": "userId",
  "targetTypeId": 1,
  "targetField": "id"
}

Response (201)

{
  "id": 1,
  "sourceTypeId": 2,
  "sourceField": "userId",
  "targetTypeId": 1,
  "targetField": "id"
}

Returns 422 if either type doesn't belong to this Project, either field doesn't exist on its type, the two fields aren't the same scalar type, or targetField isn't a configured identifier/unique field.

DELETE
/admin/projects/{projectId}/relationships/{relationshipId}
Delete a relationship

Permanently removes the relationship, along with any nested Route that depends on it - that Route only ever existed to expose this relationship, so it's removed automatically rather than blocking the delete. Requires effective Project editor or admin. Existing template/runtime data is unaffected, since configuration and generated data are intentionally kept separate; a future regeneration reflects the new relationship configuration.