Vibecode Bettermode
track this build4 phases, 12 steps, beginner friendly0%The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Bettermode, publish a branded community portal with spaces, posts, and member profiles. The hard boundary is embeds, apis, customization, analytics, moderation, and enterprise integrations, plus network, moderation, and communication infrastructure.
You are building a lean indie version of Bettermode. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Bettermode · indie build 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-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Fastify and PostgreSQL | posts, membership and permissions are relational | | Spaces | Nested spaces with inherited permissions | the permission model is the part that becomes unfixable if it is wrong | | Moderation | Notice, decision and statement of reasons as first-class records | these are obligations rather than features | | Feeds | Computed per member with permission filtering in the query | a feed that filters after fetching has already fetched | | Notification | Batched digests with per-member control | a community that emails on every reply is a community people leave | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the digest worker. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Nested spaces, permissions and moderation records with real constraints. - Get it: Docker or your package manager. - Verify: psql connects and a recursive space query returns correctly - [ ] **Which obligations apply to you, written down** · free 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. - Verify: A written note names the jurisdictions and the specific obligations - [ ] **Your community rules and moderation policy** · free - 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 - [ ] **A real community of at least thirty people** · free, 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Legal advice on which obligations apply. - Automated content classification. - Trending and recommendation. - Monetisation and paid memberships. - embeds, APIs, customization, analytics, moderation, and enterprise integrations - network effects - native mobile apps - video and voice infrastructure - enterprise moderation and discovery If one of those is essential to you, that is the reason to keep paying for Bettermode, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Bettermode The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a community platform whose moderation obligations are built in rather than added later to replace Bettermode. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Hosting other people's posts makes you a hosting service provider, and in the European Union that carries obligations under the Digital Services Act regardless of how small you are. The distinction worth understanding: Article 19 exempts micro and small enterprises from the additional obligations placed on online platforms, but it does not exempt anyone from the hosting provider obligations. Article 16 requires an accessible electronic notice mechanism letting anyone report content they consider illegal, and Article 17 requires a statement of reasons to the affected user when you restrict content. So a notice mechanism and a statement of reasons are not features to add when the community grows. They are the baseline, and building them into the moderation model from the start costs a phase, whereas retrofitting them means reconstructing decisions you did not record. The second thing: moderation decisions are the most contested part of any community, so every one of them is recorded with who, when and why, and every restriction is reversible. A community whose moderators cannot explain a removal loses its members' trust faster than it loses them to the content it removed. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Fastify and PostgreSQL · posts, membership and permissions are relational - Spaces: Nested spaces with inherited permissions · the permission model is the part that becomes unfixable if it is wrong - Moderation: Notice, decision and statement of reasons as first-class records · these are obligations rather than features - Feeds: Computed per member with permission filtering in the query · a feed that filters after fetching has already fetched - Notification: Batched digests with per-member control · a community that emails on every reply is a community people leave ### Data model (create this before Phase 1) - 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. ### Phase 1 · Spaces and permissions that hold Build: Nested spaces with visibility and join policies, and roles whose permissions are evaluated server side on every read. Verify that a private space's content never appears in any API response for a non-member. Trap: 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. Done when: A private space's posts are absent from every API response for a non-member, asserted across response shapes in the suite. Nested spaces inherit permissions predictably and the inheritance is documented. Changing a space's visibility takes effect immediately for existing sessions. Role changes apply without requiring a re-login. Do not build yet: No posting, moderation or feeds. ### Phase 2 · Anyone can report, and it goes somewhere Build: An accessible electronic notice form available to any visitor, not only members, capturing what is being reported and why. Every notice becomes a tracked record with a state. Acknowledge receipt to the reporter. Trap: 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. Done when: Any visitor including a non-member can submit a notice electronically. Every notice creates a tracked record with a state and an acknowledgement to the reporter. Notices are queued for review with the reported content in context. A notice about already-removed content is handled rather than erroring. Do not build yet: No decisions, statements or appeals yet. ### Phase 3 · Decisions that explain themselves Build: Moderation decisions recording the outcome, the reason and the deciding moderator. Send a statement of reasons to the affected user automatically. Make every restriction reversible and record the reversal. Trap: 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. Done when: A restriction cannot be applied without a recorded reason, enforced in the decision path. The affected member receives a statement of reasons automatically. Every restriction can be lifted and the lifting is recorded. Moderation history for a member is retrievable including reversals. Do not build yet: No appeals, feeds or notifications yet. ### Phase 4 · Appeals, because moderators are wrong sometimes Build: An appeal path against any decision, reviewed by someone other than the original decider where possible. Record the appeal outcome. Report how often appeals succeed. Trap: 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. Done when: Any decision can be appealed and the appeal produces a recorded outcome. Where more than one moderator exists, appeals route to someone other than the original decider. Appeal success rate is reported to moderators. A successful appeal lifts the restriction and records that it did. Do not build yet: No feeds or notifications yet. ### Phase 5 · Feeds and notifications people will tolerate Build: Feeds computed per member with permission filtering in the query. Batched notification digests with per-space control. Handle a member who joins a space with years of history without flooding them. Trap: Notifying on every reply in every space a member has joined produces dozens of emails a day, and the response is not to adjust settings but to filter the whole domain to spam, which removes even the notifications that mattered. Batch by default and make per-space control easy to find. Done when: Feeds filter by permission in the query and never include content the member cannot access. Notifications batch into digests by default with per-space control. Joining a space with years of history does not produce a flood. Unsubscribing works in one click and from a single message. Do not build yet: Nothing after this. ### Out of scope (and why) - Legal advice about which obligations apply to you, which depends on where your members are and how you are established. - Automated content classification, which is unreliable enough that a human decision and a stated reason remain the requirement. - Scale features such as trending and recommendation, which are where the heavier platform obligations begin. - Monetisation, memberships and paid tiers, which is a commerce layer on top of everything here. ### README must contain - That hosting other people's posts makes you a hosting service provider with obligations regardless of size - That the micro and small enterprise exemption covers the additional platform obligations, not the hosting ones - That a notice mechanism must be available to anyone electronically, not only to members - That every restriction requires a recorded reason and produces a statement to the affected member - That every restriction is reversible and reversals are recorded ===== AGENTS.md ===== # Agent instructions · Bettermode indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, Nested spaces with inherited permissions, Notice, decision and statement of reasons as first-class records, Computed per member with permission filtering in the query, Batched digests with per-member control. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · Bettermode 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Spaces and permissions that hold Who can see and do what is decided in the query, not the interface. ### Steps 1. Evaluate permissions in the query Interface filtering means the data was already sent. Files: `src/permissions.ts`, `tests/permission-leak.test.ts` 2. Document inheritance Nested spaces confuse everyone if the rule is implicit. Files: `docs/permissions.md` 3. Apply changes immediately A revoked role that persists until logout is not revoked. Files: `src/roles.ts` ### Done when - [ ] Private content is absent from every response shape - [ ] Inheritance is predictable and documented - [ ] Visibility changes take effect immediately - [ ] Role changes apply without re-login ### 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. ## Phase 2 · Anyone can report, and it goes somewhere A notice mechanism exists, is easy to find and produces a tracked record. ### Steps 1. Make notices available to anyone The obligation is not limited to members. Files: `src/notices.ts` 2. Record rather than email An inbox has no state and produces no evidence of what was done. Files: `src/notices.ts` 3. Acknowledge receipt The reporter should know it arrived. Files: `src/notify.ts` ### Done when - [ ] Any visitor can submit a notice electronically - [ ] Notices create tracked records with states - [ ] Receipt is acknowledged - [ ] Notices about removed content are handled ### 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. ## Phase 3 · Decisions that explain themselves Every restriction produces a statement of reasons to the affected member. ### Steps 1. Require a reason at decision time Moderators forget within a week and members ask later. Files: `src/decisions.ts` 2. Send the statement automatically Not as a moderator's optional courtesy. Files: `src/statements.ts` 3. Make every restriction liftable A removal you cannot reverse is one you cannot correct. Files: `src/restrictions.ts` ### Done when - [ ] A restriction requires a recorded reason - [ ] Statements of reasons send automatically - [ ] Every restriction can be lifted and the lifting recorded - [ ] Member moderation history is retrievable ### 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. ## Phase 4 · Appeals, because moderators are wrong sometimes A member can contest a decision and get an answer. ### Steps 1. Allow appeal against every decision Including the ones that seem obvious. Files: `src/appeals.ts` 2. Route away from the original decider Where possible. Otherwise it is a formality members see through. Files: `src/appeals.ts` 3. Report the success rate A high rate means moderation is going wrong, which is worth knowing. Files: `src/moderation-report.ts` ### Done when - [ ] Every decision can be appealed - [ ] Appeals route away from the original decider where possible - [ ] Appeal success rate is reported - [ ] A successful appeal lifts the restriction ### 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. ## Not in this build - Legal advice on which obligations apply. - Automated content classification. - Trending and recommendation. - Monetisation and paid memberships. ## After v1, if you want it - Moderator workload reporting, since burnout is what actually ends volunteer moderation - Transparency reporting on notices and outcomes, which builds trust and is increasingly expected - Bulk moderation for coordinated abuse, once single decisions are properly recorded ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Members' posts and moderation records, including reports about identified people. DATABASE_URL=postgres://localhost:5432/community # Required. The notice mechanism must be available to anyone, not only members. NOTICE_FORM_PUBLIC=true # Required. Present as a constant. A removal you cannot explain is one you cannot defend. REQUIRE_DECISION_REASON=always # Required. Per-reply email is how a community loses its notifications to spam filters. NOTIFICATION_MODE=digest # Required. Appeals go to someone other than the original decider where possible. APPEAL_ROUTE_AWAY=true # Required. Decisions and statements outlive the content they concerned. MODERATION_RETENTION_YEARS=3
You are building a lean indie version of Bettermode. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Bettermode · indie build 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-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Fastify and PostgreSQL | posts, membership and permissions are relational | | Spaces | Nested spaces with inherited permissions | the permission model is the part that becomes unfixable if it is wrong | | Moderation | Notice, decision and statement of reasons as first-class records | these are obligations rather than features | | Feeds | Computed per member with permission filtering in the query | a feed that filters after fetching has already fetched | | Notification | Batched digests with per-member control | a community that emails on every reply is a community people leave | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the digest worker. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Nested spaces, permissions and moderation records with real constraints. - Get it: Docker or your package manager. - Verify: psql connects and a recursive space query returns correctly - [ ] **Which obligations apply to you, written down** · free 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. - Verify: A written note names the jurisdictions and the specific obligations - [ ] **Your community rules and moderation policy** · free - 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 - [ ] **A real community of at least thirty people** · free, 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Legal advice on which obligations apply. - Automated content classification. - Trending and recommendation. - Monetisation and paid memberships. - embeds, APIs, customization, analytics, moderation, and enterprise integrations - network effects - native mobile apps - video and voice infrastructure - enterprise moderation and discovery If one of those is essential to you, that is the reason to keep paying for Bettermode, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Bettermode The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a community platform whose moderation obligations are built in rather than added later to replace Bettermode. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Hosting other people's posts makes you a hosting service provider, and in the European Union that carries obligations under the Digital Services Act regardless of how small you are. The distinction worth understanding: Article 19 exempts micro and small enterprises from the additional obligations placed on online platforms, but it does not exempt anyone from the hosting provider obligations. Article 16 requires an accessible electronic notice mechanism letting anyone report content they consider illegal, and Article 17 requires a statement of reasons to the affected user when you restrict content. So a notice mechanism and a statement of reasons are not features to add when the community grows. They are the baseline, and building them into the moderation model from the start costs a phase, whereas retrofitting them means reconstructing decisions you did not record. The second thing: moderation decisions are the most contested part of any community, so every one of them is recorded with who, when and why, and every restriction is reversible. A community whose moderators cannot explain a removal loses its members' trust faster than it loses them to the content it removed. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Fastify and PostgreSQL · posts, membership and permissions are relational - Spaces: Nested spaces with inherited permissions · the permission model is the part that becomes unfixable if it is wrong - Moderation: Notice, decision and statement of reasons as first-class records · these are obligations rather than features - Feeds: Computed per member with permission filtering in the query · a feed that filters after fetching has already fetched - Notification: Batched digests with per-member control · a community that emails on every reply is a community people leave ### Data model (create this before Phase 1) - 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. ### Phase 1 · Spaces and permissions that hold Build: Nested spaces with visibility and join policies, and roles whose permissions are evaluated server side on every read. Verify that a private space's content never appears in any API response for a non-member. Trap: 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. Done when: A private space's posts are absent from every API response for a non-member, asserted across response shapes in the suite. Nested spaces inherit permissions predictably and the inheritance is documented. Changing a space's visibility takes effect immediately for existing sessions. Role changes apply without requiring a re-login. Do not build yet: No posting, moderation or feeds. ### Phase 2 · Anyone can report, and it goes somewhere Build: An accessible electronic notice form available to any visitor, not only members, capturing what is being reported and why. Every notice becomes a tracked record with a state. Acknowledge receipt to the reporter. Trap: 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. Done when: Any visitor including a non-member can submit a notice electronically. Every notice creates a tracked record with a state and an acknowledgement to the reporter. Notices are queued for review with the reported content in context. A notice about already-removed content is handled rather than erroring. Do not build yet: No decisions, statements or appeals yet. ### Phase 3 · Decisions that explain themselves Build: Moderation decisions recording the outcome, the reason and the deciding moderator. Send a statement of reasons to the affected user automatically. Make every restriction reversible and record the reversal. Trap: 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. Done when: A restriction cannot be applied without a recorded reason, enforced in the decision path. The affected member receives a statement of reasons automatically. Every restriction can be lifted and the lifting is recorded. Moderation history for a member is retrievable including reversals. Do not build yet: No appeals, feeds or notifications yet. ### Phase 4 · Appeals, because moderators are wrong sometimes Build: An appeal path against any decision, reviewed by someone other than the original decider where possible. Record the appeal outcome. Report how often appeals succeed. Trap: 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. Done when: Any decision can be appealed and the appeal produces a recorded outcome. Where more than one moderator exists, appeals route to someone other than the original decider. Appeal success rate is reported to moderators. A successful appeal lifts the restriction and records that it did. Do not build yet: No feeds or notifications yet. ### Phase 5 · Feeds and notifications people will tolerate Build: Feeds computed per member with permission filtering in the query. Batched notification digests with per-space control. Handle a member who joins a space with years of history without flooding them. Trap: Notifying on every reply in every space a member has joined produces dozens of emails a day, and the response is not to adjust settings but to filter the whole domain to spam, which removes even the notifications that mattered. Batch by default and make per-space control easy to find. Done when: Feeds filter by permission in the query and never include content the member cannot access. Notifications batch into digests by default with per-space control. Joining a space with years of history does not produce a flood. Unsubscribing works in one click and from a single message. Do not build yet: Nothing after this. ### Out of scope (and why) - Legal advice about which obligations apply to you, which depends on where your members are and how you are established. - Automated content classification, which is unreliable enough that a human decision and a stated reason remain the requirement. - Scale features such as trending and recommendation, which are where the heavier platform obligations begin. - Monetisation, memberships and paid tiers, which is a commerce layer on top of everything here. ### README must contain - That hosting other people's posts makes you a hosting service provider with obligations regardless of size - That the micro and small enterprise exemption covers the additional platform obligations, not the hosting ones - That a notice mechanism must be available to anyone electronically, not only to members - That every restriction requires a recorded reason and produces a statement to the affected member - That every restriction is reversible and reversals are recorded ===== AGENTS.md ===== # Agent instructions · Bettermode indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, Nested spaces with inherited permissions, Notice, decision and statement of reasons as first-class records, Computed per member with permission filtering in the query, Batched digests with per-member control. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · Bettermode 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Spaces and permissions that hold Who can see and do what is decided in the query, not the interface. ### Steps 1. Evaluate permissions in the query Interface filtering means the data was already sent. Files: `src/permissions.ts`, `tests/permission-leak.test.ts` 2. Document inheritance Nested spaces confuse everyone if the rule is implicit. Files: `docs/permissions.md` 3. Apply changes immediately A revoked role that persists until logout is not revoked. Files: `src/roles.ts` ### Done when - [ ] Private content is absent from every response shape - [ ] Inheritance is predictable and documented - [ ] Visibility changes take effect immediately - [ ] Role changes apply without re-login ### 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. ## Phase 2 · Anyone can report, and it goes somewhere A notice mechanism exists, is easy to find and produces a tracked record. ### Steps 1. Make notices available to anyone The obligation is not limited to members. Files: `src/notices.ts` 2. Record rather than email An inbox has no state and produces no evidence of what was done. Files: `src/notices.ts` 3. Acknowledge receipt The reporter should know it arrived. Files: `src/notify.ts` ### Done when - [ ] Any visitor can submit a notice electronically - [ ] Notices create tracked records with states - [ ] Receipt is acknowledged - [ ] Notices about removed content are handled ### 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. ## Phase 3 · Decisions that explain themselves Every restriction produces a statement of reasons to the affected member. ### Steps 1. Require a reason at decision time Moderators forget within a week and members ask later. Files: `src/decisions.ts` 2. Send the statement automatically Not as a moderator's optional courtesy. Files: `src/statements.ts` 3. Make every restriction liftable A removal you cannot reverse is one you cannot correct. Files: `src/restrictions.ts` ### Done when - [ ] A restriction requires a recorded reason - [ ] Statements of reasons send automatically - [ ] Every restriction can be lifted and the lifting recorded - [ ] Member moderation history is retrievable ### 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. ## Phase 4 · Appeals, because moderators are wrong sometimes A member can contest a decision and get an answer. ### Steps 1. Allow appeal against every decision Including the ones that seem obvious. Files: `src/appeals.ts` 2. Route away from the original decider Where possible. Otherwise it is a formality members see through. Files: `src/appeals.ts` 3. Report the success rate A high rate means moderation is going wrong, which is worth knowing. Files: `src/moderation-report.ts` ### Done when - [ ] Every decision can be appealed - [ ] Appeals route away from the original decider where possible - [ ] Appeal success rate is reported - [ ] A successful appeal lifts the restriction ### 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. ## Not in this build - Legal advice on which obligations apply. - Automated content classification. - Trending and recommendation. - Monetisation and paid memberships. ## After v1, if you want it - Moderator workload reporting, since burnout is what actually ends volunteer moderation - Transparency reporting on notices and outcomes, which builds trust and is increasingly expected - Bulk moderation for coordinated abuse, once single decisions are properly recorded ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Members' posts and moderation records, including reports about identified people. DATABASE_URL=postgres://localhost:5432/community # Required. The notice mechanism must be available to anyone, not only members. NOTICE_FORM_PUBLIC=true # Required. Present as a constant. A removal you cannot explain is one you cannot defend. REQUIRE_DECISION_REASON=always # Required. Per-reply email is how a community loses its notifications to spam filters. NOTIFICATION_MODE=digest # Required. Appeals go to someone other than the original decider where possible. APPEAL_ROUTE_AWAY=true # Required. Decisions and statements outlive the content they concerned. MODERATION_RETENTION_YEARS=3
You are building a production product version of Bettermode. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Bettermode · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Bettermode, publish a branded community portal with spaces, posts, and member profiles. The hard boundary is embeds, apis, customization, analytics, moderation, and enterprise integrations, plus network, moderation, and communication infrastructure. ## Product outcome A community platform where private spaces are actually private, anyone can report something, every removal comes with a reason the member receives, and every decision can be appealed and reversed. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Docker - PostgreSQL - public HTTPS deployment - email provider - object storage for attachments ## Explicit non-goals for v1 - Legal advice on which obligations apply. - Automated content classification. - Trending and recommendation. - Monetisation and paid memberships. - embeds, APIs, customization, analytics, moderation, and enterprise integrations - network effects - native mobile apps - video and voice infrastructure - enterprise moderation and discovery ## Success criteria - Private content is confirmed absent from every response shape - A notice is confirmed submittable by a non-member - A restriction without a reason is confirmed impossible - A statement of reasons is confirmed sent automatically - Every restriction is confirmed reversible ===== BRIEF.md ===== # Build brief · Bettermode The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a community platform whose moderation obligations are built in rather than added later to replace Bettermode. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Hosting other people's posts makes you a hosting service provider, and in the European Union that carries obligations under the Digital Services Act regardless of how small you are. The distinction worth understanding: Article 19 exempts micro and small enterprises from the additional obligations placed on online platforms, but it does not exempt anyone from the hosting provider obligations. Article 16 requires an accessible electronic notice mechanism letting anyone report content they consider illegal, and Article 17 requires a statement of reasons to the affected user when you restrict content. So a notice mechanism and a statement of reasons are not features to add when the community grows. They are the baseline, and building them into the moderation model from the start costs a phase, whereas retrofitting them means reconstructing decisions you did not record. The second thing: moderation decisions are the most contested part of any community, so every one of them is recorded with who, when and why, and every restriction is reversible. A community whose moderators cannot explain a removal loses its members' trust faster than it loses them to the content it removed. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Fastify and PostgreSQL · posts, membership and permissions are relational - Spaces: Nested spaces with inherited permissions · the permission model is the part that becomes unfixable if it is wrong - Moderation: Notice, decision and statement of reasons as first-class records · these are obligations rather than features - Feeds: Computed per member with permission filtering in the query · a feed that filters after fetching has already fetched - Notification: Batched digests with per-member control · a community that emails on every reply is a community people leave ### Data model (create this before Phase 1) - 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. ### Phase 1 · Spaces and permissions that hold Build: Nested spaces with visibility and join policies, and roles whose permissions are evaluated server side on every read. Verify that a private space's content never appears in any API response for a non-member. Trap: 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. Done when: A private space's posts are absent from every API response for a non-member, asserted across response shapes in the suite. Nested spaces inherit permissions predictably and the inheritance is documented. Changing a space's visibility takes effect immediately for existing sessions. Role changes apply without requiring a re-login. Do not build yet: No posting, moderation or feeds. ### Phase 2 · Anyone can report, and it goes somewhere Build: An accessible electronic notice form available to any visitor, not only members, capturing what is being reported and why. Every notice becomes a tracked record with a state. Acknowledge receipt to the reporter. Trap: 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. Done when: Any visitor including a non-member can submit a notice electronically. Every notice creates a tracked record with a state and an acknowledgement to the reporter. Notices are queued for review with the reported content in context. A notice about already-removed content is handled rather than erroring. Do not build yet: No decisions, statements or appeals yet. ### Phase 3 · Decisions that explain themselves Build: Moderation decisions recording the outcome, the reason and the deciding moderator. Send a statement of reasons to the affected user automatically. Make every restriction reversible and record the reversal. Trap: 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. Done when: A restriction cannot be applied without a recorded reason, enforced in the decision path. The affected member receives a statement of reasons automatically. Every restriction can be lifted and the lifting is recorded. Moderation history for a member is retrievable including reversals. Do not build yet: No appeals, feeds or notifications yet. ### Phase 4 · Appeals, because moderators are wrong sometimes Build: An appeal path against any decision, reviewed by someone other than the original decider where possible. Record the appeal outcome. Report how often appeals succeed. Trap: 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. Done when: Any decision can be appealed and the appeal produces a recorded outcome. Where more than one moderator exists, appeals route to someone other than the original decider. Appeal success rate is reported to moderators. A successful appeal lifts the restriction and records that it did. Do not build yet: No feeds or notifications yet. ### Phase 5 · Feeds and notifications people will tolerate Build: Feeds computed per member with permission filtering in the query. Batched notification digests with per-space control. Handle a member who joins a space with years of history without flooding them. Trap: Notifying on every reply in every space a member has joined produces dozens of emails a day, and the response is not to adjust settings but to filter the whole domain to spam, which removes even the notifications that mattered. Batch by default and make per-space control easy to find. Done when: Feeds filter by permission in the query and never include content the member cannot access. Notifications batch into digests by default with per-space control. Joining a space with years of history does not produce a flood. Unsubscribing works in one click and from a single message. Do not build yet: Nothing after this. ### Out of scope (and why) - Legal advice about which obligations apply to you, which depends on where your members are and how you are established. - Automated content classification, which is unreliable enough that a human decision and a stated reason remain the requirement. - Scale features such as trending and recommendation, which are where the heavier platform obligations begin. - Monetisation, memberships and paid tiers, which is a commerce layer on top of everything here. ### README must contain - That hosting other people's posts makes you a hosting service provider with obligations regardless of size - That the micro and small enterprise exemption covers the additional platform obligations, not the hosting ones - That a notice mechanism must be available to anyone electronically, not only to members - That every restriction requires a recorded reason and produces a statement to the affected member - That every restriction is reversible and reversals are recorded ===== ARCHITECTURE.md ===== # Architecture · Bettermode ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Fastify and PostgreSQL | posts, membership and permissions are relational | | Spaces | Nested spaces with inherited permissions | the permission model is the part that becomes unfixable if it is wrong | | Moderation | Notice, decision and statement of reasons as first-class records | these are obligations rather than features | | Feeds | Computed per member with permission filtering in the query | a feed that filters after fetching has already fetched | | Notification | Batched digests with per-member control | a community that emails on every reply is a community people leave | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Permissions | Query-level access control. | Not swappable for interface filtering, which sends the data anyway. | | Notices | Public electronic reporting with tracked state. | Any implementation, provided it is a record rather than an email. | | Decisions | Reasons and automatic statements. | Any workflow, provided a reason is required and the statement is automatic. | | Appeals | Contesting and reversal. | Any process, provided reversal is possible and recorded. | | Notifications | Batched digests with per-space control. | Any transport, provided per-reply email is not the default. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Members' posts and moderation records, including reports about identified people. - `NOTICE_FORM_PUBLIC` · required · The notice mechanism must be available to anyone, not only members. - `REQUIRE_DECISION_REASON` · required · Present as a constant. A removal you cannot explain is one you cannot defend. - `NOTIFICATION_MODE` · required · Per-reply email is how a community loses its notifications to spam filters. - `APPEAL_ROUTE_AWAY` · required · Appeals go to someone other than the original decider where possible. - `MODERATION_RETENTION_YEARS` · required · Decisions and statements outlive the content they concerned. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Bettermode product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, Nested spaces with inherited permissions, Notice, decision and statement of reasons as first-class records, Computed per member with permission filtering in the query, Batched digests with per-member control. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - Notifying on every reply in every space a member has joined produces dozens of emails a day, and the response is not to adjust settings but to filter the whole domain to spam, which removes even the notifications that mattered. Batch by default and make per-space control easy to find. ===== MILESTONES.md ===== # Delivery milestones · Bettermode Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Spaces and permissions that hold Who can see and do what is decided in the query, not the interface. ### Steps 1. Evaluate permissions in the query Interface filtering means the data was already sent. Files: `src/permissions.ts`, `tests/permission-leak.test.ts` 2. Document inheritance Nested spaces confuse everyone if the rule is implicit. Files: `docs/permissions.md` 3. Apply changes immediately A revoked role that persists until logout is not revoked. Files: `src/roles.ts` ### Done when - [ ] Private content is absent from every response shape - [ ] Inheritance is predictable and documented - [ ] Visibility changes take effect immediately - [ ] Role changes apply without re-login ### 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. ## M2 · Anyone can report, and it goes somewhere A notice mechanism exists, is easy to find and produces a tracked record. ### Steps 1. Make notices available to anyone The obligation is not limited to members. Files: `src/notices.ts` 2. Record rather than email An inbox has no state and produces no evidence of what was done. Files: `src/notices.ts` 3. Acknowledge receipt The reporter should know it arrived. Files: `src/notify.ts` ### Done when - [ ] Any visitor can submit a notice electronically - [ ] Notices create tracked records with states - [ ] Receipt is acknowledged - [ ] Notices about removed content are handled ### 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. ## M3 · Decisions that explain themselves Every restriction produces a statement of reasons to the affected member. ### Steps 1. Require a reason at decision time Moderators forget within a week and members ask later. Files: `src/decisions.ts` 2. Send the statement automatically Not as a moderator's optional courtesy. Files: `src/statements.ts` 3. Make every restriction liftable A removal you cannot reverse is one you cannot correct. Files: `src/restrictions.ts` ### Done when - [ ] A restriction requires a recorded reason - [ ] Statements of reasons send automatically - [ ] Every restriction can be lifted and the lifting recorded - [ ] Member moderation history is retrievable ### 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. ## M4 · Appeals, because moderators are wrong sometimes A member can contest a decision and get an answer. ### Steps 1. Allow appeal against every decision Including the ones that seem obvious. Files: `src/appeals.ts` 2. Route away from the original decider Where possible. Otherwise it is a formality members see through. Files: `src/appeals.ts` 3. Report the success rate A high rate means moderation is going wrong, which is worth knowing. Files: `src/moderation-report.ts` ### Done when - [ ] Every decision can be appealed - [ ] Appeals route away from the original decider where possible - [ ] Appeal success rate is reported - [ ] A successful appeal lifts the restriction ### 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. ## M5 · Feeds and notifications people will tolerate (production only) Members see what matters without being emailed constantly. ### Steps 1. Filter feeds in the query Same rule as phase one. A feed is just another read path. Files: `src/feed.ts` 2. Batch notifications by default Per-reply email is how a community loses its own notifications to spam filters. Files: `src/digests.ts` 3. Handle joining an old space Years of history is not a day's notifications. Files: `src/membership.ts` ### Done when - [ ] Feeds never include inaccessible content - [ ] Notifications batch by default with per-space control - [ ] Joining an old space does not flood - [ ] Unsubscribing works in one click ### Watch out - Notifying on every reply in every space a member has joined produces dozens of emails a day, and the response is not to adjust settings but to filter the whole domain to spam, which removes even the notifications that mattered. Batch by default and make per-space control easy to find. ===== OPERATIONS.md ===== # Operations · Bettermode ## Backup Moderation records are the evidence of how decisions were made and may be needed long after the content is gone. Back them up with a longer retention than the posts themselves. ## Restore Restore and confirm restrictions and their lifted states are correct, since restoring a lifted restriction as active re-removes content that was reinstated, usually for someone who already complained once. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Notice backlog age and appeal success rate. A growing backlog is a moderation capacity problem, and a high appeal success rate means decisions are going wrong systematically rather than occasionally. ## Incident checklist Private content appearing to a non-member is the failure that costs a community its trust, and it means a read path filtered after fetching rather than in the query. Error responses are where it usually reappears. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Private content is confirmed absent from every response shape - [ ] A notice is confirmed submittable by a non-member - [ ] A restriction without a reason is confirmed impossible - [ ] A statement of reasons is confirmed sent automatically - [ ] Every restriction is confirmed reversible ## Launch constraint Do not market omitted Bettermode capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Members' posts and moderation records, including reports about identified people. DATABASE_URL=postgres://localhost:5432/community # Required. The notice mechanism must be available to anyone, not only members. NOTICE_FORM_PUBLIC=true # Required. Present as a constant. A removal you cannot explain is one you cannot defend. REQUIRE_DECISION_REASON=always # Required. Per-reply email is how a community loses its notifications to spam filters. NOTIFICATION_MODE=digest # Required. Appeals go to someone other than the original decider where possible. APPEAL_ROUTE_AWAY=true # Required. Decisions and statements outlive the content they concerned. MODERATION_RETENTION_YEARS=3
# Bettermode · indie build 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-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Fastify and PostgreSQL | posts, membership and permissions are relational | | Spaces | Nested spaces with inherited permissions | the permission model is the part that becomes unfixable if it is wrong | | Moderation | Notice, decision and statement of reasons as first-class records | these are obligations rather than features | | Feeds | Computed per member with permission filtering in the query | a feed that filters after fetching has already fetched | | Notification | Batched digests with per-member control | a community that emails on every reply is a community people leave | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the digest worker. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Nested spaces, permissions and moderation records with real constraints. - Get it: Docker or your package manager. - Verify: psql connects and a recursive space query returns correctly - [ ] **Which obligations apply to you, written down** · free 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. - Verify: A written note names the jurisdictions and the specific obligations - [ ] **Your community rules and moderation policy** · free - 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 - [ ] **A real community of at least thirty people** · free, 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Legal advice on which obligations apply. - Automated content classification. - Trending and recommendation. - Monetisation and paid memberships. - embeds, APIs, customization, analytics, moderation, and enterprise integrations - network effects - native mobile apps - video and voice infrastructure - enterprise moderation and discovery If one of those is essential to you, that is the reason to keep paying for Bettermode, and the README should say so rather than pretend.
# Build brief · Bettermode The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a community platform whose moderation obligations are built in rather than added later to replace Bettermode. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Hosting other people's posts makes you a hosting service provider, and in the European Union that carries obligations under the Digital Services Act regardless of how small you are. The distinction worth understanding: Article 19 exempts micro and small enterprises from the additional obligations placed on online platforms, but it does not exempt anyone from the hosting provider obligations. Article 16 requires an accessible electronic notice mechanism letting anyone report content they consider illegal, and Article 17 requires a statement of reasons to the affected user when you restrict content. So a notice mechanism and a statement of reasons are not features to add when the community grows. They are the baseline, and building them into the moderation model from the start costs a phase, whereas retrofitting them means reconstructing decisions you did not record. The second thing: moderation decisions are the most contested part of any community, so every one of them is recorded with who, when and why, and every restriction is reversible. A community whose moderators cannot explain a removal loses its members' trust faster than it loses them to the content it removed. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Fastify and PostgreSQL · posts, membership and permissions are relational - Spaces: Nested spaces with inherited permissions · the permission model is the part that becomes unfixable if it is wrong - Moderation: Notice, decision and statement of reasons as first-class records · these are obligations rather than features - Feeds: Computed per member with permission filtering in the query · a feed that filters after fetching has already fetched - Notification: Batched digests with per-member control · a community that emails on every reply is a community people leave ### Data model (create this before Phase 1) - 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. ### Phase 1 · Spaces and permissions that hold Build: Nested spaces with visibility and join policies, and roles whose permissions are evaluated server side on every read. Verify that a private space's content never appears in any API response for a non-member. Trap: 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. Done when: A private space's posts are absent from every API response for a non-member, asserted across response shapes in the suite. Nested spaces inherit permissions predictably and the inheritance is documented. Changing a space's visibility takes effect immediately for existing sessions. Role changes apply without requiring a re-login. Do not build yet: No posting, moderation or feeds. ### Phase 2 · Anyone can report, and it goes somewhere Build: An accessible electronic notice form available to any visitor, not only members, capturing what is being reported and why. Every notice becomes a tracked record with a state. Acknowledge receipt to the reporter. Trap: 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. Done when: Any visitor including a non-member can submit a notice electronically. Every notice creates a tracked record with a state and an acknowledgement to the reporter. Notices are queued for review with the reported content in context. A notice about already-removed content is handled rather than erroring. Do not build yet: No decisions, statements or appeals yet. ### Phase 3 · Decisions that explain themselves Build: Moderation decisions recording the outcome, the reason and the deciding moderator. Send a statement of reasons to the affected user automatically. Make every restriction reversible and record the reversal. Trap: 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. Done when: A restriction cannot be applied without a recorded reason, enforced in the decision path. The affected member receives a statement of reasons automatically. Every restriction can be lifted and the lifting is recorded. Moderation history for a member is retrievable including reversals. Do not build yet: No appeals, feeds or notifications yet. ### Phase 4 · Appeals, because moderators are wrong sometimes Build: An appeal path against any decision, reviewed by someone other than the original decider where possible. Record the appeal outcome. Report how often appeals succeed. Trap: 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. Done when: Any decision can be appealed and the appeal produces a recorded outcome. Where more than one moderator exists, appeals route to someone other than the original decider. Appeal success rate is reported to moderators. A successful appeal lifts the restriction and records that it did. Do not build yet: No feeds or notifications yet. ### Phase 5 · Feeds and notifications people will tolerate Build: Feeds computed per member with permission filtering in the query. Batched notification digests with per-space control. Handle a member who joins a space with years of history without flooding them. Trap: Notifying on every reply in every space a member has joined produces dozens of emails a day, and the response is not to adjust settings but to filter the whole domain to spam, which removes even the notifications that mattered. Batch by default and make per-space control easy to find. Done when: Feeds filter by permission in the query and never include content the member cannot access. Notifications batch into digests by default with per-space control. Joining a space with years of history does not produce a flood. Unsubscribing works in one click and from a single message. Do not build yet: Nothing after this. ### Out of scope (and why) - Legal advice about which obligations apply to you, which depends on where your members are and how you are established. - Automated content classification, which is unreliable enough that a human decision and a stated reason remain the requirement. - Scale features such as trending and recommendation, which are where the heavier platform obligations begin. - Monetisation, memberships and paid tiers, which is a commerce layer on top of everything here. ### README must contain - That hosting other people's posts makes you a hosting service provider with obligations regardless of size - That the micro and small enterprise exemption covers the additional platform obligations, not the hosting ones - That a notice mechanism must be available to anyone electronically, not only to members - That every restriction requires a recorded reason and produces a statement to the affected member - That every restriction is reversible and reversals are recorded
# Agent instructions · Bettermode indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, Nested spaces with inherited permissions, Notice, decision and statement of reasons as first-class records, Computed per member with permission filtering in the query, Batched digests with per-member control. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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.
# Build plan · Bettermode 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Spaces and permissions that hold Who can see and do what is decided in the query, not the interface. ### Steps 1. Evaluate permissions in the query Interface filtering means the data was already sent. Files: `src/permissions.ts`, `tests/permission-leak.test.ts` 2. Document inheritance Nested spaces confuse everyone if the rule is implicit. Files: `docs/permissions.md` 3. Apply changes immediately A revoked role that persists until logout is not revoked. Files: `src/roles.ts` ### Done when - [ ] Private content is absent from every response shape - [ ] Inheritance is predictable and documented - [ ] Visibility changes take effect immediately - [ ] Role changes apply without re-login ### 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. ## Phase 2 · Anyone can report, and it goes somewhere A notice mechanism exists, is easy to find and produces a tracked record. ### Steps 1. Make notices available to anyone The obligation is not limited to members. Files: `src/notices.ts` 2. Record rather than email An inbox has no state and produces no evidence of what was done. Files: `src/notices.ts` 3. Acknowledge receipt The reporter should know it arrived. Files: `src/notify.ts` ### Done when - [ ] Any visitor can submit a notice electronically - [ ] Notices create tracked records with states - [ ] Receipt is acknowledged - [ ] Notices about removed content are handled ### 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. ## Phase 3 · Decisions that explain themselves Every restriction produces a statement of reasons to the affected member. ### Steps 1. Require a reason at decision time Moderators forget within a week and members ask later. Files: `src/decisions.ts` 2. Send the statement automatically Not as a moderator's optional courtesy. Files: `src/statements.ts` 3. Make every restriction liftable A removal you cannot reverse is one you cannot correct. Files: `src/restrictions.ts` ### Done when - [ ] A restriction requires a recorded reason - [ ] Statements of reasons send automatically - [ ] Every restriction can be lifted and the lifting recorded - [ ] Member moderation history is retrievable ### 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. ## Phase 4 · Appeals, because moderators are wrong sometimes A member can contest a decision and get an answer. ### Steps 1. Allow appeal against every decision Including the ones that seem obvious. Files: `src/appeals.ts` 2. Route away from the original decider Where possible. Otherwise it is a formality members see through. Files: `src/appeals.ts` 3. Report the success rate A high rate means moderation is going wrong, which is worth knowing. Files: `src/moderation-report.ts` ### Done when - [ ] Every decision can be appealed - [ ] Appeals route away from the original decider where possible - [ ] Appeal success rate is reported - [ ] A successful appeal lifts the restriction ### 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. ## Not in this build - Legal advice on which obligations apply. - Automated content classification. - Trending and recommendation. - Monetisation and paid memberships. ## After v1, if you want it - Moderator workload reporting, since burnout is what actually ends volunteer moderation - Transparency reporting on notices and outcomes, which builds trust and is increasingly expected - Bulk moderation for coordinated abuse, once single decisions are properly recorded
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Members' posts and moderation records, including reports about identified people. DATABASE_URL=postgres://localhost:5432/community # Required. The notice mechanism must be available to anyone, not only members. NOTICE_FORM_PUBLIC=true # Required. Present as a constant. A removal you cannot explain is one you cannot defend. REQUIRE_DECISION_REASON=always # Required. Per-reply email is how a community loses its notifications to spam filters. NOTIFICATION_MODE=digest # Required. Appeals go to someone other than the original decider where possible. APPEAL_ROUTE_AWAY=true # Required. Decisions and statements outlive the content they concerned. MODERATION_RETENTION_YEARS=3
# Bettermode · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Bettermode, publish a branded community portal with spaces, posts, and member profiles. The hard boundary is embeds, apis, customization, analytics, moderation, and enterprise integrations, plus network, moderation, and communication infrastructure. ## Product outcome A community platform where private spaces are actually private, anyone can report something, every removal comes with a reason the member receives, and every decision can be appealed and reversed. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Docker - PostgreSQL - public HTTPS deployment - email provider - object storage for attachments ## Explicit non-goals for v1 - Legal advice on which obligations apply. - Automated content classification. - Trending and recommendation. - Monetisation and paid memberships. - embeds, APIs, customization, analytics, moderation, and enterprise integrations - network effects - native mobile apps - video and voice infrastructure - enterprise moderation and discovery ## Success criteria - Private content is confirmed absent from every response shape - A notice is confirmed submittable by a non-member - A restriction without a reason is confirmed impossible - A statement of reasons is confirmed sent automatically - Every restriction is confirmed reversible
# Build brief · Bettermode The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a community platform whose moderation obligations are built in rather than added later to replace Bettermode. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Hosting other people's posts makes you a hosting service provider, and in the European Union that carries obligations under the Digital Services Act regardless of how small you are. The distinction worth understanding: Article 19 exempts micro and small enterprises from the additional obligations placed on online platforms, but it does not exempt anyone from the hosting provider obligations. Article 16 requires an accessible electronic notice mechanism letting anyone report content they consider illegal, and Article 17 requires a statement of reasons to the affected user when you restrict content. So a notice mechanism and a statement of reasons are not features to add when the community grows. They are the baseline, and building them into the moderation model from the start costs a phase, whereas retrofitting them means reconstructing decisions you did not record. The second thing: moderation decisions are the most contested part of any community, so every one of them is recorded with who, when and why, and every restriction is reversible. A community whose moderators cannot explain a removal loses its members' trust faster than it loses them to the content it removed. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Fastify and PostgreSQL · posts, membership and permissions are relational - Spaces: Nested spaces with inherited permissions · the permission model is the part that becomes unfixable if it is wrong - Moderation: Notice, decision and statement of reasons as first-class records · these are obligations rather than features - Feeds: Computed per member with permission filtering in the query · a feed that filters after fetching has already fetched - Notification: Batched digests with per-member control · a community that emails on every reply is a community people leave ### Data model (create this before Phase 1) - 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. ### Phase 1 · Spaces and permissions that hold Build: Nested spaces with visibility and join policies, and roles whose permissions are evaluated server side on every read. Verify that a private space's content never appears in any API response for a non-member. Trap: 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. Done when: A private space's posts are absent from every API response for a non-member, asserted across response shapes in the suite. Nested spaces inherit permissions predictably and the inheritance is documented. Changing a space's visibility takes effect immediately for existing sessions. Role changes apply without requiring a re-login. Do not build yet: No posting, moderation or feeds. ### Phase 2 · Anyone can report, and it goes somewhere Build: An accessible electronic notice form available to any visitor, not only members, capturing what is being reported and why. Every notice becomes a tracked record with a state. Acknowledge receipt to the reporter. Trap: 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. Done when: Any visitor including a non-member can submit a notice electronically. Every notice creates a tracked record with a state and an acknowledgement to the reporter. Notices are queued for review with the reported content in context. A notice about already-removed content is handled rather than erroring. Do not build yet: No decisions, statements or appeals yet. ### Phase 3 · Decisions that explain themselves Build: Moderation decisions recording the outcome, the reason and the deciding moderator. Send a statement of reasons to the affected user automatically. Make every restriction reversible and record the reversal. Trap: 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. Done when: A restriction cannot be applied without a recorded reason, enforced in the decision path. The affected member receives a statement of reasons automatically. Every restriction can be lifted and the lifting is recorded. Moderation history for a member is retrievable including reversals. Do not build yet: No appeals, feeds or notifications yet. ### Phase 4 · Appeals, because moderators are wrong sometimes Build: An appeal path against any decision, reviewed by someone other than the original decider where possible. Record the appeal outcome. Report how often appeals succeed. Trap: 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. Done when: Any decision can be appealed and the appeal produces a recorded outcome. Where more than one moderator exists, appeals route to someone other than the original decider. Appeal success rate is reported to moderators. A successful appeal lifts the restriction and records that it did. Do not build yet: No feeds or notifications yet. ### Phase 5 · Feeds and notifications people will tolerate Build: Feeds computed per member with permission filtering in the query. Batched notification digests with per-space control. Handle a member who joins a space with years of history without flooding them. Trap: Notifying on every reply in every space a member has joined produces dozens of emails a day, and the response is not to adjust settings but to filter the whole domain to spam, which removes even the notifications that mattered. Batch by default and make per-space control easy to find. Done when: Feeds filter by permission in the query and never include content the member cannot access. Notifications batch into digests by default with per-space control. Joining a space with years of history does not produce a flood. Unsubscribing works in one click and from a single message. Do not build yet: Nothing after this. ### Out of scope (and why) - Legal advice about which obligations apply to you, which depends on where your members are and how you are established. - Automated content classification, which is unreliable enough that a human decision and a stated reason remain the requirement. - Scale features such as trending and recommendation, which are where the heavier platform obligations begin. - Monetisation, memberships and paid tiers, which is a commerce layer on top of everything here. ### README must contain - That hosting other people's posts makes you a hosting service provider with obligations regardless of size - That the micro and small enterprise exemption covers the additional platform obligations, not the hosting ones - That a notice mechanism must be available to anyone electronically, not only to members - That every restriction requires a recorded reason and produces a statement to the affected member - That every restriction is reversible and reversals are recorded
# Architecture · Bettermode ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Fastify and PostgreSQL | posts, membership and permissions are relational | | Spaces | Nested spaces with inherited permissions | the permission model is the part that becomes unfixable if it is wrong | | Moderation | Notice, decision and statement of reasons as first-class records | these are obligations rather than features | | Feeds | Computed per member with permission filtering in the query | a feed that filters after fetching has already fetched | | Notification | Batched digests with per-member control | a community that emails on every reply is a community people leave | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Permissions | Query-level access control. | Not swappable for interface filtering, which sends the data anyway. | | Notices | Public electronic reporting with tracked state. | Any implementation, provided it is a record rather than an email. | | Decisions | Reasons and automatic statements. | Any workflow, provided a reason is required and the statement is automatic. | | Appeals | Contesting and reversal. | Any process, provided reversal is possible and recorded. | | Notifications | Batched digests with per-space control. | Any transport, provided per-reply email is not the default. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Members' posts and moderation records, including reports about identified people. - `NOTICE_FORM_PUBLIC` · required · The notice mechanism must be available to anyone, not only members. - `REQUIRE_DECISION_REASON` · required · Present as a constant. A removal you cannot explain is one you cannot defend. - `NOTIFICATION_MODE` · required · Per-reply email is how a community loses its notifications to spam filters. - `APPEAL_ROUTE_AWAY` · required · Appeals go to someone other than the original decider where possible. - `MODERATION_RETENTION_YEARS` · required · Decisions and statements outlive the content they concerned. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Bettermode product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, Nested spaces with inherited permissions, Notice, decision and statement of reasons as first-class records, Computed per member with permission filtering in the query, Batched digests with per-member control. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - Notifying on every reply in every space a member has joined produces dozens of emails a day, and the response is not to adjust settings but to filter the whole domain to spam, which removes even the notifications that mattered. Batch by default and make per-space control easy to find.
# Delivery milestones · Bettermode Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Spaces and permissions that hold Who can see and do what is decided in the query, not the interface. ### Steps 1. Evaluate permissions in the query Interface filtering means the data was already sent. Files: `src/permissions.ts`, `tests/permission-leak.test.ts` 2. Document inheritance Nested spaces confuse everyone if the rule is implicit. Files: `docs/permissions.md` 3. Apply changes immediately A revoked role that persists until logout is not revoked. Files: `src/roles.ts` ### Done when - [ ] Private content is absent from every response shape - [ ] Inheritance is predictable and documented - [ ] Visibility changes take effect immediately - [ ] Role changes apply without re-login ### 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. ## M2 · Anyone can report, and it goes somewhere A notice mechanism exists, is easy to find and produces a tracked record. ### Steps 1. Make notices available to anyone The obligation is not limited to members. Files: `src/notices.ts` 2. Record rather than email An inbox has no state and produces no evidence of what was done. Files: `src/notices.ts` 3. Acknowledge receipt The reporter should know it arrived. Files: `src/notify.ts` ### Done when - [ ] Any visitor can submit a notice electronically - [ ] Notices create tracked records with states - [ ] Receipt is acknowledged - [ ] Notices about removed content are handled ### 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. ## M3 · Decisions that explain themselves Every restriction produces a statement of reasons to the affected member. ### Steps 1. Require a reason at decision time Moderators forget within a week and members ask later. Files: `src/decisions.ts` 2. Send the statement automatically Not as a moderator's optional courtesy. Files: `src/statements.ts` 3. Make every restriction liftable A removal you cannot reverse is one you cannot correct. Files: `src/restrictions.ts` ### Done when - [ ] A restriction requires a recorded reason - [ ] Statements of reasons send automatically - [ ] Every restriction can be lifted and the lifting recorded - [ ] Member moderation history is retrievable ### 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. ## M4 · Appeals, because moderators are wrong sometimes A member can contest a decision and get an answer. ### Steps 1. Allow appeal against every decision Including the ones that seem obvious. Files: `src/appeals.ts` 2. Route away from the original decider Where possible. Otherwise it is a formality members see through. Files: `src/appeals.ts` 3. Report the success rate A high rate means moderation is going wrong, which is worth knowing. Files: `src/moderation-report.ts` ### Done when - [ ] Every decision can be appealed - [ ] Appeals route away from the original decider where possible - [ ] Appeal success rate is reported - [ ] A successful appeal lifts the restriction ### 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. ## M5 · Feeds and notifications people will tolerate (production only) Members see what matters without being emailed constantly. ### Steps 1. Filter feeds in the query Same rule as phase one. A feed is just another read path. Files: `src/feed.ts` 2. Batch notifications by default Per-reply email is how a community loses its own notifications to spam filters. Files: `src/digests.ts` 3. Handle joining an old space Years of history is not a day's notifications. Files: `src/membership.ts` ### Done when - [ ] Feeds never include inaccessible content - [ ] Notifications batch by default with per-space control - [ ] Joining an old space does not flood - [ ] Unsubscribing works in one click ### Watch out - Notifying on every reply in every space a member has joined produces dozens of emails a day, and the response is not to adjust settings but to filter the whole domain to spam, which removes even the notifications that mattered. Batch by default and make per-space control easy to find.
# Operations · Bettermode ## Backup Moderation records are the evidence of how decisions were made and may be needed long after the content is gone. Back them up with a longer retention than the posts themselves. ## Restore Restore and confirm restrictions and their lifted states are correct, since restoring a lifted restriction as active re-removes content that was reinstated, usually for someone who already complained once. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Notice backlog age and appeal success rate. A growing backlog is a moderation capacity problem, and a high appeal success rate means decisions are going wrong systematically rather than occasionally. ## Incident checklist Private content appearing to a non-member is the failure that costs a community its trust, and it means a read path filtered after fetching rather than in the query. Error responses are where it usually reappears. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Private content is confirmed absent from every response shape - [ ] A notice is confirmed submittable by a non-member - [ ] A restriction without a reason is confirmed impossible - [ ] A statement of reasons is confirmed sent automatically - [ ] Every restriction is confirmed reversible ## Launch constraint Do not market omitted Bettermode capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Members' posts and moderation records, including reports about identified people. DATABASE_URL=postgres://localhost:5432/community # Required. The notice mechanism must be available to anyone, not only members. NOTICE_FORM_PUBLIC=true # Required. Present as a constant. A removal you cannot explain is one you cannot defend. REQUIRE_DECISION_REASON=always # Required. Per-reply email is how a community loses its notifications to spam filters. NOTIFICATION_MODE=digest # Required. Appeals go to someone other than the original decider where possible. APPEAL_ROUTE_AWAY=true # Required. Decisions and statements outlive the content they concerned. MODERATION_RETENTION_YEARS=3
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Bettermode because community software is valuable when members are already there and the operator does not have to own realtime reliability and abuse response. The recurring cost buys realtime delivery, notifications, abuse handling, search, media, permissions, mobile clients, backups, and community operations, not just the visible interface.
xembeds, APIs, customization, analytics, moderation, and enterprise integrations
xnetwork effects
xnative mobile apps
xvideo and voice infrastructure
xenterprise moderation and discovery
Don't feel like building it? These folks already made it free.
all 5 free alternatives to Bettermode →· no votes, no pay-to-list · just what's real
Vibecode Bettermode
Kinda. The core of Bettermode is buildable in a weekend with the prompt on this page, but there are real gaps: embeds, APIs, customization, analytics, moderation, and enterprise integrations, network effects. Read the honest list above before committing.
How much does Bettermode cost?
Bettermode costs about $49/month (Starter, checked 2026-07-31), which is $588 per year.
What do I lose by replacing Bettermode?
Honestly: embeds, APIs, customization, analytics, moderation, and enterprise integrations; network effects; native mobile apps; video and voice infrastructure; enterprise moderation and discovery. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Bettermode?
Yes: Discourse (Forums, chat and member profiles with a decade of battle scars; Ruby, Postgres and Redis become your hobbies.) Forem (A branded publishing community with profiles, feeds and discussion; deployment is considerably less friendly than the members.) NodeBB (A fast modern forum with profiles and real-time chat; MongoDB, Redis or Postgres becomes your problem.) All 5 curated free alternatives are at vibecodeit.com/bettermode/alternatives. The prompt is for when you want it exactly your way.