{
  "topic": "messages",
  "path": [
    "messages"
  ],
  "title": "messages — edge message store API",
  "synopsis": "An edge message is an arbitrary JSON payload stored under a server-generated time-UUID together with a fixed set of AMQP-aligned headers and an optional flat metadata map. The store is standalone: a message is not an entity, and creating one does not touch the workflow engine — no transition fires, no processor or criterion runs. Edge messaging is a durable, tenant-scoped staging buffer at the platform edge, a peer of the entity store rather than part of it.",
  "body": "# messages\n\n## NAME\n\nmessages — edge message create, read, and delete REST API.\n\n## SYNOPSIS\n\n```\nPOST   /api/message/new/{subject}\nGET    /api/message/{messageId}\nDELETE /api/message/{messageId}\nDELETE /api/message\n```\n\nContext path prefix is `CYODA_CONTEXT_PATH` (default `/api`). All endpoints require `Authorization: Bearer <token>` except when `CYODA_IAM_MODE=mock`. No role is required beyond a valid token.\n\n## DESCRIPTION\n\nAn edge message is an arbitrary JSON payload stored under a server-generated time-UUID together with a fixed set of AMQP-aligned headers and an optional flat metadata map. The store is standalone: a message is not an entity, and creating one does not touch the workflow engine — no transition fires, no processor or criterion runs. Edge messaging is a durable, tenant-scoped staging buffer at the platform edge, a peer of the entity store rather than part of it.\n\nThe surface is HTTP-only; there is no gRPC message service.\n\nMessages are keyed by `(tenant, messageId)`. The tenant is derived from the authenticated caller, never from a request parameter, so a message ID only resolves within the tenant that created it (see TENANT ISOLATION).\n\nBody size limit on all write endpoints: 10 MiB.\n\n## MESSAGE SHAPE\n\nThe create request and the read response are different shapes: you POST a payload plus metadata, and you GET back the same payload and metadata wrapped alongside the stored header. The metadata values round-trip unchanged — a flat map in, the same flat map out.\n\n**Create request** — a single JSON object:\n\n- `payload` (required): any valid JSON value — object, array, string, number, boolean, or `null`. It is stored verbatim (whitespace is compacted). `Content-Type` is informational only; the store does not parse the payload against it. Non-JSON content must be stringified by the client (for example base64) and sent as a JSON string.\n- `metaData` (optional): a flat map of string keys to JSON values, stored alongside the message and returned verbatim on read. There is no message-query endpoint — messages are retrieved by ID, not searched by metadata.\n\n**Read response** (`EdgeMessageDto`) — three top-level fields:\n\n- `header`: the stored `EdgeMessageHeader` (below); empty optional fields are omitted.\n- `metaData`: the metadata map, returned flat and unchanged — the same `metaData` supplied at creation.\n- `content`: the payload, echoed back verbatim as it was stored.\n\n**Header** (`EdgeMessageHeader`) — AMQP-aligned:\n\n- `subject` (string): taken from the `{subject}` path segment on create.\n- `contentType` (string): from the `Content-Type` request header; informational.\n- `contentLength` (int64): from the `Content-Length` request header.\n- `contentEncoding` (string): from the `Content-Encoding` request header (default `UTF-8`).\n- `messageId`, `userId`, `recipient`, `replyTo`, `correlationId` (all optional strings): from the `X-Message-ID`, `X-User-ID`, `X-Recipient`, `X-Reply-To`, `X-Correlation-ID` request headers respectively. These are caller-supplied envelope fields and are distinct from the server-generated store ID in the URL.\n\n## ENDPOINTS\n\n**POST /api/message/new/{subject}** — Create and store a message\n\n- `subject` (path): string matching `^[a-zA-Z0-9._-]{1,256}$`\n- `Content-Type` (header, required): MIME type of the payload (informational)\n- `Content-Length` (header, required): payload size in bytes\n- `Content-Encoding` (header, optional): default `UTF-8`\n- `X-Message-ID`, `X-User-ID`, `X-Recipient`, `X-Reply-To`, `X-Correlation-ID` (headers, optional): AMQP envelope fields, each 1..1024 chars\n- `transactionTimeoutMillis` (query, optional): int64, default `10000` — accepted for Cyoda Cloud parity; parsed but currently has no behavioural effect in cyoda-go.\n\nRequest body: a `NewMessageRequest` object `{ payload, metaData }` as described in MESSAGE SHAPE. Missing `payload` returns `400 BAD_REQUEST`.\n\nThe server generates a time-UUID for the message and a separate time-UUID for the transaction. Response: `200 OK`, `application/json`, an `EntityTransactionResponse` array with a single element:\n\n```json\n[{\n  \"entityIds\": [\"8824c480-c166-11ee-bf9f-ae468cd3ed16\"],\n  \"transactionId\": \"9f3a0000-c166-11ee-bf9f-ae468cd3ed16\"\n}]\n```\n\n**GET /api/message/{messageId}** — Retrieve a message by ID\n\n- `messageId` (path): UUID\n\nResponse: `200 OK`, `application/json`, an `EdgeMessageDto`:\n\n```json\n{\n  \"header\": {\n    \"subject\": \"nobel.prize.events\",\n    \"contentType\": \"application/json\",\n    \"contentLength\": 1024,\n    \"contentEncoding\": \"UTF-8\",\n    \"messageId\": \"msg-nobel-2024-physics\",\n    \"userId\": \"nobel-committee\",\n    \"recipient\": \"scientific-community\",\n    \"replyTo\": \"announcements@nobelprize.org\",\n    \"correlationId\": \"nobel-2024-physics-announcement\"\n  },\n  \"metaData\": {\n    \"eventType\": \"nobel.prize.announced\",\n    \"timestamp\": \"2024-10-09T12:00:00Z\"\n  },\n  \"content\": { \"category\": \"physics\", \"year\": \"2024\" }\n}\n```\n\nA malformed `messageId` returns `400 BAD_REQUEST`; a well-formed UUID that does not exist for the calling tenant returns `404 ENTITY_NOT_FOUND`.\n\n**DELETE /api/message/{messageId}** — Delete a single message\n\n- `messageId` (path): UUID\n\nDeletes the message and its payload blob. Returns `404 ENTITY_NOT_FOUND` if the ID does not exist for the tenant. Response: `200 OK`, `application/json`, a `MessageDeleteResponse`:\n\n```json\n{ \"entityIds\": [\"8824c480-c166-11ee-bf9f-ae468cd3ed16\"] }\n```\n\nMessage delete is not transactional in the same sense as entity create/update.\n\n**DELETE /api/message** — Delete multiple messages by ID\n\n- `transactionSize` (query, optional): int32, default `1000` — accepted for Cyoda Cloud parity; parsed but currently has no behavioural effect in cyoda-go.\n\nRequest body: a JSON array of UUID strings. Every element must parse as a UUID; otherwise `400 BAD_REQUEST`. Response: `200 OK`, `application/json`, a `MessageDeleteBatchResponse` array:\n\n```json\n[{\n  \"entityIds\": [\n    \"8824c480-c166-11ee-9cc7-ae468cd3ed16\",\n    \"31134900-d9cb-11ee-9cc7-ae468cd3ed16\"\n  ],\n  \"success\": true\n}]\n```\n\n## TENANT ISOLATION\n\nEvery operation is scoped to the authenticated caller's tenant. The tenant is resolved from the user context, and each backend keys and filters messages by `(tenant, messageId)`:\n\n- postgres / sqlite — the primary key is `(tenant_id, message_id)` and every query filters on `tenant_id`.\n- memory — metadata is held per tenant and each payload blob lives under a per-tenant directory.\n\nA message ID is an unguessable time-UUID and is only ever retrievable or deletable by the tenant that created it. A cross-tenant read or delete resolves to `404 ENTITY_NOT_FOUND`, not a leak.\n\n## ERRORS\n\nErrors are RFC 9457 `application/problem+json` with `properties.errorCode` set to the machine-readable code:\n\n- `errors.BAD_REQUEST` — `400` — invalid JSON, missing `payload`, malformed `messageId`, or a delete body that is not a JSON array of UUID strings\n- `errors.ENTITY_NOT_FOUND` — `404` — no message with this ID exists for the calling tenant (get and single-delete)\n- `413` — request body exceeds the 10 MiB limit (create and batch-delete)\n- `401` — missing or invalid Bearer token\n- `403` — authenticated but not authorized\n- `500` — internal error; generic message plus a ticket UUID, full detail logged server-side\n\n## EXAMPLES\n\n**Create a message:**\n\n```\ncurl -s -X POST \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-Correlation-ID: nobel-2024-physics\" \\\n  -d '{\n    \"metaData\": { \"eventType\": \"nobel.prize.announced\" },\n    \"payload\":   { \"category\": \"physics\", \"year\": \"2024\" }\n  }' \\\n  \"http://localhost:8080/api/message/new/nobel.prize.events\"\n```\n\n**Retrieve a message:**\n\n```\ncurl -s -H \"Authorization: Bearer $TOKEN\" \\\n  \"http://localhost:8080/api/message/8824c480-c166-11ee-bf9f-ae468cd3ed16\"\n```\n\n**Delete a single message:**\n\n```\ncurl -s -X DELETE \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  \"http://localhost:8080/api/message/8824c480-c166-11ee-bf9f-ae468cd3ed16\"\n```\n\n**Delete several messages by ID:**\n\n```\ncurl -s -X DELETE \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '[\"8824c480-c166-11ee-9cc7-ae468cd3ed16\",\"31134900-d9cb-11ee-9cc7-ae468cd3ed16\"]' \\\n  \"http://localhost:8080/api/message\"\n```\n\n## SEE ALSO\n\n- crud\n- cloudevents\n- openapi\n- errors.ENTITY_NOT_FOUND\n- errors.BAD_REQUEST\n",
  "sections": [
    {
      "name": "NAME",
      "body": "messages — edge message create, read, and delete REST API."
    },
    {
      "name": "SYNOPSIS",
      "body": "```\nPOST   /api/message/new/{subject}\nGET    /api/message/{messageId}\nDELETE /api/message/{messageId}\nDELETE /api/message\n```\n\nContext path prefix is `CYODA_CONTEXT_PATH` (default `/api`). All endpoints require `Authorization: Bearer <token>` except when `CYODA_IAM_MODE=mock`. No role is required beyond a valid token."
    },
    {
      "name": "DESCRIPTION",
      "body": "An edge message is an arbitrary JSON payload stored under a server-generated time-UUID together with a fixed set of AMQP-aligned headers and an optional flat metadata map. The store is standalone: a message is not an entity, and creating one does not touch the workflow engine — no transition fires, no processor or criterion runs. Edge messaging is a durable, tenant-scoped staging buffer at the platform edge, a peer of the entity store rather than part of it.\n\nThe surface is HTTP-only; there is no gRPC message service.\n\nMessages are keyed by `(tenant, messageId)`. The tenant is derived from the authenticated caller, never from a request parameter, so a message ID only resolves within the tenant that created it (see TENANT ISOLATION).\n\nBody size limit on all write endpoints: 10 MiB."
    },
    {
      "name": "MESSAGE SHAPE",
      "body": "The create request and the read response are different shapes: you POST a payload plus metadata, and you GET back the same payload and metadata wrapped alongside the stored header. The metadata values round-trip unchanged — a flat map in, the same flat map out.\n\n**Create request** — a single JSON object:\n\n- `payload` (required): any valid JSON value — object, array, string, number, boolean, or `null`. It is stored verbatim (whitespace is compacted). `Content-Type` is informational only; the store does not parse the payload against it. Non-JSON content must be stringified by the client (for example base64) and sent as a JSON string.\n- `metaData` (optional): a flat map of string keys to JSON values, stored alongside the message and returned verbatim on read. There is no message-query endpoint — messages are retrieved by ID, not searched by metadata.\n\n**Read response** (`EdgeMessageDto`) — three top-level fields:\n\n- `header`: the stored `EdgeMessageHeader` (below); empty optional fields are omitted.\n- `metaData`: the metadata map, returned flat and unchanged — the same `metaData` supplied at creation.\n- `content`: the payload, echoed back verbatim as it was stored.\n\n**Header** (`EdgeMessageHeader`) — AMQP-aligned:\n\n- `subject` (string): taken from the `{subject}` path segment on create.\n- `contentType` (string): from the `Content-Type` request header; informational.\n- `contentLength` (int64): from the `Content-Length` request header.\n- `contentEncoding` (string): from the `Content-Encoding` request header (default `UTF-8`).\n- `messageId`, `userId`, `recipient`, `replyTo`, `correlationId` (all optional strings): from the `X-Message-ID`, `X-User-ID`, `X-Recipient`, `X-Reply-To`, `X-Correlation-ID` request headers respectively. These are caller-supplied envelope fields and are distinct from the server-generated store ID in the URL."
    },
    {
      "name": "ENDPOINTS",
      "body": "**POST /api/message/new/{subject}** — Create and store a message\n\n- `subject` (path): string matching `^[a-zA-Z0-9._-]{1,256}$`\n- `Content-Type` (header, required): MIME type of the payload (informational)\n- `Content-Length` (header, required): payload size in bytes\n- `Content-Encoding` (header, optional): default `UTF-8`\n- `X-Message-ID`, `X-User-ID`, `X-Recipient`, `X-Reply-To`, `X-Correlation-ID` (headers, optional): AMQP envelope fields, each 1..1024 chars\n- `transactionTimeoutMillis` (query, optional): int64, default `10000` — accepted for Cyoda Cloud parity; parsed but currently has no behavioural effect in cyoda-go.\n\nRequest body: a `NewMessageRequest` object `{ payload, metaData }` as described in MESSAGE SHAPE. Missing `payload` returns `400 BAD_REQUEST`.\n\nThe server generates a time-UUID for the message and a separate time-UUID for the transaction. Response: `200 OK`, `application/json`, an `EntityTransactionResponse` array with a single element:\n\n```json\n[{\n  \"entityIds\": [\"8824c480-c166-11ee-bf9f-ae468cd3ed16\"],\n  \"transactionId\": \"9f3a0000-c166-11ee-bf9f-ae468cd3ed16\"\n}]\n```\n\n**GET /api/message/{messageId}** — Retrieve a message by ID\n\n- `messageId` (path): UUID\n\nResponse: `200 OK`, `application/json`, an `EdgeMessageDto`:\n\n```json\n{\n  \"header\": {\n    \"subject\": \"nobel.prize.events\",\n    \"contentType\": \"application/json\",\n    \"contentLength\": 1024,\n    \"contentEncoding\": \"UTF-8\",\n    \"messageId\": \"msg-nobel-2024-physics\",\n    \"userId\": \"nobel-committee\",\n    \"recipient\": \"scientific-community\",\n    \"replyTo\": \"announcements@nobelprize.org\",\n    \"correlationId\": \"nobel-2024-physics-announcement\"\n  },\n  \"metaData\": {\n    \"eventType\": \"nobel.prize.announced\",\n    \"timestamp\": \"2024-10-09T12:00:00Z\"\n  },\n  \"content\": { \"category\": \"physics\", \"year\": \"2024\" }\n}\n```\n\nA malformed `messageId` returns `400 BAD_REQUEST`; a well-formed UUID that does not exist for the calling tenant returns `404 ENTITY_NOT_FOUND`.\n\n**DELETE /api/message/{messageId}** — Delete a single message\n\n- `messageId` (path): UUID\n\nDeletes the message and its payload blob. Returns `404 ENTITY_NOT_FOUND` if the ID does not exist for the tenant. Response: `200 OK`, `application/json`, a `MessageDeleteResponse`:\n\n```json\n{ \"entityIds\": [\"8824c480-c166-11ee-bf9f-ae468cd3ed16\"] }\n```\n\nMessage delete is not transactional in the same sense as entity create/update.\n\n**DELETE /api/message** — Delete multiple messages by ID\n\n- `transactionSize` (query, optional): int32, default `1000` — accepted for Cyoda Cloud parity; parsed but currently has no behavioural effect in cyoda-go.\n\nRequest body: a JSON array of UUID strings. Every element must parse as a UUID; otherwise `400 BAD_REQUEST`. Response: `200 OK`, `application/json`, a `MessageDeleteBatchResponse` array:\n\n```json\n[{\n  \"entityIds\": [\n    \"8824c480-c166-11ee-9cc7-ae468cd3ed16\",\n    \"31134900-d9cb-11ee-9cc7-ae468cd3ed16\"\n  ],\n  \"success\": true\n}]\n```"
    },
    {
      "name": "TENANT ISOLATION",
      "body": "Every operation is scoped to the authenticated caller's tenant. The tenant is resolved from the user context, and each backend keys and filters messages by `(tenant, messageId)`:\n\n- postgres / sqlite — the primary key is `(tenant_id, message_id)` and every query filters on `tenant_id`.\n- memory — metadata is held per tenant and each payload blob lives under a per-tenant directory.\n\nA message ID is an unguessable time-UUID and is only ever retrievable or deletable by the tenant that created it. A cross-tenant read or delete resolves to `404 ENTITY_NOT_FOUND`, not a leak."
    },
    {
      "name": "ERRORS",
      "body": "Errors are RFC 9457 `application/problem+json` with `properties.errorCode` set to the machine-readable code:\n\n- `errors.BAD_REQUEST` — `400` — invalid JSON, missing `payload`, malformed `messageId`, or a delete body that is not a JSON array of UUID strings\n- `errors.ENTITY_NOT_FOUND` — `404` — no message with this ID exists for the calling tenant (get and single-delete)\n- `413` — request body exceeds the 10 MiB limit (create and batch-delete)\n- `401` — missing or invalid Bearer token\n- `403` — authenticated but not authorized\n- `500` — internal error; generic message plus a ticket UUID, full detail logged server-side"
    },
    {
      "name": "EXAMPLES",
      "body": "**Create a message:**\n\n```\ncurl -s -X POST \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-Correlation-ID: nobel-2024-physics\" \\\n  -d '{\n    \"metaData\": { \"eventType\": \"nobel.prize.announced\" },\n    \"payload\":   { \"category\": \"physics\", \"year\": \"2024\" }\n  }' \\\n  \"http://localhost:8080/api/message/new/nobel.prize.events\"\n```\n\n**Retrieve a message:**\n\n```\ncurl -s -H \"Authorization: Bearer $TOKEN\" \\\n  \"http://localhost:8080/api/message/8824c480-c166-11ee-bf9f-ae468cd3ed16\"\n```\n\n**Delete a single message:**\n\n```\ncurl -s -X DELETE \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  \"http://localhost:8080/api/message/8824c480-c166-11ee-bf9f-ae468cd3ed16\"\n```\n\n**Delete several messages by ID:**\n\n```\ncurl -s -X DELETE \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '[\"8824c480-c166-11ee-9cc7-ae468cd3ed16\",\"31134900-d9cb-11ee-9cc7-ae468cd3ed16\"]' \\\n  \"http://localhost:8080/api/message\"\n```"
    },
    {
      "name": "SEE ALSO",
      "body": "- crud\n- cloudevents\n- openapi\n- errors.ENTITY_NOT_FOUND\n- errors.BAD_REQUEST"
    }
  ],
  "see_also": [
    "crud",
    "cloudevents",
    "openapi",
    "errors.ENTITY_NOT_FOUND",
    "errors.BAD_REQUEST"
  ],
  "stability": "stable",
  "actions": []
}
