# Current relationships

Use current relationship records to determine whether a connection or resource
membership exists in an exact scope. Your application decides the resulting
resource permissions. Historical accepted events do not prove a bond is still
active.

## Routes and authority

All routes use POST under `https://api.relationship.service.m7.org/v1/` and the
[authorization contract](https://m7.org/docs/api/api.relationship.service.m7.org/authorization.md). Read requires `relationship.list`.
Mutations require `relationship.manage` and project Owner or Admin.

Every route requires the [scope](https://m7.org/docs/api/api.relationship.service.m7.org/scopes.md): UUID strings `project_id`,
`space_id` and optional `subspace_id` (omitted/null means the root).

| Route | Other required fields | Optional fields | Result inside `data` |
| --- | --- | --- | --- |
| `/projects/relationships/list` | — | `status`, `source`, `count`, `offset` | `project_id`, `items`, `next_offset`, `space`. |
| `/projects/relationships/create` | `source`; pairwise also requires `target` | `metadata` | `relationship`. |
| `/projects/relationships/save` | `relationship_id`, `version`, `status` | `metadata` | `relationship`. |
| `/projects/relationships/remove` | `relationship_id`, `version`, `confirm_relationship_id` | `removed_by`, `reason` | `removed: true`, `relationship_id`. |

## List

`status`, if supplied, must be the string `active`, the only current-record
status. `source` is a principal object; it matches either endpoint for pairwise
records and the member for resource membership. There is no separate `target`
list filter. `count` and `offset` follow the common pagination rules.

```bash
curl --fail-with-body --silent --show-error \
  --request POST 'https://api.relationship.service.m7.org/v1/projects/relationships/list' \
  --header 'Authorization: Bearer ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{"project_id":"PROJECT_UUID","space_id":"SPACE_UUID","source":{"namespace":"example.app","principal_id":"PERSON_UUID"},"count":100,"offset":0}'
```

Each record includes `id`, `project_id`, `space_id`, nullable `subspace_id`,
`source_ref`, `target_ref`, `status`, `metadata`, positive integer `version`,
creation/update timestamps and actor attribution. Membership `target_ref` is
null. Pairwise records also identify their direction. Store the returned `id`
and version for later mutations.

## Create and update

Pairwise request body:

```json
{
  "project_id": "PROJECT_UUID",
  "space_id": "SPACE_UUID",
  "source": {"namespace": "example.app", "principal_id": "FIRST_PERSON_UUID"},
  "target": {"namespace": "example.app", "principal_id": "SECOND_PERSON_UUID"},
  "metadata": {}
}
```

Resource membership request body:

```json
{
  "project_id": "PROJECT_UUID",
  "space_id": "SPACE_UUID",
  "subspace_id": "SUBSPACE_UUID",
  "source": {"namespace": "example.app", "principal_id": "PERSON_UUID"},
  "metadata": {"label": "Reader"}
}
```

`source` and pairwise `target` follow the exact principal-reference contract.
Omit `target` for membership. `metadata` defaults to an empty object and follows
the [metadata limits](https://m7.org/docs/api/api.relationship.service.m7.org/scopes.md#metadata-and-missing-scopes).

An identical active create returns the existing record without another create
event. Creating the same bond with changed metadata returns body code 409;
save its current version instead:

```json
{
  "project_id": "PROJECT_UUID",
  "space_id": "SPACE_UUID",
  "relationship_id": "RELATIONSHIP_UUID",
  "version": 1,
  "status": "active",
  "metadata": {"label": "Updated reader"}
}
```

`relationship_id` is a UUID; `version` is the returned positive integer,
not a string. `status` is required on save and must be `active`. A save cannot
move a bond or change its principals. Omit metadata to preserve it or send `{}`
to clear it. A stale version returns body code 409; a record from another scope
returns body code 404.

## Removal, leaving and re-adding

Use the same remove command for a consumer's Leave or Remove action, after
applying your own user permissions:

```json
{
  "project_id": "PROJECT_UUID",
  "space_id": "SPACE_UUID",
  "relationship_id": "RELATIONSHIP_UUID",
  "version": 1,
  "confirm_relationship_id": "RELATIONSHIP_UUID",
  "removed_by": "PERSON_UUID",
  "reason": "Left the group"
}
```

`confirm_relationship_id` must exactly identify the relationship being removed.
`reason` is an optional UTF-8 string, at most 8,192 bytes; an empty value is
recorded as null. `removed_by` is optional UUID attribution:

| `removed_by` | Logged removal type |
| --- | --- |
| Omitted, empty string or nil UUID | `system_kick`. |
| UUID matches the member or either pairwise endpoint | `voluntary_leave`. |
| Another non-nil UUID | `kick`. |

This attribution does not authenticate the remover or grant authority; the
actual authenticated actor is recorded separately. Removal deletes the
current bond and retains its previous record in an event with
`snapshot.removal`. Re-adding creates a fresh record and UUID. Reload current
relationships after removal rather than treating a historical event as an
active membership.
