Build Bettermode

KINDAreplaces $49/mosaves $588/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 community platform where the interesting engineering is making spaces, permissions and feeds behave predictably, and the governing design is that every moderation action is notifiable, explicable and reversible from the first version.

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

RuntimeTypeScript with Fastify and PostgreSQLSpacesNested spaces with inherited permissionsModerationNotice, decision and statement of reasons as first-class recordsFeedsComputed per member with permission filtering in the queryNotificationBatched digests with per-member control

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 and the digest worker.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. installfree locally

    Why Nested spaces, permissions and moderation records with real constraints.

    Get it Docker or your package manager. open ↗

    Verify psql connects and a recursive space query returns correctly

  3. decidefree to read; advice worth having if the community is commercial

    Why They depend on where your members are and how you are established, and the notice and statement mechanisms are built in phase two rather than added later.

    Get it Read the hosting provider obligations for the jurisdictions your members are in and write down what each requires of you. open ↗

    Verify A written note names the jurisdictions and the specific obligations

  4. decidefree

    Why A statement of reasons has to reference something, and rules written after the first contested removal are rules written to justify it.

    Get it Write the rules, the categories a notice can use, and what each restriction level means.

    Verify Written rules exist and every restriction level has a stated meaning

  5. have readyfree, and it requires people's patience

    Why Permission edge cases, notification tolerance and moderation load are all social rather than technical, and none of them appear with five test accounts.

    Get it A genuine group willing to use it, including at least two moderators who are not you.

    Verify Thirty members have used it for a fortnight with at least one real moderation decision

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- spaces(id, parent_id, name, visibility, join_policy, archived_at)
- members(id, user_id, space_id, role, joined_at, suspended_until, banned_at)
- posts(id, space_id, author_id, title, body, created_at, edited_at, state) · state is visible, restricted or removed
- notices(id, target_kind, target_id, reporter_id, category, body, received_at, decided_at) · the report mechanism
- decisions(notice_id, outcome, reason, decided_by, at, statement_sent_at) · what was done and why
- restrictions(id, target_kind, target_id, kind, reason, applied_by, at, lifted_at) · reversible by design
- appeals(decision_id, body, submitted_at, outcome, at)
- A restriction is a record with a lifting path rather than a deletion, because a removal you cannot reverse is a removal you cannot correct.

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/communityMembers' posts and moderation records, including reports about identified people.
NOTICE_FORM_PUBLICrequiredtrueThe notice mechanism must be available to anyone, not only members.
REQUIRE_DECISION_REASONrequiredalwaysPresent as a constant. A removal you cannot explain is one you cannot defend.
NOTIFICATION_MODErequireddigestPer-reply email is how a community loses its notifications to spam filters.
APPEAL_ROUTE_AWAYrequiredtrueAppeals go to someone other than the original decider where possible.
MODERATION_RETENTION_YEARSrequired3Decisions and statements outlive the content they concerned.

The build, in order

  1. Spaces and permissions that hold

    Who can see and do what is decided in the query, not the interface.

    1. Interface filtering means the data was already sent.

      Files src/permissions.tstests/permission-leak.test.ts

    2. Nested spaces confuse everyone if the rule is implicit.

      Files docs/permissions.md

    3. A revoked role that persists until logout is not revoked.

      Files src/roles.ts

    done when · tick each as it passes
    watch out
    • Filtering content by permission in the interface while the API returns everything means a private space's posts are one network inspection away from anyone, and in a community that is a breach of the trust that made it private. Filter in the query and assert absence across list, single-item and error responses.
  2. Anyone can report, and it goes somewhere

    A notice mechanism exists, is easy to find and produces a tracked record.

    1. The obligation is not limited to members.

      Files src/notices.ts

    2. An inbox has no state and produces no evidence of what was done.

      Files src/notices.ts

    3. The reporter should know it arrived.

      Files src/notify.ts

    done when · tick each as it passes
    watch out
    • A report button that sends an email to a shared inbox produces no record, no state and no way to show what was done, which fails the obligation and also fails the community. Make a notice a database record with a lifecycle from the first version.
  3. Decisions that explain themselves

    Every restriction produces a statement of reasons to the affected member.

    1. Moderators forget within a week and members ask later.

      Files src/decisions.ts

    2. Not as a moderator's optional courtesy.

      Files src/statements.ts

    3. A removal you cannot reverse is one you cannot correct.

      Files src/restrictions.ts

    done when · tick each as it passes
    watch out
    • Removing a post without recording why makes the decision impossible to explain when the member asks, and moderators genuinely forget within a week. Require a reason at decision time, send it, and keep the record, so a contested removal is a conversation about a stated reason rather than about memory.
  4. Appeals, because moderators are wrong sometimes

    A member can contest a decision and get an answer.

    1. Including the ones that seem obvious.

      Files src/appeals.ts

    2. Where possible. Otherwise it is a formality members see through.

      Files src/appeals.ts

    3. A high rate means moderation is going wrong, which is worth knowing.

      Files src/moderation-report.ts

    done when · tick each as it passes
    watch out
    • An appeal reviewed by the same moderator who made the decision is a formality, and members work that out quickly and stop appealing, which removes your only signal that moderation is going wrong. Route to a different reviewer where the community is large enough, and report the success rate either way.
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.