Topolo Docs Topolo Platform
Open workspace

Set a Consent project notification recipient

Assign the intended notification user with admin workspace access, verify the stored field and understand delivery and compensation limits.

Outcome

Choose the user id stored as the recipient for a Consent project’s notifications. The assignment does not send a notification or verify that the selected user can receive one.

Action: app_topolo_consent.projects.notification_recipient.update.

PUT /api/projects/{projectKey}/notification-recipient

Before you start

  1. Verify the original credential, production environment, organization and actual canonical Consent workspace. Bind the workspace separately from action input. A Consent project key identifies a surface inside a workspace; it is not the workspace id or an authorization grant.

  2. This mutation requires app_topolo_consent.workspace:write AND a verified admin-level workspace grant for the PUT notification-recipient route. An ordinary read_write grant or organization membership alone does not establish that admin level. Preserve API-key resource restrictions; never forge identity headers or switch to another principal after refusal.

  3. Review the intended project and recipient, retain the prior notificationUserId, and obtain approval for this actual assignment before execution. The example identities are fictional and confer no permission. This documentation publication does not approve a recipient change.

Discover the project and user

Use the actual credential-scoped Consent workspace discovery and current app_topolo_consent.projects.list action to identify the project and its current notificationUserId. For yourself, topolo whoami --json supplies the authenticated user id. For another person, use supported organization identity discovery and verify the intended recipient with its existing owner; this handler itself performs no Auth user lookup.

Request and effects

Action input includes the project key used in the route and the intended user id. Workspace binding is separate.

{
  "projectKey": "project_fixture_docs",
  "userId": "usr_fixture_recipient"
}

These ids are synthetic and unrun. Discover actual ids and validate/plan the actual approved input before execution. Do not execute this fixture as a real assignment.

The worker verifies the caller, resolves the canonical workspace through requireWorkspace with route-derived minimumAccess:admin, and applies the app workspace:write permission gate before dispatching to adminUpdateNotificationRecipient. Omitted selection resolves the verified platform default; a refused named workspace is not silently replaced. Internal platform-operator policy is outside this ordinary-caller guide, not an instruction to bypass a denial.

requireAdminProject requires control storage, a project reachable through the verified organization/workspace scope, and a project that is not archived. Project identity and workspace identity are separate even when older records once used matching ids.

The handler reads and trims userId. An empty or whitespace-only value returns400/user_id_required. It does not look up that user in Auth, validate their membership, prove notification eligibility or accept an email/display name as a verified recipient. Choose a real intended user id from supported identity/organization discovery before authorizing the write.

One SQL UPDATE sets consent_projects.notification_user_id and updated_at for the scoped project id. The success body is exactly {projectKey,notificationUserId}; this handler does not add success:true, a project object or delivery receipt. It performs no immediate email, notification, model/provider call or config-cache write. Future notification behavior is a separate workflow.

No expected version, compare-and-swap predicate or idempotency key is accepted by this action. A later writer can replace the value, and repeating the assignment also updates updated_at. A transport failure after SQL is an uncertain effect, not proof of no change. Read the current project before retrying or attempting a separately approved compensation.

Complete worked responses

Assign an intended notification user

Complete source-derived synthetic/unrun success. Assumes an active fixture project inside the original caller’s verified admin-level workspace, workspace:write eligibility and separately approved intended recipient. No user/project lookup, assignment or notification was executed.

Input:

{
  "projectKey": "project_fixture_docs",
  "userId": "usr_fixture_recipient"
}

Response:

{
  "projectKey": "project_fixture_docs",
  "notificationUserId": "usr_fixture_recipient"
}

Read back the same project’s notificationUserId; this echoed response is not membership or delivery proof.

Reject a whitespace-only user id

Source-derived synthetic/unrun HTTP400 refusal. This input passes the current minLength1 action schema, but stringValue trims it to no value after successful caller/workspace/project checks. No business request was sent.

Input:

{
  "projectKey": "project_fixture_docs",
  "userId": "   "
}

Response:

{
  "error": "user_id_required"
}

Obtain the actual intended user id; do not retry whitespace, null or an invented identity.

Refuse an archived project

Source-derived synthetic/unrun HTTP409 refusal. Assumes the original caller passed verification/permission/admin workspace checks and the scoped project exists with status archived. This is an external fixture precondition, not an undeclared input flag.

Input:

{
  "projectKey": "project_fixture_docs",
  "userId": "usr_fixture_recipient"
}

Response:

{
  "error": "project_archived"
}

Preserve the refusal. Restoring or choosing another project is a separate authorized decision; do not bypass archive state to assign a recipient.

Verify and recover

Read the original-credential project list in the same verified workspace and identify the exact project key/id. Compare its notificationUserId to the intended saved user id; workspace:read eligibility is separately required for that verification read.

A returned notificationUserId echoes the normalized write value. It does not prove Auth membership, recipient acceptance, an email address, a sent notification or future delivery. Preserve the prior and current project state and actual request evidence.

If the result is uncertain or the read-back differs, reconcile current state and concurrent work before another approved mutation. Do not guess another project key, broaden a resource binding or repeatedly write to force a match.

There is no transactional rollback. If the prior notificationUserId was a known nonempty valid intended user id, first read current state and review concurrent changes, then separately authorize and plan this same action with that prior value. If the prior value was null/absent, this strict action cannot clear the recipient: do not send null/empty text or invent a delete endpoint. Preserve evidence and route the unsupported restore case to the existing owner. A compensation does not reverse any future notification that may already have occurred.

Other refusals

400 / user_id_required: Choose a verified intended user id. The handler only trims and checks for nonempty text: it does not enforce user-id format, resolve names/emails or validate recipient eligibility, so nonempty email-shaped or name-shaped strings are not rejected by this check.

404 / project_not_found: Reconcile the real project key with the original verified organization/workspace. Absent or unreachable projects are not permission to guess another key or broaden scope.

409 / project_archived: Do not assign against an archived project. Any restoration is a separate current-contract/authority decision.

503 / database_unavailable: The handler has no control database. Preserve the actual failure and ask the existing service owner to restore the dependency before an authorized retry.

Outer authentication, permission and workspace refusals may occur before the handler and use their actual published shared carrier. Preserve status/code/request id rather than inventing a precise shared error body. A malformed JSON body is treated as an empty object by the route helper and can reach user_id_required only after the earlier checks; local action validation normally rejects missing required fields before transport.

Change log

  • 2026-10-21: Added source-qualified admin workspace access, recipient normalization, exact stored-field mutation, complete success/refusal examples and conditional compensation limits. Examples are synthetic and unrun; no business/source/model/provider/notification operation or serving deployment proof was performed.