Skip to content
Allay

WikiAllay Server

Security and privacy

Allay keeps what players ask, what it remembers about them, and their game state, so the backend treats every request as a stranger until it says who it is. This page covers who may call what, how servers stay apart, what the backend keeps and for how long, and how a player gets a copy of their data or has it erased.

#Every request says who sends it

Since 6.0, the backend answers a plugin only when the request carries Authorization: Bearer …:

  • On a hosted backend (billing on, like allay.chat) that's the server's license. A server that brings its own key gets a license free from POST /api/v2/license; Allay 6.0 does that by itself. A license identifies the server, and paying for it is what turns on the backend's own key.
  • On a self-hosted backend (SELFHOSTED=1) that's PLUGIN_TOKEN, a secret of at least 24 characters you set in the backend's environment and in the plugin's server.token. Without it the backend answers no plugin at all, rather than everyone who can reach it.

Only three things stay open: GET /api/health, POST /api/v2/license (a new license, limited per address), and Telegram's webhook, which has its own secret. The admin panel wants a session; its sign-in locks after failed tries. /allay init wants the token too, since its recovery link resets any admin's password.

#Servers never meet

The backend files every player under a tenant: license:<id> on a hosted backend, self on a self-hosted one. Memories, sessions, agents' threads, task outcomes, the public thread, the statistics, and the rate limits all carry it. A server names any player id it likes, but only ever reaches its own players: the same Minecraft account on two servers is two players. A tool result counts only from the server whose run asked for it.

#What the backend keeps

What Where How long
The session: questions, tool results, answers, and a summary Redis 24 hours after the last question (CHAT_TTL_SECONDS)
A server's public thread Redis 2 hours after the last public question
An automation agent's thread, and how its tasks ended Redis 2 hours after its last wake; outcomes until the next question
Memories: up to 30 short facts MongoDB Until the player forgets them, or 365 days after their last question
A player's consent: which notice they agreed to, and when MongoDB Until the player withdraws it or is erased
A block: the server, the pseudonym, who blocked, and when MongoDB Until an admin unblocks the player, even past an erasure
Runs: numbers per question, the player as a pseudonym MongoDB 400 days
The player directory: a pseudonym's name, UUID, and operator status per server, from the last question MongoDB 400 days after the last question
Days: sums of runs, with the day's pseudonyms MongoDB Sums forever; who played, 400 days
Traces: prompts, answers, tool calls MongoDB TRACE_DAYS (7)
A server: the address it connects from, the address players join with, the game and plugin versions MongoDB 400 days after its last question
The audit log: who changed what in the panel, from which address MongoDB 365 days

Runs never hold a player's id or name. Each player shows up there as a pseudonym, p_ and 20 hex digits: an HMAC of their id under a key derived from SECRETS_KEY. Since 6.4 a separate player directory maps each pseudonym on a server to the name, UUID, and operator status of the player's last question, and the panel shows those to every role. The panel's operators run the service and need to tell players apart, for support and for erasure on request (GDPR art. 6(1)(f)). A rename shows everywhere at once, and an erasure takes the name off every run. A player who turns on full privacy in the plugin (since 6.5 and Allay 6.3) stays out of the directory: the panel forgets their name and UUID at once and shows them by pseudonym, with Steve's head. Memories, the session, and traces work as before. The panel shows players' faces, which the backend fetches from mc-heads.net by UUID (or by name on offline-mode servers) and caches for 6 hours, so viewers' browsers never call a third party.

The backend's log carries run ids and tenants. It never holds a player's name, id, question, or answer, nor a key, a license, or a token. The one code it prints is a hosted backend's first setup code.

Since 6.1 the plugin (Allay 6.1) shows each player a notice before their first question and asks them to agree. The backend keeps the answer per tenant and player: the notice's version and when they agreed, as proof under GDPR art. 7(1). GET, PUT, and DELETE /api/v2/users/{id}/consent read, record, and withdraw it; a player withdraws with /allay privacy withdraw. A copy of the player's data shows it, an erasure removes it, and migrate retag moves it with the memories.

#A player's copy and erasure

Under the GDPR a player may ask what a server keeps about them and have it erased.

  • In the game, an operator runs /allay privacy export <player> for a copy and /allay privacy erase <player> to erase it; see the plugin's wiki.
  • Through the API, GET /api/v2/users/{id} returns everything about the player of the calling server, and DELETE erases it.
  • In the panel, an admin erases a player from their page, or under Erase a player on the Players page by server and a player picked by name, or a pseudonym or UUID.

Erasure deletes the player's name in the directory, memories, consent, session, pending outcomes, agents' threads, and the traces of their runs. Their runs keep only numbers, under erased, so the statistics still add up and point at nobody. The server's public thread holds other players' talk too; it expires within 2 hours by itself. The audit log keeps that an erasure happened, under the pseudonym. A block stays: it holds only the pseudonym, and lifting it with an erasure would let a blocked player back in.

#Behind a proxy

The backend sees a proxy's address, not the player's. Set TRUSTED_PROXIES to your proxies' addresses or CIDR ranges, such as 172.18.0.0/16 for a Docker network, and CLIENT_IP_HEADER to the header they set. Forwarding headers from anyone else don't count, and X-Forwarded-For is read from the right. Without TRUSTED_PROXIES, everyone behind a proxy shares one address: one shared limit for new licenses and sign-ins.

#Moving a self-hosted backend to a hosted one

A self-hosted backend's data belongs to the tenant self. To carry it over to a license on a hosted backend:

  1. Dump the self-hosted MongoDB and restore the collections you want into the hosted one: memories, runs, run_days, and, when the hosted backend is the same operator's, settings, admin_accounts, admin_audit, and Mastra's mastra_ai_spans. Redis sessions don't move; they expire within a day anyway.

  2. Inside the hosted backend's container, check and move:

    bun run dist/application/migrate.js status
    bun run dist/application/migrate.js retag --from self --to license:<id> --dry-run
    bun run dist/application/migrate.js retag --from self --to license:<id>

retag moves memories (also those from before 6.0, which have no tenant) and runs to the license, and turns players' ids in the statistics into pseudonyms under the hosted backend's key. It does nothing a second time. Settings sealed with the old SECRETS_KEY open only with the same key; runs pseudonymised by a 6.0 self-hosted backend keep its pseudonyms, so give both backends the same SECRETS_KEY, or dump from 5.x, to keep players' statistics continuous. On a hosted backend, memories without a tenant reach nobody: the log warns about them on start, and purge-untagged deletes them.

#Naming players from before 6.4

The player directory fills with each question since 6.4, so a player who hasn't asked since shows as a pseudonym. When you know who they are, give their names and UUIDs (with or without dashes; for online-mode servers Mojang's https://api.mojang.com/users/profiles/minecraft/<name> gives a UUID, and the server's usercache.json lists everyone who joined) inside the backend's container:

bun run dist/application/migrate.js players Steve=8667ba71b85a4004af54457a9734eed7 Alex=ec561538f3fd461daff5086b22154bce

It prints each player's pseudonym and the servers whose runs had them. The runs give the first and last question; a UUID without runs changes nothing, and a name a newer question brought stays.

Next: What Allay can do