An interactive user registry for your Discord server.
Deployed via a Cloudflare Worker with HTTP interactions only — no gateway connections, no always-on process, scales to zero. Storage is handled by Cloudflare D1.
| Command | Who | What |
|---|---|---|
/aka whois <user> |
anyone | →@foobar is also known as Bryson |
/aka register <user> <name> |
admin | register a name |
/aka forget <user> |
admin | remove someone's entry |
/aka forgetme |
anyone | remove your own entry |
/aka list [page] |
anyone* | the whole roster |
/aka history <user> |
admin | audit trail for one member |
/aka about |
anyone | source URL and license |
* /aka list can be restricted to only server administrators — see Configuration.
Every reply is ephemeral — visible only to the member who ran the command, and never posted to a channel.
The Deploy to Cloudflare button above forks this repository, provisions the D1 database, prompts for the Worker secrets, and deploys. It cannot talk to Discord for you, so you still need step 1 below to create the application, and step 4 to point Discord at the deployed URL and register the command. The steps below are the manual equivalent.
- Go to discord.com/developers/applications → New Application.
- Bot → create a bot. Note the token.
- General Information → note the Application ID and Public Key.
- Installation → Guild Install, scopes
applications.commandsandbot.
No bot permissions are needed: the bot never reads messages or calls the REST API at runtime, it only answers interactions.
npm install
npx wrangler d1 create akiraPaste the printed database_id into wrangler.jsonc, then:
npm run db:migrate # remote
npm run db:migrate:local # local devnpx wrangler secret put DISCORD_PUBLIC_KEYNote
That is the Worker's only secret. scripts/register.ts is a local Node script rather than part of the Worker, so its variables live in a local .env and are never deployed.
Two example files, because the two sides need different things:
cp .dev.vars.example .dev.vars # the Worker, for `wrangler dev`
cp .env.example .env # scripts/register.tsBoth .dev.vars and .env are gitignored.
npm run deployCopy the deployed URL into Discord → your app → General Information → Interactions Endpoint URL and save. Discord immediately POSTs a signed PING plus a deliberately-invalid probe; the Worker must answer both correctly, which it will if DISCORD_PUBLIC_KEY is right.
Tip
A wrong public key shows up as "interactions endpoint could not be verified".
Then register the command:
npm run register:guild # one guild, appears instantly — use while developing
npm run register # global, can take up to an hour to propagate| Variable | Where | Purpose |
|---|---|---|
DISCORD_PUBLIC_KEY |
Worker secret | Ed25519 signature verification |
DISCORD_APPLICATION_ID |
local.env only |
Tellsscripts/register.ts which application to register against |
DISCORD_TOKEN |
local.env only |
Authorizesscripts/register.ts |
DISCORD_TEST_GUILD_ID |
local.env only |
Target guild fornpm run register:guild |
LIST_ADMIN_ONLY |
vars in wrangler.jsonc |
"true" restricts /aka list to administrators |
Admin subcommands require Discord's Administrator permission, checked at runtime against the member.permissions bitfield Discord computes and sends with each interaction. No role IDs to configure, and no REST lookup.
The check is runtime-only by necessity. default_member_permissions applies to a whole top-level command, so setting it on /aka would also hide whois from everyone. The visible cost: non-admins see register, forget and history in the command picker and get an ephemeral refusal if they try them. The gate itself is not bypassable — member.permissions comes from Discord inside the signed request body, not from the client.
Every /aka response — lookups, misses, registration confirmations, the roster, audit history, permission refusals, errors — carries the Ephemeral flag and is visible only to the member who ran the command. No information this bot holds is ever posted to a channel, so nothing it says can be screenshotted out of context, searched in history, or seen by someone who didn't ask.
This is also enforced structurally: src/respond.ts exports no non-ephemeral response builder, and test/interaction.test.ts asserts the flag across every subcommand, including the failure paths.
The user argument is a Discord USER option type, which resolves to exactly one real account — roles and @everyone aren't selectable, so there's no bulk registration path. Bots are rejected too.
A name like @everyone would otherwise turn every subsequent lookup into a server-wide ping. Three layers:
- Every outbound response sets
allowed_mentions: { parse: [] }. This is the one that actually works. sanitizeName()rejects mass-ping tokens and entity mentions (including ones split by zero-width characters), escapes Discord markdown, collapses whitespace, and caps visible length at 64.- The command definition sets
max_length, so Discord rejects oversized input client-side.
/aka register on someone who already has an entry fails and names the existing value. It's a single conditional INSERT ... WHERE NOT EXISTS, so two concurrent registrations can't both win.
aka_audit is append-only and survives both removal paths, including /aka forgetme — the retained name is only readable through /aka history, which is admin-gated. /aka forgetme does not blocklist; an admin can register the member again afterwards.
Every command is one or two D1 queries answered inline, well inside Discord's 3-second interaction window. No deferred responses, so no follow-up webhook and no bot token at runtime.
npm run typecheck # Worker config + Node config
npm test # 40 tests
npm run dev # wrangler devtest/interaction.test.ts drives the real Worker entrypoint end to end: it generates an Ed25519 keypair, signs actual interaction payloads, and runs the real SQL against an in-memory SQLite stand-in for D1 (test/d1-shim.ts). So signature verification, routing, permission gating, duplicate rejection and the audit trail are all covered without wrangler, network, or a Cloudflare account.
- Reverse lookup —
/aka find <name>. Theidx_aka_nameindex onname_normalizedis already there for it. - User context menu — right-click a member → Apps → "Who is this?". A second command of type
USER, roughly 20 lines, no typing required to look someone up. - Autocomplete on
/aka forget— currently a user picker; could suggest only members who actually have entries.
Copyright © 2026 Bryson Reece.
Licensed under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later). You may use, modify and redistribute this software, commercially included, provided derivative works are licensed under the same terms.
Important
Because Akira is a hosted network service, AGPL §13 applies: if you run a modified version and let others interact with it over a network — including as a Discord bot in a server you don't own — you must offer those users the corresponding source of your modified version. Running an unmodified copy carries no such obligation.