Skip to content

Repository files navigation

Akira

明 — bright, clear.

Deploy to Cloudflare


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.

Setup

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.

1. Create the Discord application

  1. Go to discord.com/developers/applications → New Application.
  2. Bot → create a bot. Note the token.
  3. General Information → note the Application ID and Public Key.
  4. Installation → Guild Install, scopes applications.commands and bot.

No bot permissions are needed: the bot never reads messages or calls the REST API at runtime, it only answers interactions.

2. Create the database

npm install
npx wrangler d1 create akira

Paste the printed database_id into wrangler.jsonc, then:

npm run db:migrate          # remote
npm run db:migrate:local    # local dev

3. Set secrets

npx wrangler secret put DISCORD_PUBLIC_KEY

Note

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.ts

Both .dev.vars and .env are gitignored.

4. Deploy and register

npm run deploy

Copy 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

Configuration

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

Design notes

Permissions

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.

Everything is ephemeral

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.

Mass targeting

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.

Mention injection

A name like @everyone would otherwise turn every subsequent lookup into a server-wide ping. Three layers:

  1. Every outbound response sets allowed_mentions: { parse: [] }. This is the one that actually works.
  2. 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.
  3. The command definition sets max_length, so Discord rejects oversized input client-side.

Duplicates

/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.

Audit trail

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.

Latency

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.

Development

npm run typecheck   # Worker config + Node config
npm test            # 40 tests
npm run dev         # wrangler dev

test/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.

Possible next steps

  • Reverse lookup — /aka find <name>. The idx_aka_name index on name_normalized is 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.

License

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.

About

An interactive user registry for your Discord server.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages