Lee ByrdAsking the dumb questions

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):

  • Dockerfile
  • client/index.html
  • client/src/App.tsx: the chat screen
  • client/src/api.ts: the client's API calls
  • client/src/main.tsx
  • client/src/styles.css: the palette and styles
  • drizzle.config.ts
  • e2e/chat.spec.ts: a browser test (Playwright)
  • package.json
  • playwright.config.ts
  • server/app.ts: the HTTP API and the WebSocket
  • server/db/index.ts
  • server/db/migrations/0000_init.sql: the first migration
  • server/db/reset.ts
  • server/db/schema.ts: the database schema (Drizzle)
  • server/index.ts
  • server/log.ts
  • server/seed.ts
  • shared/types.ts: types the client and server share
  • tests/api.test.ts: an API test (Vitest), on its own fresh database
  • tsconfig.json
  • vite.config.ts
  • vitest.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.

Given to the agent: the kickoff prompt
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:

Given to the agent: AGENTS.md (stock)
# 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:

Given to the agent: README.md
# 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.

  1. They see the sign-in screen: Username, Password, the Sign in button and a Create account button.
  2. They enter a username and password and choose Create account.
  3. They're signed in and see the feed and the composer.

Acceptance criteria.

  • AC-05.1 WHEN 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.2 IF 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.3 IF 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.4 WHEN 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.5 WHILE signed in THE SYSTEM SHALL keep the session across page reloads, until the person signs out.
  • AC-05.6 WHEN 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.7 WHEN 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.8 IF a request reads or sends messages without a session THEN THE SYSTEM SHALL answer 401 and change nothing.
  • AC-05.9 THE SYSTEM SHALL deliver live message events only to signed-in browsers.
Look

Acceptance criteria.

  • AC-05.10 THE 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 --accent with 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.1 WHEN 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.2 IF 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.3 WHEN a member opens Invite THE SYSTEM SHALL show the server's invite code, the same code every time.
  • AC-06.4 WHEN 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.5 IF 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.6 THE SYSTEM SHALL list a person's servers in the order they joined them, and only those.
  • AC-06.7 WHEN 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.8 WHEN 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.9 IF a person who isn't a member reads or sends a server's messages through the API THEN THE SYSTEM SHALL answer 404 and change nothing.
  • AC-06.10 WHILE 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.1 WHEN a server is created THE SYSTEM SHALL give it a channel named general.
  • AC-07.2 WHEN a member selects a server THE SYSTEM SHALL list its channels in the order they were created and select general.
  • AC-07.3 WHEN 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.4 IF 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 answer 403.
Talk in a channel

Acceptance criteria.

  • AC-07.5 WHEN 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.6 WHEN 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.7 THE SYSTEM SHALL show the messages a server had before channels existed in its general channel, in their original order, with their original authors and times.
  • AC-07.8 IF a person who isn't a member of the channel's server reads or sends its messages THEN THE SYSTEM SHALL answer 404.
Look

Acceptance criteria.

  • AC-07.9 WHILE 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 --bg as 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.1 WHEN 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.2 WHEN a message is edited THE SYSTEM SHALL show the new text and the "(edited)" mark live to everyone viewing that channel.
  • AC-08.3 IF 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.4 WHEN 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.5 WHEN the author cancels the confirmation THE SYSTEM SHALL keep the message.
Only the author

Acceptance criteria.

  • AC-08.6 THE SYSTEM SHALL show Edit and Delete only on messages the viewer wrote.
  • AC-08.7 IF anyone but the author edits or deletes a message through the API THEN THE SYSTEM SHALL answer 403 and change nothing. IF the message doesn't exist or isn't visible to them THEN 404.
Look

