Topolo Docs Topolo Platform
Open workspace

Replace user notification preferences

Replace the complete exact user/org preference scope with sequential writes; empty rows clears it and failure can leave partial rules.

Outcome

Replace the complete exact user/org preference scope with sequential writes; empty rows clears it and failure can leave partial rules.

These examples prove local handler behavior with real isolated notification SQL and explicitly simulated identity dependencies.

Before you start

  • Verify pinned identity and the exact published action. Preference/address storage permits self or platform authority; organization admin alone cannot manage another user. Organization forbid rules require authority over the exact organization.

  • Discover the intended user/org independently. These storage handlers do not verify target existence and stored preference lists do not resolve effective delivery. Omitted preference org_id means global wildcard *, while replacement selects organization scope through query.org_id.

  • Before replacement capture the complete previous scope, review the full intended rows and inspect the validated plan. Empty rows clears explicit rules and may permit more delivery. Replacement is sequential and can leave partial state.

  • Channel verify only sets a stored flag and performs no possession challenge. Retain independent authorized ownership evidence before approving it; fixture addresses are unused and authorize no live verification or outreach.

Discover the target

topolo whoami --json
topolo apps --json
topolo actions get app_topolo_auth.user_notification_preferences.replace --detail --json

Discover the authorized target user and exact organization. Read user_notification_preferences.list for each scope separately, user_notification_channels.list for stored channel_id values, or organization_notification_policy.list for the managed organization. Copy ids only from the exact selected scope. Effective delivery requires the published resolve action and delivery read-back separately.

Input fields

FieldRequiredMeaning
userIdYesExisting intended Auth user from authorized discovery. User preference/address handlers allow self or platform authority; organization administrator alone does not authorize another user. These storage handlers do not independently verify target-user existence.
queryNoOptional query parameters, separate from the preference replacement body. query.org_id selects the exact stored scope; omitted means global *. Do not put org_id beside rows in replacement input.
query.org_idNoExact stored preference organization scope; omitted means the global wildcard scope *. Listing this scope does not merge global/organization rules or resolve effective delivery.
rowsYesComplete replacement of the selected stored scope, up to 500 unique event_type/channel pairs. Empty rows removes all its explicit rules. Capture the complete previous scope first; replacement uses sequential DELETE/INSERT statements and may leave partial rows on failure.
rows[].event_typeYesSpecific event type with two to four lowercase dot-separated segments, or * to match all event types. Together with channel this is the unique natural key inside the selected scope.
rows[].channelYesDelivery channel. Preference/forbid rows accept email, push, sms, in_app, whatsapp or *; an address registration accepts email, push, sms or whatsapp only, since in_app has no address row.
rows[].enabledYesStored preference boolean. It does not create a usable address or independently authorize message delivery; effective resolution also considers organization rules and other policy.

Effects

  • Deletes the selected scope then inserts each replacement row sequentially; other scopes are preserved.

  • Empty rows clears explicit toggles and can change effective delivery; no notification is sent.

Interpret the response

Replaces the whole exact user/org scope rather than patching individual rows. query.org_id selects organization scope; omission selects *. Other scopes are preserved. Duplicate event/channel pairs are rejected before any write.

Rows [] clears the explicit scope and may change effective delivery. DELETE and each INSERT execute sequentially; failure can leave a partial set. The response reads the actual resulting rows and no message is sent. Capture previous rows and independently resolve delivery before approval.

Worked examples

These are complete raw target-handler responses, not CLI/gateway envelopes or live authorization proof. Synthetic users, organizations and unused addresses exist only in isolated fixtures. Discover real intended targets and review effects separately. Save only reviewed input as payload.json; validate and inspect the unchanged plan before an authorized call. --confirm assumes intended approval. Cookie/Bearer identities must consistently select the intended principal. Stored rows, ownership evidence, effective policy and actual delivered messages are separate outcomes. No fixture sends a message or performs an address challenge.

Replace the exact global scope with bounded unique event/channel rows

Input · payload.json:

{
  "userId": "usr_self",
  "rows": [
    {
      "event_type": "campaigns.send_completed",
      "channel": "email",
      "enabled": true
    },
    {
      "event_type": "*",
      "channel": "push",
      "enabled": false
    }
  ]
}

CLI:

topolo actions validate app_topolo_auth.user_notification_preferences.replace --data @payload.json --json
topolo actions plan app_topolo_auth.user_notification_preferences.replace --data @payload.json --confirm --json
topolo actions call app_topolo_auth.user_notification_preferences.replace --data @payload.json --confirm --json

MCP call after validation and plan review:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_auth.user_notification_preferences.replace",
    "input": {
      "userId": "usr_self",
      "rows": [
        {
          "event_type": "campaigns.send_completed",
          "channel": "email",
          "enabled": true
        },
        {
          "event_type": "*",
          "channel": "push",
          "enabled": false
        }
      ]
    },
    "confirm": true
  }
}

Captured local handler response · HTTP 200:

{
  "success": true,
  "message": "Notification preferences replaced",
  "data": {
    "userId": "usr_self",
    "orgId": "*",
    "rows": [
      {
        "user_id": "usr_self",
        "organization_id": "*",
        "event_type": "*",
        "channel": "push",
        "enabled": false,
        "updated_at": 1790971419
      },
      {
        "user_id": "usr_self",
        "organization_id": "*",
        "event_type": "campaigns.send_completed",
        "channel": "email",
        "enabled": true,
        "updated_at": 1790971419
      }
    ]
  },
  "timestamp": "2026-10-02T00:00:00.000Z"
}

Organization-scope replacement retains the same user global rows

Input · payload.json:

{
  "userId": "usr_self",
  "query": {
    "org_id": "org_example"
  },
  "rows": [
    {
      "event_type": "auth.password.changed",
      "channel": "email",
      "enabled": false
    }
  ]
}

