# Projects, membership and capabilities

A Post project contains message groups, templates, delivery policies, and jobs.
Choose a [workspace](https://m7.org/docs/api/api.post.m7.org/workspaces.md), then create or select a project. Project
UUIDs are independent of user and organization principal UUIDs.

## Authentication and project scope

All routes below use `POST` and require a Bearer access token, current
service-list admission, and the named action. Project-specific routes require
the effective workspace principal's active Management membership. Service
Admin/Root does not bypass that membership. Organization grants further restrict
project grants; see [Authorization](https://m7.org/docs/api/api.post.m7.org/authorization.md#project-capability-boundary).

Every `/v1/members/policy/*`, `/v1/members/message/*`, and `/v1/members/job/*`
request requires `project_id` (a non-nil UUID). The API derives resource scope
from the authorized project and rejects `owner`, `project`, `principal_id`, and
`actor_id` overrides on those routes. Resource IDs and names are resolved inside that project, even when a caller
belongs to multiple projects. A message group can also link a service-provided
shared template by its known ID; this does not grant cross-project access.

## Project routes

| Route | Action | Required fields | Optional fields / result |
| --- | --- | --- | --- |
| `/v1/projects/list` | `project.list` | None | `items` with project contexts; `can_create`. |
| `/v1/projects/create` | `project.create` | `name` | Nonempty string, at most 255 bytes; returns new project context. |
| `/v1/projects/context` | `project.view` | `project_id` | `include_deletion_summary` boolean, default false; a true value requires Owner and `project.delete`. |
| `/v1/projects/delete` | `project.delete`, Owner | `project_id`, matching `confirm_project_id` | Deletes an eligible project and its project content atomically. |

Unknown project fields are rejected. Creation assigns the effective principal
as the initial active Owner and records the real actor separately. Context
contains `project`, `membership`, `management_group_id`, and primitive
`capabilities`. Membership has `id`, `principal`, `role`, and `status`. Use the
current `capabilities` to offer actions; every action is rechecked on the server.

```bash
curl -sS https://api.post.service.m7.org/v1/projects/create \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Transactional messages"}'
```

Store `data.project.id`, then select that project on delivery requests:

```json
{
  "project_id": "PROJECT_UUID",
  "purpose": "Confirm an email address",
  "proof_policy": "single",
  "message_group": "email-proof",
  "email": ["recipient@example.test"]
}
```

## Membership routes

Member writes use a collaborator's local roster `principal_id`. Do not send a
native user or organization ID. Target principals must be active and admitted.

| Route | Action | Required fields | Optional fields / result |
| --- | --- | --- | --- |
| `/v1/projects/members/list` | `project.member.list` | `project_id` | `limit` (1–100, default 50), `offset` (0–1,000,000, default 0); `members`, `total_count`, nullable `next_offset`, current roles and Owner count. |
| `/v1/projects/members/add` | `project.member.add`, Owner/Admin | `project_id`, `principal_id` | Adds an active ordinary Member; existing membership returns `409`. |
| `/v1/projects/members/save` | `project.member.save` | `project_id`, `principal_id` | Existing `member_id`; `role`, `status`; returns `saved`, `member`, and nullable updated `context`. |
| `/v1/projects/members/remove` | `project.member.remove` | `project_id`, `principal_id`, exact `member_id` | Returns `removed` and nullable updated `context`. |
| `/v1/projects/members/leave` | `project.member.leave` | `project_id` | Current workspace only; returns `left`. Target identity, role, and status overrides are rejected. |
| `/v1/projects/members/capabilities/list` | `project.member.list` | `project_id` | Read-only `tags` and `roles`. |
| `/v1/projects/members/capabilities/save` | `project.member.save` | `project_id`, `principal_id`, exact `member_id`, `mode`, `tag_ids` | Returns `saved` and updated `context`. |

Roles are `owner`, `admin`, and `member`; editable statuses are `active` and
`disabled`. Owner manages all levels; Admin manages ordinary Members. Role
changes and edits/removal of privileged members additionally require
`project.member.assign_roles`. New memberships default to active Member.
Omitted role/status preserve an existing membership's state. The final active
Owner cannot be disabled, demoted, removed, or leave. Authority is rechecked
when applying each membership change.

Member results include safe principal name/kind/status, `capabilities_mode`,
selected `capability_tag_ids`, `can_update`, `can_remove`, `can_leave`,
`can_edit_capabilities`, `allowed_roles`, and `allowed_statuses`. After a
self-change, `context: null` means the caller lost access; discard old context.

## Capability selections

Load the project's read-only catalogue and use its `tag_id` values. Each tag
has `capability`, `name`, and `description`. The role data supplies current
defaults and allowed selections.

```json
{
  "project_id": "PROJECT_UUID",
  "principal_id": "COLLABORATOR_PRINCIPAL_UUID",
  "member_id": "MEMBERSHIP_UUID",
  "mode": "custom",
  "tag_ids": ["CAPABILITY_TAG_UUID"]
}
```

`mode` is `defaults` or `custom`. Use `tag_ids: []` with defaults. Custom with
an empty list intentionally selects no optional delivery actions. IDs must be
unique and valid for the current target role. A role change clears its old
custom assignments. Protected management rights and last-owner safeguards
remain tied to the role. For organizations, both protected and optional grants
are intersected with current verified Management grants.

## Public capability discovery

`GET /v1/capabilities.json` returns the current read-only service capability
catalogue without authentication. It is a raw JSON document, outside the usual
response envelope, with `schema_version`, `service`, `catalogue_version`,
`families`, `capabilities`, `presets`, and `role_defaults`. Use capability `id`
values to understand action names and profile expansions. Catalogue definitions
do not grant the caller those actions; the selected resource context remains
the access decision. An unavailable catalogue returns `503`.

## Deletion and failures

Load context with `include_deletion_summary: true` before deleting. Authorized
Owners receive counts for jobs, policy groups, message groups, templates, DNE
entries, and domain rules, plus `can_delete`. A project containing any job
cannot be deleted (`409`), preserving delivery and proof history. Counts alone
do not authorize deletion; the server rechecks membership and jobs at commit.

Invalid UUIDs, selectors, field names, or capability IDs return `400`. Missing
membership or action returns `403`. A missing project returns `404`. Existing
membership, final-owner conflicts, and jobs blocking deletion return `409`.
An unavailable catalogue or receiver authorization fails closed.
