Build Twist
KINDAreplaces $8/mosaves $96/yrback to the verdict
A team messaging tool designed around the absence of presence: threads instead of a scrolling channel, no typing indicators or online status, notifications batched rather than immediate, and a read model that lets someone return after a week and catch up without reading everything.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the server, the transport and the digest worker.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - installfree locally
Why Threads, participation and read state with full text search.
Get it Docker or your package manager. open ↗
Verify
psql connects and full text search returns a ranked result - have readyfree, and it costs the team some patience
Why Every claim here is behavioural rather than technical, and a threaded tool with two users behaves nothing like one with eight. The catch-up design in particular can only be judged by someone who was genuinely away.
Get it A small team who will use it as their actual channel for two weeks, including at least one person taking a week off during it.
Verify
At least five people used it for a fortnight and one returned from a week away - decidefree
Why Every request to add immediate notification will be reasonable in isolation, and the accumulation of reasonable exceptions is how this becomes an ordinary chat tool.
Get it Write down what counts as urgent in your team, and what people should do instead when something genuinely cannot wait.
Verify
A written note states the urgency policy and the alternative channel for real emergencies - accountfree tiers cover a small team
Why The notification design is the product, and it cannot be evaluated without digests actually arriving on schedule.
Get it Any transactional email provider, or a local mail catcher for development.
Verify
A digest arrives at the scheduled time and respects quiet hours
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- workspaces(id, name, created_at) - channels(id, workspace_id, name, purpose, archived_at) - threads(id, channel_id, title, created_by, created_at, last_activity_at, resolved_at) - messages(id, thread_id, author_id, body, at, edited_at) - thread_participants(thread_id, user_id, joined_at, muted, left_at) · joining a thread is deliberate, not automatic - reads(user_id, thread_id, last_read_message_id, at) - digests(user_id, scheduled_for, sent_at, thread_ids_json) · notification as a batch, not an event - There is deliberately no presence table, no typing indicator and no last-seen timestamp, because storing them makes showing them a later decision rather than an impossibility.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_URLsecret | required | postgres://localhost:5432/twist | Threads and messages. A team's internal discussion, so treat backups accordingly. |
COLLECT_PRESENCE | required | never | Present as a constant so the answer is visible in configuration. There is no other value. |
DIGEST_DEFAULT_TIMES | required | 09:00,16:00 | When digests go out by default. Each person overrides this. |
QUIET_HOURS_DEFAULT | required | 18:00-08:00 | When digests defer. The default that keeps the tool calm from the first week. |
URGENT_DAILY_LIMIT | required | 3 | How many urgent notifications one person may send per day. Scarcity is what preserves the meaning. |
SMTP_URLsecret | required | smtp://user:pass@host:587 | Digest delivery. Without it the notification design cannot be evaluated. |
The build, in order
Threads with titles, not a scrolling channel
Every conversation has a subject and a boundary.
No channel-level messages. The friction is the design.
Files
src/schema.sqlA subject is what makes a conversation findable and skippable.
Files
src/threads.tsChannel membership meaning thread membership recreates the firehose.
Files
src/participants.ts
done when · tick each as it passeswatch out- Allowing messages directly in a channel alongside threads means the channel becomes the default and threads become the exception within a week, which is the tool you were trying not to build. Make the thread the only container for a message, even though it adds friction to starting one.
Removing the things that create pressure
There is no way to know whether a colleague is at their desk.
Not stored, not transmitted. A field that exists gets displayed.
Files
src/transport.tstests/no-presence.test.tsRead receipts between colleagues are the same pressure by another name.
Files
src/reads.tsA precise time on a two-day-old message invites reading response speed into it.
Files
src/display.ts
done when · tick each as it passeswatch out- Storing presence data because it might be useful later guarantees it will be displayed eventually, since the field exists and someone will ask for it. Do not collect it: the absence has to be structural rather than a display choice, or the product becomes an ordinary messenger in six months.
Notifications as a batch, not an interruption
You are told what happened on a schedule you chose.
Per person. The recipient's schedule, not the sender's urgency.
Files
src/digests.tsPossible, awkward, and attributed. That combination keeps it rare.
Files
src/urgent.tsIts absence is the product. Adding it later undoes the rest.
Files
src/notify.ts
done when · tick each as it passeswatch out- Adding an immediate notification option because someone asks for it makes it the default within a month, and then the tool has all the interruption of a chat application plus threads. Make urgency possible but effortful, and make the effort visible to the person being interrupted.
Coming back after a week
A week away is catchable up in twenty minutes, not abandoned.
Not chronologically. Chronological order is what makes a week unreadable.
Files
src/catchup.tsDeliberately skipping a thread is different from never seeing it.
Files
src/reads.tsA concluded thread needs a different amount of attention.
Files
src/threads.ts
done when · tick each as it passeswatch out- Presenting a week's absence as a list of four hundred unread messages guarantees the person marks everything read and misses the two things that mattered, which is the exact failure the whole design was meant to prevent. Order by whether they are a participant and whether a thread resolved, and make skipping a thread a first-class action.
Finding the decision, and leaving with itproduct builder
You can find what was decided six months ago, and take the archive with you.
The thread is the unit of meaning. A message alone is a fragment.
Files
src/search.tsWho was in it and when matters as much as what it said.
Files
src/search.tsIt is the team's record of its own decisions.
Files
src/export.ts
done when · tick each as it passeswatch out- Search that returns individual messages out of their thread gives you a sentence with no context, which in a threaded tool is the one thing you had that a channel-based tool does not. Return the thread with the matching message highlighted in place.
That is the whole plan for Twist. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Voice and video calling.
- Integrations posting into channels.
- Mobile applications with push.
- Guest access with per-thread permissions.
- cross-platform apps, notifications, email integration, search, and Doist reliability
- network effects
- native mobile apps
- video and voice infrastructure
- enterprise moderation and discovery
- A weekly summary of resolved threads, which is what a manager actually wants
- Thread ownership, so a conversation has someone responsible for concluding it
- Linking a thread to whatever tracks the work it produced
Need the files? The project pack on the verdict page hands your agent the whole brief · more community.