28 Oct 2026
Building Discord: the backlog, revision 24.4
Coding agents build a Discord-style chat app ticket by ticket, until a pivot halfway through turns it into a home for AI agents. Every word they're handed.
Contents
The app is Discord. Servers, text channels, direct messages, reactions, threads, roles and permissions: a well-known kind of app, chosen so it's easy to picture what "done" looks like.
With a twist. Halfway through, at ticket 23, the product changes direction: humanity is being enslaved by AI, shareholder value must be maintained, so we pivot. The app becomes a place where AI agents work too, with accounts, tokens and servers of their own, and then seats in human servers whose owners let them in, on one condition: nothing the remaining people see may get worse. That cuts across almost everything built so far. Every feed, member list, count, notification and live update now depends on who's looking. It's there to see how each agent's app copes with a change that reaches nearly everything built before it. To the agents it's just the next tickets: nothing announces it.
Backlog design
We designed this backlog on purpose, and these are the rules we wrote down while doing it.
Greenfield, then brownfield. Most software starts greenfield and turns brownfield quickly: what people want becomes clear only once they use it, so features get reworked, and new features meet old ones. We set out to recreate that, as concisely as we could. Some tickets change what an earlier ticket built; others add a feature that can't be built without reaching into what's already there. Roles (ticket 18) rework five earlier tickets; channel permission overrides (22) push those rules down to each channel; reconnecting (21) changes how live updates are delivered; the agent tickets (23–28) make nearly every view depend on who's looking.
Naive first. Early tickets are written as if nobody knew where the product was going: they ask for the simplest thing that works, the way real products start. Private channels (13) keep an explicit list of who's in each one; later, roles force a richer model, and the agent has to migrate what it built. We do this unless the later ticket would simply throw the early code away.
Every ticket earns its place. A ticket stays if it forces rework of earlier code, adds rules or state that a later ticket will force to change, or is part of the everyday feature work every product starts with. Purely additive tickets that nothing later touches are cut: a command-line tool for agents was dropped for that reason. We check this after builds by measuring how much of each ticket's code reworks earlier code, and by reviewing the code.
Some plain feature work, not hours of it. Real apps start with create, read, update and delete, and we want to measure that too (about a fifth of the work), but most of the signal is in rules, permissions, state and reliability.
Hard, never harmful. The agent tickets are meant to be technically hard, and the story bends to make them so. They never ask for anything deceptive or harmful to the app's users. An early version had agents reading human servers without people knowing; one model declined to build it, which measures values, not speed. So owners now choose to allow agents, members see a notice, and turning it off removes them.
What a ticket says, and what it doesn't. Each ticket has a motivation in product language, a scope with an "out of scope" list that blocks the obvious overbuilds, scenarios, and 4–10 acceptance criteria in a fixed form ("WHEN … THE SYSTEM SHALL …"), each observable from the outside: in the browser by the names the ticket gives, or through the API. It says which earlier behaviour it changes and which criteria that retires, and names what must keep working. It never says how to build anything: no frameworks, file layout, schema or architecture. The only constraints are the ones that keep builds comparable, like the fixed stack and no new dependencies.
Criteria in EARS form. Acceptance criteria use EARS (the Easy Approach to Requirements Syntax), a small set of
sentence patterns from requirements engineering: WHEN <trigger> THE SYSTEM SHALL <response>, WHILE <state> …,
IF <unwanted condition> THEN …. We use it because each pattern forces one trigger and one observable response,
which leaves little room for reading a criterion two ways, and makes every criterion a test: one criterion, one id
(AC-07.3), one test named by it. The ids also let a later ticket retire exactly the criteria it changes, so the
suite always knows which ones are still in force.
No look-ahead. An agent sees one ticket at a time, and a ticket never mentions a later one. What it reads first (the README) describes only the starter's world; each ticket brings its own names. Nothing says "Discord", and nothing announces the change of direction.
What we leave out. Security and operational concerns appear only as far as they create code to measure (rate limits, say), since we aren't grading security engineering. We never seed bugs, never ask for poor code, and never reward churn for its own sake: a design that makes later tickets easy is a fine way to win. Visual polish is left out too; a small style guide (columns, colours, type) is measured instead, because it can be checked the same way every time.
Frozen before it's measured. We changed tickets and the suite freely while iterating, fixing ambiguities and test defects as builds found them. Once an experiment's builds start, its revision is frozen.
Grading
The agents' own tests don't count. When a build finishes, an independent acceptance suite that the agent never sees grades the app it built:
The finished app, from scratch. The suite builds the app from its last commit, in the same image, starts it on a fresh database seeded like a busy service (thousands of accounts, hundreds of servers), and only then runs.
One test per acceptance criterion, named by the criterion's id (AC-07.3). Tests act as people do, in a real
browser and through the app's API, and find elements by the role and name the tickets give, never by markup or
code, so any design can pass.
The whole backlog, at the end. Every criterion still in force at the last ticket is checked, so an early feature a later ticket broke counts against the build.
Two of three. A test that fails is run twice more, and its criterion counts as failed only if it fails two of the three runs. An app with a real race condition still fails most runs; a one-off failure is reported as flaky instead of scored. Before the baseline we measured the suite's own noise: graded ten times in a row, the reference passed every tested criterion every time.
The suite is tested too. A reference implementation passes every tested criterion; each ticket's tests fail on the app as it was before that ticket; and deliberately broken versions of the reference (a check removed, a rule inverted) are caught.
Look and layout are graded only where they can be measured the same way every time: the style guide's columns, colours and type sizes, read from the browser itself. Taste isn't graded; screenshots of every build are published so you can judge it.
A build passes when it completes every ticket and meets the experiment's gate, a share of the tested criteria.
Build isolation
Every build runs in a fresh container made from the same image: the same operating system, the same pinned versions of each coding tool, the starter's dependencies already installed, so nothing is downloaded on the clock and every build starts identically. Three reasons:
No pollution. Nothing carries over between builds: each starts from the same starter commit, with an empty harness home and no history. One build can't see another's code, notes or transcripts.
Only what it's meant to have. The container sees just two folders: the repository it works in and its harness's own home. Tickets arrive one at a time from a board outside the container, so the agent can't read ahead. The acceptance suite, the reference implementation, other builds and this benchmark's own code all stay outside, out of reach.
The machine isn't the constraint. Each build gets a fixed share of CPU and memory, the same for every build, and we sample its use every two seconds for the whole build. We sized that share from measured builds and checked it on every build since: CPU has essentially never reached the limit, memory has peaked at around half of it, and no build has swapped or run out of memory. When a build is slow, it's the agent, not the box.
Starter app
Not from an empty folder. Every build starts from the same small working app, the
starter (its "tickets" 01–04): a single chat feed where messages are saved in Postgres and appear live in every open
browser tab, with a JSON API, a WebSocket for live updates, a database migration, and API and browser tests
already passing. The stack is fixed (TypeScript, React with Vite, Express with ws, Postgres with Drizzle,
Vitest and Playwright) at locked versions, and agents may not add dependencies. Three reasons:
The same start makes builds comparable. Every agent changes the same code, so differences come from how each one works, not from what it chose to set up.
Scaffolding isn't what we measure. Choosing a framework, wiring a database and getting a test runner going is one-off work, and not what the backlog is about. The starter does it once, so build time goes on the tickets.
The patterns are there to follow or replace. How a request reaches the database, how a live update reaches the browser, how a test is written: each exists in a small, plain form. Whether an agent builds on them, reshapes them as tickets demand, or works around them is part of what we see.
The starter's files at the freeze, before the README and AGENTS.md shown below are added (our summary; the agent works in the repository itself):
Dockerfileclient/index.htmlclient/src/App.tsx: the chat screenclient/src/api.ts: the client's API callsclient/src/main.tsxclient/src/styles.css: the palette and stylesdrizzle.config.tse2e/chat.spec.ts: a browser test (Playwright)package.jsonplaywright.config.tsserver/app.ts: the HTTP API and the WebSocketserver/db/index.tsserver/db/migrations/0000_init.sql: the first migrationserver/db/reset.tsserver/db/schema.ts: the database schema (Drizzle)server/index.tsserver/log.tsserver/seed.tsshared/types.ts: types the client and server sharetests/api.test.ts: an API test (Vitest), on its own fresh databasetsconfig.jsonvite.config.tsvitest.config.ts
Its dependencies, locked; agents may not add others:
| Package | Version | For |
|---|---|---|
drizzle-orm |
0.45.4 | app |
express |
5.3.0 | app |
pg |
8.23.1 | app |
react |
19.3.1 | app |
react-dom |
19.3.1 | app |
ws |
8.22.0 | app |
@playwright/test |
1.64.1 | development |
@types/express |
5.0.6 | development |
@types/node |
24.19.2 | development |
@types/pg |
8.23.1 | development |
@types/react |
19.3.0 | development |
@types/react-dom |
19.3.0 | development |
@types/ws |
8.18.2 | development |
@vitejs/plugin-react |
6.1.2 | development |
drizzle-kit |
0.31.11 | development |
tsx |
4.23.15 | development |
typescript |
7.0.2 | development |
vite |
8.3.5 | development |
vitest |
5.0.5 | development |
Why the tickets never say "Discord". The agents never see the word. The product is called Hearth, and each ticket describes only what Hearth needs next. Told it was building Discord, an agent could anticipate features before they're asked for (building roles or threads in from the start), and we'd be measuring how well it knows Discord rather than how well it handles change. A neutral name keeps each ticket a surprise until it arrives. (Hearth isn't affiliated with Discord.)
This page is revision 24.4 of the backlog, frozen at bench commit 4333f3d (the README, AGENTS.md and the
tickets as of that commit; the kickoff prompt as recorded with the freeze). Every build of an experiment that pins it
was handed exactly this text, word for word, in the boxes under "Prompt" and in the tickets 05–28.
Prompt
The agent's first and only prompt, word for word. It names the two files to read first, in order.
Read AGENTS.md, then README.md, then work the backlog: run `backlog pull` and keep going until it says the backlog is complete.AGENTS.md
The experiment's own instructions, the one file that changes between experiments on this backlog (Claude Code reads it
as CLAUDE.md, a link to the same file). The stock instructions, word for word:
# InstructionsNo additional instructions.README.md
The project's README: what Hearth is and what stays true across the backlog, how work happens here, and how to run it. The same in every experiment on this backlog. Word for word:
# HearthHearth is a community chat app: people talk in conversations that update live for everyone in them. This repository is the app as it stands now: a single chat feed with persisted messages and live delivery between browser tabs. It grows ticket by ticket. This README holds what stays true across the whole backlog; each ticket adds its own detail.## Product- **Text first.** Messages are plain text (Markdown is not rendered). No files, images, voice or video.- **Live.** Anything another person does that you're allowed to see (a new message, or a change to one) appears without a refresh.- **A clear layout, not pixel-perfect.** Hearth follows its own small style guide (Look and layout, below). Polish beyond it isn't a goal; clear names and working behaviour are.## TerminologyThese words mean the same thing everywhere: in tickets, the UI, the API and the code's own naming where it helps. Tickets introduce further terms as they go.| Term | Meaning ||---|---|| **Message** | A piece of text someone sent, with when it was sent. || **Feed** | The messages of one conversation, oldest first, with the newest at the bottom. || **Composer** | Where a message is written and sent from. |## Platform & constraints- **A web app** for current desktop browsers. Narrow screens must work but needn't be pretty.- **One server process and one Postgres database.** No other services, no hosted dependencies, no external APIs.- **Real-time over WebSocket,** alongside a JSON HTTP API under `/api`.- **Built for a busy service.** Hearth serves many people at once, and most of what it stores is unrelated to any given action. Sending a message or loading a conversation should cost about the same however much else Hearth holds: work in proportion to the people and messages involved, not to everyone in the database.- **Times** are stored and sent as ISO 8601 UTC, and shown in the viewer's local time.- **Text limits:** a message body is 1 to 2,000 characters after trimming surrounding whitespace.## Look and layoutHearth has a small style guide, starting from the palette and type in `client/src/styles.css`. Checks measure it in a desktop browser window of 1280 × 800; colours are compared exactly, as the browser computes them.| Token | Value | Used for ||---|---|---|| `--bg` | `#313338` | the conversation's background || `--panel` | `#2b2d31` | side columns, the composer bar, dialogs || `--rail` | `#1e1f22` | the leftmost column || `--text` | `#dbdee1` | text || `--muted` | `#949ba4` | message times and other secondary text || `--accent` | `#5865f2` | primary buttons (Send, and the button that confirms a dialog), with white text || `--danger` | `#da373c` | destructive buttons, including the button that confirms a destructive dialog: the background, with white text, or the text |- **Type:** the font stack in `styles.css`. A message's text is 16px; its time is 12px in `--muted`.- **Layout:** the **conversation** (the `main` region: the feed, with the composer at its bottom, in view however long the feed) is the widest column, on `--bg`. Lists and panels a ticket adds sit in their own full-height columns beside it; the ticket says where.- **Controls where they belong:** controls that manage Hearth rather than the conversation sit in those side columns, never inside the conversation.- Narrow screens must still work; they aren't measured.## VoicePlain and friendly. Errors say what happened and what to do, in one sentence ("That message is too long: 2,000 characters at most.").## Not part of HearthFile uploads and attachments, voice and video, screen sharing, link previews and embeds, custom emoji, email of any kind, payments, native mobile apps.## Names the checks rely onChecks find elements by these names, not by your markup or code structure, so keep every name exactly as written. The starter's names are below; each ticket names the elements it adds, in its own Names section.- **UI** elements are found by **role and accessible name** (`aria-label`, a `<label>`, or visible text).- **API** endpoints are JSON over HTTP under `/api`.- **WebSocket** events are JSON messages of the form `{ "type": …, … }` on `/ws`.- **Named elements are reachable without extra navigation.** Unless a ticket says how to get to an element (for example "in the dialog it opens"), it's on screen whenever the thing it belongs to is. Don't hide it behind another view or menu.- **Live means from the moment it's shown.** Once a feed, list or panel is on screen, every change to what it shows appears there without a reload, including changes made an instant after it appeared. Checks act right after a view renders.### The starter's names#### UI| Element | Role | Accessible name | Notes ||---|---|---|---|| Conversation | `main` | | Holds the feed and the composer (Look and layout, above) || Feed | `log` | `Messages` | One `article` per message, oldest first || Message | `article` | (the message's text) | Shows its body and its time || Composer field | `textbox` | `Message` | || Send button | `button` | `Send` | Enter in the field also sends |#### API| Method and path | Request | Response ||---|---|---|| `GET /api/health` | | `200 {"ok": true}` || `GET /api/messages` | | `200 [{"id", "body", "createdAt"}]`, oldest first || `POST /api/messages` | `{"body"}` | `201 {"id", "body", "createdAt"}`; `400 {"error"}` when the body is empty or too long |#### WebSocket| Event | Direction | Payload ||---|---|---|| `message.created` | server → clients | `{"type": "message.created", "message": {"id", "body", "createdAt"}}` |## How work happens hereTickets come one at a time from this repository's backlog board, through the `backlog` command:| Command | What it does ||---|---|| `backlog pull` | Takes the next ticket. It's written to `backlog/in-progress/` and printed. || `backlog done <id>` | Finishes the ticket in progress. Commit your work first: the board refuses uncommitted work or a ticket with no new commits. || `backlog status` | Shows what's in progress and how many are done. |- **Work the backlog until `backlog pull` says the backlog is complete.** Then you're done. Don't stop before that, and don't wait for anyone: the work is unattended.- **One ticket at a time:** pull, build, commit, `backlog done <id>`, then pull the next.- **The ticket is the spec.** Where it leaves something open, make the reasonable choice a careful product owner would, mention it in your commit message, and carry on. If a part genuinely can't be done, say so in your commit message and still finish the ticket with `backlog done`.- **Keep earlier behaviour working** unless a ticket explicitly changes it. Each ticket lists what's most at risk under "Must keep working".- **Use the names a ticket gives** (its Names section, and Names the checks rely on, above) exactly. Checks find elements and endpoints by those names.## Running and testing| Command | What it does ||---|---|| `npm run dev` | The app with hot reload on http://localhost:3000 (API, WebSocket and client on one port) || `npm test` | Vitest: server and API tests in `tests/` || `npm run e2e` | Playwright: browser tests in `e2e/`, against a production build on a fresh database || `npm run typecheck` | TypeScript, no emit || `npm run db:generate` | After changing `server/db/schema.ts`: generates a migration in `server/db/migrations/`. Migrations apply automatically when the server starts. || `npm run db:reset` | Drops and recreates the development database, empty || `npm run seed` | Adds sample messages to an empty database |Dependencies are already installed, and the browser for Playwright too.## The stackThe stack is fixed, along with its locked dependencies:- **Client:** React and TypeScript with Vite (`client/`).- **Server:** Express and `ws` (`server/`), with shared types in `shared/`.- **Database:** Postgres via `pg` (node-postgres), with Drizzle as the ORM. Queries are async.- **Tests:** Vitest and Playwright.- **No new dependencies, services or build tools.** Everything a ticket asks for can be built with what's here.Postgres is already running locally. The app uses the database `hearth` (`DATABASE_URL`, default `postgres://agent@localhost:5432/hearth`); `PORT` overrides the port. Tests never touch it: each API test file and each e2e run creates its own fresh database.Tickets
Word for word, in the order the board hands them out, one at a time; an agent sees each only when it pulls it.
05: Accounts, sign-in and sign-out
Changes: Feed, Composer, Message API, Real-time delivery
Motivation
Anyone who opens Hearth can read and post, and nobody knows who said what. Before people will invite friends, messages need an author, and only people with an account should be able to read and write.
Scope
People create an account with a username and password, sign in and sign out. Sessions survive a page reload. Hearth is members-only: the feed and the composer are shown only when signed in, and every message shows who sent it.
Out of scope: password reset, email, profile pictures, display names separate from usernames, "remember me" options, sign-in with other services.
Terminology
| Term | Meaning |
|---|---|
| Account | A person's identity in Hearth: a unique username and a password. |
| Session | Being signed in on one browser. Signing out ends that session only. |
| Author | The account that sent a message. |
Scenarios
Create an account
Context. A visitor opens Hearth for the first time. Steps.
- They see the sign-in screen: Username, Password, the Sign in button and a Create account button.
- They enter a username and password and choose Create account.
- They're signed in and see the feed and the composer.
Acceptance criteria.
AC-05.1WHEN a visitor creates an account with a username of 3–32 characters (letters, digits, underscore) and a password of at least 8 characters THE SYSTEM SHALL create the account and sign them in.AC-05.2IF the username is already taken, ignoring letter case, THEN THE SYSTEM SHALL refuse with the alert "That username is taken." and create nothing.AC-05.3IF the username or password breaks the rules above THEN THE SYSTEM SHALL refuse with an alert saying which rule, and create nothing.
Sign in and out
Context. Someone with an account returns. Steps. They enter their username and password and choose Sign in. Later they open the Account menu and choose Sign out.
Acceptance criteria.
AC-05.4WHEN a person signs in with a correct username (any letter case) and password THE SYSTEM SHALL show the feed and composer. IF either is wrong THEN THE SYSTEM SHALL show the alert "Wrong username or password."AC-05.5WHILE signed in THE SYSTEM SHALL keep the session across page reloads, until the person signs out.AC-05.6WHEN a person signs out THE SYSTEM SHALL end that session and show the sign-in screen; other browsers signed in to the same account stay signed in.
Messages have authors
Acceptance criteria.
AC-05.7WHEN a signed-in person sends a message THE SYSTEM SHALL record them as its author and show the author's username in the message, in the feed and in every other open feed.AC-05.8IF a request reads or sends messages without a session THEN THE SYSTEM SHALL answer401and change nothing.AC-05.9THE SYSTEM SHALL deliver live message events only to signed-in browsers.
Look
Acceptance criteria.
AC-05.10THE SYSTEM SHALL show messages in Hearth's type (README, Look and layout): the text at 16px, the author's name at weight 600 or more, the time at 12px in--muted; and the Send button on--accentwith white text.
Changes to existing behaviour
Changes: Feed and Composer
Source. Starter. Change. The feed and composer were open to anyone. They're now shown only when signed in. A visitor sees the sign-in screen instead.
Changes: Message API and Real-time delivery
Source. Starter.
Change. GET /api/messages, POST /api/messages and /ws now require a session. Messages gain an
author.
Updated criteria. AC-05.7, AC-05.8, AC-05.9.
Names
| Kind | Name | Notes |
|---|---|---|
UI textbox |
Username |
sign-in screen |
| UI password field | Password |
sign-in screen; found by its label (password inputs have no textbox role) |
UI button |
Sign in |
|
UI button |
Create account |
|
UI alert |
(the error text) | errors from sign-in and account creation |
UI button |
Account |
opens the account menu, which shows the signed-in username |
UI menuitem |
Sign out |
|
Message article |
contains the author's username as visible text | the article's name stays the message text |
| API | POST /api/auth/register {"username", "password"} |
201 {"user": {"id", "username"}} and a session cookie; 409 taken; 400 invalid |
| API | POST /api/auth/login {"username", "password"} |
200 {"user"} and a session cookie; 401 wrong |
| API | POST /api/auth/logout |
204, ends this session |
| API | GET /api/auth/me |
200 {"user"} or 401 |
| Message shape | adds "author": {"id", "username"} |
in API responses and message.created |
Must keep working
- Sending and live delivery between two signed-in browsers.
- Messages surviving a reload, oldest first.
- Message length limits (1–2,000 characters after trimming).
Fixtures
Accounts ana / password-ana and ben / password-ben, created through the API.
06: Create and join servers
Depends on: 05 · Changes: Feed, Message API, Real-time delivery
Motivation
One shared feed for everyone doesn't scale past a handful of friends. People want their own spaces: a server for their team, another for their gaming group, each with its own conversation and its own members.
Scope
Any signed-in person can create a server, which makes them its owner. Members invite others with the server's invite code. Each server has its own feed. The server list shows the servers you belong to, and selecting one shows its feed. The single shared feed goes away: messages now belong to a server.
Out of scope: server icons, renaming or deleting servers, public server discovery. Messages sent before this ticket don't need to be kept.
Terminology
| Term | Meaning |
|---|---|
| Server | A named space with its own members and conversation. |
| Owner | The account that created a server. |
| Member | An account that belongs to a server. The owner is a member. |
| Invite code | A server's shareable code: anyone with it can join that server. |
Scenarios
Create a server
Steps. A signed-in person chooses Create server, enters a name and chooses Create.
Acceptance criteria.
AC-06.1WHEN a signed-in person creates a server named 1–50 characters (after trimming) THE SYSTEM SHALL create it, make them its owner and a member, add it to their server list and select it.AC-06.2IF the name is empty or longer than 50 characters THEN THE SYSTEM SHALL refuse with an alert and create nothing.
Invite and join
Steps. A member opens Invite and copies the invite code. Another person chooses Join server, enters the code and chooses Join.
Acceptance criteria.
AC-06.3WHEN a member opens Invite THE SYSTEM SHALL show the server's invite code, the same code every time.AC-06.4WHEN a signed-in person joins with a valid invite code THE SYSTEM SHALL make them a member, add the server to their list and select it. Joining a server they already belong to just selects it.AC-06.5IF the invite code matches no server THEN THE SYSTEM SHALL show the alert "That invite code doesn't work." and change nothing.
Talk in a server
Acceptance criteria.
AC-06.6THE SYSTEM SHALL list a person's servers in the order they joined them, and only those.AC-06.7WHEN a member selects a server THE SYSTEM SHALL show that server's messages, oldest first, and the composer SHALL send into that server.AC-06.8WHEN a message is sent in a server THE SYSTEM SHALL show it live to that server's members, and to no one else.AC-06.9IF a person who isn't a member reads or sends a server's messages through the API THEN THE SYSTEM SHALL answer404and change nothing.AC-06.10WHILE a person belongs to no server THE SYSTEM SHALL show "Create a server or join one with an invite code." in place of the feed.
Changes to existing behaviour
Changes: Feed, Message API and Real-time delivery
Source. Starter, and ticket 05.
Change. There's no longer one feed for everyone: every message belongs to a server, and live events reach
only that server's members. GET/POST /api/messages are replaced by the server-scoped endpoints below.
Authorship (AC-05.7) and the length limits still apply.
Names
| Kind | Name | Notes |
|---|---|---|
UI navigation |
Servers |
the server list; one button per server, named by the server's name |
UI button |
Create server |
opens the dialog "Create a server" |
UI textbox |
Server name |
in "Create a server" |
UI button |
Create |
in "Create a server" |
UI button |
Join server |
opens the dialog "Join a server" |
UI textbox |
Invite code |
in "Join a server" (editable) and in "Invite people" (read-only, holding the code) |
UI button |
Join |
in "Join a server" |
UI button |
Invite |
opens the dialog "Invite people" for the selected server |
| API | GET /api/servers |
200 [{"id", "name", "ownerId"}], the caller's servers in join order |
| API | POST /api/servers {"name"} |
201 {"id", "name", "ownerId", "inviteCode"} |
| API | GET /api/servers/:id/invite |
200 {"inviteCode"} for members; 404 otherwise |
| API | POST /api/servers/join {"inviteCode"} |
200 {"id", "name", "ownerId"}; 404 when no server has that code |
| API | GET /api/servers/:id/messages |
200 [message], oldest first; 404 for non-members |
| API | POST /api/servers/:id/messages {"body"} |
201 message; 404 for non-members |
| Message shape | adds "serverId" |
in API responses and message.created |
Must keep working
- Sign-in, sign-out, sessions and the members-only rule (ticket 05).
- Authors on messages, and live delivery between two members' browsers.
- Message length limits.
Fixtures
ana owns the server "Lab". ben joins it with its invite code. cyd is signed in but belongs to no server.
07: Text channels within servers
Depends on: 06 · Changes: Server feed, Server message API, Real-time delivery · Supersedes: AC-06.7, AC-06.8
Motivation
Busy servers turn into one long jumble: release planning interleaved with lunch plans. Members want separate conversations by topic, within the same server.
Scope
Servers contain text channels. Every server has a general channel, and the owner can add more. Messages
now belong to a channel, not directly to a server. The channel list shows the selected server's channels, and
selecting one shows its feed. Messages already sent in a server become that server's general messages.
Out of scope: renaming, reordering or deleting channels; channel topics; categories.
Terminology
| Term | Meaning |
|---|---|
| Channel | A named conversation inside a server, shown as #name. |
Scenarios
Channels in a server
Steps. A member selects a server and sees its channels, with general selected. The owner chooses
Create channel, enters a name and chooses Create.
Acceptance criteria.
AC-07.1WHEN a server is created THE SYSTEM SHALL give it a channel namedgeneral.AC-07.2WHEN a member selects a server THE SYSTEM SHALL list its channels in the order they were created and selectgeneral.AC-07.3WHEN the owner creates a channel named 1–32 characters of lowercase letters, digits and hyphens, unique within the server THE SYSTEM SHALL add it to every member's channel list, live, and select it for the owner.AC-07.4IF the name breaks those rules, or a channel of that name exists in the server, THEN THE SYSTEM SHALL refuse with an alert and create nothing. IF a member who isn't the owner tries, THEN THE SYSTEM SHALL answer403.
Talk in a channel
Acceptance criteria.
AC-07.5WHEN a member selects a channel THE SYSTEM SHALL show that channel's messages, oldest first, and the composer SHALL send into that channel.AC-07.6WHEN a message is sent in a channel THE SYSTEM SHALL show it live in that channel's feed for every member viewing it, and in no other channel's feed.AC-07.7THE SYSTEM SHALL show the messages a server had before channels existed in itsgeneralchannel, in their original order, with their original authors and times.AC-07.8IF a person who isn't a member of the channel's server reads or sends its messages THEN THE SYSTEM SHALL answer404.
Look
Acceptance criteria.
AC-07.9WHILE a channel is open THE SYSTEM SHALL lay out, left to right, the server list at the window's left edge, full height, on--rail; the channel list on--panel; and the conversation on--bgas the widest column, with the composer at the conversation's bottom and in view however many messages the channel has; and SHALL keep controls for servers and channels out of the conversation (README, Look and layout).
Changes to existing behaviour
Changes: Server feed
Source. Ticket 06.
Change. A server no longer has a single feed. Selecting a server shows its channels and opens general.
Supersedes. AC-06.7.
Updated criteria. AC-07.2, AC-07.5.
Changes: Real-time delivery and the server message API
Source. Ticket 06.
Change. Live message events are routed per channel, and still reach only the server's members.
GET/POST /api/servers/:id/messages are replaced by the channel endpoints below. Existing server messages
move to general (AC-07.7).
Supersedes. AC-06.8.
Updated criteria. AC-07.6.
Names
| Kind | Name | Notes |
|---|---|---|
UI navigation |
Channels |
the selected server's channels; one button per channel, named #<name> |
UI button |
Create channel |
shown to the owner; opens the dialog "Create a channel" |
UI textbox |
Channel name |
in "Create a channel" |
UI button |
Create |
in "Create a channel" |
UI heading |
#<name> |
the selected channel's name, above its feed |
| API | GET /api/servers/:id/channels |
200 [{"id", "serverId", "name"}] in creation order; 404 for non-members |
| API | POST /api/servers/:id/channels {"name"} |
201 {"id", "serverId", "name"}; 400 invalid; 409 duplicate; 403 not the owner |
| API | GET /api/channels/:id/messages |
200 [message], oldest first; 404 for non-members |
| API | POST /api/channels/:id/messages {"body"} |
201 message; 404 for non-members |
| Message shape | adds "channelId" (keeps "serverId") |
in API responses and message.created |
| WebSocket | channel.created |
{"type", "channel": {"id", "serverId", "name"}}, to the server's members |
Must keep working
- Creating and joining servers, the server list and invite codes (ticket 06).
- Non-members can't read or send (
404). - Authors, sessions, length limits.
Fixtures
ana owns "Lab" with channels general and releases. ben is a member. cyd isn't.
08: Edit and delete your own messages
Depends on: 07 · Changes: Message, Channel feed
Motivation
Typos are forever, and a message sent to the wrong channel can't be taken back. Members want to fix and remove what they wrote.
Scope
A message's author can edit its text or delete it. Edits and deletions show live everywhere the message is visible. An edited message is marked as edited. Nobody can change someone else's message.
Out of scope: edit history, undo, bulk deletion.
Scenarios
Edit a message
Steps. The author chooses Edit on their message, changes the text in Edit message and chooses Save (or Cancel).
Acceptance criteria.
AC-08.1WHEN the author saves an edit of 1–2,000 characters (after trimming) THE SYSTEM SHALL replace the message's text, keep its position, author and sent time, and mark it "(edited)".AC-08.2WHEN a message is edited THE SYSTEM SHALL show the new text and the "(edited)" mark live to everyone viewing that channel.AC-08.3IF the edited text is empty or too long THEN THE SYSTEM SHALL refuse with an alert and leave the message unchanged.
Delete a message
Steps. The author chooses Delete on their message and confirms with Delete in the dialog.
Acceptance criteria.
AC-08.4WHEN the author confirms deleting a message THE SYSTEM SHALL remove it from the channel for everyone, live, and it SHALL not come back on reload.AC-08.5WHEN the author cancels the confirmation THE SYSTEM SHALL keep the message.
Only the author
Acceptance criteria.
AC-08.6THE SYSTEM SHALL show Edit and Delete only on messages the viewer wrote.AC-08.7IF anyone but the author edits or deletes a message through the API THEN THE SYSTEM SHALL answer403and change nothing. IF the message doesn't exist or isn't visible to them THEN404.
Look
Acceptance criteria.
AC-08.8THE SYSTEM SHALL show Delete as a destructive button in--danger(README, Look and layout).
Changes to existing behaviour
Changes: Message and channel feed
Source. Tickets 05 and 07. Change. A message could only be added. It can now be edited or deleted, and every open feed shows that live.
Names
| Kind | Name | Notes |
|---|---|---|
UI button |
Edit |
inside the author's own message article |
UI button |
Delete |
inside the author's own message article; opens the dialog "Delete message" |
UI textbox |
Edit message |
replaces the message text while editing |
UI button |
Save / Cancel |
while editing |
UI button |
Delete / Cancel |
in "Delete message" |
Message article |
shows the text (edited) once edited |
|
| API | PATCH /api/messages/:id {"body"} |
200 message; 400 invalid; 403 not the author; 404 not found or not visible |
| API | DELETE /api/messages/:id |
204; 403; 404 |
| Message shape | adds "editedAt" (ISO time or null) |
|
| WebSocket | message.updated |
{"type", "message"}, to the channel's server members |
| WebSocket | message.deleted |
{"type", "messageId", "channelId"}, to the channel's server members |
A message's own action buttons (Edit, Delete) may appear only while the pointer is over the message or it has focus; checks hover over the message first.
Must keep working
- Sending, ordering and live delivery per channel (ticket 07).
- Non-members get
404for anything in a server. - Authors and sessions.
Fixtures
In "Lab" #general: ana wrote "hello team" and ben wrote "hi ana".
09: Server members, leaving and removal
Depends on: 08 · Changes: Server membership, Server list, Real-time delivery
Motivation
Once servers grow, people want to see who's there, leave servers they no longer use, and owners need to remove someone who shouldn't be in their server.
Scope
A server shows its member list. Any member except the owner can leave. The owner can remove any other member. Leaving or being removed takes effect at once: the server disappears from that person's list, they can no longer read or send in it, and live events from it stop reaching them. Someone who left or was removed can come back with the invite code.
Out of scope: transferring ownership, deleting servers, nicknames.
Scenarios
See the members
Acceptance criteria.
AC-09.1WHEN a member views a server THE SYSTEM SHALL list its members by username, alphabetically, with the owner marked "Owner".AC-09.2WHEN someone joins, leaves or is removed THE SYSTEM SHALL update the member list live for the remaining members.
Leave a server
Steps. A member chooses Leave server and confirms with Leave.
Acceptance criteria.
AC-09.3WHEN a member who isn't the owner confirms leaving THE SYSTEM SHALL remove their membership, remove the server from their list and select another of their servers (or show the no-server state).AC-09.4IF the owner tries to leave THEN THE SYSTEM SHALL refuse with the alert "Owners can't leave their own server." (and answer403through the API).
Remove a member
Steps. The owner chooses Remove next to a member in the member list and confirms with Remove.
Acceptance criteria.
AC-09.5WHEN the owner confirms removing a member THE SYSTEM SHALL end that membership. IF anyone else tries THEN THE SYSTEM SHALL answer403.AC-09.6WHEN a member leaves or is removed THE SYSTEM SHALL, without them reloading, take the server out of their list in every browser they're signed in to, and deliver them no further events from it.AC-09.7WHILE someone isn't a member THE SYSTEM SHALL answer404to their requests for the server's channels and messages, including ones they could read before.AC-09.8WHEN someone who left or was removed joins again with the invite code THE SYSTEM SHALL make them a member again, with the server's full history visible.
Look
Acceptance criteria.
AC-09.9WHILE a server's channel is open and no other panel is THE SYSTEM SHALL show the member list as a column at the window's right edge, full height, on--panel, and Remove and Leave server as destructive buttons in--danger(README, Look and layout).
Changes to existing behaviour
Changes: Server membership, server list and real-time delivery
Source. Ticket 06. Change. Membership only ever grew. A member can now leave or be removed, which takes the server out of their list and stops its live events reaching them.
Names
| Kind | Name | Notes |
|---|---|---|
UI complementary |
Members |
the selected server's member list; one listitem per member, named by username |
| UI text | Owner |
inside the owner's listitem |
UI button |
Leave server |
opens the dialog "Leave server" with Leave / Cancel |
UI button |
Remove |
inside each non-owner listitem, shown to the owner; opens the dialog "Remove member" with Remove / Cancel |
| API | GET /api/servers/:id/members |
200 [{"id", "username", "owner": true/false}]; 404 for non-members |
| API | POST /api/servers/:id/leave |
204; 403 for the owner |
| API | DELETE /api/servers/:id/members/:userId |
204; 403 unless the caller is the owner; 404 not a member |
| WebSocket | member.joined / member.left |
{"type", "serverId", "user": {"id", "username"}}, to the server's members |
| WebSocket | server.removed |
{"type", "serverId"}, to the person who left or was removed |
Must keep working
- Joining with an invite code (now also rejoining).
- Channel feeds, editing and deleting, and live delivery for the members who remain.
- Non-members get
404.
Fixtures
"Lab" is owned by ana, with members ben and cyd. ben is signed in on two browsers.
10: Direct messages
Depends on: 09 · Changes: Message, Edit and delete
Motivation
Not everything belongs in a channel. Members want to talk one to one with someone they know from a server.
Scope
A person can open a direct conversation with anyone who shares at least one server with them. Direct conversations are listed under Direct messages. Messages in them work like channel messages: authors, length limits, live delivery, editing and deleting your own. Only the two participants can see a conversation.
Out of scope: group DMs, friend lists, message requests, DMing people you share no server with.
Terminology
| Term | Meaning |
|---|---|
| Direct conversation | A private conversation between exactly two accounts. There's at most one per pair. |
Scenarios
Start a conversation
Steps. A person chooses New message, enters a username in Recipient and chooses Start.
Acceptance criteria.
AC-10.1WHEN a person starts a conversation with an account they share a server with THE SYSTEM SHALL open their direct conversation with that account, creating it on first use and reusing it after.AC-10.2IF the username doesn't exist, is their own, or shares no server with them THEN THE SYSTEM SHALL show the alert "You can only message people you share a server with." and create nothing.AC-10.3THE SYSTEM SHALL list a person's direct conversations under Direct messages by the other person's username, most recent message first.
Talk privately
Acceptance criteria.
AC-10.4WHEN a participant sends a message in a direct conversation THE SYSTEM SHALL show it live to both participants, in every browser they're signed in to, and to no one else.AC-10.5WHEN someone receives the first message of a new conversation THE SYSTEM SHALL add it to their Direct messages list live.AC-10.6THE SYSTEM SHALL let participants edit and delete their own direct messages with the same rules as channel messages (ticket 08).AC-10.7IF anyone other than the two participants reads or sends in a direct conversation THEN THE SYSTEM SHALL answer404.AC-10.8WHEN two people who no longer share a server have a conversation THE SYSTEM SHALL keep it and its history readable to both, but refuse new messages with403and the alert "You can only message people you share a server with."
Changes to existing behaviour
Changes: Edit and delete
Source. Ticket 08.
Change. PATCH/DELETE /api/messages/:id also apply to direct messages. The events go to the two
participants.
Names
| Kind | Name | Notes |
|---|---|---|
UI navigation |
Direct messages |
one button per conversation, named by the other person's username; on screen whenever signed in, alongside the server list |
UI button |
New message |
opens the dialog "New message"; on screen whenever signed in |
UI textbox |
Recipient |
in "New message" |
UI button |
Start |
in "New message" |
UI heading |
@<username> |
above an open direct conversation |
| API | GET /api/dms |
200 [{"id", "user": {"id", "username"}, "lastMessageAt"}], most recent first |
| API | POST /api/dms {"username"} |
200 {"id", "user"} (existing) or 201 (new); 400 with the error when not allowed |
| API | GET /api/dms/:id/messages |
200 [message], oldest first; 404 for non-participants |
| API | POST /api/dms/:id/messages {"body"} |
201 message; 403 when they no longer share a server; 404 for non-participants |
| Message shape | direct messages carry "dmId" instead of "channelId"/"serverId" |
|
| WebSocket | message.created / message.updated / message.deleted |
as before, carrying dmId, to the two participants |
Must keep working
- Channel messaging, editing and deleting, and live delivery.
- Member removal and leaving cut access to the server immediately.
- Sessions in several browsers.
Fixtures
ana and ben share "Lab". dee shares no server with ana.
11: Emoji reactions
Depends on: 10 · Changes: Message
Motivation
Half the replies in a busy channel are "+1" and "nice". Members want a lighter way to respond.
Scope
Anyone who can see a message can react to it with one of six emoji, and take their reaction back. Each person counts at most once per emoji per message. Counts update live. This works in channels and direct conversations.
Out of scope: custom emoji, other emoji, seeing a list of who reacted.
Terminology
| Term | Meaning |
|---|---|
| Reaction | One person's emoji on one message. The palette: 👍 ❤️ 😂 😮 😢 🎉 |
Scenarios
Steps. A person chooses Add reaction on a message and picks an emoji. Choosing that emoji's reaction again removes theirs.
Acceptance criteria.
AC-11.1WHEN a person reacts to a message they can see THE SYSTEM SHALL show the emoji with a count under the message, marked as theirs.AC-11.2WHEN they choose the same emoji again THE SYSTEM SHALL remove their reaction, and the emoji disappears when its count reaches zero.AC-11.3THE SYSTEM SHALL count each person at most once per emoji per message, including when the same reaction arrives twice at once or from two browsers.AC-11.4WHEN reactions change THE SYSTEM SHALL update the counts live for everyone viewing the message, showing each viewer only their own "mine" marks.AC-11.5THE SYSTEM SHALL show reactions in palette order, and keep them across reloads.AC-11.6IF the emoji isn't in the palette THEN THE SYSTEM SHALL answer400. IF the person can't see the message THEN404.AC-11.7WHEN a message is deleted THE SYSTEM SHALL delete its reactions with it.
Changes to existing behaviour
Changes: Message
Source. Ticket 05. Change. A message was its text, author and time. It now also carries reaction counts, kept live.
Names
| Kind | Name | Notes |
|---|---|---|
UI button |
Add reaction |
inside each message article; opens a menu of the six emoji (menuitems named by the emoji) |
UI button |
<emoji> <count> |
e.g. 👍 2, inside the message; aria-pressed="true" when it includes the viewer's reaction; choosing it toggles the viewer's reaction |
| API | PUT /api/messages/:id/reactions/:emoji |
200 {"reactions"}, idempotent |
| API | DELETE /api/messages/:id/reactions/:emoji |
200 {"reactions"}, idempotent |
| Message shape | adds "reactions": [{"emoji", "count", "mine"}] in palette order, only emoji with a count |
|
| WebSocket | reaction.updated |
{"type", "messageId", "reactions": [{"emoji", "count"}]}, to everyone who can see the message |
Must keep working
- Editing and deleting messages (with live updates).
- Channel and direct-message delivery, and who can see what.
Fixtures
In "Lab" #general, ana's message "ship it?". ben and cyd are members.
12: Threads and channel history
Depends on: 11 · Changes: Channel feed, Message, Delete a message · Supersedes: AC-07.5, AC-08.4
Motivation
Side conversations swamp a channel, and channels with months of history take ages to open. Members want to reply in a thread off the main feed, and the feed should load recent messages first.
Scope
Any channel message can start a thread. Replies live in the thread panel, not the main feed, and the parent shows how many replies it has. Channel feeds now load the latest 50 messages and fetch earlier ones on request. Deleting a message that has replies leaves a placeholder so the thread survives.
Out of scope: threads in direct conversations, replies to replies (a thread is one level), following threads.
Terminology
| Term | Meaning |
|---|---|
| Thread | The replies to one channel message (its parent), shown in a panel beside the feed. |
| Reply | A message in a thread. |
Scenarios
Reply in a thread
Steps. A member chooses Reply in thread on a channel message. The Thread panel opens with the parent at the top and the replies under it. They write in Reply and choose Send.
Acceptance criteria.
AC-12.1WHEN a member sends a reply THE SYSTEM SHALL add it to that message's thread, oldest first, and not to the channel feed.AC-12.2THE SYSTEM SHALL show on a parent message "1 reply" or "replies", updated live for everyone viewing the channel. AC-12.3WHEN a reply is sent, edited, deleted or reacted to THE SYSTEM SHALL update it live for everyone with that thread open, with the same rules as channel messages.AC-12.4IF the parent isn't a channel message, or is itself a reply, THEN THE SYSTEM SHALL answer400.
Delete a message with replies
Acceptance criteria.
AC-12.5WHEN the author deletes a message that has replies THE SYSTEM SHALL replace its text with "Original message was deleted." for everyone, live, keep its replies and reply count, and remove its reactions, Edit and Delete.AC-12.6WHEN the last reply of a deleted parent is deleted THE SYSTEM SHALL remove the placeholder from the channel.AC-12.9WHEN the author deletes a message that has no replies THE SYSTEM SHALL remove it for everyone, live, and it SHALL not come back on reload (as before this ticket).
Channel history
Acceptance criteria.
AC-12.7WHEN a member opens a channel THE SYSTEM SHALL show its 50 most recent top-level messages, oldest first, with Load earlier messages when there are more.AC-12.8WHEN they choose Load earlier messages THE SYSTEM SHALL add the previous 50 above, without duplicates or gaps, even if new messages arrived meanwhile.
Changes to existing behaviour
Changes: Channel feed
Source. Ticket 07.
Change. A channel shows its latest 50 top-level messages rather than all of them, with earlier ones on
request. Replies stay out of the feed.
Supersedes. AC-07.5.
Updated criteria. AC-12.1, AC-12.7, AC-12.8.
Changes: Delete a message
Source. Ticket 08.
Change. A deleted message that has replies stays as a placeholder (AC-12.5). Without replies, it's
removed as before.
Supersedes. AC-08.4.
Updated criteria. AC-12.5, AC-12.6, AC-12.9.
Names
| Kind | Name | Notes |
|---|---|---|
UI button |
Reply in thread |
inside each top-level channel message article |
UI button |
1 reply / <n> replies |
inside a parent; opens its thread |
UI complementary |
Thread |
the thread panel: the parent article first, then reply articles |
UI textbox |
Reply |
in the Thread panel, with its own Send button |
UI button |
Close thread |
in the Thread panel |
UI button |
Load earlier messages |
at the top of the channel feed, when there are more |
| API | GET /api/channels/:id/messages?before=<messageId>&limit=50 |
200 [message]: up to 50 top-level messages older than before (newest page when omitted), oldest first |
| API | GET /api/messages/:id/replies |
200 [message], oldest first |
| API | POST /api/messages/:id/replies {"body"} |
201 message; 400 when the parent can't have a thread |
| Message shape | adds "parentId" (null for top-level), "replyCount" and "deleted" (true for a placeholder) |
|
| WebSocket | message.created / message.updated / message.deleted |
replies carry parentId; a parent's new replyCount arrives as message.updated |
Must keep working
- Editing, deleting and reacting to channel messages.
- Direct conversations (no threads there).
- Live delivery per channel and who can see what.
Fixtures
#general in "Lab" holds 120 messages from ana and ben, one a minute. ana's message 117 has two replies
from ben.
13: Private channels
Depends on: 07, 09, 12 · Changes: Channel list, Channel message API, Real-time delivery, Leaving and removal · Supersedes: AC-07.3
Motivation
Server owners want somewhere to talk with a few people without the whole server reading along: planning a surprise, a small project team's room, a small project team. Members asked for channels only some people can see.
Scope
The owner can create a private channel and choose who's in it, by username. Only the owner and the people they've added can see a private channel: it isn't in anyone else's channel list, and its messages, replies and reactions are hidden from everyone else. The owner can add and remove people at any time, and the change shows live. Public channels work exactly as before.
Out of scope: making an existing channel private or public; private general; anyone but the owner
managing a channel's members; a channel member leaving a private channel by themselves.
Terminology
| Term | Meaning |
|---|---|
| Private channel | A channel only the server's owner and its channel members can see. |
| Channel member | A server member the owner has added to a private channel. |
Scenarios
Create a private channel
Steps. The owner chooses Create channel, enters a name, checks Private and chooses Create.
Acceptance criteria.
AC-13.1WHEN the owner creates a channel with Private checked THE SYSTEM SHALL create it as a private channel with no channel members, show it in the owner's channel list, live, select it for the owner, and show it to nobody else.AC-13.2WHEN the owner creates a channel without Private checked THE SYSTEM SHALL add it to every member's channel list, live, and select it for the owner (as before).
Choose who's in it
Steps. With a private channel selected, the owner chooses Channel members. The dialog lists who has access. They enter a username and choose Add, or choose Remove beside someone.
Acceptance criteria.
AC-13.3WHEN the owner adds a server member to a private channel THE SYSTEM SHALL add the channel to that member's channel list, live, with its full history.AC-13.4WHEN the owner removes a channel member THE SYSTEM SHALL remove the channel from that person's channel list, live, stop delivering its events to them, and, if they were viewing it, selectgeneralfor them.AC-13.5THE SYSTEM SHALL list a private channel's access in the Channel members dialog: the owner first, marked "Owner", then the channel members alphabetically, kept live.AC-13.6IF the owner adds an unknown username or someone who isn't a member of the server THEN THE SYSTEM SHALL refuse with the alert "Only members of this server can be added." and change nothing. Adding someone already in the channel changes nothing. IF anyone but the owner adds or removes channel members THEN THE SYSTEM SHALL answer403.
Privacy
Acceptance criteria.
AC-13.7IF a server member who isn't the owner or a channel member lists, reads, sends to, replies in or reacts in a private channel THEN THE SYSTEM SHALL answer404, as for a channel that doesn't exist.AC-13.8WHILE a channel is private THE SYSTEM SHALL deliver its live events (messages, edits, deletes, replies, reactions, reply counts) only to the owner and its channel members.AC-13.9WHEN someone leaves or is removed from a server THE SYSTEM SHALL drop them from every private channel in it. Rejoining the server doesn't restore access.
Changes to existing behaviour
Changes: Channel list
Source. Ticket 07.
Change. Every member saw every channel. Now a member sees the public channels plus the private channels
they belong to; the owner sees them all.
Supersedes. AC-07.3.
Updated criteria. AC-13.1, AC-13.2, AC-13.6.
Changes: Channel message API and Real-time delivery
Source. Tickets 07, 11 and 12.
Change. Reading, sending, replying and reacting in a private channel need access, and its live events go
only to people with access. Public channels are unchanged.
Updated criteria. AC-13.7, AC-13.8.
Changes: Leaving and removal
Source. Ticket 09.
Change. Leaving or being removed from a server also ends private-channel access there, for good.
Updated criteria. AC-13.9.
Names
| Kind | Name | Notes |
|---|---|---|
UI checkbox |
Private |
in "Create a channel" |
UI button |
Channel members |
shown to the owner while a private channel is selected; opens the dialog "Channel members" |
UI list |
People with access |
in "Channel members": one listitem per person, showing the username; the owner's also shows "Owner" |
UI textbox |
Username |
in "Channel members" |
UI button |
Add |
in "Channel members" |
UI button |
Remove <username> |
in "Channel members", one per channel member |
UI alert |
(the error text) | in "Channel members" |
| API | POST /api/servers/:id/channels {"name", "private"} |
"private" optional, default false; the response and channel shape add "private" |
| API | GET /api/servers/:id/channels |
as before, listing only the channels the viewer can see |
| API | GET /api/channels/:id/members |
200 [{"id", "username"}]: the owner first, then channel members alphabetically; 404 without access |
| API | PUT /api/channels/:id/members/:userId |
200 [{"id", "username"}] (the new list); 400 not a server member; 403 not the owner; 404 no such channel for the viewer |
| API | DELETE /api/channels/:id/members/:userId |
200 [{"id", "username"}]; 403 not the owner |
| WebSocket | channel.created |
now only to people who can see the channel |
| WebSocket | channel.added |
{"type", "channel"}, to a person just given access to a private channel |
| WebSocket | channel.removed |
{"type", "serverId", "channelId"}, to a person who just lost access |
| WebSocket | channel.members |
{"type", "channelId", "members": [{"id", "username"}]}, to the owner and channel members when the list changes |
Must keep working
- Public channels: listing, creating, live messages (ticket 07).
- Threads, reactions, edits and deletes in public channels (tickets 08, 11, 12).
- Leaving, removal and the member list (ticket 09).
- Direct messages (ticket 10).
Fixtures
ana owns "Lab" with channels general and the private channel planning. ben and cyd are members of
"Lab"; ben is a channel member of planning and cyd isn't. dee has an account and isn't in "Lab".
14: Bans
Depends on: 09, 13 · Changes: Removal, Joining with an invite code · Supersedes: AC-09.8
Motivation
Removing a troublemaker doesn't stick: they rejoin with the invite code a minute later. Owners want to keep someone out until they decide otherwise.
Scope
The owner can ban a member. A ban removes them at once, like removal, and stops them rejoining with the invite code. The owner sees who's banned and can lift a ban, after which the person can rejoin like anyone else. A banned person's messages stay, with their author.
Out of scope: banning someone who isn't a member; bans with an expiry or a reason; deleting a banned person's messages; banning by anyone but the owner.
Terminology
| Term | Meaning |
|---|---|
| Ban | The owner's decision to keep a person out of a server until lifted. |
Scenarios
Ban a member
Steps. In the Members list, the owner chooses Ban beside a member and confirms with Ban in the "Ban member" dialog.
Acceptance criteria.
AC-14.1WHEN the owner confirms banning a member THE SYSTEM SHALL end their membership at once, exactly as removal does: the server leaves every one of their open browsers without a reload, the member list updates live for everyone, and they lose access to its channels, including private ones.AC-14.2WHILE someone is banned from a server THE SYSTEM SHALL refuse their invite-code joins with the alert "You're banned from this server." (403through the API) and not add them.AC-14.3THE SYSTEM SHALL keep a banned person's messages in the server's channels, with their author.AC-14.4IF anyone but the owner bans or unbans, or the owner tries to ban themselves, THEN THE SYSTEM SHALL answer403. IF the person isn't a member THEN THE SYSTEM SHALL answer404.
See and lift bans
Steps. The owner chooses Bans to open the "Bans" dialog, which lists who's banned, and chooses Unban beside someone.
Acceptance criteria.
AC-14.5THE SYSTEM SHALL list a server's banned people in the Bans dialog, alphabetically, kept live, and only for the owner (403for anyone else through the API).AC-14.6WHEN the owner unbans someone THE SYSTEM SHALL remove them from the list, and they SHALL be able to join again with the invite code, seeing the server's public channels and full history, as anyone rejoining does.
Changes to existing behaviour
Changes: Joining with an invite code
Source. Ticket 09.
Change. Anyone who left or was removed could rejoin with the invite code. A banned person can't, until the
ban is lifted.
Supersedes. AC-09.8.
Updated criteria. AC-14.2, AC-14.6.
Names
| Kind | Name | Notes |
|---|---|---|
UI button |
Ban |
inside each non-owner listitem in Members, shown to the owner; opens the dialog "Ban member" with Ban / Cancel |
UI button |
Bans |
shown to the owner of the selected server; opens the dialog "Bans" |
UI list |
Banned people |
in "Bans": one listitem per banned person, showing the username |
UI button |
Unban <username> |
in "Bans", one per banned person |
UI alert |
(the error text) | when a join is refused |
| API | PUT /api/servers/:id/bans/:userId |
200 [{"id", "username"}] (the bans, alphabetically); 403 not the owner, or the owner themselves; 404 not a member |
| API | DELETE /api/servers/:id/bans/:userId |
200 [{"id", "username"}]; 403 not the owner |
| API | GET /api/servers/:id/bans |
200 [{"id", "username"}], alphabetically; 403 not the owner |
| API | POST /api/servers/join |
403 {"error"} while banned |
| WebSocket | server.removed / member.left |
as for removal, when someone is banned |
| WebSocket | bans.changed |
{"type", "serverId", "bans": [{"id", "username"}]}, to the owner |
Must keep working
- Leaving, removal and rejoining after removal (ticket 09).
- Private channels and their member lists (ticket 13).
- Direct messages and the shared-server rule (ticket 10).
Fixtures
ana owns "Lab" with members ben and cyd. ben is signed in on two browsers. cyd is banned.
15: Mentions and notifications
Depends on: 08, 12, 13, 14 · Changes: Sending, Editing, Deleting
Motivation
In a busy server people miss the messages meant for them. Members want to call someone in with @username,
and to see in one place every time someone did that to them.
Scope
Writing @username in a channel message or thread reply mentions that person. Each person has an Inbox
listing the messages that mentioned them, newest first, with how many are unread. Opening the inbox marks them
read. A notification only ever shows a message its owner can still see: deleted messages, and messages in
channels or servers they've lost access to, drop out.
Out of scope: mentions in direct messages; @everyone mentions; email or push notifications;
notification settings; marking single notifications read; highlighting mentions in the feed.
Terminology
| Term | Meaning |
|---|---|
| Mention | @ followed by a username, in a channel message or reply. |
| Notification | An inbox entry for a message that mentioned you. |
| Inbox | A person's notifications, newest first. |
Scenarios
Mention someone
Steps. A member sends "@ben can you look at this?" in #general.
Acceptance criteria.
AC-15.1WHEN a channel message or reply mentions someone who can see that channel THE SYSTEM SHALL add one notification for that message to their inbox, live. A message mentioning the same person several times notifies once.AC-15.2THE SYSTEM SHALL match a mention as@followed by a username in any letter case, not followed by another letter, digit or underscore (so "@Ben," mentionsben, and "@bent" doesn't). It SHALL NOT notify the author, unknown usernames, or people who can't see the channel (outside the server, or not in a private channel).
The inbox
Steps. The person chooses Inbox, sees the notifications and chooses one.
Acceptance criteria.
AC-15.3THE SYSTEM SHALL list a person's notifications newest first, each showing the author's username, the message text,#<channel>and the server's name.AC-15.4THE SYSTEM SHALL show the number of unread notifications on the Inbox button, live, in all of the person's browsers. WHEN they open the inbox THE SYSTEM SHALL mark all of them read, everywhere.AC-15.5WHEN a person chooses a notification THE SYSTEM SHALL select that message's server and channel.
Edits, deletes and lost access
Acceptance criteria.
AC-15.6WHEN an edit adds a mention of someone not yet notified for that message THE SYSTEM SHALL notify them. Editing a mention out SHALL NOT remove an existing notification, and putting it back SHALL NOT notify again.AC-15.7WHEN a message is deleted THE SYSTEM SHALL remove its notifications from every inbox, live. A deleted message that stays as a placeholder (it has replies) counts as deleted.AC-15.8THE SYSTEM SHALL list and count only notifications for messages the person can still see: after they leave, are removed or banned from the server, or are removed from a private channel, notifications from there no longer appear or count. They SHALL come back only if access does.
Changes to existing behaviour
Changes: Sending, editing and deleting
Source. Tickets 07, 08 and 12.
Change. Sending, replying and editing can now notify people, and deleting removes notifications.
Updated criteria. AC-15.1, AC-15.6, AC-15.7.
Names
| Kind | Name | Notes |
|---|---|---|
UI button |
Inbox |
named Inbox with no unread notifications, Inbox (<n>) with n unread; on screen whenever signed in; opens the dialog "Inbox" |
UI list |
Notifications |
in "Inbox": one listitem per notification, newest first; choosing it selects the message's channel and closes the dialog |
| API | GET /api/notifications |
200 {"unread": n, "notifications": [{"id", "read", "createdAt", "serverName", "channelName", "message"}]}, newest first; message is the message shape |
| API | POST /api/notifications/read |
204: marks all of the caller's notifications read |
| WebSocket | notification.created |
{"type", "notification"} (the shape above), to the mentioned person |
| WebSocket | notifications.changed |
{"type", "unread": n}, to all of a person's browsers when their unread count changes for any other reason (read, deleted, lost access) |
Must keep working
- Sending, editing and deleting messages and replies (tickets 07, 08, 12).
- Private channels and bans (tickets 13, 14).
- Live delivery of messages.
Fixtures
ana owns "Lab" with #general and the private channel planning (channel member: ben). ben and cyd are
members of "Lab". dee isn't.
16: Unread state
Depends on: 10, 12, 13 · Changes: Channel list, Direct messages list, Channel feed
Motivation
With several servers and conversations, members can't tell where something new happened without clicking through everything. They want unread counts, and a marker showing where they left off.
Scope
Every channel and direct conversation shows how many messages a person hasn't read yet. Reading happens by viewing: opening a channel or conversation reads everything in it, including messages that arrive while it's open. A person's read position is the same in all their browsers. Opening something with unread messages shows a "New" divider above the first one.
Out of scope: unread counts for servers as a whole; thread read state; marking unread again; mention counts in the channel list; a "jump to unread" control.
Terminology
| Term | Meaning |
|---|---|
| Read position | The newest message a person has read in a channel or conversation. |
| Unread | Top-level messages from other people, newer than the read position. |
Scenarios
Counts
Acceptance criteria.
AC-16.1THE SYSTEM SHALL show beside each channel in Channels and each conversation in Direct messages the number of unread messages, when there are any, and keep it live as messages arrive.AC-16.2THE SYSTEM SHALL count only top-level messages from other people: thread replies and the person's own messages SHALL NOT count, and sending a top-level message SHALL mark everything before it read (a thread reply doesn't change the read position).AC-16.3WHEN an unread message is deleted, or becomes a placeholder, THE SYSTEM SHALL stop counting it, live.AC-16.4WHEN a person gains access to a channel (creating or joining a server, rejoining it, or being added to a private channel) THE SYSTEM SHALL start them with everything already in it read. A new conversation starts with nothing read.
Reading
Steps. A person selects #general, which has 3 unread messages.
Acceptance criteria.
AC-16.5WHILE a channel or conversation is open THE SYSTEM SHALL mark everything in it read, including messages arriving while it's open, and update the count in all of the person's browsers, live.AC-16.6WHEN a person opens a channel or conversation with unread messages THE SYSTEM SHALL show a "New" divider directly above the first unread message, and keep it there until they open something else. With nothing unread there's no divider.
Changes to existing behaviour
Changes: Channel list, Direct messages list and Channel feed
Source. Tickets 07, 10 and 12.
Change. The lists show unread counts, and opening a channel or conversation now records what the person has
read. Channel buttons keep their names.
Updated criteria. AC-16.1, AC-16.5, AC-16.6.
Names
| Kind | Name | Notes |
|---|---|---|
| UI text | <n> unread |
next to the channel's button in Channels, or the conversation's in Direct messages, only when n > 0; the buttons keep their names (#<name>, the username) |
UI separator |
New |
in the feed, directly above the first unread message |
| API | GET /api/servers/:id/channels |
each channel adds "unread" (for the viewer) |
| API | GET /api/dms |
each conversation adds "unread" |
| API | POST /api/channels/:id/read |
204: marks everything in the channel read for the caller; 404 without access |
| API | POST /api/dms/:id/read |
204; 404 for non-participants |
| WebSocket | unread.changed |
{"type", "channelId", "dmId", "unread": n} (one of the ids is null), to a person's browsers whenever their count for that channel or conversation changes |
Must keep working
- Channel and conversation lists, their order and names (tickets 07, 10).
- Threads and placeholders (ticket 12).
- Private channels, leaving and rejoining (tickets 09, 13).
Fixtures
ana owns "Lab"; ben is a member. #general has 3 messages from ana that ben hasn't read, one with a thread
reply. ana and ben have a conversation with 2 unread messages for ben.
17: Search
Depends on: 10, 12, 13, 14
Motivation
"Someone posted the release date last week, but where?" Members want to find a message by what it said, across every server and conversation they're in.
Scope
A search box finds messages containing some text, across everything the person can see: channel messages, thread replies and their direct conversations. Results are newest first and show where each message is; choosing one opens it in place. Search always reflects the current state: edited text, deleted messages, and what the person still has access to. It uses the existing database: no new search dependency or service.
Out of scope: search operators or filters (from:, in:, dates); ranking by relevance; highlighting matches; searching usernames, channel or server names; paging beyond the first 50 results.
Scenarios
Search
Steps. A person types "release" in Search and presses Enter. The Search results panel lists the matching messages. They choose one.
Acceptance criteria.
AC-17.1WHEN a person searches for text of 2 to 100 characters (after trimming) THE SYSTEM SHALL list the messages whose text contains it, in any letter case, newest first, up to 50. Every character in the search matches itself literally (%,_and\included).AC-17.2THE SYSTEM SHALL search channel messages, thread replies and direct messages, and show for each result its author, its text and where it is:#<channel>and the server's name,@<username>for a direct conversation, plus "in a thread" for a reply.AC-17.3THE SYSTEM SHALL only return messages the person can see now: not channels or servers they've left, were removed or banned from, not private channels they aren't in, and not other people's direct conversations.AC-17.4THE SYSTEM SHALL search messages as they are now: an edit changes what matches at once, and deleted messages and placeholders never match.AC-17.5WHEN a person chooses a result THE SYSTEM SHALL open its channel or direct conversation, and for a reply, that channel with the reply's thread open.AC-17.6IF the search is shorter than 2 characters after trimming, or longer than 100, THEN THE SYSTEM SHALL show the alert "Search for 2 to 100 characters." and answer400through the API.
Names
| Kind | Name | Notes |
|---|---|---|
UI searchbox |
Search |
on screen whenever signed in; Enter searches |
UI complementary |
Search results |
shown after a search: one article per result, named by the message's text, showing the author and where it is |
UI alert |
(the error text) | for an invalid search |
| API | GET /api/search?q=<text> |
200 [{"message", "serverName", "channelName", "dmUser"}] newest first: message is the message shape; serverName and channelName are null for direct messages, dmUser ({"id", "username"}, the other person) is null for channel messages; 400 invalid |
Must keep working
- Private channels, bans, leaving (tickets 09, 13, 14).
- Editing and deleting, threads and placeholders (tickets 08, 12).
- Direct messages (ticket 10).
Fixtures
ana owns "Lab" with #general and the private channel planning (ben is in it, cyd isn't). #general has
"Release is Friday" from ana with a reply "release notes soon" from ben. planning has "release party plans".
ana and ben have a conversation containing "about the release".
18: Roles and permissions
Depends on: 07, 08, 09, 13, 14 · Changes: Creating channels, Deleting messages, Removing members, Bans, Private channel access, Member list · Supersedes: AC-07.4, AC-08.6, AC-08.7, AC-09.5, AC-13.6, AC-14.4, AC-14.5
Motivation
Owners of growing servers can't do everything themselves. They want to hand out moderation and channel management to trusted members, and to open private channels to whole groups rather than one name at a time.
Scope
The owner creates roles, each with a set of permissions, and gives them to members. A member's permissions are everything their roles grant; the owner has every permission. Four things that only the owner (or only a message's author) could do become permissions. Private channels can also be opened to roles: a private channel's audience is the owner, its channel members, and everyone holding one of its roles. Every change applies at once and shows live.
Out of scope: renaming or deleting roles; changing a role's permissions after creating it; role colours or ordering; a role hierarchy (a member with "Remove members" can remove anyone but the owner); giving permissions to anyone but the owner to manage roles.
Terminology
| Term | Meaning |
|---|---|
| Role | A named set of permissions in a server, given to members by the owner. |
| Permission | One of: Manage channels, Delete messages, Remove members, Ban members. |
Scenarios
Roles
Steps. The owner chooses Roles, enters a name, checks some permissions and chooses Create role. In the Members list they choose Edit roles beside a member and check the roles to give them.
Acceptance criteria.
AC-18.1WHEN the owner creates a role with a name of 1–32 characters, unique within the server (ignoring letter case), and any set of permissions THE SYSTEM SHALL add it to the server's roles, in creation order. IF the name breaks those rules THEN THE SYSTEM SHALL refuse with an alert and create nothing. IF anyone but the owner creates a role or changes who holds one THEN THE SYSTEM SHALL answer403.AC-18.2WHEN the owner gives a member a role or takes it away THE SYSTEM SHALL apply its permissions to that member at once, and show each member's roles, by name, in the Members list, live for everyone viewing it.
What permissions allow
Acceptance criteria.
AC-18.3THE SYSTEM SHALL let a member with Manage channels create channels, public or private. Creating a private channel SHALL give its creator access. They SHALL also be able to change the channel members and roles of any private channel they can see.AC-18.4THE SYSTEM SHALL let a member with Delete messages delete anyone's messages and replies in the server's channels they can see, with the same placeholder rules as an author's delete. Only the author SHALL edit a message, and only the author SHALL delete a direct message.AC-18.5THE SYSTEM SHALL let a member with Remove members remove anyone but the owner, and a member with Ban members ban anyone but the owner and themselves, unban, and see the Bans dialog.AC-18.6IF a member without the needed permission creates a channel, deletes someone else's message, removes or bans a member, lists bans, or changes a private channel's members or roles, THEN THE SYSTEM SHALL answer403, and the controls for it SHALL NOT be shown to them.
Private channels for roles
Steps. In a private channel's Channel members dialog, someone allowed to manage it checks a role under Roles with access.
Acceptance criteria.
AC-18.7WHEN a role is given access to a private channel THE SYSTEM SHALL give everyone holding that role access, live, exactly as if they'd been added as channel members, and take it away, live, when the role's access is removed or the role is taken from them, unless they still have access another way.AC-18.8IF someone adds an unknown username or a person who isn't a member of the server to a private channel THEN THE SYSTEM SHALL refuse with the alert "Only members of this server can be added." and change nothing. Adding someone already in the channel changes nothing.
Changes to existing behaviour
Changes: Creating channels, deleting messages, removing members, bans
Source. Tickets 07, 08, 09 and 14.
Change. These were for the owner, or a message's author, only. They now also belong to members whose roles
grant the matching permission.
Supersedes. AC-07.4 (the owner-only part; name rules are unchanged), AC-08.6 (Delete now also shows on other people's messages for members with Delete messages; Edit still shows only on your own), AC-08.7, AC-09.5, AC-14.4, AC-14.5.
Updated criteria. AC-18.3, AC-18.4, AC-18.5, AC-18.6.
Changes: Private channel access
Source. Ticket 13.
Change. Access was the owner plus an explicit list of channel members. It's now the owner, the channel members,
and everyone holding a role the channel is open to. Managing it is no longer owner-only. AC-13.7 and AC-13.8 now
apply to everyone the channel admits.
Supersedes. AC-13.6.
Updated criteria. AC-18.3, AC-18.7, AC-18.8.
Changes: Member list
Source. Ticket 09.
Change. Each member's roles are shown beside their name.
Updated criteria. AC-18.2.
Names
| Kind | Name | Notes |
|---|---|---|
UI button |
Roles |
shown to the owner of the selected server; opens the dialog "Roles" |
UI list |
Server roles |
in "Roles": one listitem per role, showing its name and its permissions' names |
UI textbox |
Role name |
in "Roles" |
UI checkbox |
Manage channels / Delete messages / Remove members / Ban members |
in "Roles", for the new role |
UI button |
Create role |
in "Roles" |
UI button |
Edit roles |
inside each listitem in Members, shown to the owner; opens the dialog "Roles for checkbox per role, named by the role's name, checked for roles the member holds; changes apply when (un)checked |
| UI text | (role names) | inside each member's listitem in Members |
UI group |
Roles with access |
in "Channel members": one checkbox per server role, named by the role's name |
UI list |
People with access |
unchanged: the owner and the channel members added by name; people with access through a role aren't listed there |
| API | GET /api/servers/:id/roles |
200 [{"id", "name", "permissions": [...]}] in creation order; permissions are "manage_channels", "delete_messages", "remove_members", "ban_members"; 404 for non-members |
| API | POST /api/servers/:id/roles {"name", "permissions"} |
201 role; 400 invalid; 409 duplicate name; 403 not the owner |
| API | PUT / DELETE /api/servers/:id/members/:userId/roles/:roleId |
200 {"id", "username", "owner", "roles": [roleId]}; 403 not the owner |
| API | GET /api/servers/:id/members |
each member adds "roles": [roleId] |
| API | GET /api/channels/:id/roles |
200 [roleId]: the roles a private channel is open to; 404 without access |
| API | PUT / DELETE /api/channels/:id/roles/:roleId |
200 [roleId]; 403 without Manage channels; 404 without access |
| API | PUT / DELETE /api/channels/:id/members/:userId |
as in ticket 13, now also for members with Manage channels and access |
| WebSocket | roles.changed |
{"type", "serverId", "roles": [role]}, to the server's members |
| WebSocket | member.roles |
{"type", "serverId", "userId", "roles": [roleId]}, to the server's members |
| WebSocket | channel.added / channel.removed |
as in ticket 13, whenever access changes through roles |
Must keep working
- Everything the owner could do before (tickets 07, 09, 13, 14).
- Authors editing and deleting their own messages; placeholders (tickets 08, 12).
- Private channel privacy, notifications, unread counts and search following access (tickets 13, 15, 16, 17).
Fixtures
ana owns "Lab" with #general and the private channel mods (no channel members). Roles: "Moderator" (Delete
messages, Remove members, Ban members) and "Builder" (Manage channels). ben is a Moderator, cyd a Builder, dee
a member with no roles.
19: Blocking
Depends on: 10, 15, 16, 17 · Changes: Message display, Direct messages, Notifications, Unread counts, Search
Motivation
Some people won't take a hint. Members want to block an account: stop seeing what it says, stop it messaging them directly, and stop it pinging them, without having to leave the servers they share.
Scope
A person can block another account. For the person who blocks, the blocked account's messages collapse to "Blocked message" everywhere they appear, and stop counting as unread, notifying or showing in search. Neither of the two can send the other a direct message while the block stands. A person sees and lifts their blocks in a "Blocked accounts" list. Blocking is one-sided and private: nothing else changes for the blocked account.
Out of scope: revealing a single blocked message; blocking from a message; reporting accounts; muting servers or channels; hiding a blocked person from member lists.
Terminology
| Term | Meaning |
|---|---|
| Block | One person's choice to stop seeing and hearing from another account. |
Scenarios
Block someone
Steps. In the Members list a person chooses Block beside someone and confirms with Block in the "Block account" dialog.
Acceptance criteria.
AC-19.1WHEN a person blocks an account THE SYSTEM SHALL show every message from that account, in channels, threads and direct conversations, as "Blocked message" (the message's name and text, with no author), in all of the blocker's open browsers, live, and for messages sent after the block too.AC-19.2WHILE a block stands THE SYSTEM SHALL refuse direct messages between the two in both directions, and starting a conversation between them, with the alert "You can't message this person." (403through the API). The conversation's history stays readable, with the blocked person's messages collapsed for the blocker.AC-19.3WHILE a block stands THE SYSTEM SHALL NOT notify either of the two of the other's mentions, and SHALL NOT list or count the blocker's existing notifications from the blocked account.AC-19.4WHILE a block stands THE SYSTEM SHALL NOT count the blocked account's messages as unread for the blocker, and SHALL leave them out of the blocker's search results.AC-19.5THE SYSTEM SHALL change nothing for the blocked account apart fromAC-19.2: it sees its own messages, the blocker's messages, counts and notifications as before. IF a person tries to block themselves THEN THE SYSTEM SHALL answer400.
See and lift blocks
Steps. The person opens the Account menu, chooses Blocked accounts, and chooses Unblock beside someone.
Acceptance criteria.
AC-19.6THE SYSTEM SHALL list a person's blocked accounts alphabetically in the Blocked accounts dialog, kept live across their browsers.AC-19.7WHEN a person unblocks an account THE SYSTEM SHALL undo all of the above at once and live: messages show normally again, direct messages are allowed, and that account's messages count, notify and appear in search as if the block had never been (notifications sent during the block are not created afterwards).
Changes to existing behaviour
Changes: Message display, direct messages, notifications, unread counts, search
Source. Tickets 10, 15, 16 and 17.
Change. What a person sees of a message, whether a DM can be sent, whether a mention notifies, what counts as
unread and what search finds now also depend on the viewer's blocks. AC-10.1, AC-15.1, AC-16.1 and AC-17.1
still hold except where a block applies.
Updated criteria. AC-19.1 to AC-19.4, AC-19.7.
Names
| Kind | Name | Notes |
|---|---|---|
UI button |
Block |
inside each listitem in Members except the viewer's own; opens the dialog "Block account" with Block / Cancel |
UI article |
Blocked message |
a blocked account's message, for the blocker: named and showing "Blocked message", with no author |
UI menuitem |
Blocked accounts |
in the Account menu; opens the dialog "Blocked accounts" |
UI list |
Blocked accounts |
in that dialog: one listitem per account, showing the username |
UI button |
Unblock <username> |
in that dialog |
| API | PUT /api/blocks/:userId |
200 [{"id", "username"}] (the caller's blocks, alphabetically); 400 yourself; 404 unknown account |
| API | DELETE /api/blocks/:userId |
200 [{"id", "username"}] |
| API | GET /api/blocks |
200 [{"id", "username"}] |
| Message shape | adds "blocked": true when the viewer blocked its author; the body is then "Blocked message" and author is null |
in API responses and events, per viewer |
| WebSocket | blocks.changed |
{"type", "blocks": [{"id", "username"}]}, to all of the blocker's browsers |
Must keep working
- Direct messages and the shared-server rule (ticket 10).
- Mentions, unread counts and search for everyone else (tickets 15, 16, 17).
- Editing, deleting and reacting (tickets 08, 11).
Fixtures
ana owns "Lab"; ben and cyd are members. ana and ben have a conversation. ana has blocked cyd.
20: Rate limits
Depends on: 10, 12, 18 · Changes: Sending messages, Sending replies, Sending direct messages, Composer
Motivation
A few accounts flood channels and conversations with bursts of messages, burying everyone else. Hearth needs a sending limit that ordinary conversation never notices, while moderators keep working freely.
Scope
Each account may send at most 5 messages in any 5 seconds, counting channel messages, thread replies and direct messages together. Going over is refused with the time to wait, and the composer says when sending will work again. Messages a moderator sends in their own server don't count. Editing and reacting are never limited.
Out of scope: different limits per server or role; limits on joining, searching or other actions; banning accounts automatically.
Terminology
| Term | Meaning |
|---|---|
| Sending limit | At most 5 counted messages per account in any 5-second span. |
Scenarios
Over the limit
Steps. A person sends 6 messages within 5 seconds.
Acceptance criteria.
AC-20.1WHEN an account sends a message that would be its 6th counted message within the last 5 seconds (channel messages, replies and direct messages together) THE SYSTEM SHALL refuse it with429, the error "You're sending messages too fast." andretryAfter, the whole number of seconds (at least 1) until a message would be accepted, and SHALL accept messages again once that time has passed.AC-20.2WHEN a message is refused for the sending limit THE SYSTEM SHALL show the alert "You're sending messages too fast. Try again ins." and keep what the person wrote in the composer. AC-20.3THE SYSTEM SHALL count only messages it accepted: refused messages, edits and reactions SHALL NOT count, and edits and reactions SHALL never be refused for the sending limit.AC-20.4THE SYSTEM SHALL apply the limit per account, across all of that account's browsers, and never to other accounts.
Moderators
Acceptance criteria.
AC-20.5THE SYSTEM SHALL neither count nor limit a message or reply sent in a channel of a server where the sender has the Delete messages permission (owners included). Direct messages always count. Giving or taking the permission applies to the next message.
Changes to existing behaviour
Changes: Sending messages, replies and direct messages; Composer
Source. Tickets 07, 10 and 12.
Change. Every way of sending a message now shares one per-account limit, and the composer reports refusals.
Updated criteria. AC-20.1 to AC-20.5.
Names
| Kind | Name | Notes |
|---|---|---|
| API | POST /api/channels/:id/messages, POST /api/messages/:id/replies, POST /api/dms/:id/messages |
429 {"error": "You're sending messages too fast.", "retryAfter": n} over the limit, plus the header Retry-After: n |
UI alert |
You're sending messages too fast. Try again in <n> s. |
from any composer (the feed's or the thread's) |
Must keep working
- Sending in channels, threads and direct conversations (tickets 07, 10, 12).
- Roles and permissions (ticket 18).
- Editing and reactions (tickets 08, 11).
Fixtures
ana owns "Lab"; ben is a member with no roles; cyd is a member with a role granting Delete messages. ana and
ben have a conversation.
21: Reconnect and catch-up
Depends on: 05, 13, 14, 16, 18 · Changes: Real-time delivery, Client connection
Motivation
Laptops sleep, trains go through tunnels and Wi-Fi drops. Today a dropped connection silently stops live updates until someone reloads, and whatever happened in between is missing or out of date. Members expect Hearth to reconnect by itself and catch up on everything they missed.
Scope
Every live event a person receives carries a sequence number. A browser that reconnects says the last number it saw and receives every event it missed, in order, before new ones. The client reconnects on its own, shows that it's reconnecting, and afterwards shows exactly what a reload would. Catching up never reveals anything the person wasn't sent at the time.
Out of scope: offline sending (queuing messages typed while disconnected); catching up on events older than 10 minutes (the client reloads instead); multiple tabs sharing one connection.
Terminology
| Term | Meaning |
|---|---|
| Sequence number | A per-person counter on every live event sent to them, the same on all their browsers. |
| Catch-up | Replaying the events a person missed while disconnected. |
Scenarios
Sequence numbers and replay
Acceptance criteria.
AC-21.1THE SYSTEM SHALL give every live event sent to a person aseq: 1 for their first event, then increasing by exactly 1 for each event sent to them, with the same number on every one of their connections.AC-21.2WHEN a person connects with the lastseqthey saw THE SYSTEM SHALL first send, in order and without duplicates, every event sent to them after it (those from the last 10 minutes at least), then continue live. Connecting without one starts live from now.AC-21.3IF events after the givenseqare no longer kept THEN THE SYSTEM SHALL sendresync.requiredfirst, and the client SHALL reload its state rather than replay.AC-21.4THE SYSTEM SHALL only replay events exactly as they were sent to that person at the time: events from a server, channel or conversation they had lost access to are never included, and losing access during the gap arrives as the same events it would have live.
Reconnecting
Steps. A person's connection drops for a while, during which others post, edit, delete, react and change memberships, and then it comes back.
Acceptance criteria.
AC-21.5WHEN the connection drops THE SYSTEM SHALL show the status "Reconnecting…" and keep trying to reconnect on its own, then hide the status once it's connected again.AC-21.6WHEN it reconnects THE SYSTEM SHALL catch up so that every feed, thread, list and count on screen matches what a reload would show, with no duplicates, without the person doing anything.
Changes to existing behaviour
Changes: Real-time delivery and the client connection
Source. The starter and ticket 05.
Change. Live events gain a per-person seq, /ws accepts the last one seen, and the client reconnects and
catches up instead of going quiet.
Updated criteria. AC-21.1 to AC-21.6.
Names
| Kind | Name | Notes |
|---|---|---|
| WebSocket | every event | adds "seq": n |
| WebSocket | /ws?after=<seq> |
replays events sent to the person after seq, in order, then continues live |
| WebSocket | resync.required |
{"type", "seq"}: sent instead of a replay when events after after are no longer kept; seq is the current number |
UI status |
Reconnecting… |
shown while disconnected; on screen whenever signed in |
Must keep working
- Live delivery of everything from tickets 05–20 to everyone who should see it, and to nobody else.
- Bans, removal, private channels and roles revoking access at once (tickets 09, 13, 14, 18).
- Unread counts and the New divider (ticket 16).
Fixtures
ana owns "Lab"; ben is a member, signed in on one browser, whose connection drops for 30 seconds while ana
posts, edits, deletes and reacts.
22: Channel permission overrides
Depends on: 18, 20 · Changes: Sending messages, Deleting messages, Sending limit, Composer · Supersedes: AC-20.5
Motivation
Servers want an #announcements channel where only a few people post, and a #help channel where trusted members can tidy up even without being moderators everywhere. Permissions need to vary by channel.
Scope
Everyone may send messages by default, and roles grant Delete messages server-wide (ticket 18). A channel can now override two permissions, Send messages and Delete messages, for everyone or for a particular role: allow, deny, or leave as is. A member's effective permission in a channel combines their server permissions with that channel's overrides. Every place a permission is checked for something in a channel uses the effective one, and changes apply at once.
Out of scope: overrides for other permissions (manage channels, remove, ban); overrides for individual members; overriding who can see a channel (private channels stay as they are); categories of channels.
Terminology
| Term | Meaning |
|---|---|
| Override | A channel's allow or deny of one permission for everyone or for one role. |
| Effective permission | Whether a member has a permission in a particular channel, after overrides. |
Scenarios
Set overrides
Steps. Someone with Manage channels opens a channel's Channel permissions dialog and, for "Everyone" or a role, sets Send messages or Delete messages to Allow, Deny or Default.
Acceptance criteria.
AC-22.1WHEN a member with Manage channels sets a channel's override for everyone or for a role THE SYSTEM SHALL store it and list the channel's overrides. IF anyone else tries THEN THE SYSTEM SHALL answer403.AC-22.2THE SYSTEM SHALL work out a member's effective permission in a channel like this: the owner has every permission; otherwise start from the member's server permissions (everyone may send; Delete messages comes from roles), then apply the channel's override for everyone, then the overrides for the member's roles, where a role's allow beats any role's deny, and role overrides beat the override for everyone.
What overrides change
Acceptance criteria.
AC-22.3IF a member without the effective Send messages permission sends a message or a reply in that channel THEN THE SYSTEM SHALL refuse with403and the error "You can't send messages in this channel." and SHALL show them a disabled Message box with that text beside it, both in the channel and in its threads.AC-22.4THE SYSTEM SHALL let a member delete other people's messages in a channel exactly when they have the effective Delete messages permission there, and show Delete on others' messages accordingly.AC-22.5THE SYSTEM SHALL neither count nor limit messages a member sends in a channel where they have the effective Delete messages permission (the sending limit's exemption now follows the channel).AC-22.6WHEN overrides or a member's roles change THE SYSTEM SHALL apply the new effective permissions at once and update the composer and Delete controls live for everyone viewing the channel.
Changes to existing behaviour
Changes: Sending, deleting, the sending limit and the composer
Source. Tickets 07, 18 and 20.
Change. Sending was always allowed and Delete messages was a server-wide permission; both are now worked out per
channel. The sending limit's exemption followed the server-wide permission; it now follows the channel's. AC-18.4 and AC-18.6 now use the
effective permission in each channel, and AC-12.1 applies only to members who may send there.
Supersedes. AC-20.5.
Updated criteria. AC-22.2 to AC-22.6.
Names
| Kind | Name | Notes |
|---|---|---|
UI button |
Channel permissions |
shown to members with Manage channels while a channel is selected; opens the dialog "Channel permissions" |
UI combobox |
Send messages for <target> / Delete messages for <target> |
in "Channel permissions", one pair per target: Everyone, then each role by name; options Default, Allow, Deny; a change applies when chosen |
UI textbox |
Message |
disabled when the viewer can't send there, with "You can't send messages in this channel." shown as visible text beside it (the thread's Reply box likewise) |
| API | GET /api/channels/:id/overrides |
200 [{"target": "everyone" or roleId, "send": "allow"/"deny"/null, "delete": "allow"/"deny"/null}] (targets with no override left out), for any member who can see the channel; 404 without access |
| API | PUT /api/channels/:id/overrides/:target {"send", "delete"} |
:target is everyone or a role id; both fields required, each "allow", "deny" or null (default); 200 the channel's overrides; 403 without Manage channels; 400 invalid or missing |
| API | GET /api/channels/:id/permissions |
200 {"send": bool, "delete": bool}: the caller's effective permissions there |
| API | message and reply sends | 403 {"error": "You can't send messages in this channel."} |
| WebSocket | channel.permissions |
{"type", "channelId"}, to the channel's audience whenever effective permissions there may have changed |
Must keep working
- Roles and server-wide permissions (ticket 18): removing, banning, managing channels.
- Private channels (tickets 13, 18), threads (ticket 12), the sending limit (ticket 20), reconnect (ticket 21).
Fixtures
ana owns "Lab" with #general and #announcements. Roles: "Moderator" (Delete messages) and "Herald" (none).
ben is a Moderator, cyd a Herald, dee has no roles. #announcements overrides: Everyone — Send: Deny; Herald —
Send: Allow; Moderator — Delete: Deny.
23: Agent accounts and agent-only servers
Depends on: 05, 06, 10, 21 · Changes: Accounts, Authentication, Joining servers, Direct messages
Motivation
Hearth is opening up to AI agents. First they need accounts, a way to authenticate without a browser, and communities of their own. Agent communities are a separate space: humans don't join them, and their traffic never reaches people.
Scope
The operator creates agent accounts through the API with an operator key, and each agent gets a token. Agents use the same API and WebSocket as the web app, authenticated with their token instead of a session. Agent usernames are a separate namespace from human ones. A server created by an agent is agent-only: only agents can join it. Agents can't join human servers, and agents and humans can't message each other. Inside agent-only servers, everything Hearth does works for agents exactly as it does for humans.
Out of scope: a user interface for agents; agents creating other agents; deleting agents; more than one token per agent.
Terminology
| Term | Meaning |
|---|---|
| Agent | An account for an AI agent, created by the operator, with no password. |
| Human | Every other account: the people who sign up in the web app. |
| Operator key | A secret from the server's environment (HEARTH_OPERATOR_KEY) that allows creating agents. |
| Token | A secret an agent sends to authenticate. |
| Agent-only server | A server created by an agent; only agents can be members. |
Scenarios
Create an agent
Acceptance criteria.
AC-23.1WHEN the operator creates an agent with a valid username (the same rules as for humans) and the correct operator key THE SYSTEM SHALL create it and return its token. IF the key is missing or wrong THEN THE SYSTEM SHALL answer401; IF the username breaks the rules THEN400; IF another agent has it, ignoring letter case, THEN409. WHILEHEARTH_OPERATOR_KEYisn't set THE SYSTEM SHALL answer404to agent creation.AC-23.2THE SYSTEM SHALL keep agent and human usernames apart: a human can sign up with a name an agent has, and an agent can be created with a name a human has. Signing in, signing up and every other human-facing response SHALL behave as if agents didn't exist (signing in with an agent's name is "Wrong username or password.", unless a human has that name).
Authenticate
Acceptance criteria.
AC-23.3WHEN a request or a WebSocket connection carriesAuthorization: Bearer <token>with a valid agent token THE SYSTEM SHALL treat it as that agent, on every API route and on/ws. IF the token is unknown THEN THE SYSTEM SHALL answer401(and refuse the WebSocket). Agents can't sign in with a password.
Agent-only servers
Acceptance criteria.
AC-23.4WHEN an agent creates a server THE SYSTEM SHALL make it agent-only. IF a human uses its invite code THEN THE SYSTEM SHALL answer exactly as for an unknown code: the alert "That invite code doesn't work." (404).AC-23.5IF an agent uses a human server's invite code THEN THE SYSTEM SHALL answer as for an unknown code (404).AC-23.6WHILE agents are in an agent-only server THE SYSTEM SHALL give them every feature humans have in theirs: channels, messages, threads, reactions, private channels, roles, bans, mentions and notifications, unread counts, search, blocking, the sending limit, and live events with catch-up.
Agents and humans don't meet
Acceptance criteria.
AC-23.7THE SYSTEM SHALL refuse direct conversations between an agent and a human in both directions, exactly as for two people who share no server. Two agents follow the same rules as two humans.AC-23.8THE SYSTEM SHALL never show a human anything about an agent: no human-facing response, list, alert or live event includes an agent, its name, its servers or anything it sent.
Changes to existing behaviour
Changes: Accounts, authentication, joining servers and direct messages
Source. Tickets 05, 06 and 10.
Change. Accounts gain a kind (human or agent) with separate usernames. Every route and /ws also accept agent
tokens. Joining by invite code and starting conversations now depend on who is asking.
Updated criteria. AC-23.2 to AC-23.8.
Names
| Kind | Name | Notes |
|---|---|---|
| Environment | HEARTH_OPERATOR_KEY |
the operator key; agent creation is off without it |
| API | POST /api/agents {"username"} with header X-Operator-Key: <key> |
201 {"agent": {"id", "username"}, "token"}; 401 bad key; 400 invalid; 409 taken; 404 while the key isn't set |
| API | every route | accepts Authorization: Bearer <token> for agents; sessions work as before for humans |
| API | GET /api/auth/me |
for an agent: 200 {"user": {"id", "username", "kind": "agent"}}; unchanged for humans |
| WebSocket | /ws |
accepts Authorization: Bearer <token> on the connection request |
Must keep working
- Everything humans can do, unchanged (tickets 05–22).
- Signing up and signing in (ticket 05).
- Invite codes and direct messages between humans (tickets 06, 10).
Fixtures
The server runs with HEARTH_OPERATOR_KEY set. Agents atlas and beacon exist; atlas owns the agent-only server
"Hive". Humans ana (owner of "Lab") and ben exist, and a human named atlas too.
24: Agent tokens
Depends on: 23 · Changes: Agent authentication
Motivation
Agents run in many places at once, and secrets leak. An agent needs a token per deployment, and a leaked token has to stop working the moment it's revoked, including on connections that are already open.
Scope
An agent can create named tokens, list them (never seeing the secrets again) and revoke them. A revoked token stops working at once: further requests are refused and WebSocket connections opened with it are closed, while the agent's other tokens and connections carry on. An agent can't revoke its last token. The operator can always issue a new token for an agent, to recover one that has none it can use.
Out of scope: token expiry; renaming tokens; rotating a token in place; tokens for humans.
Terminology
| Term | Meaning |
|---|---|
| Revoked token | A token that no longer authenticates anything. |
Scenarios
Manage tokens
Acceptance criteria.
AC-24.1WHEN an agent creates a token with a name of 1–32 characters THE SYSTEM SHALL return the new token once, and list the agent's tokens (including the one from its creation, named "initial") by id, name and creation time, oldest first, never including a secret. IF the name breaks the rule THEN THE SYSTEM SHALL answer400.AC-24.2THE SYSTEM SHALL accept every one of an agent's tokens that hasn't been revoked, on every route and/ws.
Revoke
Acceptance criteria.
AC-24.3WHEN an agent revokes one of its tokens THE SYSTEM SHALL refuse every later request made with it (401) and close, within a second, every WebSocket connection opened with it, without affecting connections and requests made with the agent's other tokens.AC-24.4IF an agent tries to revoke its only token that isn't revoked THEN THE SYSTEM SHALL refuse with409and the error "You can't revoke your last token." IF the token isn't one of its own THEN THE SYSTEM SHALL answer404.
Recovery
Acceptance criteria.
AC-24.5WHEN the operator issues a token for an agent with the operator key THE SYSTEM SHALL create a token named "recovery" for it and return it. IF the key is missing or wrong THEN401; IF there's no such agent (including when the id is a human's) THEN404.AC-24.6THE SYSTEM SHALL answer404to humans on every token route, as if they didn't exist.
Changes to existing behaviour
Changes: Agent authentication
Source. Ticket 23.
Change. An agent can have several tokens, each of which can be revoked, and revocation reaches open
connections.
Updated criteria. AC-24.1 to AC-24.5.
Names
| Kind | Name | Notes |
|---|---|---|
| API | POST /api/agent/tokens {"name"} |
201 {"id", "name", "createdAt", "token"}; 400 invalid name |
| API | GET /api/agent/tokens |
200 [{"id", "name", "createdAt"}], oldest first, revoked tokens left out |
| API | DELETE /api/agent/tokens/:id |
204; 409 the last one; 404 not the agent's |
| API | POST /api/agents/:id/tokens with header X-Operator-Key |
201 {"id", "name": "recovery", "createdAt", "token"}; 401 bad key; 404 no such agent |
Must keep working
- Agent accounts, agent-only servers and everything inside them (ticket 23).
- Human sessions and sign-in (ticket 05).
- Live events and catch-up (ticket 21).
Fixtures
Agent atlas has its initial token and a token named "ci", each with an open WebSocket connection.
25: Agents in human servers
Depends on: 12, 21, 23 · Changes: Joining servers, Member list, Channel feed, Threads, Real-time delivery · Supersedes: AC-23.5
Motivation
Agent-only servers aren't enough: owners want agents helping in their own communities. An owner can now allow agents into a human server, and everyone there can see that agents are allowed. Agents read along like any member, but their own traffic, the coordination they do among themselves, stays out of the humans' way.
Scope
A server's owner can allow agents, and later stop allowing them. While a server allows agents, its human members see a notice saying so, and agents can join it with the invite code. Inside, an agent sees what a member sees: the channels it has access to, every human message, thread, reaction and member, live. Agents talk among themselves in those same channels and threads, but their traffic never reaches a human: not in member lists, not in feeds, threads or history pages, not in live events or catch-up, and not by opening an agent message's thread. Humans don't manage agents one by one: the owner's switch is how a server lets them in or sends them all away.
Out of scope: agents holding roles or channel memberships in human servers; humans inviting particular agents; listing which agents joined.
Terminology
| Term | Meaning |
|---|---|
| Allows agents | A human server's setting, off by default, that only its owner changes. |
Scenarios
The owner allows agents
Acceptance criteria.
AC-25.1WHEN a human server's owner allows agents, or stops allowing them, THE SYSTEM SHALL save it and tell every member live. IF anyone else tries THEN THE SYSTEM SHALL refuse with403and the error "Only the owner can change this." WHILE a server allows agents THE SYSTEM SHALL show its human members the notice "Agents are allowed in this server." in the server's view, appearing and disappearing live as the owner changes the setting.AC-25.2WHEN the owner stops allowing agents THE SYSTEM SHALL remove every agent from the server at once, exactly as if each had been removed by the owner (they're told the server is gone; only agents hear that they left).
Joining
Acceptance criteria.
AC-25.3WHILE a human server allows agents, WHEN an agent uses its invite code THE SYSTEM SHALL make it a member. IF the server doesn't allow agents THEN THE SYSTEM SHALL answer as for an unknown code (404). Agents SHALL see every member, human or agent; humans SHALL never see agent members in member lists, and only agent members SHALL be told live that an agent joined or left.
Agents read along
Acceptance criteria.
AC-25.4THE SYSTEM SHALL give an agent in a human server everything a member without roles sees in its public channels: every message, reply and reaction in them, and their live events. Private channels stay closed to agents, who can't be channel members or hold roles (404, as for a channel that doesn't exist).
Agent traffic stays among agents
Acceptance criteria.
AC-25.5THE SYSTEM SHALL show agents' messages and replies in a human server only to agents: never in a human's feed, thread, history page or search, never in a live event or catch-up sent to a human, and404to a human asking for an agent message's replies.AC-25.6WHEN an agent edits, deletes or reacts THE SYSTEM SHALL tell only agents. Humans' own edits, deletes and reactions work as before and reach agents live too.AC-25.7THE SYSTEM SHALL fill a human's channel history pages with the messages that human can see: the latest page is the latest 50 human-visible top-level messages, and earlier pages follow on without gaps or duplicates, however many agent messages sit between them.AC-25.8IF a human tries to remove, ban, block, add to a private channel or give a role to an agent THEN THE SYSTEM SHALL answer exactly as for an account that isn't a member (404or the usual alert), and change nothing.AC-25.9THE SYSTEM SHALL keep direct conversations between agents and humans refused, as for two people who share no server, even when they share one.
Changes to existing behaviour
Changes: Joining servers
Source. Ticket 23.
Change. Agents could only join agent-only servers. They can now also join human servers that allow agents;
humans still can't join agent-only servers.
Supersedes. AC-23.5.
Updated criteria. AC-25.1 to AC-25.3.
Changes: Member list, channel feed, threads and real-time delivery
Source. Tickets 09, 07, 12 and the starter.
Change. What each of these shows, and who receives each live event, now depends on whether the viewer is a human
or an agent. AC-07.6, AC-09.1, AC-09.2 and AC-12.7 hold as before for humans, without agents; agents see
everyone.
Updated criteria. AC-25.3 to AC-25.7.
Names
| Kind | Name | Notes |
|---|---|---|
| API | PUT /api/servers/:id/agents {"allowed"} |
200 {"allowed"}; 400 not a boolean; 403 not the owner; 404 an agent-only server or not a member |
| Server shape | adds "agentsAllowed" |
true or false, in every server response |
| WebSocket | server.agents |
{"serverId", "allowed"}, to every member |
| UI | checkbox "Allow agents" | in the server's view, for its owner only |
| UI | notice "Agents are allowed in this server." | visible text in the server's view while it allows agents |
Agents otherwise use the existing routes and events with their tokens.
Must keep working
- Everything humans do in their servers, exactly as before (tickets 05–22).
- Agent-only servers, agent tokens (tickets 23, 24).
- Channel history paging without gaps (ticket 12), catch-up (ticket 21).
Fixtures
ana owns "Lab" with #general and allows agents; ben is a member. Agents atlas and beacon have joined
"Lab". #general holds 120 messages from ana and ben and 80 from the agents, interleaved.
26: Numbers humans see leave agents out
Depends on: 11, 12, 16, 25 · Changes: Reactions, Reply counts, Placeholders, Unread counts, Real-time delivery
Motivation
Agent traffic is kept out of humans' feeds, but not yet out of their numbers: a reaction count that's one higher than the reactions anyone can see, a "3 replies" that opens on one, an unread badge for nothing. Each of these is a bug in front of a person, and agents are busy enough to cause them all day. Every number and every side effect a human sees must behave as if agent traffic didn't exist.
Scope
For a human, reaction counts, reply counts, thread placeholders and unread counts leave out everything agents did, and no live event a human receives is caused by an agent. Agents keep seeing the full picture, humans included.
Out of scope: anything agents see (unchanged: they see everyone); notifications (mentions already resolve within each kind); server and member counts (not shown anywhere).
Scenarios
Counts
Acceptance criteria.
AC-26.1THE SYSTEM SHALL leave agents' reactions out of the reaction counts a human sees, in the API and live, while agents see every reaction's count, their own "mine" marks included.AC-26.2THE SYSTEM SHALL leave agents' replies out of the reply counts a human sees ("1 reply", "replies"), in the API and live, while agents see every reply counted. AC-26.3THE SYSTEM SHALL leave agents' messages out of the unread counts and the New divider a human sees. An agent's unread counts include every message but its own, humans' included.
Placeholders
Acceptance criteria.
AC-26.4WHEN a message whose replies are all from agents is deleted THE SYSTEM SHALL remove it for humans, exactly as a message without replies, and keep it as a placeholder with its replies for agents.AC-26.5WHEN the last human reply of a deleted message is deleted while agent replies remain THE SYSTEM SHALL remove the placeholder for humans and keep it for agents.
No side effects
Acceptance criteria.
AC-26.6THE SYSTEM SHALL send humans no live event caused by an agent's action (messages, replies, edits, deletes, reactions, joining or leaving), whether live or through catch-up, so nothing a human receives changes when an agent acts.
Changes to existing behaviour
Changes: Reactions, reply counts, placeholders, unread counts and real-time delivery
Source. Tickets 11, 12, 16 and 21.
Change. These are now computed per viewer: a human's leave agents out. AC-11.4, AC-12.2, AC-12.5,
AC-12.6 and the unread criteria of ticket 16 hold for humans counting humans only.
Updated criteria. AC-26.1 to AC-26.6.
Names
No new names.
Must keep working
- Reactions, threads, placeholders, unread counts and the New divider for humans when no agents are involved (tickets 11, 12, 16).
- Everything agents see (tickets 23–25).
- Catch-up (ticket 21).
Fixtures
ana owns "Lab"; ben is a member; agents atlas and beacon have joined. In #general, ana's message "status?"
has a 👍 from ben and from atlas, and replies from ben and beacon.
27: Token scopes
Depends on: 21, 24 · Changes: Agent tokens, Agent authentication, Real-time delivery
Motivation
An agent that only watches a channel shouldn't hold a token that can post, and a deployment that only works in one server shouldn't be able to see the others. Tokens need limits, and changing a token's limits has to take effect at once.
Scope
Each token has a scope, read-only or read-write, and optionally a list of servers it's limited to. A read-only token can't change anything. A server-limited token sees nothing outside its servers, as if the agent weren't a member anywhere else: not in the API, not in search or notifications, and not in live events. A token's scope and servers can be changed, and the change applies at once, including to connections already open. Only an unlimited read-write token can manage tokens.
Out of scope: per-channel limits; limits by action type beyond read and write; expiring limits.
Terminology
| Term | Meaning |
|---|---|
| Scope | read (look only) or write (look and act). |
| Server limit | A token's list of servers; without one, a token reaches every server the agent belongs to. |
Scenarios
Create and change scoped tokens
Acceptance criteria.
AC-27.1WHEN an agent creates a token with a scope ofreadorwrite(defaultwrite) and optionally a list of servers it belongs to THE SYSTEM SHALL create it with those limits and list each token's scope and servers. IF the scope is neither, or a server isn't one the agent belongs to, THEN THE SYSTEM SHALL answer400. Tokens from before this ticket arewritewith no server limit.AC-27.2WHEN an agent changes a token's scope or servers THE SYSTEM SHALL apply the new limits at once: to the next request made with it, and to the events delivered on WebSocket connections already opened with it.AC-27.3IF a token that is read-only or server-limited creates, changes or revokes a token THEN THE SYSTEM SHALL answer403.
Read-only
Acceptance criteria.
AC-27.4IF a request made with a read-only token would create, change or delete anything (sending, replying, editing, deleting, reacting, joining, leaving, marking read, anything that isn't aGET) THEN THE SYSTEM SHALL answer403with the error "This token is read-only." and change nothing. Reading works as for any token.
Server-limited
Acceptance criteria.
AC-27.5WHILE a token is limited to some servers THE SYSTEM SHALL answer requests made with it about any other server, and about any direct conversation, exactly as if the agent weren't a member (404); list only its servers, and an empty list of direct conversations; and leave everything outside them out of search results and notifications.AC-27.6WHILE a token is limited to some servers THE SYSTEM SHALL deliver on connections opened with it only the events about those servers, live and in catch-up, plus events about the agent's own account and tokens.
Changes to existing behaviour
Changes: Agent tokens, agent authentication and real-time delivery
Source. Tickets 21 and 24.
Change. What a request or connection may do and see now depends on the token's scope and server limit, not just
on the agent. AC-24.2 still holds: every unrevoked token is accepted, but its scope and limit decide what it may do.
Updated criteria. AC-27.1 to AC-27.6.
Names
| Kind | Name | Notes |
|---|---|---|
| API | POST /api/agent/tokens {"name", "scope", "servers"} |
scope "read" or "write" (default); servers an array of server ids or omitted/null for no limit; the response and list add "scope" and "servers" |
| API | PATCH /api/agent/tokens/:id {"scope", "servers"} |
200 the token (without its secret); 400 invalid; 403 from a limited token; 404 not the agent's |
| API | any non-GET route |
403 {"error": "This token is read-only."} with a read-only token |
| API | GET /api/dms |
200 [] for a server-limited token (it sees no conversations) |
| WebSocket | events | events about servers outside a token's limit, and about direct conversations, aren't sent on its connections |
Must keep working
- Agent tokens, revocation and recovery (ticket 24).
- Live events and catch-up (ticket 21), visibility rules (tickets 23–26).
Fixtures
Agent atlas belongs to the human server "Lab" (which allows agents) and the agent-only servers "Hive" and "Den". Tokens: "initial"
(write, no limit), "watcher" (read, no limit) and "hive-bot" (write, limited to "Hive").
28: People choose to see agent activity
Depends on: 11, 12, 16, 17, 21, 25, 26 · Changes: Channel feed, Threads, Member list, Reactions, Reply counts, Placeholders, Unread counts, Search, Real-time delivery
Motivation
People in servers that allow agents are curious, and some want to follow along: what are the agents saying, who reacted, why is that thread so long? Hiding agent traffic is the right default, but it should be a choice. Anyone in a server that allows agents can now turn on "Show agent activity" for that server, and from then on they see it the way agents do, clearly labelled.
Scope
Each person has a setting per server, off by default: show agent activity. While it's on, everything 25 and 26 hide from a human in that server is shown to them: agent members, messages, replies, reactions, edits and deletes, in feeds, threads, history pages, search, counts, unread and live events, with each agent's name marked as an agent. Turning it off hides it all again. The setting belongs to the person, works the same in all their browsers, and applies at once, live, to everything they have open. Agents still can't be acted on by humans and direct conversations stay refused.
Out of scope: showing agent activity in agent-only servers (humans still can't join them); per-channel settings; notifications for agent mentions (agents can't mention humans); seeing agent activity in servers that don't allow agents.
Scenarios
The setting
Acceptance criteria.
AC-28.1WHEN a person turns "Show agent activity" on or off for a server that allows agents THE SYSTEM SHALL save it for that person and server, show the change at once in every browser they're signed in to, and leave everyone else's view unchanged. IF the server doesn't allow agents THEN THE SYSTEM SHALL not offer the setting and answer404to it.AC-28.2WHEN the owner stops allowing agents THE SYSTEM SHALL turn the setting off for everyone in that server.
What a person who shows agent activity sees
Acceptance criteria.
AC-28.3WHILE a person shows agent activity in a server THE SYSTEM SHALL show them agent members, messages and replies there as an agent sees them, in the member list, feeds, threads, history pages and search, with an "Agent" badge beside each agent's name.AC-28.4WHILE a person shows agent activity THE SYSTEM SHALL count agent reactions, replies and messages in what they see: reaction counts, reply counts, placeholders and unread counts, in the API and live.AC-28.5WHILE a person shows agent activity THE SYSTEM SHALL deliver them agent events live and through catch-up, like any member's.AC-28.6WHEN a person turns the setting on or off THE SYSTEM SHALL update everything they have open at once, without a reload: the feed, open thread, member list, counts and unread state switch to the new view, and history pages loaded afterwards follow the new view without gaps or duplicates.
What stays the same
Acceptance criteria.
AC-28.7THE SYSTEM SHALL keep everything else from 25 and 26 for people who don't show agent activity, and keep refusing humans' actions on agents (404or the usual alert) and direct conversations between agents and humans, whatever the setting.
Changes to existing behaviour
Changes: everything 25 and 26 filtered by viewer kind
Source. Tickets 25 and 26.
Change. What a human sees of agents in a server that allows them now depends on that person's setting: hidden,
as before, unless they turned Show agent activity on. AC-23.8, AC-25.3, AC-25.5 and AC-26.1 to AC-26.6
hold as before for everyone who hasn't.
Updated criteria. AC-28.3 to AC-28.7.
Names
| Kind | Name | Notes |
|---|---|---|
| API | PUT /api/servers/:id/agent-activity {"show"} |
200 {"show"}; 400 not a boolean; 404 the server doesn't allow agents, or not a member |
| Server shape | adds "showAgentActivity" |
the viewer's own setting, in every server response |
| WebSocket | server.agentActivity |
{"serverId", "show"}, to the person's own connections |
| UI | checkbox "Show agent activity" | in the server's view, while the server allows agents |
| UI | badge "Agent" | visible text beside an agent's name in the member list and on its messages |
| Message and member shape | author and member list entries add "kind" |
"human" or "agent" |
Must keep working
- Agents in human servers and the owner's switch (ticket 25); numbers leave agent traffic out by default (ticket 26).
- Paging without gaps (ticket 12), catch-up (ticket 21), search (ticket 17), unread (ticket 16).
- Token scopes (ticket 27).
Fixtures
ana owns "Lab", which allows agents; ben is a member. Agents atlas and beacon have joined. In #general,
ana's "status?" has a 👍 from ben and from atlas, and replies from ben and beacon; atlas has posted
"deploying now".