CLI:

topolo actions validate app_topolo_auth.user_notification_preferences.replace --data @payload.json --json
topolo actions plan app_topolo_auth.user_notification_preferences.replace --data @payload.json --confirm --json
topolo actions call app_topolo_auth.user_notification_preferences.replace --data @payload.json --confirm --json

MCP call after validation and plan review:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_auth.user_notification_preferences.replace",
    "input": {
      "userId": "usr_self",
      "query": {
        "org_id": "org_example"
      },
      "rows": [
        {
          "event_type": "auth.password.changed",
          "channel": "email",
          "enabled": false
        }
      ]
    },
    "confirm": true
  }
}

Captured local handler response · HTTP 200:

{
  "success": true,
  "message": "Notification preferences replaced",
  "data": {
    "userId": "usr_self",
    "orgId": "org_example",
    "rows": [
      {
        "user_id": "usr_self",
        "organization_id": "org_example",
        "event_type": "auth.password.changed",
        "channel": "email",
        "enabled": false,
        "updated_at": 1790971419
      }
    ]
  },
  "timestamp": "2026-10-02T00:00:00.000Z"
}

Empty rows clears only the selected preference scope

Input · payload.json:

{
  "userId": "usr_self",
  "rows": []
}

CLI:

topolo actions validate app_topolo_auth.user_notification_preferences.replace --data @payload.json --json
topolo actions plan app_topolo_auth.user_notification_preferences.replace --data @payload.json --confirm --json
topolo actions call app_topolo_auth.user_notification_preferences.replace --data @payload.json --confirm --json

MCP call after validation and plan review:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_auth.user_notification_preferences.replace",
    "input": {
      "userId": "usr_self",
      "rows": []
    },
    "confirm": true
  }
}

Captured local handler response · HTTP 200:

{
  "success": true,
  "message": "Notification preferences replaced",
  "data": {
    "userId": "usr_self",
    "orgId": "*",
    "rows": []
  },
  "timestamp": "2026-10-02T00:00:00.000Z"
}

Verify the result

  • Read user_notification_preferences.list with the exact same user and org_id, requiring the complete intended set without omitted rules.

  • Confirm unrelated scopes remain and resolve the intended event separately; inspect actual rows after any failure before reviewing a retry.

Errors and recovery

Organization admin cannot replace another user preferences

Captured local refusal · HTTP 401:

{
  "input": {
    "userId": "usr_self",
    "rows": [
      {
        "event_type": "campaigns.send_completed",
        "channel": "email",
        "enabled": true
      },
      {
        "event_type": "*",
        "channel": "push",
        "enabled": false
      }
    ]
  },
  "response": {
    "success": false,
    "error": {
      "message": "Access denied",
      "code": "AUTH_ERROR",
      "details": null,
      "timestamp": "2026-10-02T00:00:00.000Z"
    }
  }
}

Duplicate event/channel pairs are refused before deleting prior rows

Captured local refusal · HTTP 400:

{
  "input": {
    "userId": "usr_self",
    "rows": [
      {
        "event_type": "campaigns.send_completed",
        "channel": "email",
        "enabled": true
      },
      {
        "event_type": "campaigns.send_completed",
        "channel": "email",
        "enabled": true
      }
    ]
  },
  "response": {
    "success": false,
    "error": {
      "message": "Invalid preferences payload",
      "code": "VALIDATION_ERROR",
      "details": [
        "rows[1] duplicates (event_type=campaigns.send_completed, channel=email)"
      ],
      "timestamp": "2026-10-02T00:00:00.000Z"
    }
  }
}

Sequential replacement can fail after deleting old rows and writing only its first new row

Captured local refusal · HTTP 500:

{
  "input": {
    "userId": "usr_self",
    "rows": [
      {
        "event_type": "campaigns.send_completed",
        "channel": "email",
        "enabled": true
      },
      {
        "event_type": "*",
        "channel": "push",
        "enabled": false
      }
    ]
  },
  "response": {
    "success": false,
    "error": {
      "message": "Failed to replace notification preferences: Isolated second preference insert failure",
      "code": "SERVER_ERROR",
      "details": null,
      "timestamp": "2026-10-02T00:00:00.000Z"
    }
  }
}

Published recovery guidance

  • state_conflict (HTTP 409): Read the current resource state, reconcile the intended change, and retry only if still valid.

  • AUTH_ERROR (HTTP 401): Verify pinned identity. If authenticated, respect self/platform user authority or exact organization management denial; organization ownership alone does not authorize another user address/preferences. Do not assume OAuth expiry or change context automatically.

  • VALIDATION_ERROR (HTTP 400): Correct the reported fields only. Use unique event/channel pairs and valid channel-specific addresses. Validate and review the exact complete replacement again before approval.

  • NOT_FOUND (HTTP 404): Refresh the target user channels list. A row for another user is not authorized by a known channel id; repeated delete is already absent, not a new successful deletion.

  • SERVER_ERROR (HTTP 500): Preserve the result and read the exact stored scope before any new write. A replacement may have deleted old rules and inserted only some new ones; review finishing the intended set or restoring the captured set as a new approved replacement.

Recovery boundaries

Capture the complete previous scope before approval. Read actual rows after an uncertain outcome; review replacing with the intended or captured complete set as a separate approved mutation. This does not restore prior delivery outcomes or queued messages and may itself fail partially.

Discovery and evidence

This record belongs to app_topolo_auth. Fixtures prove actual NotificationPreferencesController and AuthController authority, validation, canonical notification SQL, duplicate/address identity and sequential replacement failure boundaries. Authenticated identities and unused targets/addresses are simulated and second-insert SQL failures injected. No live storage, ownership challenge, notification, deployment or production publication is proved.