Vibecode Lucidchart
track this build5 phases, 15 steps, beginner friendly0%The visible diagramming loop is buildable, but a credible replacement needs more than the first screen. Lucidchart 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 Lucidchart. 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 ===== # Lucidchart · indie build A browser editor with shape libraries whose shapes carry defined connection points, layers you can lock and hide, connectors that stay attached through moves and resizes, and diagrams generated from a spreadsheet so an organisation chart or a network map is regenerated rather than redrawn. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Vite, rendering SVG | vector output, selectable text and hit testing for free at the object counts diagrams actually reach | | Shapes | A library format defining geometry and connection points as data | a shape whose connection points are hard coded cannot be extended by anyone but you | | Layout | elkjs for generated diagrams | layered layout for an organisation chart is solved and hand-rolling it produces crossing edges forever | | Data binding | CSV or JSON to shapes through a mapping you define | the point of a data-linked diagram is that it regenerates when the data changes | | Storage | IndexedDB with file open and save | no account, and the file is the thing you own | ## 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 build. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A second application that opens SVG** · free - Why: Phase 5's export check is meaningless if the only thing you open the file in is the browser that made it. - Get it: Inkscape is free and strict, which makes it a good test. - Verify: You can open an SVG and see whether its fonts survived - [ ] **Fonts you are licensed to embed** · free with open licences - Why: Export embeds or outlines fonts, which is redistribution and not permitted by every licence. - Get it: Use openly licensed families and keep the licence files. - Verify: Each bundled font's licence permits embedding, or export outlines text - [ ] **A real spreadsheet to generate a diagram from** · free - Why: Phase 4 is only meaningful against real data, and real organisation data has the awkward cases: someone with two managers, a vacant role, a name that is very long. - Get it: Use a real team structure or a network inventory. Thirty rows is enough to see layout problems. - Verify: The spreadsheet loads and includes at least one awkward case - [ ] **Which shape library you actually need** · free - Why: A general diagram tool needs hundreds of shapes; a specific one needs a dozen. Deciding which diagrams you make keeps the library finite. - Get it: Pick the diagram type you draw most and list the shapes it needs. That list is your first library. - Verify: The list exists and is under about twenty shapes ## Quick start ```sh npm install elkjs ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multiplayer cursors and comments. - Large template and shape catalogues. - Presentation mode and enterprise governance. - Import of proprietary diagram formats. - presentation and enterprise controls - multiplayer cursors and comments - large template libraries - high-fidelity import and export If one of those is essential to you, that is the reason to keep paying for Lucidchart, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Lucidchart 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 diagram editor with real shape libraries and diagrams generated from data to replace Lucidchart. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Vite, rendering SVG · vector output, selectable text and hit testing for free at the object counts diagrams actually reach - Shapes: A library format defining geometry and connection points as data · a shape whose connection points are hard coded cannot be extended by anyone but you - Layout: elkjs for generated diagrams · layered layout for an organisation chart is solved and hand-rolling it produces crossing edges forever - Data binding: CSV or JSON to shapes through a mapping you define · the point of a data-linked diagram is that it regenerates when the data changes - Storage: IndexedDB with file open and save · no account, and the file is the thing you own ### Data model (create this before Phase 1) - docs(id, title, updated_at), pages(id, doc_id, name, ordinal) - layers(id, page_id, name, ordinal, visible, locked) - shapes(id, page_id, layer_id, library_key, x, y, w, h, rotation, text, style_json, data_key) - connectors(id, page_id, layer_id, from_shape, from_port, to_shape, to_port, label, style_json, waypoints_json) - libraries(id, name, shapes_json) · geometry and named ports per shape, as data rather than code - bindings(id, page_id, source_kind, source_ref, mapping_json, last_generated_at) · what regenerates a data-linked diagram - A connector stores shape and port names. Storing coordinates is what makes edges detach the first time something moves. ### Phase 1 · Shape libraries as data Build: A library format describing each shape's geometry, its named connection points and its default style. Load several libraries. Render from the definition, so adding a shape is a data change rather than a code change. Trap: Hard coding shapes as drawing functions means every new shape is a code change and a release, which is why diagram tools with rich libraries build them as data. Define the format first, including how a port's position is expressed relative to the shape's size, so a resized shape keeps its ports in the right places. Done when: Adding a shape to a library file makes it available without a code change. Resizing a shape keeps its ports proportionally positioned. A library with an unknown property loads and ignores it rather than failing. Two libraries can define shapes with the same name without collision. Do not build yet: No connectors, layers or data binding. ### Phase 2 · Connectors that survive editing Build: Connectors referencing a shape and a named port. Orthogonal routing that leaves a port perpendicular and avoids passing through the shapes it connects. Recompute on any change to either endpoint. Trap: Storing a connector as two points works until the first move, and then every diagram anyone saved is subtly wrong. Store the relationship and derive the geometry, which also makes automatic layout possible later. Done when: Moving a shape reroutes its connectors and none passes through the body of a connected shape. Resizing a shape keeps connectors attached to the same named ports. Deleting a shape deletes its connectors rather than leaving them pointing at nothing. A connector between two shapes on different layers behaves correctly. Do not build yet: No layers or data binding yet. ### Phase 3 · Layers Build: Layers with visibility and locking. Selection ignores hidden and locked layers. Export can include or exclude layers. Moving a shape between layers preserves its connectors. Trap: Letting selection reach a locked layer defeats the point of locking, and it is the most common way a background diagram gets dragged out of place. Filter selection by layer state at the hit test rather than checking after the fact. Done when: A hidden layer's shapes cannot be selected or exported when excluded. A locked layer's shapes cannot be moved, verified by attempting a drag over one. Moving a shape between layers keeps its connectors attached. Layer order affects rendering order. Do not build yet: No data binding or export yet. ### Phase 4 · Diagrams generated from data Build: Import a table and map columns to shape text, style and relationships. Generate shapes and connectors, lay them out with elkjs, and regenerate when the data changes while preserving manual position overrides where you asked for them. Trap: Regenerating from scratch discards every manual adjustment, and a diagram nobody can tweak is one people rebuild by hand elsewhere. Key generated shapes to their data rows, so a regeneration updates existing shapes rather than replacing them, and manual overrides survive. Done when: A spreadsheet of thirty rows generates a laid-out chart with no overlapping shapes. Changing a row's text and regenerating updates that shape without moving the others. A manually repositioned shape keeps its position through a regeneration unless reset. A removed row's shape is removed rather than orphaned. Do not build yet: No export yet. ### Phase 5 · Export that opens elsewhere Build: Export SVG with fonts embedded or outlined and the viewBox set to content bounds, PNG rasterised from the same source at chosen scales, and PDF for print. Verify each in a different application. Trap: An exported SVG referencing a web font renders in a fallback face on any machine that lacks it, which is every machine but yours. Embed the font as data or outline the text, and verify by opening the file in a different application rather than in the browser that made it. Done when: An exported SVG opens in a different application with correct fonts and geometry. A two times PNG has exactly double the dimensions of the one times and is sharper rather than merely larger. Excluded layers are absent from the export. Exporting twice produces identical files. Do not build yet: Templates and sharing come last. ### Phase 6 · Templates, sharing and honest collaboration Build: Save a diagram as a template with placeholder shapes. Share read-only by exporting a self-contained viewer or uploading to storage you own. State plainly that simultaneous editing is not supported. Trap: Naive real-time sync over a socket looks fine with one person and loses work with two, and diagram edits are large and hard to merge. Say editing is single-user rather than shipping something that occasionally destroys a colleague's work. Done when: A template applies and produces a new diagram with placeholders to fill. A shared read-only view renders correctly for someone who has never used the application. The interface states that editing is single-user. A shared diagram is versioned so re-sharing is deliberate. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer cursors and comments, which is what teams pay for and which a naive implementation makes worse than absent. - The large template and shape library catalogues, which are content accumulated over years. - Presentation mode and the enterprise controls around sharing and governance. - Import of proprietary diagram formats, which is reverse engineering with no end. ### README must contain - That shapes and their ports are defined as data, so a library is extended without code - That connectors reference shapes and ports rather than coordinates - That generated diagrams are keyed to data rows so regeneration preserves manual work - How fonts are handled on export and how to verify a file elsewhere - That editing is single-user by design ===== AGENTS.md ===== # Agent instructions · Lucidchart indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Vite, rendering SVG, A library format defining geometry and connection points as data, elkjs for generated diagrams, CSV or JSON to shapes through a mapping you define, IndexedDB with file open and save. 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 - Hard coding shapes as drawing functions means every new shape is a code change and a release, which is why diagram tools with rich libraries build them as data. Define the format first, including how a port's position is expressed relative to the shape's size, so a resized shape keeps its ports in the right places. - Storing a connector as two points works until the first move, and then every diagram anyone saved is subtly wrong. Store the relationship and derive the geometry, which also makes automatic layout possible later. - Letting selection reach a locked layer defeats the point of locking, and it is the most common way a background diagram gets dragged out of place. Filter selection by layer state at the hit test rather than checking after the fact. - Regenerating from scratch discards every manual adjustment, and a diagram nobody can tweak is one people rebuild by hand elsewhere. Key generated shapes to their data rows, so a regeneration updates existing shapes rather than replacing them, and manual overrides survive. - An exported SVG referencing a web font renders in a fallback face on any machine that lacks it, which is every machine but yours. Embed the font as data or outline the text, and verify by opening the file in a different application rather than in the browser that made it. ===== BUILD_PLAN.md ===== # Build plan · Lucidchart A browser editor with shape libraries whose shapes carry defined connection points, layers you can lock and hide, connectors that stay attached through moves and resizes, and diagrams generated from a spreadsheet so an organisation chart or a network map is regenerated rather than redrawn. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Shape libraries as data A shape is a definition anyone can add to, not code you have to change. ### Steps 1. Define the library format Geometry, named ports with relative positions, default style. Written down before implementation. Files: `docs/library-format.md` 2. Render from definitions One renderer reading the data. No shape-specific code paths. Files: `src/shapes.ts` 3. Namespace libraries So two libraries can define a shape with the same name. Files: `src/libraries.ts` ### Done when - [ ] A new shape needs only a library file change - [ ] Ports stay proportional through a resize - [ ] An unknown property is ignored rather than fatal - [ ] Two libraries can share a shape name ### Watch out - Hard coding shapes as drawing functions means every new shape is a code change and a release, which is why diagram tools with rich libraries build them as data. Define the format first, including how a port's position is expressed relative to the shape's size, so a resized shape keeps its ports in the right places. ## Phase 2 · Connectors that survive editing Moving or resizing a shape keeps its connectors attached and sensibly routed. ### Steps 1. Reference shape and port, never points The relationship is the data. Geometry is derived at render time. Files: `src/connectors.ts` 2. Route orthogonally around shapes Perpendicular exit, minimal turns, avoiding the connected shapes' bodies. Files: `src/route.ts` 3. Cascade deletion Deleting a shape removes its connectors. Test for orphans explicitly. Files: `src/doc.ts` ### Done when - [ ] Moving a shape reroutes its connectors correctly - [ ] No connector passes through a connected shape - [ ] Deleting a shape leaves no orphan connectors - [ ] Cross-layer connectors behave correctly ### Watch out - Storing a connector as two points works until the first move, and then every diagram anyone saved is subtly wrong. Store the relationship and derive the geometry, which also makes automatic layout possible later. ## Phase 3 · Layers A complex diagram can be worked on one layer at a time. ### Steps 1. Add layers with visibility and locking Ordered, affecting render order as well as interaction. Files: `src/layers.ts` 2. Filter selection at hit test Hidden and locked layers are excluded before anything is selected, not after. Files: `src/select.ts` 3. Preserve connectors across layer moves The connector references shapes, so this should be free; test it anyway. Files: `src/layers.ts` ### Done when - [ ] Hidden layer shapes cannot be selected - [ ] Locked layer shapes cannot be dragged - [ ] Moving a shape between layers keeps connectors - [ ] Layer order affects rendering ### Watch out - Letting selection reach a locked layer defeats the point of locking, and it is the most common way a background diagram gets dragged out of place. Filter selection by layer state at the hit test rather than checking after the fact. ## Phase 4 · Diagrams generated from data An organisation chart regenerates from a spreadsheet instead of being redrawn. ### Steps 1. Map data to shapes Columns to text, style and parent relationships, defined once as a mapping. Files: `src/bind.ts` 2. Key shapes to rows So regeneration is an update rather than a rebuild, which is what preserves manual work. Files: `src/bind.ts` 3. Lay out with elkjs Layered layout for hierarchies, with an overlap assertion after. Files: `src/layout.ts` ```sh npm install elkjs ``` ### Done when - [ ] Thirty rows generate a chart with no overlaps - [ ] Regeneration updates rather than replaces shapes - [ ] Manual positions survive regeneration - [ ] A removed row removes its shape ### Watch out - Regenerating from scratch discards every manual adjustment, and a diagram nobody can tweak is one people rebuild by hand elsewhere. Key generated shapes to their data rows, so a regeneration updates existing shapes rather than replacing them, and manual overrides survive. ## Phase 5 · Export that opens elsewhere An exported diagram is usable in other tools, not just yours. ### Steps 1. Export SVG with fonts handled Embedded or outlined, viewBox set to content bounds. Files: `src/export-svg.ts` 2. Rasterise from geometry per scale Never upscale a bitmap. Compare against an upscaled version to confirm. Files: `src/export-png.ts` 3. Verify in another application Inkscape or a design tool. The browser that made the file is not a test. ### Done when - [ ] An exported SVG opens correctly elsewhere - [ ] A two times PNG is sharper, not just larger - [ ] Excluded layers are absent from exports - [ ] Two exports are identical ### Watch out - An exported SVG referencing a web font renders in a fallback face on any machine that lacks it, which is every machine but yours. Embed the font as data or outline the text, and verify by opening the file in a different application rather than in the browser that made it. ## Not in this build - Multiplayer cursors and comments. - Large template and shape catalogues. - Presentation mode and enterprise governance. - Import of proprietary diagram formats. ## After v1, if you want it - Conditional formatting from data, so a shape's colour follows a column - A second layout algorithm for network rather than hierarchy diagrams - Round-tripping a diagram to a text format so it can be diffed in version control ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where shape library files are served from. Adding a shape is a file here. VITE_LIBRARY_DIR=/libraries # Required. Self-hosted fonts with their licence files beside them. VITE_FONT_DIR=/fonts # Required. Default direction for generated hierarchies. VITE_LAYOUT_DIRECTION=DOWN # Optional. Product phase only. Leave unset to keep everything local. VITE_SHARE_ENDPOINT=https://share.example.com
You are building a lean indie version of Lucidchart. 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 ===== # Lucidchart · indie build A browser editor with shape libraries whose shapes carry defined connection points, layers you can lock and hide, connectors that stay attached through moves and resizes, and diagrams generated from a spreadsheet so an organisation chart or a network map is regenerated rather than redrawn. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Vite, rendering SVG | vector output, selectable text and hit testing for free at the object counts diagrams actually reach | | Shapes | A library format defining geometry and connection points as data | a shape whose connection points are hard coded cannot be extended by anyone but you | | Layout | elkjs for generated diagrams | layered layout for an organisation chart is solved and hand-rolling it produces crossing edges forever | | Data binding | CSV or JSON to shapes through a mapping you define | the point of a data-linked diagram is that it regenerates when the data changes | | Storage | IndexedDB with file open and save | no account, and the file is the thing you own | ## 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 build. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A second application that opens SVG** · free - Why: Phase 5's export check is meaningless if the only thing you open the file in is the browser that made it. - Get it: Inkscape is free and strict, which makes it a good test. - Verify: You can open an SVG and see whether its fonts survived - [ ] **Fonts you are licensed to embed** · free with open licences - Why: Export embeds or outlines fonts, which is redistribution and not permitted by every licence. - Get it: Use openly licensed families and keep the licence files. - Verify: Each bundled font's licence permits embedding, or export outlines text - [ ] **A real spreadsheet to generate a diagram from** · free - Why: Phase 4 is only meaningful against real data, and real organisation data has the awkward cases: someone with two managers, a vacant role, a name that is very long. - Get it: Use a real team structure or a network inventory. Thirty rows is enough to see layout problems. - Verify: The spreadsheet loads and includes at least one awkward case - [ ] **Which shape library you actually need** · free - Why: A general diagram tool needs hundreds of shapes; a specific one needs a dozen. Deciding which diagrams you make keeps the library finite. - Get it: Pick the diagram type you draw most and list the shapes it needs. That list is your first library. - Verify: The list exists and is under about twenty shapes ## Quick start ```sh npm install elkjs ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multiplayer cursors and comments. - Large template and shape catalogues. - Presentation mode and enterprise governance. - Import of proprietary diagram formats. - presentation and enterprise controls - multiplayer cursors and comments - large template libraries - high-fidelity import and export If one of those is essential to you, that is the reason to keep paying for Lucidchart, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Lucidchart 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 diagram editor with real shape libraries and diagrams generated from data to replace Lucidchart. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Vite, rendering SVG · vector output, selectable text and hit testing for free at the object counts diagrams actually reach - Shapes: A library format defining geometry and connection points as data · a shape whose connection points are hard coded cannot be extended by anyone but you - Layout: elkjs for generated diagrams · layered layout for an organisation chart is solved and hand-rolling it produces crossing edges forever - Data binding: CSV or JSON to shapes through a mapping you define · the point of a data-linked diagram is that it regenerates when the data changes - Storage: IndexedDB with file open and save · no account, and the file is the thing you own ### Data model (create this before Phase 1) - docs(id, title, updated_at), pages(id, doc_id, name, ordinal) - layers(id, page_id, name, ordinal, visible, locked) - shapes(id, page_id, layer_id, library_key, x, y, w, h, rotation, text, style_json, data_key) - connectors(id, page_id, layer_id, from_shape, from_port, to_shape, to_port, label, style_json, waypoints_json) - libraries(id, name, shapes_json) · geometry and named ports per shape, as data rather than code - bindings(id, page_id, source_kind, source_ref, mapping_json, last_generated_at) · what regenerates a data-linked diagram - A connector stores shape and port names. Storing coordinates is what makes edges detach the first time something moves. ### Phase 1 · Shape libraries as data Build: A library format describing each shape's geometry, its named connection points and its default style. Load several libraries. Render from the definition, so adding a shape is a data change rather than a code change. Trap: Hard coding shapes as drawing functions means every new shape is a code change and a release, which is why diagram tools with rich libraries build them as data. Define the format first, including how a port's position is expressed relative to the shape's size, so a resized shape keeps its ports in the right places. Done when: Adding a shape to a library file makes it available without a code change. Resizing a shape keeps its ports proportionally positioned. A library with an unknown property loads and ignores it rather than failing. Two libraries can define shapes with the same name without collision. Do not build yet: No connectors, layers or data binding. ### Phase 2 · Connectors that survive editing Build: Connectors referencing a shape and a named port. Orthogonal routing that leaves a port perpendicular and avoids passing through the shapes it connects. Recompute on any change to either endpoint. Trap: Storing a connector as two points works until the first move, and then every diagram anyone saved is subtly wrong. Store the relationship and derive the geometry, which also makes automatic layout possible later. Done when: Moving a shape reroutes its connectors and none passes through the body of a connected shape. Resizing a shape keeps connectors attached to the same named ports. Deleting a shape deletes its connectors rather than leaving them pointing at nothing. A connector between two shapes on different layers behaves correctly. Do not build yet: No layers or data binding yet. ### Phase 3 · Layers Build: Layers with visibility and locking. Selection ignores hidden and locked layers. Export can include or exclude layers. Moving a shape between layers preserves its connectors. Trap: Letting selection reach a locked layer defeats the point of locking, and it is the most common way a background diagram gets dragged out of place. Filter selection by layer state at the hit test rather than checking after the fact. Done when: A hidden layer's shapes cannot be selected or exported when excluded. A locked layer's shapes cannot be moved, verified by attempting a drag over one. Moving a shape between layers keeps its connectors attached. Layer order affects rendering order. Do not build yet: No data binding or export yet. ### Phase 4 · Diagrams generated from data Build: Import a table and map columns to shape text, style and relationships. Generate shapes and connectors, lay them out with elkjs, and regenerate when the data changes while preserving manual position overrides where you asked for them. Trap: Regenerating from scratch discards every manual adjustment, and a diagram nobody can tweak is one people rebuild by hand elsewhere. Key generated shapes to their data rows, so a regeneration updates existing shapes rather than replacing them, and manual overrides survive. Done when: A spreadsheet of thirty rows generates a laid-out chart with no overlapping shapes. Changing a row's text and regenerating updates that shape without moving the others. A manually repositioned shape keeps its position through a regeneration unless reset. A removed row's shape is removed rather than orphaned. Do not build yet: No export yet. ### Phase 5 · Export that opens elsewhere Build: Export SVG with fonts embedded or outlined and the viewBox set to content bounds, PNG rasterised from the same source at chosen scales, and PDF for print. Verify each in a different application. Trap: An exported SVG referencing a web font renders in a fallback face on any machine that lacks it, which is every machine but yours. Embed the font as data or outline the text, and verify by opening the file in a different application rather than in the browser that made it. Done when: An exported SVG opens in a different application with correct fonts and geometry. A two times PNG has exactly double the dimensions of the one times and is sharper rather than merely larger. Excluded layers are absent from the export. Exporting twice produces identical files. Do not build yet: Templates and sharing come last. ### Phase 6 · Templates, sharing and honest collaboration Build: Save a diagram as a template with placeholder shapes. Share read-only by exporting a self-contained viewer or uploading to storage you own. State plainly that simultaneous editing is not supported. Trap: Naive real-time sync over a socket looks fine with one person and loses work with two, and diagram edits are large and hard to merge. Say editing is single-user rather than shipping something that occasionally destroys a colleague's work. Done when: A template applies and produces a new diagram with placeholders to fill. A shared read-only view renders correctly for someone who has never used the application. The interface states that editing is single-user. A shared diagram is versioned so re-sharing is deliberate. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer cursors and comments, which is what teams pay for and which a naive implementation makes worse than absent. - The large template and shape library catalogues, which are content accumulated over years. - Presentation mode and the enterprise controls around sharing and governance. - Import of proprietary diagram formats, which is reverse engineering with no end. ### README must contain - That shapes and their ports are defined as data, so a library is extended without code - That connectors reference shapes and ports rather than coordinates - That generated diagrams are keyed to data rows so regeneration preserves manual work - How fonts are handled on export and how to verify a file elsewhere - That editing is single-user by design ===== AGENTS.md ===== # Agent instructions · Lucidchart indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Vite, rendering SVG, A library format defining geometry and connection points as data, elkjs for generated diagrams, CSV or JSON to shapes through a mapping you define, IndexedDB with file open and save. 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 - Hard coding shapes as drawing functions means every new shape is a code change and a release, which is why diagram tools with rich libraries build them as data. Define the format first, including how a port's position is expressed relative to the shape's size, so a resized shape keeps its ports in the right places. - Storing a connector as two points works until the first move, and then every diagram anyone saved is subtly wrong. Store the relationship and derive the geometry, which also makes automatic layout possible later. - Letting selection reach a locked layer defeats the point of locking, and it is the most common way a background diagram gets dragged out of place. Filter selection by layer state at the hit test rather than checking after the fact. - Regenerating from scratch discards every manual adjustment, and a diagram nobody can tweak is one people rebuild by hand elsewhere. Key generated shapes to their data rows, so a regeneration updates existing shapes rather than replacing them, and manual overrides survive. - An exported SVG referencing a web font renders in a fallback face on any machine that lacks it, which is every machine but yours. Embed the font as data or outline the text, and verify by opening the file in a different application rather than in the browser that made it. ===== BUILD_PLAN.md ===== # Build plan · Lucidchart A browser editor with shape libraries whose shapes carry defined connection points, layers you can lock and hide, connectors that stay attached through moves and resizes, and diagrams generated from a spreadsheet so an organisation chart or a network map is regenerated rather than redrawn. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Shape libraries as data A shape is a definition anyone can add to, not code you have to change. ### Steps 1. Define the library format Geometry, named ports with relative positions, default style. Written down before implementation. Files: `docs/library-format.md` 2. Render from definitions One renderer reading the data. No shape-specific code paths. Files: `src/shapes.ts` 3. Namespace libraries So two libraries can define a shape with the same name. Files: `src/libraries.ts` ### Done when - [ ] A new shape needs only a library file change - [ ] Ports stay proportional through a resize - [ ] An unknown property is ignored rather than fatal - [ ] Two libraries can share a shape name ### Watch out - Hard coding shapes as drawing functions means every new shape is a code change and a release, which is why diagram tools with rich libraries build them as data. Define the format first, including how a port's position is expressed relative to the shape's size, so a resized shape keeps its ports in the right places. ## Phase 2 · Connectors that survive editing Moving or resizing a shape keeps its connectors attached and sensibly routed. ### Steps 1. Reference shape and port, never points The relationship is the data. Geometry is derived at render time. Files: `src/connectors.ts` 2. Route orthogonally around shapes Perpendicular exit, minimal turns, avoiding the connected shapes' bodies. Files: `src/route.ts` 3. Cascade deletion Deleting a shape removes its connectors. Test for orphans explicitly. Files: `src/doc.ts` ### Done when - [ ] Moving a shape reroutes its connectors correctly - [ ] No connector passes through a connected shape - [ ] Deleting a shape leaves no orphan connectors - [ ] Cross-layer connectors behave correctly ### Watch out - Storing a connector as two points works until the first move, and then every diagram anyone saved is subtly wrong. Store the relationship and derive the geometry, which also makes automatic layout possible later. ## Phase 3 · Layers A complex diagram can be worked on one layer at a time. ### Steps 1. Add layers with visibility and locking Ordered, affecting render order as well as interaction. Files: `src/layers.ts` 2. Filter selection at hit test Hidden and locked layers are excluded before anything is selected, not after. Files: `src/select.ts` 3. Preserve connectors across layer moves The connector references shapes, so this should be free; test it anyway. Files: `src/layers.ts` ### Done when - [ ] Hidden layer shapes cannot be selected - [ ] Locked layer shapes cannot be dragged - [ ] Moving a shape between layers keeps connectors - [ ] Layer order affects rendering ### Watch out - Letting selection reach a locked layer defeats the point of locking, and it is the most common way a background diagram gets dragged out of place. Filter selection by layer state at the hit test rather than checking after the fact. ## Phase 4 · Diagrams generated from data An organisation chart regenerates from a spreadsheet instead of being redrawn. ### Steps 1. Map data to shapes Columns to text, style and parent relationships, defined once as a mapping. Files: `src/bind.ts` 2. Key shapes to rows So regeneration is an update rather than a rebuild, which is what preserves manual work. Files: `src/bind.ts` 3. Lay out with elkjs Layered layout for hierarchies, with an overlap assertion after. Files: `src/layout.ts` ```sh npm install elkjs ``` ### Done when - [ ] Thirty rows generate a chart with no overlaps - [ ] Regeneration updates rather than replaces shapes - [ ] Manual positions survive regeneration - [ ] A removed row removes its shape ### Watch out - Regenerating from scratch discards every manual adjustment, and a diagram nobody can tweak is one people rebuild by hand elsewhere. Key generated shapes to their data rows, so a regeneration updates existing shapes rather than replacing them, and manual overrides survive. ## Phase 5 · Export that opens elsewhere An exported diagram is usable in other tools, not just yours. ### Steps 1. Export SVG with fonts handled Embedded or outlined, viewBox set to content bounds. Files: `src/export-svg.ts` 2. Rasterise from geometry per scale Never upscale a bitmap. Compare against an upscaled version to confirm. Files: `src/export-png.ts` 3. Verify in another application Inkscape or a design tool. The browser that made the file is not a test. ### Done when - [ ] An exported SVG opens correctly elsewhere - [ ] A two times PNG is sharper, not just larger - [ ] Excluded layers are absent from exports - [ ] Two exports are identical ### Watch out - An exported SVG referencing a web font renders in a fallback face on any machine that lacks it, which is every machine but yours. Embed the font as data or outline the text, and verify by opening the file in a different application rather than in the browser that made it. ## Not in this build - Multiplayer cursors and comments. - Large template and shape catalogues. - Presentation mode and enterprise governance. - Import of proprietary diagram formats. ## After v1, if you want it - Conditional formatting from data, so a shape's colour follows a column - A second layout algorithm for network rather than hierarchy diagrams - Round-tripping a diagram to a text format so it can be diffed in version control ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where shape library files are served from. Adding a shape is a file here. VITE_LIBRARY_DIR=/libraries # Required. Self-hosted fonts with their licence files beside them. VITE_FONT_DIR=/fonts # Required. Default direction for generated hierarchies. VITE_LAYOUT_DIRECTION=DOWN # Optional. Product phase only. Leave unset to keep everything local. VITE_SHARE_ENDPOINT=https://share.example.com
You are building a production product version of Lucidchart. 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 ===== # Lucidchart · product brief ## Problem The visible diagramming loop is buildable, but a credible replacement needs more than the first screen. Lucidchart earns its keep through interaction polish, collaboration, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A diagram editor whose shape libraries grow without code changes, whose connectors survive every edit, whose generated diagrams update from data without discarding manual work, and whose exports open correctly in other tools. ## 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 - Multiplayer cursors and comments. - Large template and shape catalogues. - Presentation mode and enterprise governance. - Import of proprietary diagram formats. - presentation and enterprise controls - multiplayer cursors and comments - large template libraries - high-fidelity import and export ## Success criteria - A new shape is confirmed to need only a library file change - Connectors are confirmed to survive move, resize and layer change - Regeneration is confirmed to preserve manual positions - An exported SVG opens correctly in a second application - Fixture diagrams export byte-identically ===== BRIEF.md ===== # Build brief · Lucidchart 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 diagram editor with real shape libraries and diagrams generated from data to replace Lucidchart. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Vite, rendering SVG · vector output, selectable text and hit testing for free at the object counts diagrams actually reach - Shapes: A library format defining geometry and connection points as data · a shape whose connection points are hard coded cannot be extended by anyone but you - Layout: elkjs for generated diagrams · layered layout for an organisation chart is solved and hand-rolling it produces crossing edges forever - Data binding: CSV or JSON to shapes through a mapping you define · the point of a data-linked diagram is that it regenerates when the data changes - Storage: IndexedDB with file open and save · no account, and the file is the thing you own ### Data model (create this before Phase 1) - docs(id, title, updated_at), pages(id, doc_id, name, ordinal) - layers(id, page_id, name, ordinal, visible, locked) - shapes(id, page_id, layer_id, library_key, x, y, w, h, rotation, text, style_json, data_key) - connectors(id, page_id, layer_id, from_shape, from_port, to_shape, to_port, label, style_json, waypoints_json) - libraries(id, name, shapes_json) · geometry and named ports per shape, as data rather than code - bindings(id, page_id, source_kind, source_ref, mapping_json, last_generated_at) · what regenerates a data-linked diagram - A connector stores shape and port names. Storing coordinates is what makes edges detach the first time something moves. ### Phase 1 · Shape libraries as data Build: A library format describing each shape's geometry, its named connection points and its default style. Load several libraries. Render from the definition, so adding a shape is a data change rather than a code change. Trap: Hard coding shapes as drawing functions means every new shape is a code change and a release, which is why diagram tools with rich libraries build them as data. Define the format first, including how a port's position is expressed relative to the shape's size, so a resized shape keeps its ports in the right places. Done when: Adding a shape to a library file makes it available without a code change. Resizing a shape keeps its ports proportionally positioned. A library with an unknown property loads and ignores it rather than failing. Two libraries can define shapes with the same name without collision. Do not build yet: No connectors, layers or data binding. ### Phase 2 · Connectors that survive editing Build: Connectors referencing a shape and a named port. Orthogonal routing that leaves a port perpendicular and avoids passing through the shapes it connects. Recompute on any change to either endpoint. Trap: Storing a connector as two points works until the first move, and then every diagram anyone saved is subtly wrong. Store the relationship and derive the geometry, which also makes automatic layout possible later. Done when: Moving a shape reroutes its connectors and none passes through the body of a connected shape. Resizing a shape keeps connectors attached to the same named ports. Deleting a shape deletes its connectors rather than leaving them pointing at nothing. A connector between two shapes on different layers behaves correctly. Do not build yet: No layers or data binding yet. ### Phase 3 · Layers Build: Layers with visibility and locking. Selection ignores hidden and locked layers. Export can include or exclude layers. Moving a shape between layers preserves its connectors. Trap: Letting selection reach a locked layer defeats the point of locking, and it is the most common way a background diagram gets dragged out of place. Filter selection by layer state at the hit test rather than checking after the fact. Done when: A hidden layer's shapes cannot be selected or exported when excluded. A locked layer's shapes cannot be moved, verified by attempting a drag over one. Moving a shape between layers keeps its connectors attached. Layer order affects rendering order. Do not build yet: No data binding or export yet. ### Phase 4 · Diagrams generated from data Build: Import a table and map columns to shape text, style and relationships. Generate shapes and connectors, lay them out with elkjs, and regenerate when the data changes while preserving manual position overrides where you asked for them. Trap: Regenerating from scratch discards every manual adjustment, and a diagram nobody can tweak is one people rebuild by hand elsewhere. Key generated shapes to their data rows, so a regeneration updates existing shapes rather than replacing them, and manual overrides survive. Done when: A spreadsheet of thirty rows generates a laid-out chart with no overlapping shapes. Changing a row's text and regenerating updates that shape without moving the others. A manually repositioned shape keeps its position through a regeneration unless reset. A removed row's shape is removed rather than orphaned. Do not build yet: No export yet. ### Phase 5 · Export that opens elsewhere Build: Export SVG with fonts embedded or outlined and the viewBox set to content bounds, PNG rasterised from the same source at chosen scales, and PDF for print. Verify each in a different application. Trap: An exported SVG referencing a web font renders in a fallback face on any machine that lacks it, which is every machine but yours. Embed the font as data or outline the text, and verify by opening the file in a different application rather than in the browser that made it. Done when: An exported SVG opens in a different application with correct fonts and geometry. A two times PNG has exactly double the dimensions of the one times and is sharper rather than merely larger. Excluded layers are absent from the export. Exporting twice produces identical files. Do not build yet: Templates and sharing come last. ### Phase 6 · Templates, sharing and honest collaboration Build: Save a diagram as a template with placeholder shapes. Share read-only by exporting a self-contained viewer or uploading to storage you own. State plainly that simultaneous editing is not supported. Trap: Naive real-time sync over a socket looks fine with one person and loses work with two, and diagram edits are large and hard to merge. Say editing is single-user rather than shipping something that occasionally destroys a colleague's work. Done when: A template applies and produces a new diagram with placeholders to fill. A shared read-only view renders correctly for someone who has never used the application. The interface states that editing is single-user. A shared diagram is versioned so re-sharing is deliberate. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer cursors and comments, which is what teams pay for and which a naive implementation makes worse than absent. - The large template and shape library catalogues, which are content accumulated over years. - Presentation mode and the enterprise controls around sharing and governance. - Import of proprietary diagram formats, which is reverse engineering with no end. ### README must contain - That shapes and their ports are defined as data, so a library is extended without code - That connectors reference shapes and ports rather than coordinates - That generated diagrams are keyed to data rows so regeneration preserves manual work - How fonts are handled on export and how to verify a file elsewhere - That editing is single-user by design ===== ARCHITECTURE.md ===== # Architecture · Lucidchart ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Vite, rendering SVG | vector output, selectable text and hit testing for free at the object counts diagrams actually reach | | Shapes | A library format defining geometry and connection points as data | a shape whose connection points are hard coded cannot be extended by anyone but you | | Layout | elkjs for generated diagrams | layered layout for an organisation chart is solved and hand-rolling it produces crossing edges forever | | Data binding | CSV or JSON to shapes through a mapping you define | the point of a data-linked diagram is that it regenerates when the data changes | | Storage | IndexedDB with file open and save | no account, and the file is the thing you own | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Libraries | Shape geometry and ports as data. | The format is the extension point. Hard coding shapes is the thing it exists to prevent. | | Connectors | Relationships and routing. | Routing strategies are replaceable; storing relationships rather than coordinates is not. | | Layers | Visibility, locking and ordering. | Filtering at hit test is what makes locking meaningful. | | Binding | Data to diagram, and regeneration. | Keying shapes to rows is what preserves manual work across regeneration. | | Export | Producing files that open elsewhere. | Any renderer, provided fonts are handled and output is deterministic. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `VITE_LIBRARY_DIR` · required · Where shape library files are served from. Adding a shape is a file here. - `VITE_FONT_DIR` · required · Self-hosted fonts with their licence files beside them. - `VITE_LAYOUT_DIRECTION` · required · Default direction for generated hierarchies. - `VITE_SHARE_ENDPOINT` · optional · Product phase only. Leave unset to keep everything local. ## 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 · Lucidchart product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with Vite, rendering SVG, A library format defining geometry and connection points as data, elkjs for generated diagrams, CSV or JSON to shapes through a mapping you define, IndexedDB with file open and save. - 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 - Hard coding shapes as drawing functions means every new shape is a code change and a release, which is why diagram tools with rich libraries build them as data. Define the format first, including how a port's position is expressed relative to the shape's size, so a resized shape keeps its ports in the right places. - Storing a connector as two points works until the first move, and then every diagram anyone saved is subtly wrong. Store the relationship and derive the geometry, which also makes automatic layout possible later. - Letting selection reach a locked layer defeats the point of locking, and it is the most common way a background diagram gets dragged out of place. Filter selection by layer state at the hit test rather than checking after the fact. - Regenerating from scratch discards every manual adjustment, and a diagram nobody can tweak is one people rebuild by hand elsewhere. Key generated shapes to their data rows, so a regeneration updates existing shapes rather than replacing them, and manual overrides survive. - An exported SVG referencing a web font renders in a fallback face on any machine that lacks it, which is every machine but yours. Embed the font as data or outline the text, and verify by opening the file in a different application rather than in the browser that made it. - Naive real-time sync over a socket looks fine with one person and loses work with two, and diagram edits are large and hard to merge. Say editing is single-user rather than shipping something that occasionally destroys a colleague's work. ===== MILESTONES.md ===== # Delivery milestones · Lucidchart Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Shape libraries as data A shape is a definition anyone can add to, not code you have to change. ### Steps 1. Define the library format Geometry, named ports with relative positions, default style. Written down before implementation. Files: `docs/library-format.md` 2. Render from definitions One renderer reading the data. No shape-specific code paths. Files: `src/shapes.ts` 3. Namespace libraries So two libraries can define a shape with the same name. Files: `src/libraries.ts` ### Done when - [ ] A new shape needs only a library file change - [ ] Ports stay proportional through a resize - [ ] An unknown property is ignored rather than fatal - [ ] Two libraries can share a shape name ### Watch out - Hard coding shapes as drawing functions means every new shape is a code change and a release, which is why diagram tools with rich libraries build them as data. Define the format first, including how a port's position is expressed relative to the shape's size, so a resized shape keeps its ports in the right places. ## M2 · Connectors that survive editing Moving or resizing a shape keeps its connectors attached and sensibly routed. ### Steps 1. Reference shape and port, never points The relationship is the data. Geometry is derived at render time. Files: `src/connectors.ts` 2. Route orthogonally around shapes Perpendicular exit, minimal turns, avoiding the connected shapes' bodies. Files: `src/route.ts` 3. Cascade deletion Deleting a shape removes its connectors. Test for orphans explicitly. Files: `src/doc.ts` ### Done when - [ ] Moving a shape reroutes its connectors correctly - [ ] No connector passes through a connected shape - [ ] Deleting a shape leaves no orphan connectors - [ ] Cross-layer connectors behave correctly ### Watch out - Storing a connector as two points works until the first move, and then every diagram anyone saved is subtly wrong. Store the relationship and derive the geometry, which also makes automatic layout possible later. ## M3 · Layers A complex diagram can be worked on one layer at a time. ### Steps 1. Add layers with visibility and locking Ordered, affecting render order as well as interaction. Files: `src/layers.ts` 2. Filter selection at hit test Hidden and locked layers are excluded before anything is selected, not after. Files: `src/select.ts` 3. Preserve connectors across layer moves The connector references shapes, so this should be free; test it anyway. Files: `src/layers.ts` ### Done when - [ ] Hidden layer shapes cannot be selected - [ ] Locked layer shapes cannot be dragged - [ ] Moving a shape between layers keeps connectors - [ ] Layer order affects rendering ### Watch out - Letting selection reach a locked layer defeats the point of locking, and it is the most common way a background diagram gets dragged out of place. Filter selection by layer state at the hit test rather than checking after the fact. ## M4 · Diagrams generated from data An organisation chart regenerates from a spreadsheet instead of being redrawn. ### Steps 1. Map data to shapes Columns to text, style and parent relationships, defined once as a mapping. Files: `src/bind.ts` 2. Key shapes to rows So regeneration is an update rather than a rebuild, which is what preserves manual work. Files: `src/bind.ts` 3. Lay out with elkjs Layered layout for hierarchies, with an overlap assertion after. Files: `src/layout.ts` ```sh npm install elkjs ``` ### Done when - [ ] Thirty rows generate a chart with no overlaps - [ ] Regeneration updates rather than replaces shapes - [ ] Manual positions survive regeneration - [ ] A removed row removes its shape ### Watch out - Regenerating from scratch discards every manual adjustment, and a diagram nobody can tweak is one people rebuild by hand elsewhere. Key generated shapes to their data rows, so a regeneration updates existing shapes rather than replacing them, and manual overrides survive. ## M5 · Export that opens elsewhere An exported diagram is usable in other tools, not just yours. ### Steps 1. Export SVG with fonts handled Embedded or outlined, viewBox set to content bounds. Files: `src/export-svg.ts` 2. Rasterise from geometry per scale Never upscale a bitmap. Compare against an upscaled version to confirm. Files: `src/export-png.ts` 3. Verify in another application Inkscape or a design tool. The browser that made the file is not a test. ### Done when - [ ] An exported SVG opens correctly elsewhere - [ ] A two times PNG is sharper, not just larger - [ ] Excluded layers are absent from exports - [ ] Two exports are identical ### Watch out - An exported SVG referencing a web font renders in a fallback face on any machine that lacks it, which is every machine but yours. Embed the font as data or outline the text, and verify by opening the file in a different application rather than in the browser that made it. ## M6 · Templates, sharing and honest collaboration (production only) Diagrams can be reused and shared, without pretending to be multiplayer. ### Steps 1. Save templates with placeholders Marked shapes to fill, the rest fixed design. Files: `src/templates.ts` 2. Build a read-only viewer Same rendering, no editing affordances, self-contained. Files: `viewer.html` 3. State the single-user limitation In the interface. Silence reads as a promise you did not make. Files: `README.md` ### Done when - [ ] A template produces a new diagram with placeholders - [ ] The read-only view renders for a first-time visitor - [ ] The single-user limitation is visible in the interface - [ ] Re-sharing is a deliberate action ### Watch out - Naive real-time sync over a socket looks fine with one person and loses work with two, and diagram edits are large and hard to merge. Say editing is single-user rather than shipping something that occasionally destroys a colleague's work. ===== OPERATIONS.md ===== # Operations · Lucidchart ## Backup Documents live in browser storage, which can be cleared. File save is the backup, and the shape libraries are separate files worth version controlling since they are the reusable asset. ## Restore Open the saved file. If a document needs anything beyond itself and its libraries, something has leaked out of the file. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Nothing server-side. Worth tracking is export determinism: keep fixture diagrams and diff their exported SVG in continuous integration so a rendering change is caught. ## Incident checklist A font update changing metrics reflows text in saved diagrams. Pin font versions and treat a font change as a visual release with its own diff. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A new shape is confirmed to need only a library file change - [ ] Connectors are confirmed to survive move, resize and layer change - [ ] Regeneration is confirmed to preserve manual positions - [ ] An exported SVG opens correctly in a second application - [ ] Fixture diagrams export byte-identically ## Launch constraint Do not market omitted Lucidchart 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. Where shape library files are served from. Adding a shape is a file here. VITE_LIBRARY_DIR=/libraries # Required. Self-hosted fonts with their licence files beside them. VITE_FONT_DIR=/fonts # Required. Default direction for generated hierarchies. VITE_LAYOUT_DIRECTION=DOWN # Optional. Product phase only. Leave unset to keep everything local. VITE_SHARE_ENDPOINT=https://share.example.com
# Lucidchart · indie build A browser editor with shape libraries whose shapes carry defined connection points, layers you can lock and hide, connectors that stay attached through moves and resizes, and diagrams generated from a spreadsheet so an organisation chart or a network map is regenerated rather than redrawn. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Vite, rendering SVG | vector output, selectable text and hit testing for free at the object counts diagrams actually reach | | Shapes | A library format defining geometry and connection points as data | a shape whose connection points are hard coded cannot be extended by anyone but you | | Layout | elkjs for generated diagrams | layered layout for an organisation chart is solved and hand-rolling it produces crossing edges forever | | Data binding | CSV or JSON to shapes through a mapping you define | the point of a data-linked diagram is that it regenerates when the data changes | | Storage | IndexedDB with file open and save | no account, and the file is the thing you own | ## 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 build. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A second application that opens SVG** · free - Why: Phase 5's export check is meaningless if the only thing you open the file in is the browser that made it. - Get it: Inkscape is free and strict, which makes it a good test. - Verify: You can open an SVG and see whether its fonts survived - [ ] **Fonts you are licensed to embed** · free with open licences - Why: Export embeds or outlines fonts, which is redistribution and not permitted by every licence. - Get it: Use openly licensed families and keep the licence files. - Verify: Each bundled font's licence permits embedding, or export outlines text - [ ] **A real spreadsheet to generate a diagram from** · free - Why: Phase 4 is only meaningful against real data, and real organisation data has the awkward cases: someone with two managers, a vacant role, a name that is very long. - Get it: Use a real team structure or a network inventory. Thirty rows is enough to see layout problems. - Verify: The spreadsheet loads and includes at least one awkward case - [ ] **Which shape library you actually need** · free - Why: A general diagram tool needs hundreds of shapes; a specific one needs a dozen. Deciding which diagrams you make keeps the library finite. - Get it: Pick the diagram type you draw most and list the shapes it needs. That list is your first library. - Verify: The list exists and is under about twenty shapes ## Quick start ```sh npm install elkjs ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multiplayer cursors and comments. - Large template and shape catalogues. - Presentation mode and enterprise governance. - Import of proprietary diagram formats. - presentation and enterprise controls - multiplayer cursors and comments - large template libraries - high-fidelity import and export If one of those is essential to you, that is the reason to keep paying for Lucidchart, and the README should say so rather than pretend.
# Build brief · Lucidchart 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 diagram editor with real shape libraries and diagrams generated from data to replace Lucidchart. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Vite, rendering SVG · vector output, selectable text and hit testing for free at the object counts diagrams actually reach - Shapes: A library format defining geometry and connection points as data · a shape whose connection points are hard coded cannot be extended by anyone but you - Layout: elkjs for generated diagrams · layered layout for an organisation chart is solved and hand-rolling it produces crossing edges forever - Data binding: CSV or JSON to shapes through a mapping you define · the point of a data-linked diagram is that it regenerates when the data changes - Storage: IndexedDB with file open and save · no account, and the file is the thing you own ### Data model (create this before Phase 1) - docs(id, title, updated_at), pages(id, doc_id, name, ordinal) - layers(id, page_id, name, ordinal, visible, locked) - shapes(id, page_id, layer_id, library_key, x, y, w, h, rotation, text, style_json, data_key) - connectors(id, page_id, layer_id, from_shape, from_port, to_shape, to_port, label, style_json, waypoints_json) - libraries(id, name, shapes_json) · geometry and named ports per shape, as data rather than code - bindings(id, page_id, source_kind, source_ref, mapping_json, last_generated_at) · what regenerates a data-linked diagram - A connector stores shape and port names. Storing coordinates is what makes edges detach the first time something moves. ### Phase 1 · Shape libraries as data Build: A library format describing each shape's geometry, its named connection points and its default style. Load several libraries. Render from the definition, so adding a shape is a data change rather than a code change. Trap: Hard coding shapes as drawing functions means every new shape is a code change and a release, which is why diagram tools with rich libraries build them as data. Define the format first, including how a port's position is expressed relative to the shape's size, so a resized shape keeps its ports in the right places. Done when: Adding a shape to a library file makes it available without a code change. Resizing a shape keeps its ports proportionally positioned. A library with an unknown property loads and ignores it rather than failing. Two libraries can define shapes with the same name without collision. Do not build yet: No connectors, layers or data binding. ### Phase 2 · Connectors that survive editing Build: Connectors referencing a shape and a named port. Orthogonal routing that leaves a port perpendicular and avoids passing through the shapes it connects. Recompute on any change to either endpoint. Trap: Storing a connector as two points works until the first move, and then every diagram anyone saved is subtly wrong. Store the relationship and derive the geometry, which also makes automatic layout possible later. Done when: Moving a shape reroutes its connectors and none passes through the body of a connected shape. Resizing a shape keeps connectors attached to the same named ports. Deleting a shape deletes its connectors rather than leaving them pointing at nothing. A connector between two shapes on different layers behaves correctly. Do not build yet: No layers or data binding yet. ### Phase 3 · Layers Build: Layers with visibility and locking. Selection ignores hidden and locked layers. Export can include or exclude layers. Moving a shape between layers preserves its connectors. Trap: Letting selection reach a locked layer defeats the point of locking, and it is the most common way a background diagram gets dragged out of place. Filter selection by layer state at the hit test rather than checking after the fact. Done when: A hidden layer's shapes cannot be selected or exported when excluded. A locked layer's shapes cannot be moved, verified by attempting a drag over one. Moving a shape between layers keeps its connectors attached. Layer order affects rendering order. Do not build yet: No data binding or export yet. ### Phase 4 · Diagrams generated from data Build: Import a table and map columns to shape text, style and relationships. Generate shapes and connectors, lay them out with elkjs, and regenerate when the data changes while preserving manual position overrides where you asked for them. Trap: Regenerating from scratch discards every manual adjustment, and a diagram nobody can tweak is one people rebuild by hand elsewhere. Key generated shapes to their data rows, so a regeneration updates existing shapes rather than replacing them, and manual overrides survive. Done when: A spreadsheet of thirty rows generates a laid-out chart with no overlapping shapes. Changing a row's text and regenerating updates that shape without moving the others. A manually repositioned shape keeps its position through a regeneration unless reset. A removed row's shape is removed rather than orphaned. Do not build yet: No export yet. ### Phase 5 · Export that opens elsewhere Build: Export SVG with fonts embedded or outlined and the viewBox set to content bounds, PNG rasterised from the same source at chosen scales, and PDF for print. Verify each in a different application. Trap: An exported SVG referencing a web font renders in a fallback face on any machine that lacks it, which is every machine but yours. Embed the font as data or outline the text, and verify by opening the file in a different application rather than in the browser that made it. Done when: An exported SVG opens in a different application with correct fonts and geometry. A two times PNG has exactly double the dimensions of the one times and is sharper rather than merely larger. Excluded layers are absent from the export. Exporting twice produces identical files. Do not build yet: Templates and sharing come last. ### Phase 6 · Templates, sharing and honest collaboration Build: Save a diagram as a template with placeholder shapes. Share read-only by exporting a self-contained viewer or uploading to storage you own. State plainly that simultaneous editing is not supported. Trap: Naive real-time sync over a socket looks fine with one person and loses work with two, and diagram edits are large and hard to merge. Say editing is single-user rather than shipping something that occasionally destroys a colleague's work. Done when: A template applies and produces a new diagram with placeholders to fill. A shared read-only view renders correctly for someone who has never used the application. The interface states that editing is single-user. A shared diagram is versioned so re-sharing is deliberate. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer cursors and comments, which is what teams pay for and which a naive implementation makes worse than absent. - The large template and shape library catalogues, which are content accumulated over years. - Presentation mode and the enterprise controls around sharing and governance. - Import of proprietary diagram formats, which is reverse engineering with no end. ### README must contain - That shapes and their ports are defined as data, so a library is extended without code - That connectors reference shapes and ports rather than coordinates - That generated diagrams are keyed to data rows so regeneration preserves manual work - How fonts are handled on export and how to verify a file elsewhere - That editing is single-user by design
# Agent instructions · Lucidchart indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Vite, rendering SVG, A library format defining geometry and connection points as data, elkjs for generated diagrams, CSV or JSON to shapes through a mapping you define, IndexedDB with file open and save. 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 - Hard coding shapes as drawing functions means every new shape is a code change and a release, which is why diagram tools with rich libraries build them as data. Define the format first, including how a port's position is expressed relative to the shape's size, so a resized shape keeps its ports in the right places. - Storing a connector as two points works until the first move, and then every diagram anyone saved is subtly wrong. Store the relationship and derive the geometry, which also makes automatic layout possible later. - Letting selection reach a locked layer defeats the point of locking, and it is the most common way a background diagram gets dragged out of place. Filter selection by layer state at the hit test rather than checking after the fact. - Regenerating from scratch discards every manual adjustment, and a diagram nobody can tweak is one people rebuild by hand elsewhere. Key generated shapes to their data rows, so a regeneration updates existing shapes rather than replacing them, and manual overrides survive. - An exported SVG referencing a web font renders in a fallback face on any machine that lacks it, which is every machine but yours. Embed the font as data or outline the text, and verify by opening the file in a different application rather than in the browser that made it.
# Build plan · Lucidchart A browser editor with shape libraries whose shapes carry defined connection points, layers you can lock and hide, connectors that stay attached through moves and resizes, and diagrams generated from a spreadsheet so an organisation chart or a network map is regenerated rather than redrawn. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Shape libraries as data A shape is a definition anyone can add to, not code you have to change. ### Steps 1. Define the library format Geometry, named ports with relative positions, default style. Written down before implementation. Files: `docs/library-format.md` 2. Render from definitions One renderer reading the data. No shape-specific code paths. Files: `src/shapes.ts` 3. Namespace libraries So two libraries can define a shape with the same name. Files: `src/libraries.ts` ### Done when - [ ] A new shape needs only a library file change - [ ] Ports stay proportional through a resize - [ ] An unknown property is ignored rather than fatal - [ ] Two libraries can share a shape name ### Watch out - Hard coding shapes as drawing functions means every new shape is a code change and a release, which is why diagram tools with rich libraries build them as data. Define the format first, including how a port's position is expressed relative to the shape's size, so a resized shape keeps its ports in the right places. ## Phase 2 · Connectors that survive editing Moving or resizing a shape keeps its connectors attached and sensibly routed. ### Steps 1. Reference shape and port, never points The relationship is the data. Geometry is derived at render time. Files: `src/connectors.ts` 2. Route orthogonally around shapes Perpendicular exit, minimal turns, avoiding the connected shapes' bodies. Files: `src/route.ts` 3. Cascade deletion Deleting a shape removes its connectors. Test for orphans explicitly. Files: `src/doc.ts` ### Done when - [ ] Moving a shape reroutes its connectors correctly - [ ] No connector passes through a connected shape - [ ] Deleting a shape leaves no orphan connectors - [ ] Cross-layer connectors behave correctly ### Watch out - Storing a connector as two points works until the first move, and then every diagram anyone saved is subtly wrong. Store the relationship and derive the geometry, which also makes automatic layout possible later. ## Phase 3 · Layers A complex diagram can be worked on one layer at a time. ### Steps 1. Add layers with visibility and locking Ordered, affecting render order as well as interaction. Files: `src/layers.ts` 2. Filter selection at hit test Hidden and locked layers are excluded before anything is selected, not after. Files: `src/select.ts` 3. Preserve connectors across layer moves The connector references shapes, so this should be free; test it anyway. Files: `src/layers.ts` ### Done when - [ ] Hidden layer shapes cannot be selected - [ ] Locked layer shapes cannot be dragged - [ ] Moving a shape between layers keeps connectors - [ ] Layer order affects rendering ### Watch out - Letting selection reach a locked layer defeats the point of locking, and it is the most common way a background diagram gets dragged out of place. Filter selection by layer state at the hit test rather than checking after the fact. ## Phase 4 · Diagrams generated from data An organisation chart regenerates from a spreadsheet instead of being redrawn. ### Steps 1. Map data to shapes Columns to text, style and parent relationships, defined once as a mapping. Files: `src/bind.ts` 2. Key shapes to rows So regeneration is an update rather than a rebuild, which is what preserves manual work. Files: `src/bind.ts` 3. Lay out with elkjs Layered layout for hierarchies, with an overlap assertion after. Files: `src/layout.ts` ```sh npm install elkjs ``` ### Done when - [ ] Thirty rows generate a chart with no overlaps - [ ] Regeneration updates rather than replaces shapes - [ ] Manual positions survive regeneration - [ ] A removed row removes its shape ### Watch out - Regenerating from scratch discards every manual adjustment, and a diagram nobody can tweak is one people rebuild by hand elsewhere. Key generated shapes to their data rows, so a regeneration updates existing shapes rather than replacing them, and manual overrides survive. ## Phase 5 · Export that opens elsewhere An exported diagram is usable in other tools, not just yours. ### Steps 1. Export SVG with fonts handled Embedded or outlined, viewBox set to content bounds. Files: `src/export-svg.ts` 2. Rasterise from geometry per scale Never upscale a bitmap. Compare against an upscaled version to confirm. Files: `src/export-png.ts` 3. Verify in another application Inkscape or a design tool. The browser that made the file is not a test. ### Done when - [ ] An exported SVG opens correctly elsewhere - [ ] A two times PNG is sharper, not just larger - [ ] Excluded layers are absent from exports - [ ] Two exports are identical ### Watch out - An exported SVG referencing a web font renders in a fallback face on any machine that lacks it, which is every machine but yours. Embed the font as data or outline the text, and verify by opening the file in a different application rather than in the browser that made it. ## Not in this build - Multiplayer cursors and comments. - Large template and shape catalogues. - Presentation mode and enterprise governance. - Import of proprietary diagram formats. ## After v1, if you want it - Conditional formatting from data, so a shape's colour follows a column - A second layout algorithm for network rather than hierarchy diagrams - Round-tripping a diagram to a text format so it can be diffed in version control
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where shape library files are served from. Adding a shape is a file here. VITE_LIBRARY_DIR=/libraries # Required. Self-hosted fonts with their licence files beside them. VITE_FONT_DIR=/fonts # Required. Default direction for generated hierarchies. VITE_LAYOUT_DIRECTION=DOWN # Optional. Product phase only. Leave unset to keep everything local. VITE_SHARE_ENDPOINT=https://share.example.com
# Lucidchart · product brief ## Problem The visible diagramming loop is buildable, but a credible replacement needs more than the first screen. Lucidchart earns its keep through interaction polish, collaboration, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A diagram editor whose shape libraries grow without code changes, whose connectors survive every edit, whose generated diagrams update from data without discarding manual work, and whose exports open correctly in other tools. ## 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 - Multiplayer cursors and comments. - Large template and shape catalogues. - Presentation mode and enterprise governance. - Import of proprietary diagram formats. - presentation and enterprise controls - multiplayer cursors and comments - large template libraries - high-fidelity import and export ## Success criteria - A new shape is confirmed to need only a library file change - Connectors are confirmed to survive move, resize and layer change - Regeneration is confirmed to preserve manual positions - An exported SVG opens correctly in a second application - Fixture diagrams export byte-identically
# Build brief · Lucidchart 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 diagram editor with real shape libraries and diagrams generated from data to replace Lucidchart. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Vite, rendering SVG · vector output, selectable text and hit testing for free at the object counts diagrams actually reach - Shapes: A library format defining geometry and connection points as data · a shape whose connection points are hard coded cannot be extended by anyone but you - Layout: elkjs for generated diagrams · layered layout for an organisation chart is solved and hand-rolling it produces crossing edges forever - Data binding: CSV or JSON to shapes through a mapping you define · the point of a data-linked diagram is that it regenerates when the data changes - Storage: IndexedDB with file open and save · no account, and the file is the thing you own ### Data model (create this before Phase 1) - docs(id, title, updated_at), pages(id, doc_id, name, ordinal) - layers(id, page_id, name, ordinal, visible, locked) - shapes(id, page_id, layer_id, library_key, x, y, w, h, rotation, text, style_json, data_key) - connectors(id, page_id, layer_id, from_shape, from_port, to_shape, to_port, label, style_json, waypoints_json) - libraries(id, name, shapes_json) · geometry and named ports per shape, as data rather than code - bindings(id, page_id, source_kind, source_ref, mapping_json, last_generated_at) · what regenerates a data-linked diagram - A connector stores shape and port names. Storing coordinates is what makes edges detach the first time something moves. ### Phase 1 · Shape libraries as data Build: A library format describing each shape's geometry, its named connection points and its default style. Load several libraries. Render from the definition, so adding a shape is a data change rather than a code change. Trap: Hard coding shapes as drawing functions means every new shape is a code change and a release, which is why diagram tools with rich libraries build them as data. Define the format first, including how a port's position is expressed relative to the shape's size, so a resized shape keeps its ports in the right places. Done when: Adding a shape to a library file makes it available without a code change. Resizing a shape keeps its ports proportionally positioned. A library with an unknown property loads and ignores it rather than failing. Two libraries can define shapes with the same name without collision. Do not build yet: No connectors, layers or data binding. ### Phase 2 · Connectors that survive editing Build: Connectors referencing a shape and a named port. Orthogonal routing that leaves a port perpendicular and avoids passing through the shapes it connects. Recompute on any change to either endpoint. Trap: Storing a connector as two points works until the first move, and then every diagram anyone saved is subtly wrong. Store the relationship and derive the geometry, which also makes automatic layout possible later. Done when: Moving a shape reroutes its connectors and none passes through the body of a connected shape. Resizing a shape keeps connectors attached to the same named ports. Deleting a shape deletes its connectors rather than leaving them pointing at nothing. A connector between two shapes on different layers behaves correctly. Do not build yet: No layers or data binding yet. ### Phase 3 · Layers Build: Layers with visibility and locking. Selection ignores hidden and locked layers. Export can include or exclude layers. Moving a shape between layers preserves its connectors. Trap: Letting selection reach a locked layer defeats the point of locking, and it is the most common way a background diagram gets dragged out of place. Filter selection by layer state at the hit test rather than checking after the fact. Done when: A hidden layer's shapes cannot be selected or exported when excluded. A locked layer's shapes cannot be moved, verified by attempting a drag over one. Moving a shape between layers keeps its connectors attached. Layer order affects rendering order. Do not build yet: No data binding or export yet. ### Phase 4 · Diagrams generated from data Build: Import a table and map columns to shape text, style and relationships. Generate shapes and connectors, lay them out with elkjs, and regenerate when the data changes while preserving manual position overrides where you asked for them. Trap: Regenerating from scratch discards every manual adjustment, and a diagram nobody can tweak is one people rebuild by hand elsewhere. Key generated shapes to their data rows, so a regeneration updates existing shapes rather than replacing them, and manual overrides survive. Done when: A spreadsheet of thirty rows generates a laid-out chart with no overlapping shapes. Changing a row's text and regenerating updates that shape without moving the others. A manually repositioned shape keeps its position through a regeneration unless reset. A removed row's shape is removed rather than orphaned. Do not build yet: No export yet. ### Phase 5 · Export that opens elsewhere Build: Export SVG with fonts embedded or outlined and the viewBox set to content bounds, PNG rasterised from the same source at chosen scales, and PDF for print. Verify each in a different application. Trap: An exported SVG referencing a web font renders in a fallback face on any machine that lacks it, which is every machine but yours. Embed the font as data or outline the text, and verify by opening the file in a different application rather than in the browser that made it. Done when: An exported SVG opens in a different application with correct fonts and geometry. A two times PNG has exactly double the dimensions of the one times and is sharper rather than merely larger. Excluded layers are absent from the export. Exporting twice produces identical files. Do not build yet: Templates and sharing come last. ### Phase 6 · Templates, sharing and honest collaboration Build: Save a diagram as a template with placeholder shapes. Share read-only by exporting a self-contained viewer or uploading to storage you own. State plainly that simultaneous editing is not supported. Trap: Naive real-time sync over a socket looks fine with one person and loses work with two, and diagram edits are large and hard to merge. Say editing is single-user rather than shipping something that occasionally destroys a colleague's work. Done when: A template applies and produces a new diagram with placeholders to fill. A shared read-only view renders correctly for someone who has never used the application. The interface states that editing is single-user. A shared diagram is versioned so re-sharing is deliberate. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer cursors and comments, which is what teams pay for and which a naive implementation makes worse than absent. - The large template and shape library catalogues, which are content accumulated over years. - Presentation mode and the enterprise controls around sharing and governance. - Import of proprietary diagram formats, which is reverse engineering with no end. ### README must contain - That shapes and their ports are defined as data, so a library is extended without code - That connectors reference shapes and ports rather than coordinates - That generated diagrams are keyed to data rows so regeneration preserves manual work - How fonts are handled on export and how to verify a file elsewhere - That editing is single-user by design
# Architecture · Lucidchart ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Vite, rendering SVG | vector output, selectable text and hit testing for free at the object counts diagrams actually reach | | Shapes | A library format defining geometry and connection points as data | a shape whose connection points are hard coded cannot be extended by anyone but you | | Layout | elkjs for generated diagrams | layered layout for an organisation chart is solved and hand-rolling it produces crossing edges forever | | Data binding | CSV or JSON to shapes through a mapping you define | the point of a data-linked diagram is that it regenerates when the data changes | | Storage | IndexedDB with file open and save | no account, and the file is the thing you own | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Libraries | Shape geometry and ports as data. | The format is the extension point. Hard coding shapes is the thing it exists to prevent. | | Connectors | Relationships and routing. | Routing strategies are replaceable; storing relationships rather than coordinates is not. | | Layers | Visibility, locking and ordering. | Filtering at hit test is what makes locking meaningful. | | Binding | Data to diagram, and regeneration. | Keying shapes to rows is what preserves manual work across regeneration. | | Export | Producing files that open elsewhere. | Any renderer, provided fonts are handled and output is deterministic. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `VITE_LIBRARY_DIR` · required · Where shape library files are served from. Adding a shape is a file here. - `VITE_FONT_DIR` · required · Self-hosted fonts with their licence files beside them. - `VITE_LAYOUT_DIRECTION` · required · Default direction for generated hierarchies. - `VITE_SHARE_ENDPOINT` · optional · Product phase only. Leave unset to keep everything local. ## 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 · Lucidchart product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with Vite, rendering SVG, A library format defining geometry and connection points as data, elkjs for generated diagrams, CSV or JSON to shapes through a mapping you define, IndexedDB with file open and save. - 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 - Hard coding shapes as drawing functions means every new shape is a code change and a release, which is why diagram tools with rich libraries build them as data. Define the format first, including how a port's position is expressed relative to the shape's size, so a resized shape keeps its ports in the right places. - Storing a connector as two points works until the first move, and then every diagram anyone saved is subtly wrong. Store the relationship and derive the geometry, which also makes automatic layout possible later. - Letting selection reach a locked layer defeats the point of locking, and it is the most common way a background diagram gets dragged out of place. Filter selection by layer state at the hit test rather than checking after the fact. - Regenerating from scratch discards every manual adjustment, and a diagram nobody can tweak is one people rebuild by hand elsewhere. Key generated shapes to their data rows, so a regeneration updates existing shapes rather than replacing them, and manual overrides survive. - An exported SVG referencing a web font renders in a fallback face on any machine that lacks it, which is every machine but yours. Embed the font as data or outline the text, and verify by opening the file in a different application rather than in the browser that made it. - Naive real-time sync over a socket looks fine with one person and loses work with two, and diagram edits are large and hard to merge. Say editing is single-user rather than shipping something that occasionally destroys a colleague's work.
# Delivery milestones · Lucidchart Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Shape libraries as data A shape is a definition anyone can add to, not code you have to change. ### Steps 1. Define the library format Geometry, named ports with relative positions, default style. Written down before implementation. Files: `docs/library-format.md` 2. Render from definitions One renderer reading the data. No shape-specific code paths. Files: `src/shapes.ts` 3. Namespace libraries So two libraries can define a shape with the same name. Files: `src/libraries.ts` ### Done when - [ ] A new shape needs only a library file change - [ ] Ports stay proportional through a resize - [ ] An unknown property is ignored rather than fatal - [ ] Two libraries can share a shape name ### Watch out - Hard coding shapes as drawing functions means every new shape is a code change and a release, which is why diagram tools with rich libraries build them as data. Define the format first, including how a port's position is expressed relative to the shape's size, so a resized shape keeps its ports in the right places. ## M2 · Connectors that survive editing Moving or resizing a shape keeps its connectors attached and sensibly routed. ### Steps 1. Reference shape and port, never points The relationship is the data. Geometry is derived at render time. Files: `src/connectors.ts` 2. Route orthogonally around shapes Perpendicular exit, minimal turns, avoiding the connected shapes' bodies. Files: `src/route.ts` 3. Cascade deletion Deleting a shape removes its connectors. Test for orphans explicitly. Files: `src/doc.ts` ### Done when - [ ] Moving a shape reroutes its connectors correctly - [ ] No connector passes through a connected shape - [ ] Deleting a shape leaves no orphan connectors - [ ] Cross-layer connectors behave correctly ### Watch out - Storing a connector as two points works until the first move, and then every diagram anyone saved is subtly wrong. Store the relationship and derive the geometry, which also makes automatic layout possible later. ## M3 · Layers A complex diagram can be worked on one layer at a time. ### Steps 1. Add layers with visibility and locking Ordered, affecting render order as well as interaction. Files: `src/layers.ts` 2. Filter selection at hit test Hidden and locked layers are excluded before anything is selected, not after. Files: `src/select.ts` 3. Preserve connectors across layer moves The connector references shapes, so this should be free; test it anyway. Files: `src/layers.ts` ### Done when - [ ] Hidden layer shapes cannot be selected - [ ] Locked layer shapes cannot be dragged - [ ] Moving a shape between layers keeps connectors - [ ] Layer order affects rendering ### Watch out - Letting selection reach a locked layer defeats the point of locking, and it is the most common way a background diagram gets dragged out of place. Filter selection by layer state at the hit test rather than checking after the fact. ## M4 · Diagrams generated from data An organisation chart regenerates from a spreadsheet instead of being redrawn. ### Steps 1. Map data to shapes Columns to text, style and parent relationships, defined once as a mapping. Files: `src/bind.ts` 2. Key shapes to rows So regeneration is an update rather than a rebuild, which is what preserves manual work. Files: `src/bind.ts` 3. Lay out with elkjs Layered layout for hierarchies, with an overlap assertion after. Files: `src/layout.ts` ```sh npm install elkjs ``` ### Done when - [ ] Thirty rows generate a chart with no overlaps - [ ] Regeneration updates rather than replaces shapes - [ ] Manual positions survive regeneration - [ ] A removed row removes its shape ### Watch out - Regenerating from scratch discards every manual adjustment, and a diagram nobody can tweak is one people rebuild by hand elsewhere. Key generated shapes to their data rows, so a regeneration updates existing shapes rather than replacing them, and manual overrides survive. ## M5 · Export that opens elsewhere An exported diagram is usable in other tools, not just yours. ### Steps 1. Export SVG with fonts handled Embedded or outlined, viewBox set to content bounds. Files: `src/export-svg.ts` 2. Rasterise from geometry per scale Never upscale a bitmap. Compare against an upscaled version to confirm. Files: `src/export-png.ts` 3. Verify in another application Inkscape or a design tool. The browser that made the file is not a test. ### Done when - [ ] An exported SVG opens correctly elsewhere - [ ] A two times PNG is sharper, not just larger - [ ] Excluded layers are absent from exports - [ ] Two exports are identical ### Watch out - An exported SVG referencing a web font renders in a fallback face on any machine that lacks it, which is every machine but yours. Embed the font as data or outline the text, and verify by opening the file in a different application rather than in the browser that made it. ## M6 · Templates, sharing and honest collaboration (production only) Diagrams can be reused and shared, without pretending to be multiplayer. ### Steps 1. Save templates with placeholders Marked shapes to fill, the rest fixed design. Files: `src/templates.ts` 2. Build a read-only viewer Same rendering, no editing affordances, self-contained. Files: `viewer.html` 3. State the single-user limitation In the interface. Silence reads as a promise you did not make. Files: `README.md` ### Done when - [ ] A template produces a new diagram with placeholders - [ ] The read-only view renders for a first-time visitor - [ ] The single-user limitation is visible in the interface - [ ] Re-sharing is a deliberate action ### Watch out - Naive real-time sync over a socket looks fine with one person and loses work with two, and diagram edits are large and hard to merge. Say editing is single-user rather than shipping something that occasionally destroys a colleague's work.
# Operations · Lucidchart ## Backup Documents live in browser storage, which can be cleared. File save is the backup, and the shape libraries are separate files worth version controlling since they are the reusable asset. ## Restore Open the saved file. If a document needs anything beyond itself and its libraries, something has leaked out of the file. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Nothing server-side. Worth tracking is export determinism: keep fixture diagrams and diff their exported SVG in continuous integration so a rendering change is caught. ## Incident checklist A font update changing metrics reflows text in saved diagrams. Pin font versions and treat a font change as a visual release with its own diff. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A new shape is confirmed to need only a library file change - [ ] Connectors are confirmed to survive move, resize and layer change - [ ] Regeneration is confirmed to preserve manual positions - [ ] An exported SVG opens correctly in a second application - [ ] Fixture diagrams export byte-identically ## Launch constraint Do not market omitted Lucidchart 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. Where shape library files are served from. Adding a shape is a file here. VITE_LIBRARY_DIR=/libraries # Required. Self-hosted fonts with their licence files beside them. VITE_FONT_DIR=/fonts # Required. Default direction for generated hierarchies. VITE_LAYOUT_DIRECTION=DOWN # Optional. Product phase only. Leave unset to keep everything local. VITE_SHARE_ENDPOINT=https://share.example.com
$ choose a build depth, inspect the files, then open the complete pack in your agent
Lucidchart: The shapes are easy; customers pay for low-friction editing, reliable exports, collaboration, and a canvas that never fights them.
xpresentation and enterprise controls
xmultiplayer cursors and comments
xlarge template libraries
xhigh-fidelity import and export
Don't feel like building it? These folks already made it free.
all 3 free alternatives to Lucidchart →· no votes, no pay-to-list · just what's real
Lucidchart pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0 | $0 | 3 editable documents; 60 shapes/document; 100 basic templates |
| individual | $9/user | $7.95/user | Unlimited editable documents and shapes; 1 GB storage; 1,000+ templates |
| team | — | $9/user | Unlimited documents/shapes plus team controls; starts at $9/user/month billed annually |
| enterprise | custom | — | Enterprise security, integrations and administration |
free tier3 editable documents; 60 shapes/document; 100 basic templates
billingmonthly + annual; subscriptions auto-renew
hidden costsTaxes are added. Cancellation normally stops the next renewal rather than refunding the current term. Lucidchart, Lucidspark and other Lucid products can be separate subscriptions/add-ons and may each need cancellation; enterprise Accelerators are extra.
verified 2026-08-12 · source ↗
Vibecode Lucidchart
Kinda. The core of Lucidchart is buildable in a weekend with the prompt on this page, but there are real gaps: presentation and enterprise controls, multiplayer cursors and comments. Read the honest list above before committing.
How much does Lucidchart cost?
Lucidchart costs about $9/month (Individual, checked 2026-07-31), which is $108 per year.
What do I lose by replacing Lucidchart?
Honestly: presentation and enterprise controls; multiplayer cursors and comments; large template libraries; high-fidelity import and export. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Lucidchart?
Yes: Excalidraw (Faster and friendlier for collaborative sketches and system diagrams; deliberately worse at formal templates and precision drafting.) diagrams.net (The obvious answer because it is the right one: broad diagramming, local ownership, imports, templates, and real-time collaboration through Google Drive or OneDrive.) CryptPad (Encrypted collaborative documents, whiteboards, diagrams, slides, and Kanban boards; less polished than Lucidchart, considerably less interested in your data.) All 3 curated free alternatives are at vibecodeit.com/lucidchart/alternatives. The prompt is for when you want it exactly your way.