Files
messengerapi/README.md
2025-07-05 00:58:17 +02:00

8.1 KiB

messengerapi

"send me a tip" "see my github"

Lightweight, maintenance-free, stateless .NET REST API message broker for sending and receiving messages in deliver-once fashion. Trivial HTTP request/response consumption. Works with Sql or Npg.

Why

Existing messaging solutions are not atractive (to me). They are complex, hard to set up, understand and maintain. Even if happy-day scenarios work well, or day-one setups seem easy, basic functionality is often hidden under obscure layer of arguments, parameters, settings and limitations put in place for reasons not quickly apparent. I needed a messaging system that is really simple, but sufficiently reliable and performant. I want to turn this on and forget about it forever.

FOSS project for REST API over HTTP does not exist (or I didn't find it, ChatGPT concurs). So here is my attempt.

How

Messages and user credentials are kept in database. There are no usernames and passwords, only API keys. User can only message other users if corresponding route is set up between them. Manage users, routes and api keys directly in the DB. Clients authenticate and authorize their queries by using their API keys as Bearer tokens in Authorization headers permanently.

Containerization

MessengerApi is built to be run as a container. See image registry.

Auth & Security

Senders and receivers send Bearer tokens with their HTTP request header: Bearer ba53e34b-0163-40bc-9216-4ffa1fe3efb8. Users do not request their tokens, they are assigned tokens during registration and use them permanently. This is a design choice done on purpose to put least possible amount of requirements on clients. A valid route must exist in DB between sender and receiver. Receivers can't ack someone else's message, and can't ack a message before it's delivered through /receive call. Access leak and password reset is handled through api key rotation when necessary.

Setup

Mandatory tunables are super-simple:

  • SQL_CONNECTIONSTRING

    • Must be provided.
    • Example: "Persist Security Info=False;User ID=*****;Password=*****;Initial Catalog=AdventureWorks;Server=192.168.1.2"
  • CORS_ORIGINS

    • Must be provided, if you're consuming the API from browser. Comma separated.
    • Example: www.mydomain.com,mydomain.com,anotherdomain.eu

PostgreSQL

To run MessengerApi against postgres, omit the SQL_CONNECTIONSTRING and use this:

  • PERSISTENCE_TYPE: PostgreSql
    • This tells Messenger to use PostgreSql DB Context.
  • NPG_CONNECTIONSTRING
    • Must be provided.
    • Example: "Host=localhost;Port=5432;Database=myDatabase;Username=postgres;Password=********

Additional tunables, with their sustainable default values:

  • QUERY_RATE_PER_MINUTE: 100
    • Sets maximum allowed client query rate per minute for all endpoints. Anonymous users share same limit pool.
    • If send rate is exceeded, client receives a HTTP 429 with explanation.
  • DEFAULT_MESSAGE_LIFETIME_IN_MINUTES: 1
    • Message will wait for delivery for set amount of time. After the time passes, a call to /receive will not consider it for delivery anymore.
    • Override this in message content by setting optional lifespanInSeconds value inside the request.
    • There will be no indication to the sender or to client that there was a missed message. Once it's gone, it's gone.
  • HOUSEKEEPING_ENABLED: true
    • Turns on housekeeping job that periodically removes stale, delivered and/or acknowledged messages. You can tune this further, see below. By default, it only removes messages that are 2 hours old, regardless of their delivery or acknowledgement state.
  • HOUSEKEEPING_MESSAGE_AGE_IN_MINUTES: 120
    • Housekeeping job will delete any message older than set amount of time, regardless of it's current state.
  • HOUSEKEEPING_MESSAGE_STATE: NONE
    • Allowed values: NONE, DELIVERED, ACKNOWLEDGED.
    • NONE: Housekeeping will not delete messages based on their state.
    • DELIVERED: Housekeeping will delete messages that have been delivered.
    • ACKNOWLEDGED: Housekeeping will delete messages that have been acknowledged.
    • Works in addition to HOUSEKEEPING_MESSAGE_AGE_IN_MINUTES, so messages can be removed earlier than after their age expires.

User management

Manage users and credentials in DB manually.

Alternatively, mount a config to the container's root dir with name of users.conf with structure that contains username;is_enabled;id;apikey;comma_separated_allowed_recipients_if_any per line:

user1;true;90ddab90-0b73-4c6c-8dcb-2d8d1ec3c0b8;81ccf737-d424-4f83-929c-92d20491abfa;user2,user3,user4
user2;true;8f5971c3-5e19-4b5c-88a7-e0ec4856ce44;f480568f-8884-47e5-a6d7-82480f1ffb3b;user1
user3;true;f253a157-f336-4029-b90e-80a9f64b453b;46b882b7-4b96-4fa2-ba1b-4955a9500c36
user4;true;5f20ec92-3168-4df5-b20d-5441d08b3f9a;51d11e51-efb2-43e9-beb8-52fb8e879bee;user2

Upon launch, Messenger will synchronize contents of the file with the database. Synchronization uses id as primary identifier to make it easy to rotate API keys and change names. Synchronization is done users.conf => db and treats the config file as single source of truth, meaning data present in the file but not in db will be added to db, and data not present in file, but present in db will be deleted from db. Editing the file and restarting the service will then update the data accordingly.

Integration

Tunnel on port 80 to send traffic. Consume HTTP endpoints:

POST /send

Sends message and returns it's ID. Minimal message body where user2 sends a text message to user1, and since user2 can only message to user1 and nobody else, they don't even have to specify recipient, the system infers it automatically.

Request:

http /send (post) header: Authorization: Bearer f480568f-8884-47e5-a6d7-82480f1ffb3b
{
  "payload": "This is a message."
}

Response:

http (json):
"5f33b4bd-dc2a-4ace-947a-1aadc6045995"

Optionally, messages can be complex. Here is a message from user1 to user3, both payload and payloadType are nvarchar fields and their content can be whatever.

Request:

http /send (post) header: Authorization: Bearer 81ccf737-d424-4f83-929c-92d20491abfa
{
    "payloadType": "STATUS",
    "payload": "{\n  \"system\": \"OK\",\n}",
    "toUserId": "46b882b7-4b96-4fa2-ba1b-4955a9500c36",
    "lifespanInSeconds": "3600"
}

Response:

http (json):
"5f33b4bd-dc2a-4ace-947a-1aadc6045995"

GET /receive

Receives all waiting messages.

Request:

http /receive (get) header: Authorization: Bearer 81ccf737-d424-4f83-929c-92d20491abfa

Response:

http (json):
"messages": [
    {
        "id": "5f33b4bd-dc2a-4ace-947a-1aadc6045995",
        "timestampUtc": "2025-07-04T16:32:12.4586339Z",
        "payload": "{\n  \"system\": \"OK\",\n}",
        "payloadType": "STATUS",
        "sender": "7bdea193-dce5-486e-ba7b-ec323d22bf90"
    }
]

POST /ack

Acknowledges delivered message. The client that received the message is the only party able to acknowledge.

Request:

http /ack (post) header: Authorization: Bearer 81ccf737-d424-4f83-929c-92d20491abfa
{
    "messageId": "5f33b4bd-dc2a-4ace-947a-1aadc6045995"
}

Response:

http (json) 200 OK:
(empty)

GET /verify

Sender can call this to get delivery status of message that has been sent earlier:

Request:

http /verify?messageId=5f33b4bd-dc2a-4ace-947a-1aadc6045995 (get) header: Authorization: Bearer 81ccf737-d424-4f83-929c-92d20491abfa

Response:

http (json) 200 OK:
{
    "isDelivered": true,
    "isAcknowledged": true
}

Testing

See postman collection to generate code for your desired language or test the API instance. The collection also works as test suite, just fill out the environment variables for URL and api keys of your users and it will run and test all endpoints, with expected outputs.