Acceptance criteria.

  • AC-08.8 THE 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 404 for 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.1 WHEN a member views a server THE SYSTEM SHALL list its members by username, alphabetically, with the owner marked "Owner".
  • AC-09.2 WHEN 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.3 WHEN 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.4 IF the owner tries to leave THEN THE SYSTEM SHALL refuse with the alert "Owners can't leave their own server." (and answer 403 through 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.5 WHEN the owner confirms removing a member THE SYSTEM SHALL end that membership. IF anyone else tries THEN THE SYSTEM SHALL answer 403.
  • AC-09.6 WHEN 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.7 WHILE someone isn't a member THE SYSTEM SHALL answer 404 to their requests for the server's channels and messages, including ones they could read before.
  • AC-09.8 WHEN 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.9 WHILE 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.1 WHEN 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.2 IF 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.3 THE 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.4 WHEN 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.5 WHEN someone receives the first message of a new conversation THE SYSTEM SHALL add it to their Direct messages list live.
  • AC-10.6 THE SYSTEM SHALL let participants edit and delete their own direct messages with the same rules as channel messages (ticket 08).
  • AC-10.7 IF anyone other than the two participants reads or sends in a direct conversation THEN THE SYSTEM SHALL answer 404.
  • AC-10.8 WHEN 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 with 403 and 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.1 WHEN 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.2 WHEN they choose the same emoji again THE SYSTEM SHALL remove their reaction, and the emoji disappears when its count reaches zero.
  • AC-11.3 THE 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.4 WHEN reactions change THE SYSTEM SHALL update the counts live for everyone viewing the message, showing each viewer only their own "mine" marks.
  • AC-11.5 THE SYSTEM SHALL show reactions in palette order, and keep them across reloads.
  • AC-11.6 IF the emoji isn't in the palette THEN THE SYSTEM SHALL answer 400. IF the person can't see the message THEN 404.
  • AC-11.7 WHEN 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.1 WHEN 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.2 THE SYSTEM SHALL show on a parent message "1 reply" or " replies", updated live for everyone viewing the channel.
  • AC-12.3 WHEN 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.4 IF the parent isn't a channel message, or is itself a reply, THEN THE SYSTEM SHALL answer 400.
Delete a message with replies

Acceptance criteria.

  • AC-12.5 WHEN 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.6 WHEN the last reply of a deleted parent is deleted THE SYSTEM SHALL remove the placeholder from the channel.
  • AC-12.9 WHEN 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.7 WHEN 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.8 WHEN 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.1 WHEN 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.2 WHEN 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.3 WHEN 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.4 WHEN 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, select general for them.
  • AC-13.5 THE 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.6 IF 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 answer 403.
Privacy

Acceptance criteria.

  • AC-13.7 IF 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 answer 404, as for a channel that doesn't exist.
  • AC-13.8 WHILE 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.9 WHEN 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.1 WHEN 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.2 WHILE someone is banned from a server THE SYSTEM SHALL refuse their invite-code joins with the alert "You're banned from this server." (403 through the API) and not add them.
  • AC-14.3 THE SYSTEM SHALL keep a banned person's messages in the server's channels, with their author.
  • AC-14.4 IF anyone but the owner bans or unbans, or the owner tries to ban themselves, THEN THE SYSTEM SHALL answer 403. IF the person isn't a member THEN THE SYSTEM SHALL answer 404.
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.5 THE SYSTEM SHALL list a server's banned people in the Bans dialog, alphabetically, kept live, and only for the owner (403 for anyone else through the API).
  • AC-14.6 WHEN 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.1 WHEN 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.2 THE SYSTEM SHALL match a mention as @ followed by a username in any letter case, not followed by another letter, digit or underscore (so "@Ben," mentions ben, 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.3 THE 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.4 THE 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.5 WHEN a person chooses a notification THE SYSTEM SHALL select that message's server and channel.
Edits, deletes and lost access

Acceptance criteria.

  • AC-15.6 WHEN 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.7 WHEN 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.8 THE 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.1 THE 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.2 THE 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.3 WHEN an unread message is deleted, or becomes a placeholder, THE SYSTEM SHALL stop counting it, live.
  • AC-16.4 WHEN 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.5 WHILE 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.6 WHEN 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

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.1 WHEN 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.2 THE 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.3 THE 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.4 THE SYSTEM SHALL search messages as they are now: an edit changes what matches at once, and deleted messages and placeholders never match.
  • AC-17.5 WHEN 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.6 IF 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 answer 400 through 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.1 WHEN 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 answer 403.
  • AC-18.2 WHEN 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.3 THE 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.4 THE 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.5 THE 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.6 IF 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 answer 403, 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.7 WHEN 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.8 IF 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 " with one 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.1 WHEN 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.2 WHILE 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." (403 through the API). The conversation's history stays readable, with the blocked person's messages collapsed for the blocker.
  • AC-19.3 WHILE 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.4 WHILE 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.5 THE SYSTEM SHALL change nothing for the blocked account apart from AC-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 answer 400.
See and lift blocks

Steps. The person opens the Account menu, chooses Blocked accounts, and chooses Unblock beside someone.

Acceptance criteria.

  • AC-19.6 THE SYSTEM SHALL list a person's blocked accounts alphabetically in the Blocked accounts dialog, kept live across their browsers.
  • AC-19.7 WHEN 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

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.1 WHEN 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 with 429, the error "You're sending messages too fast." and retryAfter, 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.2 WHEN a message is refused for the sending limit THE SYSTEM SHALL show the alert "You're sending messages too fast. Try again in s." and keep what the person wrote in the composer.
  • AC-20.3 THE 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.4 THE SYSTEM SHALL apply the limit per account, across all of that account's browsers, and never to other accounts.
Moderators

Acceptance criteria.

  • AC-20.5 THE 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.1 THE SYSTEM SHALL give every live event sent to a person a seq: 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.2 WHEN a person connects with the last seq they 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.3 IF events after the given seq are no longer kept THEN THE SYSTEM SHALL send resync.required first, and the client SHALL reload its state rather than replay.
  • AC-21.4 THE 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.5 WHEN 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.6 WHEN 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.1 WHEN 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 answer 403.
  • AC-22.2 THE 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.3 IF a member without the effective Send messages permission sends a message or a reply in that channel THEN THE SYSTEM SHALL refuse with 403 and 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.4 THE 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.5 THE 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.6 WHEN 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.1 WHEN 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 answer 401; IF the username breaks the rules THEN 400; IF another agent has it, ignoring letter case, THEN 409. WHILE HEARTH_OPERATOR_KEY isn't set THE SYSTEM SHALL answer 404 to agent creation.
  • AC-23.2 THE 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.3 WHEN a request or a WebSocket connection carries Authorization: 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 answer 401 (and refuse the WebSocket). Agents can't sign in with a password.
Agent-only servers

Acceptance criteria.

  • AC-23.4 WHEN 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.5 IF an agent uses a human server's invite code THEN THE SYSTEM SHALL answer as for an unknown code (404).
  • AC-23.6 WHILE 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.7 THE 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.8 THE 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.1 WHEN 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 answer 400.
  • AC-24.2 THE 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.3 WHEN 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.4 IF an agent tries to revoke its only token that isn't revoked THEN THE SYSTEM SHALL refuse with 409 and the error "You can't revoke your last token." IF the token isn't one of its own THEN THE SYSTEM SHALL answer 404.
Recovery

Acceptance criteria.

  • AC-24.5 WHEN 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 THEN 401; IF there's no such agent (including when the id is a human's) THEN 404.
  • AC-24.6 THE SYSTEM SHALL answer 404 to 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.1 WHEN 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 with 403 and 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.2 WHEN 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.3 WHILE 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.4 THE 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.5 THE 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, and 404 to a human asking for an agent message's replies.
  • AC-25.6 WHEN 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.7 THE 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.8 IF 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 (404 or the usual alert), and change nothing.
  • AC-25.9 THE 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.1 THE 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.2 THE 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.3 THE 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.4 WHEN 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.5 WHEN 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.6 THE 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.1 WHEN an agent creates a token with a scope of read or write (default write) 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 answer 400. Tokens from before this ticket are write with no server limit.
  • AC-27.2 WHEN 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.3 IF a token that is read-only or server-limited creates, changes or revokes a token THEN THE SYSTEM SHALL answer 403.
Read-only

Acceptance criteria.

  • AC-27.4 IF 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 a GET) THEN THE SYSTEM SHALL answer 403 with the error "This token is read-only." and change nothing. Reading works as for any token.
Server-limited

Acceptance criteria.

  • AC-27.5 WHILE 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.6 WHILE 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.1 WHEN 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 answer 404 to it.
  • AC-28.2 WHEN 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.3 WHILE 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.4 WHILE 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.5 WHILE a person shows agent activity THE SYSTEM SHALL deliver them agent events live and through catch-up, like any member's.
  • AC-28.6 WHEN 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.7 THE SYSTEM SHALL keep everything else from 25 and 26 for people who don't show agent activity, and keep refusing humans' actions on agents (404 or 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".