Vibecode MeisterTask
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 MeisterTask, run visual project boards with recurring tasks, timelines, and lightweight reporting. The hard boundary is cross-platform polish, integrations, automations, and team administration, plus collaboration, integrations, and organizational trust.
You are building a lean indie version of MeisterTask. 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 ===== # MeisterTask · indie build A team board whose hard problems are ordering cards two people can drag at once, enforcing roles in one place rather than in every route, and generating recurring work from rules that do not invent dates the calendar does not have. 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 | Next.js 15 and TypeScript with Docker Compose for the whole stack | one command to a working instance or nobody will run it | | Storage | PostgreSQL with Drizzle migrations, ordering held as sortable ranks | integer positions cannot survive two people dragging at once | | Access | One authorisation function every query passes through | a permission check per route is a permission check you will forget | | Recurrence | A subset of the calendar recurrence rule, instances generated on completion | the standard already answered the awkward date questions | | History | An append-only activity log that outlives deleted entities | the point of a tracker is what happened, not what is left | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Docker and Docker Compose** · free - Why: The application, the database and the job runner come up together, which is the difference between a tracker a team uses and one nobody installs. - Get it: Docker Desktop or your distribution's packages. - Verify: docker compose up brings the stack to a healthy state - [ ] **Node.js 22 or newer** · free - Why: Runs the application and the migration tooling outside the container during development. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally, a few pounds a month managed - Why: The ordering, the append-only log and the authorisation scopes all rely on constraints and permissions the database enforces. - Get it: Included in the compose file, or a managed instance for a shared deployment. - Verify: Migrations apply and the activity table refuses an update - [ ] **An email credential for invitations and mentions** · free tiers are common - Why: Invitations and mention notifications are the only outbound path, and a tracker nobody can be invited to is a personal list. - Get it: Any transactional email provider, or a mailbox with application-specific credentials. - Verify: An invitation arrives at a real address - [ ] **A server the team can reach** · a few pounds a month - Why: A project board on one person's laptop is a private list, and the permission model only means anything with more than one account on it. - Get it: A small virtual server with a domain and HTTPS. - Verify: Two accounts in different roles can use it simultaneously - [ ] **What each role may do, written down first** · free - Why: The authorisation tests are generated from this list, and deciding it while writing the routes is how viewers end up able to edit. - Get it: Write a table of role against operation before Phase 2, including the awkward ones like closing someone else's issue. - Verify: Every row in the table has a corresponding test ## 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: - Third-party integrations. - Single sign-on and directory sync. - Portfolio and capacity planning. - Native mobile clients. - cross-platform polish, integrations, automations, and team administration - deep integrations - enterprise permissions - large-scale reporting - battle-tested notifications and mobile apps If one of those is essential to you, that is the reason to keep paying for MeisterTask, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · MeisterTask 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 small-team project board whose history survives the people who left to replace MeisterTask. 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. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 and TypeScript with Docker Compose for the whole stack · one command to a working instance or nobody will run it - Storage: PostgreSQL with Drizzle migrations, ordering held as sortable ranks · integer positions cannot survive two people dragging at once - Access: One authorisation function every query passes through · a permission check per route is a permission check you will forget - Recurrence: A subset of the calendar recurrence rule, instances generated on completion · the standard already answered the awkward date questions - History: An append-only activity log that outlives deleted entities · the point of a tracker is what happened, not what is left ### Data model (create this before Phase 1) - orgs(id, name), memberships(org_id, user_id, role) · role is owner, member or viewer - projects(id, org_id, name, archived_at), columns(id, project_id, name, rank) - issues(id, project_id, column_id, title, body, assignee_id, rank, due_at, recurrence_rule, recurrence_parent_id, created_at, closed_at) - labels, milestones and watchers as join tables, all scoped to a project - comments(id, issue_id, author_id, body, created_at, edited_at, deleted_at) · soft deleted, because a thread with holes is unreadable - activity(id, org_id, project_id, actor_id, entity_type, entity_id, verb, before_json, after_json, at) · append only, never updated or deleted - The rank columns are sortable strings rather than integers, so inserting between two cards touches one row and two simultaneous drags cannot collide on a value. ### Phase 1 · One issue model and an order two people can change at once Build: Model issues once and render list, board and timeline from that single model. Order columns and issues by a sortable rank computed between its neighbours. Add a rebalance routine for when ranks grow too long, and a unique constraint per column so two cards cannot claim one position. Trap: Storing position as an integer means every drag rewrites every card below it, so two people reordering the same column at the same time produce interleaved writes, cards that snap back after a refresh, and a board nobody trusts. Compute a rank between the neighbours, write one row, and rebalance on a schedule rather than on every move. Done when: Moving a card writes exactly one issue row, asserted by counting statements. Two concurrent moves in the same column both succeed and produce a deterministic final order, tested with parallel requests. The rebalance routine runs without changing the visible order, tested on a column of a thousand issues. List, board and timeline views read the same issue rows with no view-specific duplicate state. Do not build yet: No roles, recurrence, comments or export. ### Phase 2 · Roles decided in one place Build: Define owner, member and viewer at organisation level with project level overrides. Route every query through one function that takes the actor and the resource and returns the permitted scope. Write a test per mutation asserting that a viewer is refused and that a member of another organisation sees nothing. Trap: Checking permissions in the route handler means the one endpoint somebody adds in a hurry becomes the way a viewer edits the backlog, and nothing in the test suite notices because the test suite tests the routes that were written carefully. Put the check in the data access layer and write an enumeration test that fails when a mutation exists without a matching authorisation test. Done when: Every mutation has a test asserting a viewer is refused, enforced by a test that enumerates mutations and fails on any without one. A user from another organisation receives an empty result rather than an error revealing that the resource exists, tested. Project level overrides are honoured and tested against the organisation default. No route performs its own permission logic, verified by review and a grep test. Do not build yet: No recurrence, comments or notifications yet. ### Phase 3 · Recurring work that respects the calendar Build: Support a subset of the standard recurrence rule: frequency, interval, by-weekday, by-month-day and either a count or an end date. The specification states that recurrence instances with an invalid date, such as the thirtieth of February, must be ignored and must not count towards the count, so implement that literally and offer the last-day-of-month form for people who meant end of month. Generate the next instance when the current one closes, never in advance. Trap: Materialising a year of future instances for every repeating task fills the board with cards nobody wants to see, and editing the rule afterwards leaves you deciding which of three hundred existing rows to rewrite. Generate one instance at a time on completion, keep the rule on the parent, and store the link from child to parent. Done when: A monthly rule on the thirty-first skips months without that date and those skipped occurrences do not count towards a count limit, tested across a full year including February. A rule using the last day of the month lands on the twenty-eighth, twenty-ninth, thirtieth or thirty-first correctly, tested. Closing an instance creates exactly one successor, tested including a double close. Editing the rule affects only future instances and the existing ones keep their dates. Do not build yet: No comments, notifications or export yet. ### Phase 4 · Conversation, and a log that outlives the entities Build: Add comments with mentions that notify, watchers, and assignment changes. Write every mutation to an activity log holding actor, verb, entity and the before and after values. The log is append-only, is never rewritten by an edit, and keeps entries for entities that were later deleted, holding the title as it was at the time. Trap: Rendering history by joining the activity log to the entities it mentions produces blank lines the moment something is deleted, and an issue nobody can find is exactly the one the history was needed for. Denormalise the display values into the log row at write time and accept the duplication. Done when: Deleting an issue leaves its activity entries readable with the title they had at the time, tested. The activity table rejects updates and deletes at the database level, tested by attempting both. A mention notifies exactly once per comment even when the comment is edited, tested. Comment deletion is a soft delete that leaves the thread's structure and the log intact. Do not build yet: No export, backup or search yet. ### Phase 5 · Leaving, arriving, and getting everything out Build: Export every project as JSON and CSV including issues, comments, activity and recurrence rules, and reimport it into an empty instance. Removing a member keeps their comments and activity attributed to a retained display name while revoking access. Add nightly backups, a restore test, migrations on start and an admin recovery command for a locked-out organisation. Trap: Deleting a departed colleague's user row cascades through comments and activity and quietly removes half the reasoning behind decisions the team is still living with. Revoke access, retain the attribution, and make deletion of history a separate deliberate action that names exactly what it will remove. Done when: Removing a member revokes access immediately while their comments and activity remain attributed, tested. Export and reimport into an empty instance reproduces issues, order, comments, activity and recurrence rules, verified by comparison. A nightly backup is taken and a restore is exercised in the suite rather than documented. The admin recovery command restores owner access to an organisation with no reachable owner, tested. Do not build yet: Nothing after this. ### Out of scope (and why) - Hundreds of third-party integrations, which are the paid product's real inventory and each one an ongoing maintenance commitment. - Single sign-on and enterprise directory sync, which are a project in themselves and worth buying rather than building. - Portfolio and capacity planning across many teams, which needs estimates nobody in a small team keeps accurate. - Native mobile clients, since a board that works in a mobile browser covers the cases a small team actually hits. ### README must contain - That ordering uses sortable ranks, why integer positions were rejected, and when rebalancing runs - That authorisation lives in one function and that a test enumerates mutations to prove it - How the recurrence subset behaves on dates that do not exist, and which form to use for end of month - That the activity log is append-only and keeps display values from the time of the event - How to export a project, how to restore from a backup, and how the admin recovery command works ===== AGENTS.md ===== # Agent instructions · MeisterTask indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 and TypeScript with Docker Compose for the whole stack, PostgreSQL with Drizzle migrations, ordering held as sortable ranks, One authorisation function every query passes through, A subset of the calendar recurrence rule, instances generated on completion, An append-only activity log that outlives deleted entities. 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 - Storing position as an integer means every drag rewrites every card below it, so two people reordering the same column at the same time produce interleaved writes, cards that snap back after a refresh, and a board nobody trusts. Compute a rank between the neighbours, write one row, and rebalance on a schedule rather than on every move. - Checking permissions in the route handler means the one endpoint somebody adds in a hurry becomes the way a viewer edits the backlog, and nothing in the test suite notices because the test suite tests the routes that were written carefully. Put the check in the data access layer and write an enumeration test that fails when a mutation exists without a matching authorisation test. - Materialising a year of future instances for every repeating task fills the board with cards nobody wants to see, and editing the rule afterwards leaves you deciding which of three hundred existing rows to rewrite. Generate one instance at a time on completion, keep the rule on the parent, and store the link from child to parent. - Rendering history by joining the activity log to the entities it mentions produces blank lines the moment something is deleted, and an issue nobody can find is exactly the one the history was needed for. Denormalise the display values into the log row at write time and accept the duplication. ===== BUILD_PLAN.md ===== # Build plan · MeisterTask A team board whose hard problems are ordering cards two people can drag at once, enforcing roles in one place rather than in every route, and generating recurring work from rules that do not invent dates the calendar does not have. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One issue model and an order two people can change at once Dragging a card writes one row, and concurrent drags do not fight. ### Steps 1. Rank between neighbours, write one row The drag should cost one update, not a column rewrite. Files: `src/lib/rank.ts`, `tests/rank.test.ts` 2. Test two simultaneous drags This is the bug people report as the board being flaky. Files: `tests/concurrent-move.test.ts` 3. Rebalance without moving anything A maintenance job that changes the order is worse than no job. Files: `src/jobs/rebalance.ts` ### Done when - [ ] One row written per move - [ ] Concurrent moves converge deterministically - [ ] Rebalance preserves visible order - [ ] All three views read one issue model ### Watch out - Storing position as an integer means every drag rewrites every card below it, so two people reordering the same column at the same time produce interleaved writes, cards that snap back after a refresh, and a board nobody trusts. Compute a rank between the neighbours, write one row, and rebalance on a schedule rather than on every move. ## Phase 2 · Roles decided in one place Every read and write passes the same authorisation function, and each mutation has a test. ### Steps 1. One authorisation function, called by the data layer Routes should not be the place this is decided. Files: `src/lib/authz.ts` 2. Enumerate mutations and demand a test for each The check that survives the next hurried feature. Files: `tests/authz-coverage.test.ts` 3. Return empty, not forbidden, across organisations Otherwise the error message is an existence oracle. Files: `src/db/scope.ts` ### Done when - [ ] Every mutation has a viewer-refused test - [ ] Cross-organisation reads return empty - [ ] Project overrides are honoured - [ ] No route contains its own permission logic ### Watch out - Checking permissions in the route handler means the one endpoint somebody adds in a hurry becomes the way a viewer edits the backlog, and nothing in the test suite notices because the test suite tests the routes that were written carefully. Put the check in the data access layer and write an enumeration test that fails when a mutation exists without a matching authorisation test. ## Phase 3 · Recurring work that respects the calendar A repeating task generates the next instance correctly, including on dates that do not exist. ### Steps 1. Implement the invalid-date rule literally The specification already decided this; do not improvise a friendlier answer. Files: `src/lib/recurrence.ts`, `tests/recurrence.test.ts` 2. Generate on completion, one at a time A board full of future cards is a board people stop reading. Files: `src/jobs/next-instance.ts` 3. Offer the end-of-month form explicitly Because most people asking for the thirty-first mean the last day. Files: `src/ui/RecurrenceEditor.tsx` ### Done when - [ ] Invalid dates are skipped and do not count - [ ] End-of-month rules land correctly every month - [ ] Closing creates exactly one successor - [ ] Rule edits affect only future instances ### Watch out - Materialising a year of future instances for every repeating task fills the board with cards nobody wants to see, and editing the rule afterwards leaves you deciding which of three hundred existing rows to rewrite. Generate one instance at a time on completion, keep the rule on the parent, and store the link from child to parent. ## Phase 4 · Conversation, and a log that outlives the entities Comments, mentions and assignments all land in an append-only history. ### Steps 1. Denormalise display values into the log History that depends on live rows is not history. Files: `src/lib/activity.ts` 2. Make the log append-only in the database A revoked update permission beats a code comment saying do not update. Files: `migrations/00x_activity.sql` 3. Notify once per mention, not per save Editing a comment should not page the team again. Files: `src/lib/mentions.ts`, `tests/mentions.test.ts` ### Done when - [ ] Deleted entities keep readable history - [ ] Updates and deletes on the log are refused - [ ] A mention notifies once across edits - [ ] Comment deletion preserves thread structure ### Watch out - Rendering history by joining the activity log to the entities it mentions produces blank lines the moment something is deleted, and an issue nobody can find is exactly the one the history was needed for. Denormalise the display values into the log row at write time and accept the duplication. ## Not in this build - Third-party integrations. - Single sign-on and directory sync. - Portfolio and capacity planning. - Native mobile clients. ## After v1, if you want it - Attachments, once backup and retention have run for a month without surprises - A saved-filter view built on the existing issue model rather than a new one - Time tracking, which is only worth adding if someone will actually read the numbers ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Issues, activity and memberships. DATABASE_URL=postgres://user:pass@db:5432/boards # Required. Used in invitations and mention notifications. APP_BASE_URL=https://boards.example.com # Required · secret. Signs sessions. Rotating it logs everyone out, which is the point. SESSION_SECRET=a long random string, redacted # Required · secret. Invitations and mentions only. No marketing path exists. SMTP_URL=smtps://user:pass@smtp.example.com:465 # Required. Nightly dumps, with a restore exercised in the suite. BACKUP_DIR=/var/backups/boards # Required. When rank rebalancing runs. It must never change visible order. RANK_REBALANCE_CRON=0 4 * * * # Required · secret. Grants owner access to a locked-out organisation. Store it away from the server. ADMIN_RECOVERY_TOKEN=a long random string, redacted
You are building a lean indie version of MeisterTask. 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 ===== # MeisterTask · indie build A team board whose hard problems are ordering cards two people can drag at once, enforcing roles in one place rather than in every route, and generating recurring work from rules that do not invent dates the calendar does not have. 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 | Next.js 15 and TypeScript with Docker Compose for the whole stack | one command to a working instance or nobody will run it | | Storage | PostgreSQL with Drizzle migrations, ordering held as sortable ranks | integer positions cannot survive two people dragging at once | | Access | One authorisation function every query passes through | a permission check per route is a permission check you will forget | | Recurrence | A subset of the calendar recurrence rule, instances generated on completion | the standard already answered the awkward date questions | | History | An append-only activity log that outlives deleted entities | the point of a tracker is what happened, not what is left | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Docker and Docker Compose** · free - Why: The application, the database and the job runner come up together, which is the difference between a tracker a team uses and one nobody installs. - Get it: Docker Desktop or your distribution's packages. - Verify: docker compose up brings the stack to a healthy state - [ ] **Node.js 22 or newer** · free - Why: Runs the application and the migration tooling outside the container during development. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally, a few pounds a month managed - Why: The ordering, the append-only log and the authorisation scopes all rely on constraints and permissions the database enforces. - Get it: Included in the compose file, or a managed instance for a shared deployment. - Verify: Migrations apply and the activity table refuses an update - [ ] **An email credential for invitations and mentions** · free tiers are common - Why: Invitations and mention notifications are the only outbound path, and a tracker nobody can be invited to is a personal list. - Get it: Any transactional email provider, or a mailbox with application-specific credentials. - Verify: An invitation arrives at a real address - [ ] **A server the team can reach** · a few pounds a month - Why: A project board on one person's laptop is a private list, and the permission model only means anything with more than one account on it. - Get it: A small virtual server with a domain and HTTPS. - Verify: Two accounts in different roles can use it simultaneously - [ ] **What each role may do, written down first** · free - Why: The authorisation tests are generated from this list, and deciding it while writing the routes is how viewers end up able to edit. - Get it: Write a table of role against operation before Phase 2, including the awkward ones like closing someone else's issue. - Verify: Every row in the table has a corresponding test ## 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: - Third-party integrations. - Single sign-on and directory sync. - Portfolio and capacity planning. - Native mobile clients. - cross-platform polish, integrations, automations, and team administration - deep integrations - enterprise permissions - large-scale reporting - battle-tested notifications and mobile apps If one of those is essential to you, that is the reason to keep paying for MeisterTask, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · MeisterTask 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 small-team project board whose history survives the people who left to replace MeisterTask. 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. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 and TypeScript with Docker Compose for the whole stack · one command to a working instance or nobody will run it - Storage: PostgreSQL with Drizzle migrations, ordering held as sortable ranks · integer positions cannot survive two people dragging at once - Access: One authorisation function every query passes through · a permission check per route is a permission check you will forget - Recurrence: A subset of the calendar recurrence rule, instances generated on completion · the standard already answered the awkward date questions - History: An append-only activity log that outlives deleted entities · the point of a tracker is what happened, not what is left ### Data model (create this before Phase 1) - orgs(id, name), memberships(org_id, user_id, role) · role is owner, member or viewer - projects(id, org_id, name, archived_at), columns(id, project_id, name, rank) - issues(id, project_id, column_id, title, body, assignee_id, rank, due_at, recurrence_rule, recurrence_parent_id, created_at, closed_at) - labels, milestones and watchers as join tables, all scoped to a project - comments(id, issue_id, author_id, body, created_at, edited_at, deleted_at) · soft deleted, because a thread with holes is unreadable - activity(id, org_id, project_id, actor_id, entity_type, entity_id, verb, before_json, after_json, at) · append only, never updated or deleted - The rank columns are sortable strings rather than integers, so inserting between two cards touches one row and two simultaneous drags cannot collide on a value. ### Phase 1 · One issue model and an order two people can change at once Build: Model issues once and render list, board and timeline from that single model. Order columns and issues by a sortable rank computed between its neighbours. Add a rebalance routine for when ranks grow too long, and a unique constraint per column so two cards cannot claim one position. Trap: Storing position as an integer means every drag rewrites every card below it, so two people reordering the same column at the same time produce interleaved writes, cards that snap back after a refresh, and a board nobody trusts. Compute a rank between the neighbours, write one row, and rebalance on a schedule rather than on every move. Done when: Moving a card writes exactly one issue row, asserted by counting statements. Two concurrent moves in the same column both succeed and produce a deterministic final order, tested with parallel requests. The rebalance routine runs without changing the visible order, tested on a column of a thousand issues. List, board and timeline views read the same issue rows with no view-specific duplicate state. Do not build yet: No roles, recurrence, comments or export. ### Phase 2 · Roles decided in one place Build: Define owner, member and viewer at organisation level with project level overrides. Route every query through one function that takes the actor and the resource and returns the permitted scope. Write a test per mutation asserting that a viewer is refused and that a member of another organisation sees nothing. Trap: Checking permissions in the route handler means the one endpoint somebody adds in a hurry becomes the way a viewer edits the backlog, and nothing in the test suite notices because the test suite tests the routes that were written carefully. Put the check in the data access layer and write an enumeration test that fails when a mutation exists without a matching authorisation test. Done when: Every mutation has a test asserting a viewer is refused, enforced by a test that enumerates mutations and fails on any without one. A user from another organisation receives an empty result rather than an error revealing that the resource exists, tested. Project level overrides are honoured and tested against the organisation default. No route performs its own permission logic, verified by review and a grep test. Do not build yet: No recurrence, comments or notifications yet. ### Phase 3 · Recurring work that respects the calendar Build: Support a subset of the standard recurrence rule: frequency, interval, by-weekday, by-month-day and either a count or an end date. The specification states that recurrence instances with an invalid date, such as the thirtieth of February, must be ignored and must not count towards the count, so implement that literally and offer the last-day-of-month form for people who meant end of month. Generate the next instance when the current one closes, never in advance. Trap: Materialising a year of future instances for every repeating task fills the board with cards nobody wants to see, and editing the rule afterwards leaves you deciding which of three hundred existing rows to rewrite. Generate one instance at a time on completion, keep the rule on the parent, and store the link from child to parent. Done when: A monthly rule on the thirty-first skips months without that date and those skipped occurrences do not count towards a count limit, tested across a full year including February. A rule using the last day of the month lands on the twenty-eighth, twenty-ninth, thirtieth or thirty-first correctly, tested. Closing an instance creates exactly one successor, tested including a double close. Editing the rule affects only future instances and the existing ones keep their dates. Do not build yet: No comments, notifications or export yet. ### Phase 4 · Conversation, and a log that outlives the entities Build: Add comments with mentions that notify, watchers, and assignment changes. Write every mutation to an activity log holding actor, verb, entity and the before and after values. The log is append-only, is never rewritten by an edit, and keeps entries for entities that were later deleted, holding the title as it was at the time. Trap: Rendering history by joining the activity log to the entities it mentions produces blank lines the moment something is deleted, and an issue nobody can find is exactly the one the history was needed for. Denormalise the display values into the log row at write time and accept the duplication. Done when: Deleting an issue leaves its activity entries readable with the title they had at the time, tested. The activity table rejects updates and deletes at the database level, tested by attempting both. A mention notifies exactly once per comment even when the comment is edited, tested. Comment deletion is a soft delete that leaves the thread's structure and the log intact. Do not build yet: No export, backup or search yet. ### Phase 5 · Leaving, arriving, and getting everything out Build: Export every project as JSON and CSV including issues, comments, activity and recurrence rules, and reimport it into an empty instance. Removing a member keeps their comments and activity attributed to a retained display name while revoking access. Add nightly backups, a restore test, migrations on start and an admin recovery command for a locked-out organisation. Trap: Deleting a departed colleague's user row cascades through comments and activity and quietly removes half the reasoning behind decisions the team is still living with. Revoke access, retain the attribution, and make deletion of history a separate deliberate action that names exactly what it will remove. Done when: Removing a member revokes access immediately while their comments and activity remain attributed, tested. Export and reimport into an empty instance reproduces issues, order, comments, activity and recurrence rules, verified by comparison. A nightly backup is taken and a restore is exercised in the suite rather than documented. The admin recovery command restores owner access to an organisation with no reachable owner, tested. Do not build yet: Nothing after this. ### Out of scope (and why) - Hundreds of third-party integrations, which are the paid product's real inventory and each one an ongoing maintenance commitment. - Single sign-on and enterprise directory sync, which are a project in themselves and worth buying rather than building. - Portfolio and capacity planning across many teams, which needs estimates nobody in a small team keeps accurate. - Native mobile clients, since a board that works in a mobile browser covers the cases a small team actually hits. ### README must contain - That ordering uses sortable ranks, why integer positions were rejected, and when rebalancing runs - That authorisation lives in one function and that a test enumerates mutations to prove it - How the recurrence subset behaves on dates that do not exist, and which form to use for end of month - That the activity log is append-only and keeps display values from the time of the event - How to export a project, how to restore from a backup, and how the admin recovery command works ===== AGENTS.md ===== # Agent instructions · MeisterTask indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 and TypeScript with Docker Compose for the whole stack, PostgreSQL with Drizzle migrations, ordering held as sortable ranks, One authorisation function every query passes through, A subset of the calendar recurrence rule, instances generated on completion, An append-only activity log that outlives deleted entities. 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 - Storing position as an integer means every drag rewrites every card below it, so two people reordering the same column at the same time produce interleaved writes, cards that snap back after a refresh, and a board nobody trusts. Compute a rank between the neighbours, write one row, and rebalance on a schedule rather than on every move. - Checking permissions in the route handler means the one endpoint somebody adds in a hurry becomes the way a viewer edits the backlog, and nothing in the test suite notices because the test suite tests the routes that were written carefully. Put the check in the data access layer and write an enumeration test that fails when a mutation exists without a matching authorisation test. - Materialising a year of future instances for every repeating task fills the board with cards nobody wants to see, and editing the rule afterwards leaves you deciding which of three hundred existing rows to rewrite. Generate one instance at a time on completion, keep the rule on the parent, and store the link from child to parent. - Rendering history by joining the activity log to the entities it mentions produces blank lines the moment something is deleted, and an issue nobody can find is exactly the one the history was needed for. Denormalise the display values into the log row at write time and accept the duplication. ===== BUILD_PLAN.md ===== # Build plan · MeisterTask A team board whose hard problems are ordering cards two people can drag at once, enforcing roles in one place rather than in every route, and generating recurring work from rules that do not invent dates the calendar does not have. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One issue model and an order two people can change at once Dragging a card writes one row, and concurrent drags do not fight. ### Steps 1. Rank between neighbours, write one row The drag should cost one update, not a column rewrite. Files: `src/lib/rank.ts`, `tests/rank.test.ts` 2. Test two simultaneous drags This is the bug people report as the board being flaky. Files: `tests/concurrent-move.test.ts` 3. Rebalance without moving anything A maintenance job that changes the order is worse than no job. Files: `src/jobs/rebalance.ts` ### Done when - [ ] One row written per move - [ ] Concurrent moves converge deterministically - [ ] Rebalance preserves visible order - [ ] All three views read one issue model ### Watch out - Storing position as an integer means every drag rewrites every card below it, so two people reordering the same column at the same time produce interleaved writes, cards that snap back after a refresh, and a board nobody trusts. Compute a rank between the neighbours, write one row, and rebalance on a schedule rather than on every move. ## Phase 2 · Roles decided in one place Every read and write passes the same authorisation function, and each mutation has a test. ### Steps 1. One authorisation function, called by the data layer Routes should not be the place this is decided. Files: `src/lib/authz.ts` 2. Enumerate mutations and demand a test for each The check that survives the next hurried feature. Files: `tests/authz-coverage.test.ts` 3. Return empty, not forbidden, across organisations Otherwise the error message is an existence oracle. Files: `src/db/scope.ts` ### Done when - [ ] Every mutation has a viewer-refused test - [ ] Cross-organisation reads return empty - [ ] Project overrides are honoured - [ ] No route contains its own permission logic ### Watch out - Checking permissions in the route handler means the one endpoint somebody adds in a hurry becomes the way a viewer edits the backlog, and nothing in the test suite notices because the test suite tests the routes that were written carefully. Put the check in the data access layer and write an enumeration test that fails when a mutation exists without a matching authorisation test. ## Phase 3 · Recurring work that respects the calendar A repeating task generates the next instance correctly, including on dates that do not exist. ### Steps 1. Implement the invalid-date rule literally The specification already decided this; do not improvise a friendlier answer. Files: `src/lib/recurrence.ts`, `tests/recurrence.test.ts` 2. Generate on completion, one at a time A board full of future cards is a board people stop reading. Files: `src/jobs/next-instance.ts` 3. Offer the end-of-month form explicitly Because most people asking for the thirty-first mean the last day. Files: `src/ui/RecurrenceEditor.tsx` ### Done when - [ ] Invalid dates are skipped and do not count - [ ] End-of-month rules land correctly every month - [ ] Closing creates exactly one successor - [ ] Rule edits affect only future instances ### Watch out - Materialising a year of future instances for every repeating task fills the board with cards nobody wants to see, and editing the rule afterwards leaves you deciding which of three hundred existing rows to rewrite. Generate one instance at a time on completion, keep the rule on the parent, and store the link from child to parent. ## Phase 4 · Conversation, and a log that outlives the entities Comments, mentions and assignments all land in an append-only history. ### Steps 1. Denormalise display values into the log History that depends on live rows is not history. Files: `src/lib/activity.ts` 2. Make the log append-only in the database A revoked update permission beats a code comment saying do not update. Files: `migrations/00x_activity.sql` 3. Notify once per mention, not per save Editing a comment should not page the team again. Files: `src/lib/mentions.ts`, `tests/mentions.test.ts` ### Done when - [ ] Deleted entities keep readable history - [ ] Updates and deletes on the log are refused - [ ] A mention notifies once across edits - [ ] Comment deletion preserves thread structure ### Watch out - Rendering history by joining the activity log to the entities it mentions produces blank lines the moment something is deleted, and an issue nobody can find is exactly the one the history was needed for. Denormalise the display values into the log row at write time and accept the duplication. ## Not in this build - Third-party integrations. - Single sign-on and directory sync. - Portfolio and capacity planning. - Native mobile clients. ## After v1, if you want it - Attachments, once backup and retention have run for a month without surprises - A saved-filter view built on the existing issue model rather than a new one - Time tracking, which is only worth adding if someone will actually read the numbers ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Issues, activity and memberships. DATABASE_URL=postgres://user:pass@db:5432/boards # Required. Used in invitations and mention notifications. APP_BASE_URL=https://boards.example.com # Required · secret. Signs sessions. Rotating it logs everyone out, which is the point. SESSION_SECRET=a long random string, redacted # Required · secret. Invitations and mentions only. No marketing path exists. SMTP_URL=smtps://user:pass@smtp.example.com:465 # Required. Nightly dumps, with a restore exercised in the suite. BACKUP_DIR=/var/backups/boards # Required. When rank rebalancing runs. It must never change visible order. RANK_REBALANCE_CRON=0 4 * * * # Required · secret. Grants owner access to a locked-out organisation. Store it away from the server. ADMIN_RECOVERY_TOKEN=a long random string, redacted
You are building a production product version of MeisterTask. 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 ===== # MeisterTask · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For MeisterTask, run visual project boards with recurring tasks, timelines, and lightweight reporting. The hard boundary is cross-platform polish, integrations, automations, and team administration, plus collaboration, integrations, and organizational trust. ## Product outcome A board where two people can reorder the same column without fighting, a viewer cannot edit anything through any route, a monthly task does not invent the thirty-first of February, and the reasoning behind a decision is still readable after everyone involved has left. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Docker - PostgreSQL - self-hosted server - email credentials for invitations ## Explicit non-goals for v1 - Third-party integrations. - Single sign-on and directory sync. - Portfolio and capacity planning. - Native mobile clients. - cross-platform polish, integrations, automations, and team administration - deep integrations - enterprise permissions - large-scale reporting - battle-tested notifications and mobile apps ## Success criteria - A card move is confirmed to write exactly one row - Every mutation is confirmed to have a viewer-refused test - Invalid recurrence dates are confirmed skipped and uncounted - Deleted entities are confirmed to keep readable history - A restore is confirmed exercised in the suite ===== BRIEF.md ===== # Build brief · MeisterTask 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 small-team project board whose history survives the people who left to replace MeisterTask. 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. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 and TypeScript with Docker Compose for the whole stack · one command to a working instance or nobody will run it - Storage: PostgreSQL with Drizzle migrations, ordering held as sortable ranks · integer positions cannot survive two people dragging at once - Access: One authorisation function every query passes through · a permission check per route is a permission check you will forget - Recurrence: A subset of the calendar recurrence rule, instances generated on completion · the standard already answered the awkward date questions - History: An append-only activity log that outlives deleted entities · the point of a tracker is what happened, not what is left ### Data model (create this before Phase 1) - orgs(id, name), memberships(org_id, user_id, role) · role is owner, member or viewer - projects(id, org_id, name, archived_at), columns(id, project_id, name, rank) - issues(id, project_id, column_id, title, body, assignee_id, rank, due_at, recurrence_rule, recurrence_parent_id, created_at, closed_at) - labels, milestones and watchers as join tables, all scoped to a project - comments(id, issue_id, author_id, body, created_at, edited_at, deleted_at) · soft deleted, because a thread with holes is unreadable - activity(id, org_id, project_id, actor_id, entity_type, entity_id, verb, before_json, after_json, at) · append only, never updated or deleted - The rank columns are sortable strings rather than integers, so inserting between two cards touches one row and two simultaneous drags cannot collide on a value. ### Phase 1 · One issue model and an order two people can change at once Build: Model issues once and render list, board and timeline from that single model. Order columns and issues by a sortable rank computed between its neighbours. Add a rebalance routine for when ranks grow too long, and a unique constraint per column so two cards cannot claim one position. Trap: Storing position as an integer means every drag rewrites every card below it, so two people reordering the same column at the same time produce interleaved writes, cards that snap back after a refresh, and a board nobody trusts. Compute a rank between the neighbours, write one row, and rebalance on a schedule rather than on every move. Done when: Moving a card writes exactly one issue row, asserted by counting statements. Two concurrent moves in the same column both succeed and produce a deterministic final order, tested with parallel requests. The rebalance routine runs without changing the visible order, tested on a column of a thousand issues. List, board and timeline views read the same issue rows with no view-specific duplicate state. Do not build yet: No roles, recurrence, comments or export. ### Phase 2 · Roles decided in one place Build: Define owner, member and viewer at organisation level with project level overrides. Route every query through one function that takes the actor and the resource and returns the permitted scope. Write a test per mutation asserting that a viewer is refused and that a member of another organisation sees nothing. Trap: Checking permissions in the route handler means the one endpoint somebody adds in a hurry becomes the way a viewer edits the backlog, and nothing in the test suite notices because the test suite tests the routes that were written carefully. Put the check in the data access layer and write an enumeration test that fails when a mutation exists without a matching authorisation test. Done when: Every mutation has a test asserting a viewer is refused, enforced by a test that enumerates mutations and fails on any without one. A user from another organisation receives an empty result rather than an error revealing that the resource exists, tested. Project level overrides are honoured and tested against the organisation default. No route performs its own permission logic, verified by review and a grep test. Do not build yet: No recurrence, comments or notifications yet. ### Phase 3 · Recurring work that respects the calendar Build: Support a subset of the standard recurrence rule: frequency, interval, by-weekday, by-month-day and either a count or an end date. The specification states that recurrence instances with an invalid date, such as the thirtieth of February, must be ignored and must not count towards the count, so implement that literally and offer the last-day-of-month form for people who meant end of month. Generate the next instance when the current one closes, never in advance. Trap: Materialising a year of future instances for every repeating task fills the board with cards nobody wants to see, and editing the rule afterwards leaves you deciding which of three hundred existing rows to rewrite. Generate one instance at a time on completion, keep the rule on the parent, and store the link from child to parent. Done when: A monthly rule on the thirty-first skips months without that date and those skipped occurrences do not count towards a count limit, tested across a full year including February. A rule using the last day of the month lands on the twenty-eighth, twenty-ninth, thirtieth or thirty-first correctly, tested. Closing an instance creates exactly one successor, tested including a double close. Editing the rule affects only future instances and the existing ones keep their dates. Do not build yet: No comments, notifications or export yet. ### Phase 4 · Conversation, and a log that outlives the entities Build: Add comments with mentions that notify, watchers, and assignment changes. Write every mutation to an activity log holding actor, verb, entity and the before and after values. The log is append-only, is never rewritten by an edit, and keeps entries for entities that were later deleted, holding the title as it was at the time. Trap: Rendering history by joining the activity log to the entities it mentions produces blank lines the moment something is deleted, and an issue nobody can find is exactly the one the history was needed for. Denormalise the display values into the log row at write time and accept the duplication. Done when: Deleting an issue leaves its activity entries readable with the title they had at the time, tested. The activity table rejects updates and deletes at the database level, tested by attempting both. A mention notifies exactly once per comment even when the comment is edited, tested. Comment deletion is a soft delete that leaves the thread's structure and the log intact. Do not build yet: No export, backup or search yet. ### Phase 5 · Leaving, arriving, and getting everything out Build: Export every project as JSON and CSV including issues, comments, activity and recurrence rules, and reimport it into an empty instance. Removing a member keeps their comments and activity attributed to a retained display name while revoking access. Add nightly backups, a restore test, migrations on start and an admin recovery command for a locked-out organisation. Trap: Deleting a departed colleague's user row cascades through comments and activity and quietly removes half the reasoning behind decisions the team is still living with. Revoke access, retain the attribution, and make deletion of history a separate deliberate action that names exactly what it will remove. Done when: Removing a member revokes access immediately while their comments and activity remain attributed, tested. Export and reimport into an empty instance reproduces issues, order, comments, activity and recurrence rules, verified by comparison. A nightly backup is taken and a restore is exercised in the suite rather than documented. The admin recovery command restores owner access to an organisation with no reachable owner, tested. Do not build yet: Nothing after this. ### Out of scope (and why) - Hundreds of third-party integrations, which are the paid product's real inventory and each one an ongoing maintenance commitment. - Single sign-on and enterprise directory sync, which are a project in themselves and worth buying rather than building. - Portfolio and capacity planning across many teams, which needs estimates nobody in a small team keeps accurate. - Native mobile clients, since a board that works in a mobile browser covers the cases a small team actually hits. ### README must contain - That ordering uses sortable ranks, why integer positions were rejected, and when rebalancing runs - That authorisation lives in one function and that a test enumerates mutations to prove it - How the recurrence subset behaves on dates that do not exist, and which form to use for end of month - That the activity log is append-only and keeps display values from the time of the event - How to export a project, how to restore from a backup, and how the admin recovery command works ===== ARCHITECTURE.md ===== # Architecture · MeisterTask ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 and TypeScript with Docker Compose for the whole stack | one command to a working instance or nobody will run it | | Storage | PostgreSQL with Drizzle migrations, ordering held as sortable ranks | integer positions cannot survive two people dragging at once | | Access | One authorisation function every query passes through | a permission check per route is a permission check you will forget | | Recurrence | A subset of the calendar recurrence rule, instances generated on completion | the standard already answered the awkward date questions | | History | An append-only activity log that outlives deleted entities | the point of a tracker is what happened, not what is left | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Issues | One canonical model behind three views. | Any presentation, provided no view keeps its own copy of order. | | Ordering | Sortable ranks and rebalancing. | Any ordering scheme that writes one row per move. | | Authorisation | One function used by the data layer. | Not swappable for per-route checks, which is the failure this phase exists to prevent. | | Recurrence | The rule subset and instance generation. | Any implementation that passes the invalid-date tests. | | Activity | Append-only history with denormalised display values. | Any store that refuses updates and deletes. | ## 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 · Issues, activity and memberships. - `APP_BASE_URL` · required · Used in invitations and mention notifications. - `SESSION_SECRET` · required, secret · Signs sessions. Rotating it logs everyone out, which is the point. - `SMTP_URL` · required, secret · Invitations and mentions only. No marketing path exists. - `BACKUP_DIR` · required · Nightly dumps, with a restore exercised in the suite. - `RANK_REBALANCE_CRON` · required · When rank rebalancing runs. It must never change visible order. - `ADMIN_RECOVERY_TOKEN` · required, secret · Grants owner access to a locked-out organisation. Store it away from the server. ## 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 · MeisterTask product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js 15 and TypeScript with Docker Compose for the whole stack, PostgreSQL with Drizzle migrations, ordering held as sortable ranks, One authorisation function every query passes through, A subset of the calendar recurrence rule, instances generated on completion, An append-only activity log that outlives deleted entities. - 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 - Storing position as an integer means every drag rewrites every card below it, so two people reordering the same column at the same time produce interleaved writes, cards that snap back after a refresh, and a board nobody trusts. Compute a rank between the neighbours, write one row, and rebalance on a schedule rather than on every move. - Checking permissions in the route handler means the one endpoint somebody adds in a hurry becomes the way a viewer edits the backlog, and nothing in the test suite notices because the test suite tests the routes that were written carefully. Put the check in the data access layer and write an enumeration test that fails when a mutation exists without a matching authorisation test. - Materialising a year of future instances for every repeating task fills the board with cards nobody wants to see, and editing the rule afterwards leaves you deciding which of three hundred existing rows to rewrite. Generate one instance at a time on completion, keep the rule on the parent, and store the link from child to parent. - Rendering history by joining the activity log to the entities it mentions produces blank lines the moment something is deleted, and an issue nobody can find is exactly the one the history was needed for. Denormalise the display values into the log row at write time and accept the duplication. - Deleting a departed colleague's user row cascades through comments and activity and quietly removes half the reasoning behind decisions the team is still living with. Revoke access, retain the attribution, and make deletion of history a separate deliberate action that names exactly what it will remove. ===== MILESTONES.md ===== # Delivery milestones · MeisterTask Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · One issue model and an order two people can change at once Dragging a card writes one row, and concurrent drags do not fight. ### Steps 1. Rank between neighbours, write one row The drag should cost one update, not a column rewrite. Files: `src/lib/rank.ts`, `tests/rank.test.ts` 2. Test two simultaneous drags This is the bug people report as the board being flaky. Files: `tests/concurrent-move.test.ts` 3. Rebalance without moving anything A maintenance job that changes the order is worse than no job. Files: `src/jobs/rebalance.ts` ### Done when - [ ] One row written per move - [ ] Concurrent moves converge deterministically - [ ] Rebalance preserves visible order - [ ] All three views read one issue model ### Watch out - Storing position as an integer means every drag rewrites every card below it, so two people reordering the same column at the same time produce interleaved writes, cards that snap back after a refresh, and a board nobody trusts. Compute a rank between the neighbours, write one row, and rebalance on a schedule rather than on every move. ## M2 · Roles decided in one place Every read and write passes the same authorisation function, and each mutation has a test. ### Steps 1. One authorisation function, called by the data layer Routes should not be the place this is decided. Files: `src/lib/authz.ts` 2. Enumerate mutations and demand a test for each The check that survives the next hurried feature. Files: `tests/authz-coverage.test.ts` 3. Return empty, not forbidden, across organisations Otherwise the error message is an existence oracle. Files: `src/db/scope.ts` ### Done when - [ ] Every mutation has a viewer-refused test - [ ] Cross-organisation reads return empty - [ ] Project overrides are honoured - [ ] No route contains its own permission logic ### Watch out - Checking permissions in the route handler means the one endpoint somebody adds in a hurry becomes the way a viewer edits the backlog, and nothing in the test suite notices because the test suite tests the routes that were written carefully. Put the check in the data access layer and write an enumeration test that fails when a mutation exists without a matching authorisation test. ## M3 · Recurring work that respects the calendar A repeating task generates the next instance correctly, including on dates that do not exist. ### Steps 1. Implement the invalid-date rule literally The specification already decided this; do not improvise a friendlier answer. Files: `src/lib/recurrence.ts`, `tests/recurrence.test.ts` 2. Generate on completion, one at a time A board full of future cards is a board people stop reading. Files: `src/jobs/next-instance.ts` 3. Offer the end-of-month form explicitly Because most people asking for the thirty-first mean the last day. Files: `src/ui/RecurrenceEditor.tsx` ### Done when - [ ] Invalid dates are skipped and do not count - [ ] End-of-month rules land correctly every month - [ ] Closing creates exactly one successor - [ ] Rule edits affect only future instances ### Watch out - Materialising a year of future instances for every repeating task fills the board with cards nobody wants to see, and editing the rule afterwards leaves you deciding which of three hundred existing rows to rewrite. Generate one instance at a time on completion, keep the rule on the parent, and store the link from child to parent. ## M4 · Conversation, and a log that outlives the entities Comments, mentions and assignments all land in an append-only history. ### Steps 1. Denormalise display values into the log History that depends on live rows is not history. Files: `src/lib/activity.ts` 2. Make the log append-only in the database A revoked update permission beats a code comment saying do not update. Files: `migrations/00x_activity.sql` 3. Notify once per mention, not per save Editing a comment should not page the team again. Files: `src/lib/mentions.ts`, `tests/mentions.test.ts` ### Done when - [ ] Deleted entities keep readable history - [ ] Updates and deletes on the log are refused - [ ] A mention notifies once across edits - [ ] Comment deletion preserves thread structure ### Watch out - Rendering history by joining the activity log to the entities it mentions produces blank lines the moment something is deleted, and an issue nobody can find is exactly the one the history was needed for. Denormalise the display values into the log row at write time and accept the duplication. ## M5 · Leaving, arriving, and getting everything out (production only) A departing member changes nothing about the record, and the whole project exports. ### Steps 1. Revoke access without deleting attribution The reasoning behind old decisions is the asset here. Files: `src/lib/membership.ts` 2. Round trip a whole project through export An export nobody has reimported is a guess. Files: `tests/export-roundtrip.test.ts` 3. Exercise the restore, not just the backup A backup nobody has restored is a hope. Files: `ops/backup.sh`, `tests/restore.test.ts` ### Done when - [ ] Removed members lose access and keep attribution - [ ] Export reimports into an empty instance faithfully - [ ] A restore is exercised in the suite - [ ] Admin recovery restores a locked-out organisation ### Watch out - Deleting a departed colleague's user row cascades through comments and activity and quietly removes half the reasoning behind decisions the team is still living with. Revoke access, retain the attribution, and make deletion of history a separate deliberate action that names exactly what it will remove. ===== OPERATIONS.md ===== # Operations · MeisterTask ## Backup Nightly database dumps with a retention, plus the uploaded attachments if you add them later. The activity log is the part that cannot be reconstructed from anywhere else, so it sets the retention rather than the issue table. ## Restore Restore into an empty instance, run migrations, and confirm that board order, recurrence rules and activity history all survive. A restore that loses the log has lost the reason the tracker existed. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Failed background jobs, the age of the last successful backup, and rank length, which is the early warning that rebalancing has stopped running. ## Incident checklist The two to rehearse are a locked-out organisation, handled by the admin recovery command, and a bad bulk edit, handled by reading the activity log's before values rather than by restoring a backup over everyone's day. 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 - [ ] A card move is confirmed to write exactly one row - [ ] Every mutation is confirmed to have a viewer-refused test - [ ] Invalid recurrence dates are confirmed skipped and uncounted - [ ] Deleted entities are confirmed to keep readable history - [ ] A restore is confirmed exercised in the suite ## Launch constraint Do not market omitted MeisterTask 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. Issues, activity and memberships. DATABASE_URL=postgres://user:pass@db:5432/boards # Required. Used in invitations and mention notifications. APP_BASE_URL=https://boards.example.com # Required · secret. Signs sessions. Rotating it logs everyone out, which is the point. SESSION_SECRET=a long random string, redacted # Required · secret. Invitations and mentions only. No marketing path exists. SMTP_URL=smtps://user:pass@smtp.example.com:465 # Required. Nightly dumps, with a restore exercised in the suite. BACKUP_DIR=/var/backups/boards # Required. When rank rebalancing runs. It must never change visible order. RANK_REBALANCE_CRON=0 4 * * * # Required · secret. Grants owner access to a locked-out organisation. Store it away from the server. ADMIN_RECOVERY_TOKEN=a long random string, redacted
# MeisterTask · indie build A team board whose hard problems are ordering cards two people can drag at once, enforcing roles in one place rather than in every route, and generating recurring work from rules that do not invent dates the calendar does not have. 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 | Next.js 15 and TypeScript with Docker Compose for the whole stack | one command to a working instance or nobody will run it | | Storage | PostgreSQL with Drizzle migrations, ordering held as sortable ranks | integer positions cannot survive two people dragging at once | | Access | One authorisation function every query passes through | a permission check per route is a permission check you will forget | | Recurrence | A subset of the calendar recurrence rule, instances generated on completion | the standard already answered the awkward date questions | | History | An append-only activity log that outlives deleted entities | the point of a tracker is what happened, not what is left | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Docker and Docker Compose** · free - Why: The application, the database and the job runner come up together, which is the difference between a tracker a team uses and one nobody installs. - Get it: Docker Desktop or your distribution's packages. - Verify: docker compose up brings the stack to a healthy state - [ ] **Node.js 22 or newer** · free - Why: Runs the application and the migration tooling outside the container during development. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally, a few pounds a month managed - Why: The ordering, the append-only log and the authorisation scopes all rely on constraints and permissions the database enforces. - Get it: Included in the compose file, or a managed instance for a shared deployment. - Verify: Migrations apply and the activity table refuses an update - [ ] **An email credential for invitations and mentions** · free tiers are common - Why: Invitations and mention notifications are the only outbound path, and a tracker nobody can be invited to is a personal list. - Get it: Any transactional email provider, or a mailbox with application-specific credentials. - Verify: An invitation arrives at a real address - [ ] **A server the team can reach** · a few pounds a month - Why: A project board on one person's laptop is a private list, and the permission model only means anything with more than one account on it. - Get it: A small virtual server with a domain and HTTPS. - Verify: Two accounts in different roles can use it simultaneously - [ ] **What each role may do, written down first** · free - Why: The authorisation tests are generated from this list, and deciding it while writing the routes is how viewers end up able to edit. - Get it: Write a table of role against operation before Phase 2, including the awkward ones like closing someone else's issue. - Verify: Every row in the table has a corresponding test ## 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: - Third-party integrations. - Single sign-on and directory sync. - Portfolio and capacity planning. - Native mobile clients. - cross-platform polish, integrations, automations, and team administration - deep integrations - enterprise permissions - large-scale reporting - battle-tested notifications and mobile apps If one of those is essential to you, that is the reason to keep paying for MeisterTask, and the README should say so rather than pretend.
# Build brief · MeisterTask 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 small-team project board whose history survives the people who left to replace MeisterTask. 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. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 and TypeScript with Docker Compose for the whole stack · one command to a working instance or nobody will run it - Storage: PostgreSQL with Drizzle migrations, ordering held as sortable ranks · integer positions cannot survive two people dragging at once - Access: One authorisation function every query passes through · a permission check per route is a permission check you will forget - Recurrence: A subset of the calendar recurrence rule, instances generated on completion · the standard already answered the awkward date questions - History: An append-only activity log that outlives deleted entities · the point of a tracker is what happened, not what is left ### Data model (create this before Phase 1) - orgs(id, name), memberships(org_id, user_id, role) · role is owner, member or viewer - projects(id, org_id, name, archived_at), columns(id, project_id, name, rank) - issues(id, project_id, column_id, title, body, assignee_id, rank, due_at, recurrence_rule, recurrence_parent_id, created_at, closed_at) - labels, milestones and watchers as join tables, all scoped to a project - comments(id, issue_id, author_id, body, created_at, edited_at, deleted_at) · soft deleted, because a thread with holes is unreadable - activity(id, org_id, project_id, actor_id, entity_type, entity_id, verb, before_json, after_json, at) · append only, never updated or deleted - The rank columns are sortable strings rather than integers, so inserting between two cards touches one row and two simultaneous drags cannot collide on a value. ### Phase 1 · One issue model and an order two people can change at once Build: Model issues once and render list, board and timeline from that single model. Order columns and issues by a sortable rank computed between its neighbours. Add a rebalance routine for when ranks grow too long, and a unique constraint per column so two cards cannot claim one position. Trap: Storing position as an integer means every drag rewrites every card below it, so two people reordering the same column at the same time produce interleaved writes, cards that snap back after a refresh, and a board nobody trusts. Compute a rank between the neighbours, write one row, and rebalance on a schedule rather than on every move. Done when: Moving a card writes exactly one issue row, asserted by counting statements. Two concurrent moves in the same column both succeed and produce a deterministic final order, tested with parallel requests. The rebalance routine runs without changing the visible order, tested on a column of a thousand issues. List, board and timeline views read the same issue rows with no view-specific duplicate state. Do not build yet: No roles, recurrence, comments or export. ### Phase 2 · Roles decided in one place Build: Define owner, member and viewer at organisation level with project level overrides. Route every query through one function that takes the actor and the resource and returns the permitted scope. Write a test per mutation asserting that a viewer is refused and that a member of another organisation sees nothing. Trap: Checking permissions in the route handler means the one endpoint somebody adds in a hurry becomes the way a viewer edits the backlog, and nothing in the test suite notices because the test suite tests the routes that were written carefully. Put the check in the data access layer and write an enumeration test that fails when a mutation exists without a matching authorisation test. Done when: Every mutation has a test asserting a viewer is refused, enforced by a test that enumerates mutations and fails on any without one. A user from another organisation receives an empty result rather than an error revealing that the resource exists, tested. Project level overrides are honoured and tested against the organisation default. No route performs its own permission logic, verified by review and a grep test. Do not build yet: No recurrence, comments or notifications yet. ### Phase 3 · Recurring work that respects the calendar Build: Support a subset of the standard recurrence rule: frequency, interval, by-weekday, by-month-day and either a count or an end date. The specification states that recurrence instances with an invalid date, such as the thirtieth of February, must be ignored and must not count towards the count, so implement that literally and offer the last-day-of-month form for people who meant end of month. Generate the next instance when the current one closes, never in advance. Trap: Materialising a year of future instances for every repeating task fills the board with cards nobody wants to see, and editing the rule afterwards leaves you deciding which of three hundred existing rows to rewrite. Generate one instance at a time on completion, keep the rule on the parent, and store the link from child to parent. Done when: A monthly rule on the thirty-first skips months without that date and those skipped occurrences do not count towards a count limit, tested across a full year including February. A rule using the last day of the month lands on the twenty-eighth, twenty-ninth, thirtieth or thirty-first correctly, tested. Closing an instance creates exactly one successor, tested including a double close. Editing the rule affects only future instances and the existing ones keep their dates. Do not build yet: No comments, notifications or export yet. ### Phase 4 · Conversation, and a log that outlives the entities Build: Add comments with mentions that notify, watchers, and assignment changes. Write every mutation to an activity log holding actor, verb, entity and the before and after values. The log is append-only, is never rewritten by an edit, and keeps entries for entities that were later deleted, holding the title as it was at the time. Trap: Rendering history by joining the activity log to the entities it mentions produces blank lines the moment something is deleted, and an issue nobody can find is exactly the one the history was needed for. Denormalise the display values into the log row at write time and accept the duplication. Done when: Deleting an issue leaves its activity entries readable with the title they had at the time, tested. The activity table rejects updates and deletes at the database level, tested by attempting both. A mention notifies exactly once per comment even when the comment is edited, tested. Comment deletion is a soft delete that leaves the thread's structure and the log intact. Do not build yet: No export, backup or search yet. ### Phase 5 · Leaving, arriving, and getting everything out Build: Export every project as JSON and CSV including issues, comments, activity and recurrence rules, and reimport it into an empty instance. Removing a member keeps their comments and activity attributed to a retained display name while revoking access. Add nightly backups, a restore test, migrations on start and an admin recovery command for a locked-out organisation. Trap: Deleting a departed colleague's user row cascades through comments and activity and quietly removes half the reasoning behind decisions the team is still living with. Revoke access, retain the attribution, and make deletion of history a separate deliberate action that names exactly what it will remove. Done when: Removing a member revokes access immediately while their comments and activity remain attributed, tested. Export and reimport into an empty instance reproduces issues, order, comments, activity and recurrence rules, verified by comparison. A nightly backup is taken and a restore is exercised in the suite rather than documented. The admin recovery command restores owner access to an organisation with no reachable owner, tested. Do not build yet: Nothing after this. ### Out of scope (and why) - Hundreds of third-party integrations, which are the paid product's real inventory and each one an ongoing maintenance commitment. - Single sign-on and enterprise directory sync, which are a project in themselves and worth buying rather than building. - Portfolio and capacity planning across many teams, which needs estimates nobody in a small team keeps accurate. - Native mobile clients, since a board that works in a mobile browser covers the cases a small team actually hits. ### README must contain - That ordering uses sortable ranks, why integer positions were rejected, and when rebalancing runs - That authorisation lives in one function and that a test enumerates mutations to prove it - How the recurrence subset behaves on dates that do not exist, and which form to use for end of month - That the activity log is append-only and keeps display values from the time of the event - How to export a project, how to restore from a backup, and how the admin recovery command works
# Agent instructions · MeisterTask indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 and TypeScript with Docker Compose for the whole stack, PostgreSQL with Drizzle migrations, ordering held as sortable ranks, One authorisation function every query passes through, A subset of the calendar recurrence rule, instances generated on completion, An append-only activity log that outlives deleted entities. 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 - Storing position as an integer means every drag rewrites every card below it, so two people reordering the same column at the same time produce interleaved writes, cards that snap back after a refresh, and a board nobody trusts. Compute a rank between the neighbours, write one row, and rebalance on a schedule rather than on every move. - Checking permissions in the route handler means the one endpoint somebody adds in a hurry becomes the way a viewer edits the backlog, and nothing in the test suite notices because the test suite tests the routes that were written carefully. Put the check in the data access layer and write an enumeration test that fails when a mutation exists without a matching authorisation test. - Materialising a year of future instances for every repeating task fills the board with cards nobody wants to see, and editing the rule afterwards leaves you deciding which of three hundred existing rows to rewrite. Generate one instance at a time on completion, keep the rule on the parent, and store the link from child to parent. - Rendering history by joining the activity log to the entities it mentions produces blank lines the moment something is deleted, and an issue nobody can find is exactly the one the history was needed for. Denormalise the display values into the log row at write time and accept the duplication.
# Build plan · MeisterTask A team board whose hard problems are ordering cards two people can drag at once, enforcing roles in one place rather than in every route, and generating recurring work from rules that do not invent dates the calendar does not have. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One issue model and an order two people can change at once Dragging a card writes one row, and concurrent drags do not fight. ### Steps 1. Rank between neighbours, write one row The drag should cost one update, not a column rewrite. Files: `src/lib/rank.ts`, `tests/rank.test.ts` 2. Test two simultaneous drags This is the bug people report as the board being flaky. Files: `tests/concurrent-move.test.ts` 3. Rebalance without moving anything A maintenance job that changes the order is worse than no job. Files: `src/jobs/rebalance.ts` ### Done when - [ ] One row written per move - [ ] Concurrent moves converge deterministically - [ ] Rebalance preserves visible order - [ ] All three views read one issue model ### Watch out - Storing position as an integer means every drag rewrites every card below it, so two people reordering the same column at the same time produce interleaved writes, cards that snap back after a refresh, and a board nobody trusts. Compute a rank between the neighbours, write one row, and rebalance on a schedule rather than on every move. ## Phase 2 · Roles decided in one place Every read and write passes the same authorisation function, and each mutation has a test. ### Steps 1. One authorisation function, called by the data layer Routes should not be the place this is decided. Files: `src/lib/authz.ts` 2. Enumerate mutations and demand a test for each The check that survives the next hurried feature. Files: `tests/authz-coverage.test.ts` 3. Return empty, not forbidden, across organisations Otherwise the error message is an existence oracle. Files: `src/db/scope.ts` ### Done when - [ ] Every mutation has a viewer-refused test - [ ] Cross-organisation reads return empty - [ ] Project overrides are honoured - [ ] No route contains its own permission logic ### Watch out - Checking permissions in the route handler means the one endpoint somebody adds in a hurry becomes the way a viewer edits the backlog, and nothing in the test suite notices because the test suite tests the routes that were written carefully. Put the check in the data access layer and write an enumeration test that fails when a mutation exists without a matching authorisation test. ## Phase 3 · Recurring work that respects the calendar A repeating task generates the next instance correctly, including on dates that do not exist. ### Steps 1. Implement the invalid-date rule literally The specification already decided this; do not improvise a friendlier answer. Files: `src/lib/recurrence.ts`, `tests/recurrence.test.ts` 2. Generate on completion, one at a time A board full of future cards is a board people stop reading. Files: `src/jobs/next-instance.ts` 3. Offer the end-of-month form explicitly Because most people asking for the thirty-first mean the last day. Files: `src/ui/RecurrenceEditor.tsx` ### Done when - [ ] Invalid dates are skipped and do not count - [ ] End-of-month rules land correctly every month - [ ] Closing creates exactly one successor - [ ] Rule edits affect only future instances ### Watch out - Materialising a year of future instances for every repeating task fills the board with cards nobody wants to see, and editing the rule afterwards leaves you deciding which of three hundred existing rows to rewrite. Generate one instance at a time on completion, keep the rule on the parent, and store the link from child to parent. ## Phase 4 · Conversation, and a log that outlives the entities Comments, mentions and assignments all land in an append-only history. ### Steps 1. Denormalise display values into the log History that depends on live rows is not history. Files: `src/lib/activity.ts` 2. Make the log append-only in the database A revoked update permission beats a code comment saying do not update. Files: `migrations/00x_activity.sql` 3. Notify once per mention, not per save Editing a comment should not page the team again. Files: `src/lib/mentions.ts`, `tests/mentions.test.ts` ### Done when - [ ] Deleted entities keep readable history - [ ] Updates and deletes on the log are refused - [ ] A mention notifies once across edits - [ ] Comment deletion preserves thread structure ### Watch out - Rendering history by joining the activity log to the entities it mentions produces blank lines the moment something is deleted, and an issue nobody can find is exactly the one the history was needed for. Denormalise the display values into the log row at write time and accept the duplication. ## Not in this build - Third-party integrations. - Single sign-on and directory sync. - Portfolio and capacity planning. - Native mobile clients. ## After v1, if you want it - Attachments, once backup and retention have run for a month without surprises - A saved-filter view built on the existing issue model rather than a new one - Time tracking, which is only worth adding if someone will actually read the numbers
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Issues, activity and memberships. DATABASE_URL=postgres://user:pass@db:5432/boards # Required. Used in invitations and mention notifications. APP_BASE_URL=https://boards.example.com # Required · secret. Signs sessions. Rotating it logs everyone out, which is the point. SESSION_SECRET=a long random string, redacted # Required · secret. Invitations and mentions only. No marketing path exists. SMTP_URL=smtps://user:pass@smtp.example.com:465 # Required. Nightly dumps, with a restore exercised in the suite. BACKUP_DIR=/var/backups/boards # Required. When rank rebalancing runs. It must never change visible order. RANK_REBALANCE_CRON=0 4 * * * # Required · secret. Grants owner access to a locked-out organisation. Store it away from the server. ADMIN_RECOVERY_TOKEN=a long random string, redacted
# MeisterTask · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For MeisterTask, run visual project boards with recurring tasks, timelines, and lightweight reporting. The hard boundary is cross-platform polish, integrations, automations, and team administration, plus collaboration, integrations, and organizational trust. ## Product outcome A board where two people can reorder the same column without fighting, a viewer cannot edit anything through any route, a monthly task does not invent the thirty-first of February, and the reasoning behind a decision is still readable after everyone involved has left. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Docker - PostgreSQL - self-hosted server - email credentials for invitations ## Explicit non-goals for v1 - Third-party integrations. - Single sign-on and directory sync. - Portfolio and capacity planning. - Native mobile clients. - cross-platform polish, integrations, automations, and team administration - deep integrations - enterprise permissions - large-scale reporting - battle-tested notifications and mobile apps ## Success criteria - A card move is confirmed to write exactly one row - Every mutation is confirmed to have a viewer-refused test - Invalid recurrence dates are confirmed skipped and uncounted - Deleted entities are confirmed to keep readable history - A restore is confirmed exercised in the suite
# Build brief · MeisterTask 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 small-team project board whose history survives the people who left to replace MeisterTask. 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. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 and TypeScript with Docker Compose for the whole stack · one command to a working instance or nobody will run it - Storage: PostgreSQL with Drizzle migrations, ordering held as sortable ranks · integer positions cannot survive two people dragging at once - Access: One authorisation function every query passes through · a permission check per route is a permission check you will forget - Recurrence: A subset of the calendar recurrence rule, instances generated on completion · the standard already answered the awkward date questions - History: An append-only activity log that outlives deleted entities · the point of a tracker is what happened, not what is left ### Data model (create this before Phase 1) - orgs(id, name), memberships(org_id, user_id, role) · role is owner, member or viewer - projects(id, org_id, name, archived_at), columns(id, project_id, name, rank) - issues(id, project_id, column_id, title, body, assignee_id, rank, due_at, recurrence_rule, recurrence_parent_id, created_at, closed_at) - labels, milestones and watchers as join tables, all scoped to a project - comments(id, issue_id, author_id, body, created_at, edited_at, deleted_at) · soft deleted, because a thread with holes is unreadable - activity(id, org_id, project_id, actor_id, entity_type, entity_id, verb, before_json, after_json, at) · append only, never updated or deleted - The rank columns are sortable strings rather than integers, so inserting between two cards touches one row and two simultaneous drags cannot collide on a value. ### Phase 1 · One issue model and an order two people can change at once Build: Model issues once and render list, board and timeline from that single model. Order columns and issues by a sortable rank computed between its neighbours. Add a rebalance routine for when ranks grow too long, and a unique constraint per column so two cards cannot claim one position. Trap: Storing position as an integer means every drag rewrites every card below it, so two people reordering the same column at the same time produce interleaved writes, cards that snap back after a refresh, and a board nobody trusts. Compute a rank between the neighbours, write one row, and rebalance on a schedule rather than on every move. Done when: Moving a card writes exactly one issue row, asserted by counting statements. Two concurrent moves in the same column both succeed and produce a deterministic final order, tested with parallel requests. The rebalance routine runs without changing the visible order, tested on a column of a thousand issues. List, board and timeline views read the same issue rows with no view-specific duplicate state. Do not build yet: No roles, recurrence, comments or export. ### Phase 2 · Roles decided in one place Build: Define owner, member and viewer at organisation level with project level overrides. Route every query through one function that takes the actor and the resource and returns the permitted scope. Write a test per mutation asserting that a viewer is refused and that a member of another organisation sees nothing. Trap: Checking permissions in the route handler means the one endpoint somebody adds in a hurry becomes the way a viewer edits the backlog, and nothing in the test suite notices because the test suite tests the routes that were written carefully. Put the check in the data access layer and write an enumeration test that fails when a mutation exists without a matching authorisation test. Done when: Every mutation has a test asserting a viewer is refused, enforced by a test that enumerates mutations and fails on any without one. A user from another organisation receives an empty result rather than an error revealing that the resource exists, tested. Project level overrides are honoured and tested against the organisation default. No route performs its own permission logic, verified by review and a grep test. Do not build yet: No recurrence, comments or notifications yet. ### Phase 3 · Recurring work that respects the calendar Build: Support a subset of the standard recurrence rule: frequency, interval, by-weekday, by-month-day and either a count or an end date. The specification states that recurrence instances with an invalid date, such as the thirtieth of February, must be ignored and must not count towards the count, so implement that literally and offer the last-day-of-month form for people who meant end of month. Generate the next instance when the current one closes, never in advance. Trap: Materialising a year of future instances for every repeating task fills the board with cards nobody wants to see, and editing the rule afterwards leaves you deciding which of three hundred existing rows to rewrite. Generate one instance at a time on completion, keep the rule on the parent, and store the link from child to parent. Done when: A monthly rule on the thirty-first skips months without that date and those skipped occurrences do not count towards a count limit, tested across a full year including February. A rule using the last day of the month lands on the twenty-eighth, twenty-ninth, thirtieth or thirty-first correctly, tested. Closing an instance creates exactly one successor, tested including a double close. Editing the rule affects only future instances and the existing ones keep their dates. Do not build yet: No comments, notifications or export yet. ### Phase 4 · Conversation, and a log that outlives the entities Build: Add comments with mentions that notify, watchers, and assignment changes. Write every mutation to an activity log holding actor, verb, entity and the before and after values. The log is append-only, is never rewritten by an edit, and keeps entries for entities that were later deleted, holding the title as it was at the time. Trap: Rendering history by joining the activity log to the entities it mentions produces blank lines the moment something is deleted, and an issue nobody can find is exactly the one the history was needed for. Denormalise the display values into the log row at write time and accept the duplication. Done when: Deleting an issue leaves its activity entries readable with the title they had at the time, tested. The activity table rejects updates and deletes at the database level, tested by attempting both. A mention notifies exactly once per comment even when the comment is edited, tested. Comment deletion is a soft delete that leaves the thread's structure and the log intact. Do not build yet: No export, backup or search yet. ### Phase 5 · Leaving, arriving, and getting everything out Build: Export every project as JSON and CSV including issues, comments, activity and recurrence rules, and reimport it into an empty instance. Removing a member keeps their comments and activity attributed to a retained display name while revoking access. Add nightly backups, a restore test, migrations on start and an admin recovery command for a locked-out organisation. Trap: Deleting a departed colleague's user row cascades through comments and activity and quietly removes half the reasoning behind decisions the team is still living with. Revoke access, retain the attribution, and make deletion of history a separate deliberate action that names exactly what it will remove. Done when: Removing a member revokes access immediately while their comments and activity remain attributed, tested. Export and reimport into an empty instance reproduces issues, order, comments, activity and recurrence rules, verified by comparison. A nightly backup is taken and a restore is exercised in the suite rather than documented. The admin recovery command restores owner access to an organisation with no reachable owner, tested. Do not build yet: Nothing after this. ### Out of scope (and why) - Hundreds of third-party integrations, which are the paid product's real inventory and each one an ongoing maintenance commitment. - Single sign-on and enterprise directory sync, which are a project in themselves and worth buying rather than building. - Portfolio and capacity planning across many teams, which needs estimates nobody in a small team keeps accurate. - Native mobile clients, since a board that works in a mobile browser covers the cases a small team actually hits. ### README must contain - That ordering uses sortable ranks, why integer positions were rejected, and when rebalancing runs - That authorisation lives in one function and that a test enumerates mutations to prove it - How the recurrence subset behaves on dates that do not exist, and which form to use for end of month - That the activity log is append-only and keeps display values from the time of the event - How to export a project, how to restore from a backup, and how the admin recovery command works
# Architecture · MeisterTask ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 and TypeScript with Docker Compose for the whole stack | one command to a working instance or nobody will run it | | Storage | PostgreSQL with Drizzle migrations, ordering held as sortable ranks | integer positions cannot survive two people dragging at once | | Access | One authorisation function every query passes through | a permission check per route is a permission check you will forget | | Recurrence | A subset of the calendar recurrence rule, instances generated on completion | the standard already answered the awkward date questions | | History | An append-only activity log that outlives deleted entities | the point of a tracker is what happened, not what is left | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Issues | One canonical model behind three views. | Any presentation, provided no view keeps its own copy of order. | | Ordering | Sortable ranks and rebalancing. | Any ordering scheme that writes one row per move. | | Authorisation | One function used by the data layer. | Not swappable for per-route checks, which is the failure this phase exists to prevent. | | Recurrence | The rule subset and instance generation. | Any implementation that passes the invalid-date tests. | | Activity | Append-only history with denormalised display values. | Any store that refuses updates and deletes. | ## 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 · Issues, activity and memberships. - `APP_BASE_URL` · required · Used in invitations and mention notifications. - `SESSION_SECRET` · required, secret · Signs sessions. Rotating it logs everyone out, which is the point. - `SMTP_URL` · required, secret · Invitations and mentions only. No marketing path exists. - `BACKUP_DIR` · required · Nightly dumps, with a restore exercised in the suite. - `RANK_REBALANCE_CRON` · required · When rank rebalancing runs. It must never change visible order. - `ADMIN_RECOVERY_TOKEN` · required, secret · Grants owner access to a locked-out organisation. Store it away from the server. ## 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 · MeisterTask product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js 15 and TypeScript with Docker Compose for the whole stack, PostgreSQL with Drizzle migrations, ordering held as sortable ranks, One authorisation function every query passes through, A subset of the calendar recurrence rule, instances generated on completion, An append-only activity log that outlives deleted entities. - 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 - Storing position as an integer means every drag rewrites every card below it, so two people reordering the same column at the same time produce interleaved writes, cards that snap back after a refresh, and a board nobody trusts. Compute a rank between the neighbours, write one row, and rebalance on a schedule rather than on every move. - Checking permissions in the route handler means the one endpoint somebody adds in a hurry becomes the way a viewer edits the backlog, and nothing in the test suite notices because the test suite tests the routes that were written carefully. Put the check in the data access layer and write an enumeration test that fails when a mutation exists without a matching authorisation test. - Materialising a year of future instances for every repeating task fills the board with cards nobody wants to see, and editing the rule afterwards leaves you deciding which of three hundred existing rows to rewrite. Generate one instance at a time on completion, keep the rule on the parent, and store the link from child to parent. - Rendering history by joining the activity log to the entities it mentions produces blank lines the moment something is deleted, and an issue nobody can find is exactly the one the history was needed for. Denormalise the display values into the log row at write time and accept the duplication. - Deleting a departed colleague's user row cascades through comments and activity and quietly removes half the reasoning behind decisions the team is still living with. Revoke access, retain the attribution, and make deletion of history a separate deliberate action that names exactly what it will remove.
# Delivery milestones · MeisterTask Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · One issue model and an order two people can change at once Dragging a card writes one row, and concurrent drags do not fight. ### Steps 1. Rank between neighbours, write one row The drag should cost one update, not a column rewrite. Files: `src/lib/rank.ts`, `tests/rank.test.ts` 2. Test two simultaneous drags This is the bug people report as the board being flaky. Files: `tests/concurrent-move.test.ts` 3. Rebalance without moving anything A maintenance job that changes the order is worse than no job. Files: `src/jobs/rebalance.ts` ### Done when - [ ] One row written per move - [ ] Concurrent moves converge deterministically - [ ] Rebalance preserves visible order - [ ] All three views read one issue model ### Watch out - Storing position as an integer means every drag rewrites every card below it, so two people reordering the same column at the same time produce interleaved writes, cards that snap back after a refresh, and a board nobody trusts. Compute a rank between the neighbours, write one row, and rebalance on a schedule rather than on every move. ## M2 · Roles decided in one place Every read and write passes the same authorisation function, and each mutation has a test. ### Steps 1. One authorisation function, called by the data layer Routes should not be the place this is decided. Files: `src/lib/authz.ts` 2. Enumerate mutations and demand a test for each The check that survives the next hurried feature. Files: `tests/authz-coverage.test.ts` 3. Return empty, not forbidden, across organisations Otherwise the error message is an existence oracle. Files: `src/db/scope.ts` ### Done when - [ ] Every mutation has a viewer-refused test - [ ] Cross-organisation reads return empty - [ ] Project overrides are honoured - [ ] No route contains its own permission logic ### Watch out - Checking permissions in the route handler means the one endpoint somebody adds in a hurry becomes the way a viewer edits the backlog, and nothing in the test suite notices because the test suite tests the routes that were written carefully. Put the check in the data access layer and write an enumeration test that fails when a mutation exists without a matching authorisation test. ## M3 · Recurring work that respects the calendar A repeating task generates the next instance correctly, including on dates that do not exist. ### Steps 1. Implement the invalid-date rule literally The specification already decided this; do not improvise a friendlier answer. Files: `src/lib/recurrence.ts`, `tests/recurrence.test.ts` 2. Generate on completion, one at a time A board full of future cards is a board people stop reading. Files: `src/jobs/next-instance.ts` 3. Offer the end-of-month form explicitly Because most people asking for the thirty-first mean the last day. Files: `src/ui/RecurrenceEditor.tsx` ### Done when - [ ] Invalid dates are skipped and do not count - [ ] End-of-month rules land correctly every month - [ ] Closing creates exactly one successor - [ ] Rule edits affect only future instances ### Watch out - Materialising a year of future instances for every repeating task fills the board with cards nobody wants to see, and editing the rule afterwards leaves you deciding which of three hundred existing rows to rewrite. Generate one instance at a time on completion, keep the rule on the parent, and store the link from child to parent. ## M4 · Conversation, and a log that outlives the entities Comments, mentions and assignments all land in an append-only history. ### Steps 1. Denormalise display values into the log History that depends on live rows is not history. Files: `src/lib/activity.ts` 2. Make the log append-only in the database A revoked update permission beats a code comment saying do not update. Files: `migrations/00x_activity.sql` 3. Notify once per mention, not per save Editing a comment should not page the team again. Files: `src/lib/mentions.ts`, `tests/mentions.test.ts` ### Done when - [ ] Deleted entities keep readable history - [ ] Updates and deletes on the log are refused - [ ] A mention notifies once across edits - [ ] Comment deletion preserves thread structure ### Watch out - Rendering history by joining the activity log to the entities it mentions produces blank lines the moment something is deleted, and an issue nobody can find is exactly the one the history was needed for. Denormalise the display values into the log row at write time and accept the duplication. ## M5 · Leaving, arriving, and getting everything out (production only) A departing member changes nothing about the record, and the whole project exports. ### Steps 1. Revoke access without deleting attribution The reasoning behind old decisions is the asset here. Files: `src/lib/membership.ts` 2. Round trip a whole project through export An export nobody has reimported is a guess. Files: `tests/export-roundtrip.test.ts` 3. Exercise the restore, not just the backup A backup nobody has restored is a hope. Files: `ops/backup.sh`, `tests/restore.test.ts` ### Done when - [ ] Removed members lose access and keep attribution - [ ] Export reimports into an empty instance faithfully - [ ] A restore is exercised in the suite - [ ] Admin recovery restores a locked-out organisation ### Watch out - Deleting a departed colleague's user row cascades through comments and activity and quietly removes half the reasoning behind decisions the team is still living with. Revoke access, retain the attribution, and make deletion of history a separate deliberate action that names exactly what it will remove.
# Operations · MeisterTask ## Backup Nightly database dumps with a retention, plus the uploaded attachments if you add them later. The activity log is the part that cannot be reconstructed from anywhere else, so it sets the retention rather than the issue table. ## Restore Restore into an empty instance, run migrations, and confirm that board order, recurrence rules and activity history all survive. A restore that loses the log has lost the reason the tracker existed. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Failed background jobs, the age of the last successful backup, and rank length, which is the early warning that rebalancing has stopped running. ## Incident checklist The two to rehearse are a locked-out organisation, handled by the admin recovery command, and a bad bulk edit, handled by reading the activity log's before values rather than by restoring a backup over everyone's day. 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 - [ ] A card move is confirmed to write exactly one row - [ ] Every mutation is confirmed to have a viewer-refused test - [ ] Invalid recurrence dates are confirmed skipped and uncounted - [ ] Deleted entities are confirmed to keep readable history - [ ] A restore is confirmed exercised in the suite ## Launch constraint Do not market omitted MeisterTask 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. Issues, activity and memberships. DATABASE_URL=postgres://user:pass@db:5432/boards # Required. Used in invitations and mention notifications. APP_BASE_URL=https://boards.example.com # Required · secret. Signs sessions. Rotating it logs everyone out, which is the point. SESSION_SECRET=a long random string, redacted # Required · secret. Invitations and mentions only. No marketing path exists. SMTP_URL=smtps://user:pass@smtp.example.com:465 # Required. Nightly dumps, with a restore exercised in the suite. BACKUP_DIR=/var/backups/boards # Required. When rank rebalancing runs. It must never change visible order. RANK_REBALANCE_CRON=0 4 * * * # Required · secret. Grants owner access to a locked-out organisation. Store it away from the server. ADMIN_RECOVERY_TOKEN=a long random string, redacted
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for MeisterTask because teams pay because project state has to remain authoritative through staff changes, outages, and years of accumulated workflow exceptions. The recurring cost buys auth, permissions, notifications, search, migrations, audit history, integrations, backups, and uptime, not just the visible interface.
xcross-platform polish, integrations, automations, and team administration
xdeep integrations
xenterprise permissions
xlarge-scale reporting
xbattle-tested notifications and mobile apps
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
MeisterTask pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/user | $0/user | Unlimited tasks/team members, 3 projects, 5 notes, 5 Content Manager seats, 20 Meister AI credits, 20 MB/file and 7-day version history |
| pro | $9/user | $7/user | Unlimited projects/notes, 75 Meister AI credits, 200 MB/file and 30-day version history |
| business | $16/user | $12.50/user | 350 Meister AI credits, 200 MB/file, unlimited version history, timelines/roles/security and reports |
| enterprise | custom | — | 350 Meister AI credits plus workload, portfolios, SSO, compliance and tailored support |
free tierUnlimited tasks/team members; 3 projects, 5 notes, 5 Content Manager seats, 20 AI credits, 20 MB/file and 7-day version history
billingmonthly + annual for Pro/Business; Enterprise custom
hidden costsVAT is excluded and checkout may apply regional pricing. AI credits and file/version-history limits are plan caps; Enterprise pricing is internally inconsistent on the live official page, so it is not inferred.
verified 2026-08-14 · source ↗
Vibecode MeisterTask
Kinda. The core of MeisterTask is buildable in a weekend with the prompt on this page, but there are real gaps: cross-platform polish, integrations, automations, and team administration, deep integrations. Read the honest list above before committing.
How much does MeisterTask cost?
MeisterTask costs about $9/month (Pro, checked 2026-08-14), which is $108 per year.
What do I lose by replacing MeisterTask?
Honestly: cross-platform polish, integrations, automations, and team administration; deep integrations; enterprise permissions; large-scale reporting; battle-tested notifications and mobile apps. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to MeisterTask?
Yes: YouTrack (Boards, recurring work via visual schedules, Gantt, calendars, and reports for ten users) The prompt is for when you want it exactly your way.