Build Twist

KINDAreplaces $8/mosaves $96/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort multi-daythe files for this build are in the project pack

RuntimeTypeScript with Fastify and PostgreSQLDeliveryLong polling or sockets for updates, without presenceNotificationsBatched digests with per-person schedules and quiet hoursRead modelPer-thread unread state with a catch-up orderingSearchFull text across threads with participant and date filters

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

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

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

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

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

  5. free 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.

VariableNeededExampleWhere the value comes from
DATABASE_URLsecretrequiredpostgres://localhost:5432/twistThreads and messages. A team's internal discussion, so treat backups accordingly.
COLLECT_PRESENCErequiredneverPresent as a constant so the answer is visible in configuration. There is no other value.
DIGEST_DEFAULT_TIMESrequired09:00,16:00When digests go out by default. Each person overrides this.
QUIET_HOURS_DEFAULTrequired18:00-08:00When digests defer. The default that keeps the tool calm from the first week.
URGENT_DAILY_LIMITrequired3How many urgent notifications one person may send per day. Scarcity is what preserves the meaning.
SMTP_URLsecretrequiredsmtp://user:pass@host:587Digest delivery. Without it the notification design cannot be evaluated.

The build, in order

  1. Threads with titles, not a scrolling channel

    Every conversation has a subject and a boundary.

    1. No channel-level messages. The friction is the design.

      Files src/schema.sql

    2. A subject is what makes a conversation findable and skippable.

      Files src/threads.ts

    3. Channel membership meaning thread membership recreates the firehose.

      Files src/participants.ts

    done when · tick each as it passes
    watch 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.
  2. Removing the things that create pressure

    There is no way to know whether a colleague is at their desk.

    1. Not stored, not transmitted. A field that exists gets displayed.

      Files src/transport.tstests/no-presence.test.ts

    2. Read receipts between colleagues are the same pressure by another name.

      Files src/reads.ts

    3. A precise time on a two-day-old message invites reading response speed into it.

      Files src/display.ts

    done when · tick each as it passes
    watch 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.
  3. Notifications as a batch, not an interruption

    You are told what happened on a schedule you chose.

    1. Per person. The recipient's schedule, not the sender's urgency.

      Files src/digests.ts

    2. Possible, awkward, and attributed. That combination keeps it rare.

      Files src/urgent.ts

    3. Its absence is the product. Adding it later undoes the rest.

      Files src/notify.ts

    done when · tick each as it passes
    watch 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.
  4. Coming back after a week

    A week away is catchable up in twenty minutes, not abandoned.

    1. Not chronologically. Chronological order is what makes a week unreadable.

      Files src/catchup.ts

    2. Deliberately skipping a thread is different from never seeing it.

      Files src/reads.ts

    3. A concluded thread needs a different amount of attention.

      Files src/threads.ts

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more community.