Vibecode Lucidspark
track this build4 phases, 12 steps, beginner friendly0%The visible whiteboarding loop is buildable, but a credible replacement needs more than the first screen. Lucidspark earns its keep through interaction polish, collaboration, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of Lucidspark. 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 ===== # Lucidspark · indie build A collaborative board for running workshops, where the canvas is the easy half and the difficult half is the facilitation: anonymity that actually holds, timers everyone sees identically, and voting that cannot be gamed by whoever votes last. 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 | | --- | --- | --- | | Canvas | Sticky notes and groupings as simple objects | workshop content is text on cards, not freeform drawing | | Session state | A server-authoritative phase with a synchronised timer | everyone must be in the same phase and see the same clock | | Anonymity | Authorship withheld server side, not hidden in the interface | anonymity that the API undoes is not anonymity | | Voting | Allocations hidden until reveal, enforced server side | visible running totals change how people vote | | Runtime | TypeScript with PostgreSQL and a socket layer | sessions are short, concurrent and stateful | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the socket layer. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Sessions, votes and notes with server-side visibility rules. - Get it: Docker or your package manager. - Verify: psql connects and creates a table - [ ] **Eight people willing to run a real workshop** · free, and it costs an hour of eight people's time - Why: Every claim here is about group behaviour: whether anonymity is believed, whether the timer feels fair, whether voting changes when totals are hidden. None of it is observable alone. - Get it: A real team and a real question to work on, for an hour. - Verify: A full session runs with eight participants including a vote and a reveal - [ ] **What the facilitator can see, written down** · free - Why: Anonymity is a promise to participants, and whether the facilitator can see authorship changes what people write. Deciding it silently means deciding it against them. - Get it: Decide whether the facilitator can see authorship, state it in the session, and write down why. - Verify: A written rule exists and is shown to participants when a session is anonymous - [ ] **A client with a deliberately wrong clock** · free - Why: Timer synchronisation is only testable against skew, and machine clocks in a room full of laptops are genuinely inconsistent. - Get it: Set a test machine's clock several minutes off and join a session with it. - Verify: The skewed client shows the correct remaining time ## 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: - Freeform drawing and diagramming. - Video and audio for the session. - Workshop format template libraries. - Issue tracker integration. - high-fidelity import and export - presentation and enterprise controls - multiplayer cursors and comments - large template libraries If one of those is essential to you, that is the reason to keep paying for Lucidspark, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Lucidspark 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 workshop whiteboard where the facilitation mechanics are the product to replace Lucidspark. 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) - Canvas: Sticky notes and groupings as simple objects · workshop content is text on cards, not freeform drawing - Session state: A server-authoritative phase with a synchronised timer · everyone must be in the same phase and see the same clock - Anonymity: Authorship withheld server side, not hidden in the interface · anonymity that the API undoes is not anonymity - Voting: Allocations hidden until reveal, enforced server side · visible running totals change how people vote - Runtime: TypeScript with PostgreSQL and a socket layer · sessions are short, concurrent and stateful ### Data model (create this before Phase 1) - boards(id, title, created_by, created_at, archived_at) - sessions(id, board_id, phase, phase_started_at, phase_duration_seconds, anonymous, facilitator_id) - notes(id, board_id, author_id, body, x, y, colour, group_id, created_at) · author_id withheld when anonymous - groups(id, board_id, label, created_by) - votes(session_id, voter_id, note_id, weight, cast_at) · never returned to participants before reveal - reveals(session_id, at, revealed_by) · the moment results became visible, recorded - participants(session_id, user_id, display_name, joined_at, role) - A vote is stored with its voter for facilitator audit, but no query path returns another participant's votes before the reveal. ### Phase 1 · Notes on a board, together Build: Sticky notes with text, position and colour, edited concurrently. Reconcile simultaneous moves and edits per note. Handle a participant joining a board that already has hundreds of notes. Trap: Broadcasting the whole board on every change works with twenty notes and collapses at four hundred, which is a normal workshop output, and it collapses during the session rather than afterwards. Send per-note changes and reconcile at the note level. Done when: Several participants add and move notes simultaneously without losing changes. A board with four hundred notes stays responsive with eight participants. Joining a busy board loads the current state without a long pause. Two people editing one note's text resolve deterministically. Do not build yet: No phases, anonymity, timers or voting. ### Phase 2 · Anonymity that holds at the API Build: Withhold authorship server side when a session is anonymous, so no API response carries it. Keep it stored for facilitator audit. Assert absence across every response shape including errors and socket messages. Trap: Hiding the author in the interface while the socket message still carries it means anyone who opens the developer tools can see who wrote the critical note, and in a workshop about a team's problems that is precisely the harm anonymity was for. Strip it server side and assert it in the suite. Done when: No API or socket message carries authorship during an anonymous session, asserted across list, single-note, error and socket shapes. The facilitator can see authorship where the session's rules allow. Turning anonymity off mid-session does not retroactively reveal earlier notes without an explicit action. Ordering and timing cannot be used to infer authorship trivially. Do not build yet: No phases, timers or voting yet. ### Phase 3 · One clock everyone sees Build: A server-authoritative phase with a start time and duration, from which each client derives the remaining time. Handle a client whose clock is wrong and one that reconnects mid-phase. Make the phase transition server driven. Trap: A timer counted down independently on each client drifts, so participants see different remaining times and someone is cut off mid-sentence believing they had thirty seconds. Send the phase start and duration, derive locally, and correct against the server rather than counting down in isolation. Done when: Every client shows the same remaining time within a small tolerance, tested across clients with deliberately skewed clocks. A client reconnecting mid-phase shows the correct remaining time immediately. The phase transition happens server side rather than being triggered by whichever client counts down first. Pausing a phase pauses it for everyone. Do not build yet: No voting or export yet. ### Phase 4 · Voting nobody can watch Build: Allocate a fixed number of votes per participant, stored server side and returned to nobody before the reveal. Show only your own allocation while voting. Reveal on an explicit facilitator action, recorded. Trap: Showing a running vote total during voting changes the result, because people converge on whatever is already winning, and that is a well known effect rather than a hypothetical one. Return no other participant's votes and no aggregate before the reveal, enforced in the query rather than the display. Done when: No participant can see another's votes or any aggregate before the reveal, asserted at the API level. Each participant sees their own remaining allocation. The reveal is an explicit facilitator action and is recorded with its time. A participant cannot exceed their allocation, enforced server side. Do not build yet: No grouping or export yet. ### Phase 5 · Turning a board into an outcome Build: Group notes into themes, order by vote result, and export the outcome as a document with the notes, groupings and results. Archive boards without deleting them. Handle a board revisited months later. Trap: A workshop whose output is a screenshot of a board is a workshop whose output is lost, because nobody can read four hundred sticky notes in an image and nobody opens the board again. Export a document with the themes, the notes under them and the vote results in order. Done when: Export produces a readable document with themes, their notes and vote results in order. Archived boards remain openable and searchable rather than being deleted. A board revisited months later shows its session record including phases and the reveal. Export includes anonymous notes without authorship. Do not build yet: Nothing after this. ### Out of scope (and why) - Freeform drawing and diagramming, which is a different tool and a different set of canvas problems. - Video and audio for the session itself, which is a substantial real-time project and is usually happening elsewhere anyway. - Template libraries for workshop formats, which is most of what the hosted products sell. - Integration with issue trackers, which is the step after the workshop rather than part of it. ### README must contain - That anonymity is enforced server side and asserted absent from every response shape including sockets - That the timer is derived from a server-authoritative phase, because independently counting clients drift - That no vote totals are available before the reveal, since visible running totals change how people vote - That per-note changes are sent rather than the whole board, which collapses at workshop scale - That the output is an ordered document rather than an image of a board ===== AGENTS.md ===== # Agent instructions · Lucidspark indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Sticky notes and groupings as simple objects, A server-authoritative phase with a synchronised timer, Authorship withheld server side, not hidden in the interface, Allocations hidden until reveal, enforced server side, TypeScript with PostgreSQL and a socket layer. 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 - Broadcasting the whole board on every change works with twenty notes and collapses at four hundred, which is a normal workshop output, and it collapses during the session rather than afterwards. Send per-note changes and reconcile at the note level. - Hiding the author in the interface while the socket message still carries it means anyone who opens the developer tools can see who wrote the critical note, and in a workshop about a team's problems that is precisely the harm anonymity was for. Strip it server side and assert it in the suite. - A timer counted down independently on each client drifts, so participants see different remaining times and someone is cut off mid-sentence believing they had thirty seconds. Send the phase start and duration, derive locally, and correct against the server rather than counting down in isolation. - Showing a running vote total during voting changes the result, because people converge on whatever is already winning, and that is a well known effect rather than a hypothetical one. Return no other participant's votes and no aggregate before the reveal, enforced in the query rather than the display. ===== BUILD_PLAN.md ===== # Build plan · Lucidspark A collaborative board for running workshops, where the canvas is the easy half and the difficult half is the facilitation: anonymity that actually holds, timers everyone sees identically, and voting that cannot be gamed by whoever votes last. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Notes on a board, together Several people add and move notes simultaneously without conflict. ### Steps 1. Send per-note changes Whole-board broadcast collapses at workshop scale, during the workshop. Files: `src/sync.ts` 2. Reconcile per note deterministically Two people editing one note is normal in a workshop. Files: `src/reconcile.ts`, `src/reconcile.test.ts` 3. Make joining a busy board fast People join late and the pause is visible to everyone. Files: `src/join.ts` ### Done when - [ ] Simultaneous adds and moves do not lose changes - [ ] Four hundred notes stay responsive with eight participants - [ ] Joining a busy board is fast - [ ] Concurrent edits to one note resolve deterministically ### Watch out - Broadcasting the whole board on every change works with twenty notes and collapses at four hundred, which is a normal workshop output, and it collapses during the session rather than afterwards. Send per-note changes and reconcile at the note level. ## Phase 2 · Anonymity that holds at the API An anonymous note's author cannot be discovered by anyone but the facilitator. ### Steps 1. Strip authorship server side In the serialiser and the socket path both. Files: `src/anonymity.ts`, `tests/anonymity-leak.test.ts` 2. Keep it for facilitator audit only Stored, and reachable only through a path with its own rules. Files: `src/anonymity.ts` 3. Do not reveal retroactively Notes written under anonymity were written under a promise. Files: `src/sessions.ts` ### Done when - [ ] No response shape carries authorship when anonymous - [ ] Facilitator access follows the session's stated rules - [ ] Turning anonymity off does not reveal earlier notes - [ ] Ordering does not trivially reveal authors ### Watch out - Hiding the author in the interface while the socket message still carries it means anyone who opens the developer tools can see who wrote the critical note, and in a workshop about a team's problems that is precisely the harm anonymity was for. Strip it server side and assert it in the suite. ## Phase 3 · One clock everyone sees The timer shows the same remaining time on every screen. ### Steps 1. Make the phase server authoritative Start and duration from the server, derived locally. Files: `src/phases.ts` 2. Correct for client clock skew Machine clocks are wrong and a workshop timer is visible to everyone. Files: `src/timer.ts`, `src/timer.test.ts` 3. Transition on the server Not on whichever client reaches zero first. Files: `src/phases.ts` ### Done when - [ ] All clients show the same remaining time - [ ] A reconnecting client is immediately correct - [ ] Transitions are server driven - [ ] Pausing affects everyone ### Watch out - A timer counted down independently on each client drifts, so participants see different remaining times and someone is cut off mid-sentence believing they had thirty seconds. Send the phase start and duration, derive locally, and correct against the server rather than counting down in isolation. ## Phase 4 · Voting nobody can watch Votes are invisible until the reveal, including in the traffic. ### Steps 1. Return no totals before reveal Enforced in the query. Running totals change the result. Files: `src/votes.ts`, `tests/vote-secrecy.test.ts` 2. Show only your own allocation So you know what you have left without seeing anyone else. Files: `src/votes.ts` 3. Record the reveal The moment results became visible is part of the session record. Files: `src/reveal.ts` ### Done when - [ ] No aggregate is available before the reveal - [ ] Participants see only their own allocation - [ ] The reveal is explicit and recorded - [ ] Allocations cannot be exceeded ### Watch out - Showing a running vote total during voting changes the result, because people converge on whatever is already winning, and that is a well known effect rather than a hypothetical one. Return no other participant's votes and no aggregate before the reveal, enforced in the query rather than the display. ## Not in this build - Freeform drawing and diagramming. - Video and audio for the session. - Workshop format template libraries. - Issue tracker integration. ## After v1, if you want it - Facilitator scripts that drive the phases automatically, which is where the timing gets reliable - Breakout groups within a session, which multiplies every synchronisation problem here - Comparing two sessions on the same question, which is where a workshop practice improves ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Boards, notes and votes, some written under a promise of anonymity. DATABASE_URL=postgres://localhost:5432/workshops # Required. The live session channel. SOCKET_PORT=3002 # Required. Whether new sessions are anonymous. Stated to participants either way. ANONYMOUS_DEFAULT=false # Required. A promise to participants. It is shown to them and it changes what they write. FACILITATOR_SEES_AUTHORS=false # Required. The allocation, enforced server side. VOTES_PER_PARTICIPANT=5 # Required. Present as a constant. Visible running totals change the result. REVEAL_TOTALS_BEFORE_END=never
You are building a lean indie version of Lucidspark. 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 ===== # Lucidspark · indie build A collaborative board for running workshops, where the canvas is the easy half and the difficult half is the facilitation: anonymity that actually holds, timers everyone sees identically, and voting that cannot be gamed by whoever votes last. 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 | | --- | --- | --- | | Canvas | Sticky notes and groupings as simple objects | workshop content is text on cards, not freeform drawing | | Session state | A server-authoritative phase with a synchronised timer | everyone must be in the same phase and see the same clock | | Anonymity | Authorship withheld server side, not hidden in the interface | anonymity that the API undoes is not anonymity | | Voting | Allocations hidden until reveal, enforced server side | visible running totals change how people vote | | Runtime | TypeScript with PostgreSQL and a socket layer | sessions are short, concurrent and stateful | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the socket layer. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Sessions, votes and notes with server-side visibility rules. - Get it: Docker or your package manager. - Verify: psql connects and creates a table - [ ] **Eight people willing to run a real workshop** · free, and it costs an hour of eight people's time - Why: Every claim here is about group behaviour: whether anonymity is believed, whether the timer feels fair, whether voting changes when totals are hidden. None of it is observable alone. - Get it: A real team and a real question to work on, for an hour. - Verify: A full session runs with eight participants including a vote and a reveal - [ ] **What the facilitator can see, written down** · free - Why: Anonymity is a promise to participants, and whether the facilitator can see authorship changes what people write. Deciding it silently means deciding it against them. - Get it: Decide whether the facilitator can see authorship, state it in the session, and write down why. - Verify: A written rule exists and is shown to participants when a session is anonymous - [ ] **A client with a deliberately wrong clock** · free - Why: Timer synchronisation is only testable against skew, and machine clocks in a room full of laptops are genuinely inconsistent. - Get it: Set a test machine's clock several minutes off and join a session with it. - Verify: The skewed client shows the correct remaining time ## 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: - Freeform drawing and diagramming. - Video and audio for the session. - Workshop format template libraries. - Issue tracker integration. - high-fidelity import and export - presentation and enterprise controls - multiplayer cursors and comments - large template libraries If one of those is essential to you, that is the reason to keep paying for Lucidspark, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Lucidspark 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 workshop whiteboard where the facilitation mechanics are the product to replace Lucidspark. 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) - Canvas: Sticky notes and groupings as simple objects · workshop content is text on cards, not freeform drawing - Session state: A server-authoritative phase with a synchronised timer · everyone must be in the same phase and see the same clock - Anonymity: Authorship withheld server side, not hidden in the interface · anonymity that the API undoes is not anonymity - Voting: Allocations hidden until reveal, enforced server side · visible running totals change how people vote - Runtime: TypeScript with PostgreSQL and a socket layer · sessions are short, concurrent and stateful ### Data model (create this before Phase 1) - boards(id, title, created_by, created_at, archived_at) - sessions(id, board_id, phase, phase_started_at, phase_duration_seconds, anonymous, facilitator_id) - notes(id, board_id, author_id, body, x, y, colour, group_id, created_at) · author_id withheld when anonymous - groups(id, board_id, label, created_by) - votes(session_id, voter_id, note_id, weight, cast_at) · never returned to participants before reveal - reveals(session_id, at, revealed_by) · the moment results became visible, recorded - participants(session_id, user_id, display_name, joined_at, role) - A vote is stored with its voter for facilitator audit, but no query path returns another participant's votes before the reveal. ### Phase 1 · Notes on a board, together Build: Sticky notes with text, position and colour, edited concurrently. Reconcile simultaneous moves and edits per note. Handle a participant joining a board that already has hundreds of notes. Trap: Broadcasting the whole board on every change works with twenty notes and collapses at four hundred, which is a normal workshop output, and it collapses during the session rather than afterwards. Send per-note changes and reconcile at the note level. Done when: Several participants add and move notes simultaneously without losing changes. A board with four hundred notes stays responsive with eight participants. Joining a busy board loads the current state without a long pause. Two people editing one note's text resolve deterministically. Do not build yet: No phases, anonymity, timers or voting. ### Phase 2 · Anonymity that holds at the API Build: Withhold authorship server side when a session is anonymous, so no API response carries it. Keep it stored for facilitator audit. Assert absence across every response shape including errors and socket messages. Trap: Hiding the author in the interface while the socket message still carries it means anyone who opens the developer tools can see who wrote the critical note, and in a workshop about a team's problems that is precisely the harm anonymity was for. Strip it server side and assert it in the suite. Done when: No API or socket message carries authorship during an anonymous session, asserted across list, single-note, error and socket shapes. The facilitator can see authorship where the session's rules allow. Turning anonymity off mid-session does not retroactively reveal earlier notes without an explicit action. Ordering and timing cannot be used to infer authorship trivially. Do not build yet: No phases, timers or voting yet. ### Phase 3 · One clock everyone sees Build: A server-authoritative phase with a start time and duration, from which each client derives the remaining time. Handle a client whose clock is wrong and one that reconnects mid-phase. Make the phase transition server driven. Trap: A timer counted down independently on each client drifts, so participants see different remaining times and someone is cut off mid-sentence believing they had thirty seconds. Send the phase start and duration, derive locally, and correct against the server rather than counting down in isolation. Done when: Every client shows the same remaining time within a small tolerance, tested across clients with deliberately skewed clocks. A client reconnecting mid-phase shows the correct remaining time immediately. The phase transition happens server side rather than being triggered by whichever client counts down first. Pausing a phase pauses it for everyone. Do not build yet: No voting or export yet. ### Phase 4 · Voting nobody can watch Build: Allocate a fixed number of votes per participant, stored server side and returned to nobody before the reveal. Show only your own allocation while voting. Reveal on an explicit facilitator action, recorded. Trap: Showing a running vote total during voting changes the result, because people converge on whatever is already winning, and that is a well known effect rather than a hypothetical one. Return no other participant's votes and no aggregate before the reveal, enforced in the query rather than the display. Done when: No participant can see another's votes or any aggregate before the reveal, asserted at the API level. Each participant sees their own remaining allocation. The reveal is an explicit facilitator action and is recorded with its time. A participant cannot exceed their allocation, enforced server side. Do not build yet: No grouping or export yet. ### Phase 5 · Turning a board into an outcome Build: Group notes into themes, order by vote result, and export the outcome as a document with the notes, groupings and results. Archive boards without deleting them. Handle a board revisited months later. Trap: A workshop whose output is a screenshot of a board is a workshop whose output is lost, because nobody can read four hundred sticky notes in an image and nobody opens the board again. Export a document with the themes, the notes under them and the vote results in order. Done when: Export produces a readable document with themes, their notes and vote results in order. Archived boards remain openable and searchable rather than being deleted. A board revisited months later shows its session record including phases and the reveal. Export includes anonymous notes without authorship. Do not build yet: Nothing after this. ### Out of scope (and why) - Freeform drawing and diagramming, which is a different tool and a different set of canvas problems. - Video and audio for the session itself, which is a substantial real-time project and is usually happening elsewhere anyway. - Template libraries for workshop formats, which is most of what the hosted products sell. - Integration with issue trackers, which is the step after the workshop rather than part of it. ### README must contain - That anonymity is enforced server side and asserted absent from every response shape including sockets - That the timer is derived from a server-authoritative phase, because independently counting clients drift - That no vote totals are available before the reveal, since visible running totals change how people vote - That per-note changes are sent rather than the whole board, which collapses at workshop scale - That the output is an ordered document rather than an image of a board ===== AGENTS.md ===== # Agent instructions · Lucidspark indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Sticky notes and groupings as simple objects, A server-authoritative phase with a synchronised timer, Authorship withheld server side, not hidden in the interface, Allocations hidden until reveal, enforced server side, TypeScript with PostgreSQL and a socket layer. 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 - Broadcasting the whole board on every change works with twenty notes and collapses at four hundred, which is a normal workshop output, and it collapses during the session rather than afterwards. Send per-note changes and reconcile at the note level. - Hiding the author in the interface while the socket message still carries it means anyone who opens the developer tools can see who wrote the critical note, and in a workshop about a team's problems that is precisely the harm anonymity was for. Strip it server side and assert it in the suite. - A timer counted down independently on each client drifts, so participants see different remaining times and someone is cut off mid-sentence believing they had thirty seconds. Send the phase start and duration, derive locally, and correct against the server rather than counting down in isolation. - Showing a running vote total during voting changes the result, because people converge on whatever is already winning, and that is a well known effect rather than a hypothetical one. Return no other participant's votes and no aggregate before the reveal, enforced in the query rather than the display. ===== BUILD_PLAN.md ===== # Build plan · Lucidspark A collaborative board for running workshops, where the canvas is the easy half and the difficult half is the facilitation: anonymity that actually holds, timers everyone sees identically, and voting that cannot be gamed by whoever votes last. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Notes on a board, together Several people add and move notes simultaneously without conflict. ### Steps 1. Send per-note changes Whole-board broadcast collapses at workshop scale, during the workshop. Files: `src/sync.ts` 2. Reconcile per note deterministically Two people editing one note is normal in a workshop. Files: `src/reconcile.ts`, `src/reconcile.test.ts` 3. Make joining a busy board fast People join late and the pause is visible to everyone. Files: `src/join.ts` ### Done when - [ ] Simultaneous adds and moves do not lose changes - [ ] Four hundred notes stay responsive with eight participants - [ ] Joining a busy board is fast - [ ] Concurrent edits to one note resolve deterministically ### Watch out - Broadcasting the whole board on every change works with twenty notes and collapses at four hundred, which is a normal workshop output, and it collapses during the session rather than afterwards. Send per-note changes and reconcile at the note level. ## Phase 2 · Anonymity that holds at the API An anonymous note's author cannot be discovered by anyone but the facilitator. ### Steps 1. Strip authorship server side In the serialiser and the socket path both. Files: `src/anonymity.ts`, `tests/anonymity-leak.test.ts` 2. Keep it for facilitator audit only Stored, and reachable only through a path with its own rules. Files: `src/anonymity.ts` 3. Do not reveal retroactively Notes written under anonymity were written under a promise. Files: `src/sessions.ts` ### Done when - [ ] No response shape carries authorship when anonymous - [ ] Facilitator access follows the session's stated rules - [ ] Turning anonymity off does not reveal earlier notes - [ ] Ordering does not trivially reveal authors ### Watch out - Hiding the author in the interface while the socket message still carries it means anyone who opens the developer tools can see who wrote the critical note, and in a workshop about a team's problems that is precisely the harm anonymity was for. Strip it server side and assert it in the suite. ## Phase 3 · One clock everyone sees The timer shows the same remaining time on every screen. ### Steps 1. Make the phase server authoritative Start and duration from the server, derived locally. Files: `src/phases.ts` 2. Correct for client clock skew Machine clocks are wrong and a workshop timer is visible to everyone. Files: `src/timer.ts`, `src/timer.test.ts` 3. Transition on the server Not on whichever client reaches zero first. Files: `src/phases.ts` ### Done when - [ ] All clients show the same remaining time - [ ] A reconnecting client is immediately correct - [ ] Transitions are server driven - [ ] Pausing affects everyone ### Watch out - A timer counted down independently on each client drifts, so participants see different remaining times and someone is cut off mid-sentence believing they had thirty seconds. Send the phase start and duration, derive locally, and correct against the server rather than counting down in isolation. ## Phase 4 · Voting nobody can watch Votes are invisible until the reveal, including in the traffic. ### Steps 1. Return no totals before reveal Enforced in the query. Running totals change the result. Files: `src/votes.ts`, `tests/vote-secrecy.test.ts` 2. Show only your own allocation So you know what you have left without seeing anyone else. Files: `src/votes.ts` 3. Record the reveal The moment results became visible is part of the session record. Files: `src/reveal.ts` ### Done when - [ ] No aggregate is available before the reveal - [ ] Participants see only their own allocation - [ ] The reveal is explicit and recorded - [ ] Allocations cannot be exceeded ### Watch out - Showing a running vote total during voting changes the result, because people converge on whatever is already winning, and that is a well known effect rather than a hypothetical one. Return no other participant's votes and no aggregate before the reveal, enforced in the query rather than the display. ## Not in this build - Freeform drawing and diagramming. - Video and audio for the session. - Workshop format template libraries. - Issue tracker integration. ## After v1, if you want it - Facilitator scripts that drive the phases automatically, which is where the timing gets reliable - Breakout groups within a session, which multiplies every synchronisation problem here - Comparing two sessions on the same question, which is where a workshop practice improves ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Boards, notes and votes, some written under a promise of anonymity. DATABASE_URL=postgres://localhost:5432/workshops # Required. The live session channel. SOCKET_PORT=3002 # Required. Whether new sessions are anonymous. Stated to participants either way. ANONYMOUS_DEFAULT=false # Required. A promise to participants. It is shown to them and it changes what they write. FACILITATOR_SEES_AUTHORS=false # Required. The allocation, enforced server side. VOTES_PER_PARTICIPANT=5 # Required. Present as a constant. Visible running totals change the result. REVEAL_TOTALS_BEFORE_END=never
You are building a production product version of Lucidspark. 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 ===== # Lucidspark · product brief ## Problem The visible whiteboarding loop is buildable, but a credible replacement needs more than the first screen. Lucidspark earns its keep through interaction polish, collaboration, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A workshop board where anonymity actually holds, everyone sees the same clock, votes stay hidden until the facilitator reveals them, and the session produces a document rather than a screenshot. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node.js 22 - modern browser - no account required ## Explicit non-goals for v1 - Freeform drawing and diagramming. - Video and audio for the session. - Workshop format template libraries. - Issue tracker integration. - high-fidelity import and export - presentation and enterprise controls - multiplayer cursors and comments - large template libraries ## Success criteria - Authorship is confirmed absent from every response shape when anonymous - All clients are confirmed to show the same remaining time under clock skew - No vote aggregate is confirmed available before the reveal - Four hundred notes are confirmed responsive with eight participants - Export is confirmed to be a readable ordered document ===== BRIEF.md ===== # Build brief · Lucidspark 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 workshop whiteboard where the facilitation mechanics are the product to replace Lucidspark. 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) - Canvas: Sticky notes and groupings as simple objects · workshop content is text on cards, not freeform drawing - Session state: A server-authoritative phase with a synchronised timer · everyone must be in the same phase and see the same clock - Anonymity: Authorship withheld server side, not hidden in the interface · anonymity that the API undoes is not anonymity - Voting: Allocations hidden until reveal, enforced server side · visible running totals change how people vote - Runtime: TypeScript with PostgreSQL and a socket layer · sessions are short, concurrent and stateful ### Data model (create this before Phase 1) - boards(id, title, created_by, created_at, archived_at) - sessions(id, board_id, phase, phase_started_at, phase_duration_seconds, anonymous, facilitator_id) - notes(id, board_id, author_id, body, x, y, colour, group_id, created_at) · author_id withheld when anonymous - groups(id, board_id, label, created_by) - votes(session_id, voter_id, note_id, weight, cast_at) · never returned to participants before reveal - reveals(session_id, at, revealed_by) · the moment results became visible, recorded - participants(session_id, user_id, display_name, joined_at, role) - A vote is stored with its voter for facilitator audit, but no query path returns another participant's votes before the reveal. ### Phase 1 · Notes on a board, together Build: Sticky notes with text, position and colour, edited concurrently. Reconcile simultaneous moves and edits per note. Handle a participant joining a board that already has hundreds of notes. Trap: Broadcasting the whole board on every change works with twenty notes and collapses at four hundred, which is a normal workshop output, and it collapses during the session rather than afterwards. Send per-note changes and reconcile at the note level. Done when: Several participants add and move notes simultaneously without losing changes. A board with four hundred notes stays responsive with eight participants. Joining a busy board loads the current state without a long pause. Two people editing one note's text resolve deterministically. Do not build yet: No phases, anonymity, timers or voting. ### Phase 2 · Anonymity that holds at the API Build: Withhold authorship server side when a session is anonymous, so no API response carries it. Keep it stored for facilitator audit. Assert absence across every response shape including errors and socket messages. Trap: Hiding the author in the interface while the socket message still carries it means anyone who opens the developer tools can see who wrote the critical note, and in a workshop about a team's problems that is precisely the harm anonymity was for. Strip it server side and assert it in the suite. Done when: No API or socket message carries authorship during an anonymous session, asserted across list, single-note, error and socket shapes. The facilitator can see authorship where the session's rules allow. Turning anonymity off mid-session does not retroactively reveal earlier notes without an explicit action. Ordering and timing cannot be used to infer authorship trivially. Do not build yet: No phases, timers or voting yet. ### Phase 3 · One clock everyone sees Build: A server-authoritative phase with a start time and duration, from which each client derives the remaining time. Handle a client whose clock is wrong and one that reconnects mid-phase. Make the phase transition server driven. Trap: A timer counted down independently on each client drifts, so participants see different remaining times and someone is cut off mid-sentence believing they had thirty seconds. Send the phase start and duration, derive locally, and correct against the server rather than counting down in isolation. Done when: Every client shows the same remaining time within a small tolerance, tested across clients with deliberately skewed clocks. A client reconnecting mid-phase shows the correct remaining time immediately. The phase transition happens server side rather than being triggered by whichever client counts down first. Pausing a phase pauses it for everyone. Do not build yet: No voting or export yet. ### Phase 4 · Voting nobody can watch Build: Allocate a fixed number of votes per participant, stored server side and returned to nobody before the reveal. Show only your own allocation while voting. Reveal on an explicit facilitator action, recorded. Trap: Showing a running vote total during voting changes the result, because people converge on whatever is already winning, and that is a well known effect rather than a hypothetical one. Return no other participant's votes and no aggregate before the reveal, enforced in the query rather than the display. Done when: No participant can see another's votes or any aggregate before the reveal, asserted at the API level. Each participant sees their own remaining allocation. The reveal is an explicit facilitator action and is recorded with its time. A participant cannot exceed their allocation, enforced server side. Do not build yet: No grouping or export yet. ### Phase 5 · Turning a board into an outcome Build: Group notes into themes, order by vote result, and export the outcome as a document with the notes, groupings and results. Archive boards without deleting them. Handle a board revisited months later. Trap: A workshop whose output is a screenshot of a board is a workshop whose output is lost, because nobody can read four hundred sticky notes in an image and nobody opens the board again. Export a document with the themes, the notes under them and the vote results in order. Done when: Export produces a readable document with themes, their notes and vote results in order. Archived boards remain openable and searchable rather than being deleted. A board revisited months later shows its session record including phases and the reveal. Export includes anonymous notes without authorship. Do not build yet: Nothing after this. ### Out of scope (and why) - Freeform drawing and diagramming, which is a different tool and a different set of canvas problems. - Video and audio for the session itself, which is a substantial real-time project and is usually happening elsewhere anyway. - Template libraries for workshop formats, which is most of what the hosted products sell. - Integration with issue trackers, which is the step after the workshop rather than part of it. ### README must contain - That anonymity is enforced server side and asserted absent from every response shape including sockets - That the timer is derived from a server-authoritative phase, because independently counting clients drift - That no vote totals are available before the reveal, since visible running totals change how people vote - That per-note changes are sent rather than the whole board, which collapses at workshop scale - That the output is an ordered document rather than an image of a board ===== ARCHITECTURE.md ===== # Architecture · Lucidspark ## Stack | Part | Choice | Why | | --- | --- | --- | | Canvas | Sticky notes and groupings as simple objects | workshop content is text on cards, not freeform drawing | | Session state | A server-authoritative phase with a synchronised timer | everyone must be in the same phase and see the same clock | | Anonymity | Authorship withheld server side, not hidden in the interface | anonymity that the API undoes is not anonymity | | Voting | Allocations hidden until reveal, enforced server side | visible running totals change how people vote | | Runtime | TypeScript with PostgreSQL and a socket layer | sessions are short, concurrent and stateful | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Canvas | Per-note synchronisation. | Any transport, provided whole-board broadcast is not used. | | Anonymity | Server-side authorship withholding. | Not swappable for interface hiding, which the developer tools undo. | | Phases | Server-authoritative timing. | Any implementation, provided clients derive rather than count independently. | | Voting | Hidden allocation until reveal. | Any scheme, provided no aggregate is queryable before the reveal. | | Output | Ordered document export. | Any format, provided it is readable without opening the board. | ## 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 · Boards, notes and votes, some written under a promise of anonymity. - `SOCKET_PORT` · required · The live session channel. - `ANONYMOUS_DEFAULT` · required · Whether new sessions are anonymous. Stated to participants either way. - `FACILITATOR_SEES_AUTHORS` · required · A promise to participants. It is shown to them and it changes what they write. - `VOTES_PER_PARTICIPANT` · required · The allocation, enforced server side. - `REVEAL_TOTALS_BEFORE_END` · required · Present as a constant. Visible running totals change the result. ## 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 · Lucidspark product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Sticky notes and groupings as simple objects, A server-authoritative phase with a synchronised timer, Authorship withheld server side, not hidden in the interface, Allocations hidden until reveal, enforced server side, TypeScript with PostgreSQL and a socket layer. - 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 - Broadcasting the whole board on every change works with twenty notes and collapses at four hundred, which is a normal workshop output, and it collapses during the session rather than afterwards. Send per-note changes and reconcile at the note level. - Hiding the author in the interface while the socket message still carries it means anyone who opens the developer tools can see who wrote the critical note, and in a workshop about a team's problems that is precisely the harm anonymity was for. Strip it server side and assert it in the suite. - A timer counted down independently on each client drifts, so participants see different remaining times and someone is cut off mid-sentence believing they had thirty seconds. Send the phase start and duration, derive locally, and correct against the server rather than counting down in isolation. - Showing a running vote total during voting changes the result, because people converge on whatever is already winning, and that is a well known effect rather than a hypothetical one. Return no other participant's votes and no aggregate before the reveal, enforced in the query rather than the display. - A workshop whose output is a screenshot of a board is a workshop whose output is lost, because nobody can read four hundred sticky notes in an image and nobody opens the board again. Export a document with the themes, the notes under them and the vote results in order. ===== MILESTONES.md ===== # Delivery milestones · Lucidspark Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Notes on a board, together Several people add and move notes simultaneously without conflict. ### Steps 1. Send per-note changes Whole-board broadcast collapses at workshop scale, during the workshop. Files: `src/sync.ts` 2. Reconcile per note deterministically Two people editing one note is normal in a workshop. Files: `src/reconcile.ts`, `src/reconcile.test.ts` 3. Make joining a busy board fast People join late and the pause is visible to everyone. Files: `src/join.ts` ### Done when - [ ] Simultaneous adds and moves do not lose changes - [ ] Four hundred notes stay responsive with eight participants - [ ] Joining a busy board is fast - [ ] Concurrent edits to one note resolve deterministically ### Watch out - Broadcasting the whole board on every change works with twenty notes and collapses at four hundred, which is a normal workshop output, and it collapses during the session rather than afterwards. Send per-note changes and reconcile at the note level. ## M2 · Anonymity that holds at the API An anonymous note's author cannot be discovered by anyone but the facilitator. ### Steps 1. Strip authorship server side In the serialiser and the socket path both. Files: `src/anonymity.ts`, `tests/anonymity-leak.test.ts` 2. Keep it for facilitator audit only Stored, and reachable only through a path with its own rules. Files: `src/anonymity.ts` 3. Do not reveal retroactively Notes written under anonymity were written under a promise. Files: `src/sessions.ts` ### Done when - [ ] No response shape carries authorship when anonymous - [ ] Facilitator access follows the session's stated rules - [ ] Turning anonymity off does not reveal earlier notes - [ ] Ordering does not trivially reveal authors ### Watch out - Hiding the author in the interface while the socket message still carries it means anyone who opens the developer tools can see who wrote the critical note, and in a workshop about a team's problems that is precisely the harm anonymity was for. Strip it server side and assert it in the suite. ## M3 · One clock everyone sees The timer shows the same remaining time on every screen. ### Steps 1. Make the phase server authoritative Start and duration from the server, derived locally. Files: `src/phases.ts` 2. Correct for client clock skew Machine clocks are wrong and a workshop timer is visible to everyone. Files: `src/timer.ts`, `src/timer.test.ts` 3. Transition on the server Not on whichever client reaches zero first. Files: `src/phases.ts` ### Done when - [ ] All clients show the same remaining time - [ ] A reconnecting client is immediately correct - [ ] Transitions are server driven - [ ] Pausing affects everyone ### Watch out - A timer counted down independently on each client drifts, so participants see different remaining times and someone is cut off mid-sentence believing they had thirty seconds. Send the phase start and duration, derive locally, and correct against the server rather than counting down in isolation. ## M4 · Voting nobody can watch Votes are invisible until the reveal, including in the traffic. ### Steps 1. Return no totals before reveal Enforced in the query. Running totals change the result. Files: `src/votes.ts`, `tests/vote-secrecy.test.ts` 2. Show only your own allocation So you know what you have left without seeing anyone else. Files: `src/votes.ts` 3. Record the reveal The moment results became visible is part of the session record. Files: `src/reveal.ts` ### Done when - [ ] No aggregate is available before the reveal - [ ] Participants see only their own allocation - [ ] The reveal is explicit and recorded - [ ] Allocations cannot be exceeded ### Watch out - Showing a running vote total during voting changes the result, because people converge on whatever is already winning, and that is a well known effect rather than a hypothetical one. Return no other participant's votes and no aggregate before the reveal, enforced in the query rather than the display. ## M5 · Turning a board into an outcome (production only) The workshop produces something usable after everyone leaves. ### Steps 1. Export a document, not an image Nobody reads four hundred notes in a screenshot. Files: `src/export.ts` 2. Order by result within theme The ordering is most of what the session decided. Files: `src/export.ts` 3. Keep archived boards openable The session record is the part people come back for. Files: `src/archive.ts` ### Done when - [ ] Export is a readable document with themes and results - [ ] Archived boards remain openable and searchable - [ ] The session record survives - [ ] Anonymous notes export without authorship ### Watch out - A workshop whose output is a screenshot of a board is a workshop whose output is lost, because nobody can read four hundred sticky notes in an image and nobody opens the board again. Export a document with the themes, the notes under them and the vote results in order. ===== OPERATIONS.md ===== # Operations · Lucidspark ## Backup Board content is the output of people's time and is not reconstructible. Back it up, and be aware that anonymous notes with stored authorship are more sensitive than they look. ## Restore Restore and confirm anonymity settings survived per session, since restoring an anonymous session as non-anonymous would reveal notes written under a promise, which is worse than losing them. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Socket message volume per board and timer skew across clients. The second is the one participants notice, and it is invisible from a single machine. ## Incident checklist Authorship appearing during an anonymous session is the failure that breaks a team's trust rather than a feature, and it usually reappears through a socket message or an error payload rather than the main read path. 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 - [ ] Authorship is confirmed absent from every response shape when anonymous - [ ] All clients are confirmed to show the same remaining time under clock skew - [ ] No vote aggregate is confirmed available before the reveal - [ ] Four hundred notes are confirmed responsive with eight participants - [ ] Export is confirmed to be a readable ordered document ## Launch constraint Do not market omitted Lucidspark 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. Boards, notes and votes, some written under a promise of anonymity. DATABASE_URL=postgres://localhost:5432/workshops # Required. The live session channel. SOCKET_PORT=3002 # Required. Whether new sessions are anonymous. Stated to participants either way. ANONYMOUS_DEFAULT=false # Required. A promise to participants. It is shown to them and it changes what they write. FACILITATOR_SEES_AUTHORS=false # Required. The allocation, enforced server side. VOTES_PER_PARTICIPANT=5 # Required. Present as a constant. Visible running totals change the result. REVEAL_TOTALS_BEFORE_END=never
# Lucidspark · indie build A collaborative board for running workshops, where the canvas is the easy half and the difficult half is the facilitation: anonymity that actually holds, timers everyone sees identically, and voting that cannot be gamed by whoever votes last. 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 | | --- | --- | --- | | Canvas | Sticky notes and groupings as simple objects | workshop content is text on cards, not freeform drawing | | Session state | A server-authoritative phase with a synchronised timer | everyone must be in the same phase and see the same clock | | Anonymity | Authorship withheld server side, not hidden in the interface | anonymity that the API undoes is not anonymity | | Voting | Allocations hidden until reveal, enforced server side | visible running totals change how people vote | | Runtime | TypeScript with PostgreSQL and a socket layer | sessions are short, concurrent and stateful | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the socket layer. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Sessions, votes and notes with server-side visibility rules. - Get it: Docker or your package manager. - Verify: psql connects and creates a table - [ ] **Eight people willing to run a real workshop** · free, and it costs an hour of eight people's time - Why: Every claim here is about group behaviour: whether anonymity is believed, whether the timer feels fair, whether voting changes when totals are hidden. None of it is observable alone. - Get it: A real team and a real question to work on, for an hour. - Verify: A full session runs with eight participants including a vote and a reveal - [ ] **What the facilitator can see, written down** · free - Why: Anonymity is a promise to participants, and whether the facilitator can see authorship changes what people write. Deciding it silently means deciding it against them. - Get it: Decide whether the facilitator can see authorship, state it in the session, and write down why. - Verify: A written rule exists and is shown to participants when a session is anonymous - [ ] **A client with a deliberately wrong clock** · free - Why: Timer synchronisation is only testable against skew, and machine clocks in a room full of laptops are genuinely inconsistent. - Get it: Set a test machine's clock several minutes off and join a session with it. - Verify: The skewed client shows the correct remaining time ## 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: - Freeform drawing and diagramming. - Video and audio for the session. - Workshop format template libraries. - Issue tracker integration. - high-fidelity import and export - presentation and enterprise controls - multiplayer cursors and comments - large template libraries If one of those is essential to you, that is the reason to keep paying for Lucidspark, and the README should say so rather than pretend.
# Build brief · Lucidspark 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 workshop whiteboard where the facilitation mechanics are the product to replace Lucidspark. 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) - Canvas: Sticky notes and groupings as simple objects · workshop content is text on cards, not freeform drawing - Session state: A server-authoritative phase with a synchronised timer · everyone must be in the same phase and see the same clock - Anonymity: Authorship withheld server side, not hidden in the interface · anonymity that the API undoes is not anonymity - Voting: Allocations hidden until reveal, enforced server side · visible running totals change how people vote - Runtime: TypeScript with PostgreSQL and a socket layer · sessions are short, concurrent and stateful ### Data model (create this before Phase 1) - boards(id, title, created_by, created_at, archived_at) - sessions(id, board_id, phase, phase_started_at, phase_duration_seconds, anonymous, facilitator_id) - notes(id, board_id, author_id, body, x, y, colour, group_id, created_at) · author_id withheld when anonymous - groups(id, board_id, label, created_by) - votes(session_id, voter_id, note_id, weight, cast_at) · never returned to participants before reveal - reveals(session_id, at, revealed_by) · the moment results became visible, recorded - participants(session_id, user_id, display_name, joined_at, role) - A vote is stored with its voter for facilitator audit, but no query path returns another participant's votes before the reveal. ### Phase 1 · Notes on a board, together Build: Sticky notes with text, position and colour, edited concurrently. Reconcile simultaneous moves and edits per note. Handle a participant joining a board that already has hundreds of notes. Trap: Broadcasting the whole board on every change works with twenty notes and collapses at four hundred, which is a normal workshop output, and it collapses during the session rather than afterwards. Send per-note changes and reconcile at the note level. Done when: Several participants add and move notes simultaneously without losing changes. A board with four hundred notes stays responsive with eight participants. Joining a busy board loads the current state without a long pause. Two people editing one note's text resolve deterministically. Do not build yet: No phases, anonymity, timers or voting. ### Phase 2 · Anonymity that holds at the API Build: Withhold authorship server side when a session is anonymous, so no API response carries it. Keep it stored for facilitator audit. Assert absence across every response shape including errors and socket messages. Trap: Hiding the author in the interface while the socket message still carries it means anyone who opens the developer tools can see who wrote the critical note, and in a workshop about a team's problems that is precisely the harm anonymity was for. Strip it server side and assert it in the suite. Done when: No API or socket message carries authorship during an anonymous session, asserted across list, single-note, error and socket shapes. The facilitator can see authorship where the session's rules allow. Turning anonymity off mid-session does not retroactively reveal earlier notes without an explicit action. Ordering and timing cannot be used to infer authorship trivially. Do not build yet: No phases, timers or voting yet. ### Phase 3 · One clock everyone sees Build: A server-authoritative phase with a start time and duration, from which each client derives the remaining time. Handle a client whose clock is wrong and one that reconnects mid-phase. Make the phase transition server driven. Trap: A timer counted down independently on each client drifts, so participants see different remaining times and someone is cut off mid-sentence believing they had thirty seconds. Send the phase start and duration, derive locally, and correct against the server rather than counting down in isolation. Done when: Every client shows the same remaining time within a small tolerance, tested across clients with deliberately skewed clocks. A client reconnecting mid-phase shows the correct remaining time immediately. The phase transition happens server side rather than being triggered by whichever client counts down first. Pausing a phase pauses it for everyone. Do not build yet: No voting or export yet. ### Phase 4 · Voting nobody can watch Build: Allocate a fixed number of votes per participant, stored server side and returned to nobody before the reveal. Show only your own allocation while voting. Reveal on an explicit facilitator action, recorded. Trap: Showing a running vote total during voting changes the result, because people converge on whatever is already winning, and that is a well known effect rather than a hypothetical one. Return no other participant's votes and no aggregate before the reveal, enforced in the query rather than the display. Done when: No participant can see another's votes or any aggregate before the reveal, asserted at the API level. Each participant sees their own remaining allocation. The reveal is an explicit facilitator action and is recorded with its time. A participant cannot exceed their allocation, enforced server side. Do not build yet: No grouping or export yet. ### Phase 5 · Turning a board into an outcome Build: Group notes into themes, order by vote result, and export the outcome as a document with the notes, groupings and results. Archive boards without deleting them. Handle a board revisited months later. Trap: A workshop whose output is a screenshot of a board is a workshop whose output is lost, because nobody can read four hundred sticky notes in an image and nobody opens the board again. Export a document with the themes, the notes under them and the vote results in order. Done when: Export produces a readable document with themes, their notes and vote results in order. Archived boards remain openable and searchable rather than being deleted. A board revisited months later shows its session record including phases and the reveal. Export includes anonymous notes without authorship. Do not build yet: Nothing after this. ### Out of scope (and why) - Freeform drawing and diagramming, which is a different tool and a different set of canvas problems. - Video and audio for the session itself, which is a substantial real-time project and is usually happening elsewhere anyway. - Template libraries for workshop formats, which is most of what the hosted products sell. - Integration with issue trackers, which is the step after the workshop rather than part of it. ### README must contain - That anonymity is enforced server side and asserted absent from every response shape including sockets - That the timer is derived from a server-authoritative phase, because independently counting clients drift - That no vote totals are available before the reveal, since visible running totals change how people vote - That per-note changes are sent rather than the whole board, which collapses at workshop scale - That the output is an ordered document rather than an image of a board
# Agent instructions · Lucidspark indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Sticky notes and groupings as simple objects, A server-authoritative phase with a synchronised timer, Authorship withheld server side, not hidden in the interface, Allocations hidden until reveal, enforced server side, TypeScript with PostgreSQL and a socket layer. 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 - Broadcasting the whole board on every change works with twenty notes and collapses at four hundred, which is a normal workshop output, and it collapses during the session rather than afterwards. Send per-note changes and reconcile at the note level. - Hiding the author in the interface while the socket message still carries it means anyone who opens the developer tools can see who wrote the critical note, and in a workshop about a team's problems that is precisely the harm anonymity was for. Strip it server side and assert it in the suite. - A timer counted down independently on each client drifts, so participants see different remaining times and someone is cut off mid-sentence believing they had thirty seconds. Send the phase start and duration, derive locally, and correct against the server rather than counting down in isolation. - Showing a running vote total during voting changes the result, because people converge on whatever is already winning, and that is a well known effect rather than a hypothetical one. Return no other participant's votes and no aggregate before the reveal, enforced in the query rather than the display.
# Build plan · Lucidspark A collaborative board for running workshops, where the canvas is the easy half and the difficult half is the facilitation: anonymity that actually holds, timers everyone sees identically, and voting that cannot be gamed by whoever votes last. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Notes on a board, together Several people add and move notes simultaneously without conflict. ### Steps 1. Send per-note changes Whole-board broadcast collapses at workshop scale, during the workshop. Files: `src/sync.ts` 2. Reconcile per note deterministically Two people editing one note is normal in a workshop. Files: `src/reconcile.ts`, `src/reconcile.test.ts` 3. Make joining a busy board fast People join late and the pause is visible to everyone. Files: `src/join.ts` ### Done when - [ ] Simultaneous adds and moves do not lose changes - [ ] Four hundred notes stay responsive with eight participants - [ ] Joining a busy board is fast - [ ] Concurrent edits to one note resolve deterministically ### Watch out - Broadcasting the whole board on every change works with twenty notes and collapses at four hundred, which is a normal workshop output, and it collapses during the session rather than afterwards. Send per-note changes and reconcile at the note level. ## Phase 2 · Anonymity that holds at the API An anonymous note's author cannot be discovered by anyone but the facilitator. ### Steps 1. Strip authorship server side In the serialiser and the socket path both. Files: `src/anonymity.ts`, `tests/anonymity-leak.test.ts` 2. Keep it for facilitator audit only Stored, and reachable only through a path with its own rules. Files: `src/anonymity.ts` 3. Do not reveal retroactively Notes written under anonymity were written under a promise. Files: `src/sessions.ts` ### Done when - [ ] No response shape carries authorship when anonymous - [ ] Facilitator access follows the session's stated rules - [ ] Turning anonymity off does not reveal earlier notes - [ ] Ordering does not trivially reveal authors ### Watch out - Hiding the author in the interface while the socket message still carries it means anyone who opens the developer tools can see who wrote the critical note, and in a workshop about a team's problems that is precisely the harm anonymity was for. Strip it server side and assert it in the suite. ## Phase 3 · One clock everyone sees The timer shows the same remaining time on every screen. ### Steps 1. Make the phase server authoritative Start and duration from the server, derived locally. Files: `src/phases.ts` 2. Correct for client clock skew Machine clocks are wrong and a workshop timer is visible to everyone. Files: `src/timer.ts`, `src/timer.test.ts` 3. Transition on the server Not on whichever client reaches zero first. Files: `src/phases.ts` ### Done when - [ ] All clients show the same remaining time - [ ] A reconnecting client is immediately correct - [ ] Transitions are server driven - [ ] Pausing affects everyone ### Watch out - A timer counted down independently on each client drifts, so participants see different remaining times and someone is cut off mid-sentence believing they had thirty seconds. Send the phase start and duration, derive locally, and correct against the server rather than counting down in isolation. ## Phase 4 · Voting nobody can watch Votes are invisible until the reveal, including in the traffic. ### Steps 1. Return no totals before reveal Enforced in the query. Running totals change the result. Files: `src/votes.ts`, `tests/vote-secrecy.test.ts` 2. Show only your own allocation So you know what you have left without seeing anyone else. Files: `src/votes.ts` 3. Record the reveal The moment results became visible is part of the session record. Files: `src/reveal.ts` ### Done when - [ ] No aggregate is available before the reveal - [ ] Participants see only their own allocation - [ ] The reveal is explicit and recorded - [ ] Allocations cannot be exceeded ### Watch out - Showing a running vote total during voting changes the result, because people converge on whatever is already winning, and that is a well known effect rather than a hypothetical one. Return no other participant's votes and no aggregate before the reveal, enforced in the query rather than the display. ## Not in this build - Freeform drawing and diagramming. - Video and audio for the session. - Workshop format template libraries. - Issue tracker integration. ## After v1, if you want it - Facilitator scripts that drive the phases automatically, which is where the timing gets reliable - Breakout groups within a session, which multiplies every synchronisation problem here - Comparing two sessions on the same question, which is where a workshop practice improves
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Boards, notes and votes, some written under a promise of anonymity. DATABASE_URL=postgres://localhost:5432/workshops # Required. The live session channel. SOCKET_PORT=3002 # Required. Whether new sessions are anonymous. Stated to participants either way. ANONYMOUS_DEFAULT=false # Required. A promise to participants. It is shown to them and it changes what they write. FACILITATOR_SEES_AUTHORS=false # Required. The allocation, enforced server side. VOTES_PER_PARTICIPANT=5 # Required. Present as a constant. Visible running totals change the result. REVEAL_TOTALS_BEFORE_END=never
# Lucidspark · product brief ## Problem The visible whiteboarding loop is buildable, but a credible replacement needs more than the first screen. Lucidspark earns its keep through interaction polish, collaboration, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A workshop board where anonymity actually holds, everyone sees the same clock, votes stay hidden until the facilitator reveals them, and the session produces a document rather than a screenshot. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node.js 22 - modern browser - no account required ## Explicit non-goals for v1 - Freeform drawing and diagramming. - Video and audio for the session. - Workshop format template libraries. - Issue tracker integration. - high-fidelity import and export - presentation and enterprise controls - multiplayer cursors and comments - large template libraries ## Success criteria - Authorship is confirmed absent from every response shape when anonymous - All clients are confirmed to show the same remaining time under clock skew - No vote aggregate is confirmed available before the reveal - Four hundred notes are confirmed responsive with eight participants - Export is confirmed to be a readable ordered document
# Build brief · Lucidspark 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 workshop whiteboard where the facilitation mechanics are the product to replace Lucidspark. 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) - Canvas: Sticky notes and groupings as simple objects · workshop content is text on cards, not freeform drawing - Session state: A server-authoritative phase with a synchronised timer · everyone must be in the same phase and see the same clock - Anonymity: Authorship withheld server side, not hidden in the interface · anonymity that the API undoes is not anonymity - Voting: Allocations hidden until reveal, enforced server side · visible running totals change how people vote - Runtime: TypeScript with PostgreSQL and a socket layer · sessions are short, concurrent and stateful ### Data model (create this before Phase 1) - boards(id, title, created_by, created_at, archived_at) - sessions(id, board_id, phase, phase_started_at, phase_duration_seconds, anonymous, facilitator_id) - notes(id, board_id, author_id, body, x, y, colour, group_id, created_at) · author_id withheld when anonymous - groups(id, board_id, label, created_by) - votes(session_id, voter_id, note_id, weight, cast_at) · never returned to participants before reveal - reveals(session_id, at, revealed_by) · the moment results became visible, recorded - participants(session_id, user_id, display_name, joined_at, role) - A vote is stored with its voter for facilitator audit, but no query path returns another participant's votes before the reveal. ### Phase 1 · Notes on a board, together Build: Sticky notes with text, position and colour, edited concurrently. Reconcile simultaneous moves and edits per note. Handle a participant joining a board that already has hundreds of notes. Trap: Broadcasting the whole board on every change works with twenty notes and collapses at four hundred, which is a normal workshop output, and it collapses during the session rather than afterwards. Send per-note changes and reconcile at the note level. Done when: Several participants add and move notes simultaneously without losing changes. A board with four hundred notes stays responsive with eight participants. Joining a busy board loads the current state without a long pause. Two people editing one note's text resolve deterministically. Do not build yet: No phases, anonymity, timers or voting. ### Phase 2 · Anonymity that holds at the API Build: Withhold authorship server side when a session is anonymous, so no API response carries it. Keep it stored for facilitator audit. Assert absence across every response shape including errors and socket messages. Trap: Hiding the author in the interface while the socket message still carries it means anyone who opens the developer tools can see who wrote the critical note, and in a workshop about a team's problems that is precisely the harm anonymity was for. Strip it server side and assert it in the suite. Done when: No API or socket message carries authorship during an anonymous session, asserted across list, single-note, error and socket shapes. The facilitator can see authorship where the session's rules allow. Turning anonymity off mid-session does not retroactively reveal earlier notes without an explicit action. Ordering and timing cannot be used to infer authorship trivially. Do not build yet: No phases, timers or voting yet. ### Phase 3 · One clock everyone sees Build: A server-authoritative phase with a start time and duration, from which each client derives the remaining time. Handle a client whose clock is wrong and one that reconnects mid-phase. Make the phase transition server driven. Trap: A timer counted down independently on each client drifts, so participants see different remaining times and someone is cut off mid-sentence believing they had thirty seconds. Send the phase start and duration, derive locally, and correct against the server rather than counting down in isolation. Done when: Every client shows the same remaining time within a small tolerance, tested across clients with deliberately skewed clocks. A client reconnecting mid-phase shows the correct remaining time immediately. The phase transition happens server side rather than being triggered by whichever client counts down first. Pausing a phase pauses it for everyone. Do not build yet: No voting or export yet. ### Phase 4 · Voting nobody can watch Build: Allocate a fixed number of votes per participant, stored server side and returned to nobody before the reveal. Show only your own allocation while voting. Reveal on an explicit facilitator action, recorded. Trap: Showing a running vote total during voting changes the result, because people converge on whatever is already winning, and that is a well known effect rather than a hypothetical one. Return no other participant's votes and no aggregate before the reveal, enforced in the query rather than the display. Done when: No participant can see another's votes or any aggregate before the reveal, asserted at the API level. Each participant sees their own remaining allocation. The reveal is an explicit facilitator action and is recorded with its time. A participant cannot exceed their allocation, enforced server side. Do not build yet: No grouping or export yet. ### Phase 5 · Turning a board into an outcome Build: Group notes into themes, order by vote result, and export the outcome as a document with the notes, groupings and results. Archive boards without deleting them. Handle a board revisited months later. Trap: A workshop whose output is a screenshot of a board is a workshop whose output is lost, because nobody can read four hundred sticky notes in an image and nobody opens the board again. Export a document with the themes, the notes under them and the vote results in order. Done when: Export produces a readable document with themes, their notes and vote results in order. Archived boards remain openable and searchable rather than being deleted. A board revisited months later shows its session record including phases and the reveal. Export includes anonymous notes without authorship. Do not build yet: Nothing after this. ### Out of scope (and why) - Freeform drawing and diagramming, which is a different tool and a different set of canvas problems. - Video and audio for the session itself, which is a substantial real-time project and is usually happening elsewhere anyway. - Template libraries for workshop formats, which is most of what the hosted products sell. - Integration with issue trackers, which is the step after the workshop rather than part of it. ### README must contain - That anonymity is enforced server side and asserted absent from every response shape including sockets - That the timer is derived from a server-authoritative phase, because independently counting clients drift - That no vote totals are available before the reveal, since visible running totals change how people vote - That per-note changes are sent rather than the whole board, which collapses at workshop scale - That the output is an ordered document rather than an image of a board
# Architecture · Lucidspark ## Stack | Part | Choice | Why | | --- | --- | --- | | Canvas | Sticky notes and groupings as simple objects | workshop content is text on cards, not freeform drawing | | Session state | A server-authoritative phase with a synchronised timer | everyone must be in the same phase and see the same clock | | Anonymity | Authorship withheld server side, not hidden in the interface | anonymity that the API undoes is not anonymity | | Voting | Allocations hidden until reveal, enforced server side | visible running totals change how people vote | | Runtime | TypeScript with PostgreSQL and a socket layer | sessions are short, concurrent and stateful | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Canvas | Per-note synchronisation. | Any transport, provided whole-board broadcast is not used. | | Anonymity | Server-side authorship withholding. | Not swappable for interface hiding, which the developer tools undo. | | Phases | Server-authoritative timing. | Any implementation, provided clients derive rather than count independently. | | Voting | Hidden allocation until reveal. | Any scheme, provided no aggregate is queryable before the reveal. | | Output | Ordered document export. | Any format, provided it is readable without opening the board. | ## 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 · Boards, notes and votes, some written under a promise of anonymity. - `SOCKET_PORT` · required · The live session channel. - `ANONYMOUS_DEFAULT` · required · Whether new sessions are anonymous. Stated to participants either way. - `FACILITATOR_SEES_AUTHORS` · required · A promise to participants. It is shown to them and it changes what they write. - `VOTES_PER_PARTICIPANT` · required · The allocation, enforced server side. - `REVEAL_TOTALS_BEFORE_END` · required · Present as a constant. Visible running totals change the result. ## 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 · Lucidspark product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Sticky notes and groupings as simple objects, A server-authoritative phase with a synchronised timer, Authorship withheld server side, not hidden in the interface, Allocations hidden until reveal, enforced server side, TypeScript with PostgreSQL and a socket layer. - 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 - Broadcasting the whole board on every change works with twenty notes and collapses at four hundred, which is a normal workshop output, and it collapses during the session rather than afterwards. Send per-note changes and reconcile at the note level. - Hiding the author in the interface while the socket message still carries it means anyone who opens the developer tools can see who wrote the critical note, and in a workshop about a team's problems that is precisely the harm anonymity was for. Strip it server side and assert it in the suite. - A timer counted down independently on each client drifts, so participants see different remaining times and someone is cut off mid-sentence believing they had thirty seconds. Send the phase start and duration, derive locally, and correct against the server rather than counting down in isolation. - Showing a running vote total during voting changes the result, because people converge on whatever is already winning, and that is a well known effect rather than a hypothetical one. Return no other participant's votes and no aggregate before the reveal, enforced in the query rather than the display. - A workshop whose output is a screenshot of a board is a workshop whose output is lost, because nobody can read four hundred sticky notes in an image and nobody opens the board again. Export a document with the themes, the notes under them and the vote results in order.
# Delivery milestones · Lucidspark Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Notes on a board, together Several people add and move notes simultaneously without conflict. ### Steps 1. Send per-note changes Whole-board broadcast collapses at workshop scale, during the workshop. Files: `src/sync.ts` 2. Reconcile per note deterministically Two people editing one note is normal in a workshop. Files: `src/reconcile.ts`, `src/reconcile.test.ts` 3. Make joining a busy board fast People join late and the pause is visible to everyone. Files: `src/join.ts` ### Done when - [ ] Simultaneous adds and moves do not lose changes - [ ] Four hundred notes stay responsive with eight participants - [ ] Joining a busy board is fast - [ ] Concurrent edits to one note resolve deterministically ### Watch out - Broadcasting the whole board on every change works with twenty notes and collapses at four hundred, which is a normal workshop output, and it collapses during the session rather than afterwards. Send per-note changes and reconcile at the note level. ## M2 · Anonymity that holds at the API An anonymous note's author cannot be discovered by anyone but the facilitator. ### Steps 1. Strip authorship server side In the serialiser and the socket path both. Files: `src/anonymity.ts`, `tests/anonymity-leak.test.ts` 2. Keep it for facilitator audit only Stored, and reachable only through a path with its own rules. Files: `src/anonymity.ts` 3. Do not reveal retroactively Notes written under anonymity were written under a promise. Files: `src/sessions.ts` ### Done when - [ ] No response shape carries authorship when anonymous - [ ] Facilitator access follows the session's stated rules - [ ] Turning anonymity off does not reveal earlier notes - [ ] Ordering does not trivially reveal authors ### Watch out - Hiding the author in the interface while the socket message still carries it means anyone who opens the developer tools can see who wrote the critical note, and in a workshop about a team's problems that is precisely the harm anonymity was for. Strip it server side and assert it in the suite. ## M3 · One clock everyone sees The timer shows the same remaining time on every screen. ### Steps 1. Make the phase server authoritative Start and duration from the server, derived locally. Files: `src/phases.ts` 2. Correct for client clock skew Machine clocks are wrong and a workshop timer is visible to everyone. Files: `src/timer.ts`, `src/timer.test.ts` 3. Transition on the server Not on whichever client reaches zero first. Files: `src/phases.ts` ### Done when - [ ] All clients show the same remaining time - [ ] A reconnecting client is immediately correct - [ ] Transitions are server driven - [ ] Pausing affects everyone ### Watch out - A timer counted down independently on each client drifts, so participants see different remaining times and someone is cut off mid-sentence believing they had thirty seconds. Send the phase start and duration, derive locally, and correct against the server rather than counting down in isolation. ## M4 · Voting nobody can watch Votes are invisible until the reveal, including in the traffic. ### Steps 1. Return no totals before reveal Enforced in the query. Running totals change the result. Files: `src/votes.ts`, `tests/vote-secrecy.test.ts` 2. Show only your own allocation So you know what you have left without seeing anyone else. Files: `src/votes.ts` 3. Record the reveal The moment results became visible is part of the session record. Files: `src/reveal.ts` ### Done when - [ ] No aggregate is available before the reveal - [ ] Participants see only their own allocation - [ ] The reveal is explicit and recorded - [ ] Allocations cannot be exceeded ### Watch out - Showing a running vote total during voting changes the result, because people converge on whatever is already winning, and that is a well known effect rather than a hypothetical one. Return no other participant's votes and no aggregate before the reveal, enforced in the query rather than the display. ## M5 · Turning a board into an outcome (production only) The workshop produces something usable after everyone leaves. ### Steps 1. Export a document, not an image Nobody reads four hundred notes in a screenshot. Files: `src/export.ts` 2. Order by result within theme The ordering is most of what the session decided. Files: `src/export.ts` 3. Keep archived boards openable The session record is the part people come back for. Files: `src/archive.ts` ### Done when - [ ] Export is a readable document with themes and results - [ ] Archived boards remain openable and searchable - [ ] The session record survives - [ ] Anonymous notes export without authorship ### Watch out - A workshop whose output is a screenshot of a board is a workshop whose output is lost, because nobody can read four hundred sticky notes in an image and nobody opens the board again. Export a document with the themes, the notes under them and the vote results in order.
# Operations · Lucidspark ## Backup Board content is the output of people's time and is not reconstructible. Back it up, and be aware that anonymous notes with stored authorship are more sensitive than they look. ## Restore Restore and confirm anonymity settings survived per session, since restoring an anonymous session as non-anonymous would reveal notes written under a promise, which is worse than losing them. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Socket message volume per board and timer skew across clients. The second is the one participants notice, and it is invisible from a single machine. ## Incident checklist Authorship appearing during an anonymous session is the failure that breaks a team's trust rather than a feature, and it usually reappears through a socket message or an error payload rather than the main read path. 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 - [ ] Authorship is confirmed absent from every response shape when anonymous - [ ] All clients are confirmed to show the same remaining time under clock skew - [ ] No vote aggregate is confirmed available before the reveal - [ ] Four hundred notes are confirmed responsive with eight participants - [ ] Export is confirmed to be a readable ordered document ## Launch constraint Do not market omitted Lucidspark 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. Boards, notes and votes, some written under a promise of anonymity. DATABASE_URL=postgres://localhost:5432/workshops # Required. The live session channel. SOCKET_PORT=3002 # Required. Whether new sessions are anonymous. Stated to participants either way. ANONYMOUS_DEFAULT=false # Required. A promise to participants. It is shown to them and it changes what they write. FACILITATOR_SEES_AUTHORS=false # Required. The allocation, enforced server side. VOTES_PER_PARTICIPANT=5 # Required. Present as a constant. Visible running totals change the result. REVEAL_TOTALS_BEFORE_END=never
$ choose a build depth, inspect the files, then open the complete pack in your agent
Lucidspark: The shapes are easy; customers pay for low-friction editing, reliable exports, collaboration, and a canvas that never fights them.
xhigh-fidelity import and export
xpresentation and enterprise controls
xmultiplayer cursors and comments
xlarge template libraries
Don't feel like building it? These folks already made it free.
all 5 free alternatives to Lucidspark →· no votes, no pay-to-list · just what's real
Lucidspark pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/user | $0/user | 3 editable Lucidspark boards, unlimited shapes, 25 MB total Lucid storage, Lucid AI and basic Visual Activities |
| individual | $9/user | — | Unlimited editable Lucidspark boards; 1 GB storage; premium Visual Activities |
| team | $10/user | — | Unlimited editable boards; 1 GB storage per user; guest contributors, developer platform, custom templates and advanced collaboration |
| enterprise | custom | — | Lucid Visual Collaboration Suite, 5 GB storage per user, SAML/SCIM, central administration, security and governance |
free tier3 editable Lucidspark boards, unlimited shapes and 25 MB total storage; basic Visual Activities and Lucid AI
billingfree + recurring Individual/Team subscriptions and custom Enterprise; accessible first-party pages published $9 and $10 monthly figures but not verifiable annual equivalents
hidden costsA Lucidspark-only Team license does not let the user create or edit Lucidchart documents; both products require separate Team licenses unless buying the Enterprise Suite; Accelerators and other enterprise add-ons are separate
verified 2026-08-14 · source ↗
Vibecode Lucidspark
Kinda. The core of Lucidspark is buildable in a weekend with the prompt on this page, but there are real gaps: high-fidelity import and export, presentation and enterprise controls. Read the honest list above before committing.
How much does Lucidspark cost?
Lucidspark's pricing is usage-based or varies by plan · The official pricing URL is recorded, but a stable current amount was not safely recoverable in this pass. Do not publish a number until rechecked..
What do I lose by replacing Lucidspark?
Honestly: high-fidelity import and export; presentation and enterprise controls; multiplayer cursors and comments; large template libraries. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Lucidspark?
Yes: Excalidraw (Fast multiplayer whiteboards, libraries, links, exports, and workshop-friendly sketching; fewer facilitation widgets, much less account bureaucracy.) AFFiNE (Collaborative whiteboards tied to docs, databases, and planning views; broader than Lucidspark, more infrastructure if self-hosted.) CryptPad (Persistent encrypted whiteboards inside a shared drive with permissions and team folders; visually plain, operationally serious.) All 5 curated free alternatives are at vibecodeit.com/lucidspark/alternatives. The prompt is for when you want it exactly your way.