Vibecode Canva
track this build5 phases, 15 steps, beginner friendly0%A prompt-to-template design generator is buildable, but Canva's value is templates, stock assets, collaboration, brand kits, print/export workflows, and distribution.
You are building a lean indie version of Canva. 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 ===== # Canva · indie build A browser editor for social graphics: fixed-size artboards, text with real typography, images with focal-point cropping, your own templates as data, a brand kit of colours and fonts, and PNG and PDF export at exactly the pixel dimensions each destination expects. 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 | Vite with TypeScript, no framework | one stateful canvas surface, where a component tree adds ceremony without buying anything | | Rendering | DOM and CSS for the editor, with one layout engine shared by editor and export | the only reliable way to make what you export match what you saw | | Export | satori to turn the layout into SVG, then resvg to rasterise | deterministic server-quality rendering that does not depend on the browser's own screenshot behaviour | | Fonts | Self-hosted font files with the licence recorded | embedding a font in an exported file is redistribution, and not every licence allows it | | Storage | IndexedDB for documents, with JSON import and export | no account, and the template format is a file you can share | ## 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 and the export pipeline. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Fonts you are licensed to embed** · free with open licences; commercial families vary widely - Why: Export embeds or outlines fonts, which is redistribution. A font licensed for web display is not automatically licensed for that. - Get it: Use openly licensed families such as Inter, Source Sans or Public Sans, and keep each licence file alongside the font. If you must use a commercial face, read its embedding clause before Phase 4. - Verify: Each bundled font has its licence file present and permits embedding - [ ] **Test images including a portrait and a very large photo** · free - Why: Phase 3's cropping and downscaling behaviour cannot be judged without them, and these are exactly the cases that produce bad output. - Get it: Any photo library. You need one tall portrait with a face near the top and one photograph over ten megapixels. - Verify: Both are saved as fixtures - [ ] **The exact pixel sizes you actually need** · free - Why: Export presets are only useful if they match real destinations, and platform requirements change. - Get it: Check the current specification for each destination you publish to, and write the numbers down with the date you checked. - Verify: Each preset's dimensions are recorded with a source and a date ## Quick start ```sh npm create vite@latest . -- --template vanilla-ts ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The template library, which is the product. - Stock photo, video and audio libraries. - Generative image tools and background removal. - Real-time collaboration and team brand governance. - template library - stock/licensed assets - Magic Studio depth - collaboration - brand kits - print/social integrations - mobile apps If one of those is essential to you, that is the reason to keep paying for Canva, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Canva 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 fixed-artboard graphics editor with your own templates, brand kit and exact export presets to replace Canva. 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: Vite with TypeScript, no framework · one stateful canvas surface, where a component tree adds ceremony without buying anything - Rendering: DOM and CSS for the editor, with one layout engine shared by editor and export · the only reliable way to make what you export match what you saw - Export: satori to turn the layout into SVG, then resvg to rasterise · deterministic server-quality rendering that does not depend on the browser's own screenshot behaviour - Fonts: Self-hosted font files with the licence recorded · embedding a font in an exported file is redistribution, and not every licence allows it - Storage: IndexedDB for documents, with JSON import and export · no account, and the template format is a file you can share ### Data model (create this before Phase 1) - artboards(id, doc_id, name, width, height, background) · width and height are pixels, fixed, chosen from a preset - layers(id, artboard_id, kind, z, x, y, w, h, rotation, opacity) · kind is text, image, shape or group - text_props(layer_id, content, font_family, size, weight, line_height, letter_spacing, align, color) - image_props(layer_id, asset_id, fit, focal_x, focal_y) · focal point drives cropping when the frame changes shape - brand(id, name, palette_json, fonts_json, logo_asset_id) - templates(id, name, thumbnail, document_json) · a template is a document with placeholder slots, nothing more ### Phase 1 · Artboard, layers and one layout engine Build: A fixed-size artboard rendered at a display scale that never affects stored geometry. Add, move, resize and reorder rectangle and text layers. Write the layout as one function from document to positioned boxes, and use it for the editor from day one, because Phase 4 will use the same function for export. Trap: Storing positions in screen pixels rather than artboard pixels means every export at a different scale is subtly wrong. Store artboard coordinates only, and treat the display scale as a view concern that never reaches the document. Done when: A layer placed at a known artboard coordinate reports that same coordinate after the editor is zoomed and the window resized. Reordering changes the visual stacking. Reloading the page restores the document exactly. Do not build yet: No typography, no images, no export. ### Phase 2 · Typography that behaves Build: Real typographic controls: family, size, weight, line height, letter spacing and alignment. Wrap text within the layer's box. Add auto-fit that shrinks the size until the text fits, and make wrapping a property of the layout function rather than of CSS. Trap: Letting the browser wrap the text in the editor and letting the exporter wrap it separately guarantees they disagree, usually by one word on the last line, and always on the graphic you already published. Wrap once, in the shared layout function, and have the editor render the result rather than compute its own. Done when: A paragraph wraps to identical line breaks in the editor and in the exported image, checked on a string that breaks awkwardly. Auto-fit reduces the size until the text fits its box and stops. Changing the font family changes the wrapping in both places identically. Do not build yet: No images and no templates yet. ### Phase 3 · Images, cropping and a brand kit Build: Import images, store them as assets, and place them in frames with a fit mode and a focal point so a differently shaped frame crops sensibly rather than centring blindly. Add a brand kit of palette and fonts, applied by reference so changing a brand colour updates every layer that uses it. Trap: Centre-cropping a portrait into a wide frame decapitates people with great reliability. Store a focal point per image, default it to the centre, and let the user move it. This is a small feature that prevents most of the bad outputs. Done when: Replacing a landscape image with a portrait one in the same frame keeps the focal point visible rather than cropping it out. Changing a brand palette colour updates every layer referencing it without touching layers that use a literal colour. An image larger than a size limit is downscaled on import rather than stored at full size. Do not build yet: No export and no templates yet. ### Phase 4 · Export at exact dimensions Build: Render the layout to SVG with satori, rasterise with resvg, and offer named presets with exact pixel dimensions. Embed or outline fonts. Add PDF export for print sizes with the resolution stated rather than assumed. Trap: Exporting by screenshotting the editor DOM inherits the browser's font rendering, device pixel ratio and any scrollbar, and produces a different file on a different machine. Render from the document through the shared layout function instead, which is why the layout function exists. Done when: Each preset produces exactly the pixel dimensions it names. The exported image matches the editor rendering, compared side by side on a design with wrapped text and a cropped image. Exporting the same document twice produces byte-identical files. Do not build yet: No templates or sharing yet. ### Phase 5 · Templates as data Build: Save any document as a template with named placeholder slots. Applying a template creates a document and prompts only for the slots. Templates export and import as files so they can be shared without a server. Trap: A template that hard-codes its own text and image assets is a copy, not a template, and updating the original never propagates. Mark slots explicitly and keep the rest as the design, so applying a template is filling in blanks rather than editing someone else's document. Done when: Applying a template and filling three slots produces a finished graphic without touching the canvas. Editing the template afterwards does not alter documents already created from it. A template exported to a file applies correctly after being imported into a fresh browser profile. Do not build yet: Sharing and hosting are the product phase. ### Phase 6 · Assets, licensing and sharing Build: If you add a shared asset library or publish templates, record the licence of every font, image and icon, and surface it in the interface. Add authentication before anything is hosted. Trap: Stock imagery and fonts are the substantial part of what the subscription buys, and their licences are specific about redistribution. A template shipped with a licensed photo inside it is redistribution even when your application is free. Done when: Every bundled asset has a recorded licence and source. The interface shows the licence for any asset before it is used in a document that will be shared. Hosted sharing requires authentication. Do not build yet: Nothing after this. ### Out of scope (and why) - The template library. Tens of thousands of professionally designed templates is the product, and it is content rather than code. - The stock photo, video and audio library, which is a licensing operation with a continuing bill. - The generative image and background removal features, which are model access and infrastructure. - Real-time collaborative editing and team brand governance. ### README must contain - That the editor and the exporter share one layout function, and why that is the central design decision - That geometry is stored in artboard pixels and the display scale never reaches the document - The export presets and their exact pixel dimensions - Every bundled font and asset, with its licence and whether embedding or redistribution is permitted - That templates use explicit slots, and that editing a template does not change existing documents ===== AGENTS.md ===== # Agent instructions · Canva indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Vite with TypeScript, no framework, DOM and CSS for the editor, with one layout engine shared by editor and export, satori to turn the layout into SVG, then resvg to rasterise, Self-hosted font files with the licence recorded, IndexedDB for documents, with JSON import and export. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing positions in screen pixels rather than artboard pixels means every export at a different scale is subtly wrong. Store artboard coordinates only, and treat the display scale as a view concern that never reaches the document. - Letting the browser wrap the text in the editor and letting the exporter wrap it separately guarantees they disagree, usually by one word on the last line, and always on the graphic you already published. Wrap once, in the shared layout function, and have the editor render the result rather than compute its own. - Centre-cropping a portrait into a wide frame decapitates people with great reliability. Store a focal point per image, default it to the centre, and let the user move it. This is a small feature that prevents most of the bad outputs. - Exporting by screenshotting the editor DOM inherits the browser's font rendering, device pixel ratio and any scrollbar, and produces a different file on a different machine. Render from the document through the shared layout function instead, which is why the layout function exists. - A template that hard-codes its own text and image assets is a copy, not a template, and updating the original never propagates. Mark slots explicitly and keep the rest as the design, so applying a template is filling in blanks rather than editing someone else's document. ===== BUILD_PLAN.md ===== # Build plan · Canva A browser editor for social graphics: fixed-size artboards, text with real typography, images with focal-point cropping, your own templates as data, a brand kit of colours and fonts, and PNG and PDF export at exactly the pixel dimensions each destination expects. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Artboard, layers and one layout engine You can place and move things on a fixed-size artboard, and the geometry is exact. ### Steps 1. Set up the artboard and view transform One transform from artboard coordinates to screen. Everything else works in artboard space. Files: `src/artboard.ts` ```sh npm create vite@latest . -- --template vanilla-ts ``` 2. Implement the document and layer model Plain serialisable objects. Selection and hover are view state and never stored. Files: `src/document.ts` 3. Write the shared layout function Document in, positioned boxes out, pure and testable. Both the editor and the exporter call it. Files: `src/layout.ts` ### Done when - [ ] A layer's stored coordinates do not change when the view is zoomed or resized - [ ] Reordering changes visual stacking - [ ] The document round-trips through JSON unchanged - [ ] The layout function is called by the editor rather than duplicated in it ### Watch out - Storing positions in screen pixels rather than artboard pixels means every export at a different scale is subtly wrong. Store artboard coordinates only, and treat the display scale as a view concern that never reaches the document. ## Phase 2 · Typography that behaves Text wraps, fits and renders the same in the editor as it will in the export. ### Steps 1. Measure text with the real font loaded Wait for the font to load before measuring. Measuring against a fallback and rendering with the real face is a classic source of one-word overflow. Files: `src/text-measure.ts` 2. Wrap inside the layout function Break into lines there and hand the editor positioned lines to draw. The editor never calls the browser's own wrapping. Files: `src/layout.ts` 3. Add auto-fit Binary search the font size until the wrapped height fits the box, with a floor so text never becomes unreadable. Files: `src/autofit.ts` ### Done when - [ ] An awkward string wraps to the same lines in editor and export - [ ] Auto-fit stops at a readable minimum rather than shrinking indefinitely - [ ] Changing the font changes wrapping identically in both paths - [ ] Measurement waits for the real font to load ### Watch out - Letting the browser wrap the text in the editor and letting the exporter wrap it separately guarantees they disagree, usually by one word on the last line, and always on the graphic you already published. Wrap once, in the shared layout function, and have the editor render the result rather than compute its own. ## Phase 3 · Images, cropping and a brand kit Swapping an image or a brand does not break a design. ### Steps 1. Store assets with a size limit Downscale on import to a sensible maximum. A twelve megapixel photo in a social graphic is wasted bytes and a slow editor. Files: `src/assets.ts` 2. Implement focal point cropping Fit modes of cover and contain, with a draggable focal point used by cover. Default centre, and make it visible. Files: `src/image.ts` 3. Add the brand kit with references A colour is either a literal or a reference into the brand palette. References are what make rebranding one action. Files: `src/brand.ts` ### Done when - [ ] A portrait image in a wide frame keeps its focal point visible - [ ] Changing a brand colour updates referencing layers only - [ ] Oversized images are downscaled on import - [ ] A document using no brand references is unaffected by brand changes ### Watch out - Centre-cropping a portrait into a wide frame decapitates people with great reliability. Store a focal point per image, default it to the centre, and let the user move it. This is a small feature that prevents most of the bad outputs. ## Phase 4 · Export at exact dimensions The exported file is exactly the pixel size the destination requires, and it matches the screen. ### Steps 1. Render through satori and resvg Feed the layout result to satori as element structure, then rasterise. Both run identically wherever they run, which is the property you are buying. Files: `src/export.ts` ```sh npm install satori @resvg/resvg-js ``` 2. Add presets with asserted dimensions Named sizes with a test that asserts the output dimensions rather than trusting the arithmetic. Files: `src/presets.ts`, `src/export.test.ts` 3. Handle fonts explicitly Load the font files into the renderer. Record each font's licence and whether embedding is permitted. Files: `docs/fonts.md` ### Done when - [ ] Every preset produces exactly the dimensions it names - [ ] The export matches the editor on wrapped text and a cropped image - [ ] Exporting twice produces byte-identical files - [ ] Every bundled font's licence is recorded ### Watch out - Exporting by screenshotting the editor DOM inherits the browser's font rendering, device pixel ratio and any scrollbar, and produces a different file on a different machine. Render from the document through the shared layout function instead, which is why the layout function exists. ## Phase 5 · Templates as data A design becomes a reusable template, and filling it takes seconds. ### Steps 1. Mark slots explicitly A layer can be declared a slot with a name and a kind. Everything else is fixed design. Files: `src/template.ts` 2. Build the fill flow Applying a template shows only its slots. This is the difference between a template and a starting point. Files: `src/template.ts` 3. Export and import templates as files A template plus its assets in one file. No server, and shareable by any means you already have. Files: `src/template-io.ts` ### Done when - [ ] Filling slots produces a finished graphic without canvas editing - [ ] Editing a template does not change documents already made from it - [ ] A template file imports into a fresh browser profile and applies correctly - [ ] A template with no slots still applies as a starting document ### Watch out - A template that hard-codes its own text and image assets is a copy, not a template, and updating the original never propagates. Mark slots explicitly and keep the rest as the design, so applying a template is filling in blanks rather than editing someone else's document. ## Not in this build - The template library, which is the product. - Stock photo, video and audio libraries. - Generative image tools and background removal. - Real-time collaboration and team brand governance. ## After v1, if you want it - A batch mode that fills one template from a spreadsheet row per graphic - Artboard variants so one design exports at several sizes at once - A contrast check against the brand palette before export ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Longest edge kept on import. Larger images are downscaled. VITE_MAX_IMAGE_PX=4000 # Required. The smallest font size auto-fit will produce, so text never shrinks into illegibility. VITE_AUTOFIT_MIN_PT=12 # Required. Where self-hosted font files are served from. The licence files live beside them. VITE_FONT_DIR=/fonts # Optional. Product phase only. Leave unset to keep everything local, which is a legitimate final state. VITE_SHARE_ENDPOINT=https://share.example.com
You are building a lean indie version of Canva. 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 ===== # Canva · indie build A browser editor for social graphics: fixed-size artboards, text with real typography, images with focal-point cropping, your own templates as data, a brand kit of colours and fonts, and PNG and PDF export at exactly the pixel dimensions each destination expects. 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 | Vite with TypeScript, no framework | one stateful canvas surface, where a component tree adds ceremony without buying anything | | Rendering | DOM and CSS for the editor, with one layout engine shared by editor and export | the only reliable way to make what you export match what you saw | | Export | satori to turn the layout into SVG, then resvg to rasterise | deterministic server-quality rendering that does not depend on the browser's own screenshot behaviour | | Fonts | Self-hosted font files with the licence recorded | embedding a font in an exported file is redistribution, and not every licence allows it | | Storage | IndexedDB for documents, with JSON import and export | no account, and the template format is a file you can share | ## 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 and the export pipeline. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Fonts you are licensed to embed** · free with open licences; commercial families vary widely - Why: Export embeds or outlines fonts, which is redistribution. A font licensed for web display is not automatically licensed for that. - Get it: Use openly licensed families such as Inter, Source Sans or Public Sans, and keep each licence file alongside the font. If you must use a commercial face, read its embedding clause before Phase 4. - Verify: Each bundled font has its licence file present and permits embedding - [ ] **Test images including a portrait and a very large photo** · free - Why: Phase 3's cropping and downscaling behaviour cannot be judged without them, and these are exactly the cases that produce bad output. - Get it: Any photo library. You need one tall portrait with a face near the top and one photograph over ten megapixels. - Verify: Both are saved as fixtures - [ ] **The exact pixel sizes you actually need** · free - Why: Export presets are only useful if they match real destinations, and platform requirements change. - Get it: Check the current specification for each destination you publish to, and write the numbers down with the date you checked. - Verify: Each preset's dimensions are recorded with a source and a date ## Quick start ```sh npm create vite@latest . -- --template vanilla-ts ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The template library, which is the product. - Stock photo, video and audio libraries. - Generative image tools and background removal. - Real-time collaboration and team brand governance. - template library - stock/licensed assets - Magic Studio depth - collaboration - brand kits - print/social integrations - mobile apps If one of those is essential to you, that is the reason to keep paying for Canva, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Canva 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 fixed-artboard graphics editor with your own templates, brand kit and exact export presets to replace Canva. 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: Vite with TypeScript, no framework · one stateful canvas surface, where a component tree adds ceremony without buying anything - Rendering: DOM and CSS for the editor, with one layout engine shared by editor and export · the only reliable way to make what you export match what you saw - Export: satori to turn the layout into SVG, then resvg to rasterise · deterministic server-quality rendering that does not depend on the browser's own screenshot behaviour - Fonts: Self-hosted font files with the licence recorded · embedding a font in an exported file is redistribution, and not every licence allows it - Storage: IndexedDB for documents, with JSON import and export · no account, and the template format is a file you can share ### Data model (create this before Phase 1) - artboards(id, doc_id, name, width, height, background) · width and height are pixels, fixed, chosen from a preset - layers(id, artboard_id, kind, z, x, y, w, h, rotation, opacity) · kind is text, image, shape or group - text_props(layer_id, content, font_family, size, weight, line_height, letter_spacing, align, color) - image_props(layer_id, asset_id, fit, focal_x, focal_y) · focal point drives cropping when the frame changes shape - brand(id, name, palette_json, fonts_json, logo_asset_id) - templates(id, name, thumbnail, document_json) · a template is a document with placeholder slots, nothing more ### Phase 1 · Artboard, layers and one layout engine Build: A fixed-size artboard rendered at a display scale that never affects stored geometry. Add, move, resize and reorder rectangle and text layers. Write the layout as one function from document to positioned boxes, and use it for the editor from day one, because Phase 4 will use the same function for export. Trap: Storing positions in screen pixels rather than artboard pixels means every export at a different scale is subtly wrong. Store artboard coordinates only, and treat the display scale as a view concern that never reaches the document. Done when: A layer placed at a known artboard coordinate reports that same coordinate after the editor is zoomed and the window resized. Reordering changes the visual stacking. Reloading the page restores the document exactly. Do not build yet: No typography, no images, no export. ### Phase 2 · Typography that behaves Build: Real typographic controls: family, size, weight, line height, letter spacing and alignment. Wrap text within the layer's box. Add auto-fit that shrinks the size until the text fits, and make wrapping a property of the layout function rather than of CSS. Trap: Letting the browser wrap the text in the editor and letting the exporter wrap it separately guarantees they disagree, usually by one word on the last line, and always on the graphic you already published. Wrap once, in the shared layout function, and have the editor render the result rather than compute its own. Done when: A paragraph wraps to identical line breaks in the editor and in the exported image, checked on a string that breaks awkwardly. Auto-fit reduces the size until the text fits its box and stops. Changing the font family changes the wrapping in both places identically. Do not build yet: No images and no templates yet. ### Phase 3 · Images, cropping and a brand kit Build: Import images, store them as assets, and place them in frames with a fit mode and a focal point so a differently shaped frame crops sensibly rather than centring blindly. Add a brand kit of palette and fonts, applied by reference so changing a brand colour updates every layer that uses it. Trap: Centre-cropping a portrait into a wide frame decapitates people with great reliability. Store a focal point per image, default it to the centre, and let the user move it. This is a small feature that prevents most of the bad outputs. Done when: Replacing a landscape image with a portrait one in the same frame keeps the focal point visible rather than cropping it out. Changing a brand palette colour updates every layer referencing it without touching layers that use a literal colour. An image larger than a size limit is downscaled on import rather than stored at full size. Do not build yet: No export and no templates yet. ### Phase 4 · Export at exact dimensions Build: Render the layout to SVG with satori, rasterise with resvg, and offer named presets with exact pixel dimensions. Embed or outline fonts. Add PDF export for print sizes with the resolution stated rather than assumed. Trap: Exporting by screenshotting the editor DOM inherits the browser's font rendering, device pixel ratio and any scrollbar, and produces a different file on a different machine. Render from the document through the shared layout function instead, which is why the layout function exists. Done when: Each preset produces exactly the pixel dimensions it names. The exported image matches the editor rendering, compared side by side on a design with wrapped text and a cropped image. Exporting the same document twice produces byte-identical files. Do not build yet: No templates or sharing yet. ### Phase 5 · Templates as data Build: Save any document as a template with named placeholder slots. Applying a template creates a document and prompts only for the slots. Templates export and import as files so they can be shared without a server. Trap: A template that hard-codes its own text and image assets is a copy, not a template, and updating the original never propagates. Mark slots explicitly and keep the rest as the design, so applying a template is filling in blanks rather than editing someone else's document. Done when: Applying a template and filling three slots produces a finished graphic without touching the canvas. Editing the template afterwards does not alter documents already created from it. A template exported to a file applies correctly after being imported into a fresh browser profile. Do not build yet: Sharing and hosting are the product phase. ### Phase 6 · Assets, licensing and sharing Build: If you add a shared asset library or publish templates, record the licence of every font, image and icon, and surface it in the interface. Add authentication before anything is hosted. Trap: Stock imagery and fonts are the substantial part of what the subscription buys, and their licences are specific about redistribution. A template shipped with a licensed photo inside it is redistribution even when your application is free. Done when: Every bundled asset has a recorded licence and source. The interface shows the licence for any asset before it is used in a document that will be shared. Hosted sharing requires authentication. Do not build yet: Nothing after this. ### Out of scope (and why) - The template library. Tens of thousands of professionally designed templates is the product, and it is content rather than code. - The stock photo, video and audio library, which is a licensing operation with a continuing bill. - The generative image and background removal features, which are model access and infrastructure. - Real-time collaborative editing and team brand governance. ### README must contain - That the editor and the exporter share one layout function, and why that is the central design decision - That geometry is stored in artboard pixels and the display scale never reaches the document - The export presets and their exact pixel dimensions - Every bundled font and asset, with its licence and whether embedding or redistribution is permitted - That templates use explicit slots, and that editing a template does not change existing documents ===== AGENTS.md ===== # Agent instructions · Canva indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Vite with TypeScript, no framework, DOM and CSS for the editor, with one layout engine shared by editor and export, satori to turn the layout into SVG, then resvg to rasterise, Self-hosted font files with the licence recorded, IndexedDB for documents, with JSON import and export. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing positions in screen pixels rather than artboard pixels means every export at a different scale is subtly wrong. Store artboard coordinates only, and treat the display scale as a view concern that never reaches the document. - Letting the browser wrap the text in the editor and letting the exporter wrap it separately guarantees they disagree, usually by one word on the last line, and always on the graphic you already published. Wrap once, in the shared layout function, and have the editor render the result rather than compute its own. - Centre-cropping a portrait into a wide frame decapitates people with great reliability. Store a focal point per image, default it to the centre, and let the user move it. This is a small feature that prevents most of the bad outputs. - Exporting by screenshotting the editor DOM inherits the browser's font rendering, device pixel ratio and any scrollbar, and produces a different file on a different machine. Render from the document through the shared layout function instead, which is why the layout function exists. - A template that hard-codes its own text and image assets is a copy, not a template, and updating the original never propagates. Mark slots explicitly and keep the rest as the design, so applying a template is filling in blanks rather than editing someone else's document. ===== BUILD_PLAN.md ===== # Build plan · Canva A browser editor for social graphics: fixed-size artboards, text with real typography, images with focal-point cropping, your own templates as data, a brand kit of colours and fonts, and PNG and PDF export at exactly the pixel dimensions each destination expects. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Artboard, layers and one layout engine You can place and move things on a fixed-size artboard, and the geometry is exact. ### Steps 1. Set up the artboard and view transform One transform from artboard coordinates to screen. Everything else works in artboard space. Files: `src/artboard.ts` ```sh npm create vite@latest . -- --template vanilla-ts ``` 2. Implement the document and layer model Plain serialisable objects. Selection and hover are view state and never stored. Files: `src/document.ts` 3. Write the shared layout function Document in, positioned boxes out, pure and testable. Both the editor and the exporter call it. Files: `src/layout.ts` ### Done when - [ ] A layer's stored coordinates do not change when the view is zoomed or resized - [ ] Reordering changes visual stacking - [ ] The document round-trips through JSON unchanged - [ ] The layout function is called by the editor rather than duplicated in it ### Watch out - Storing positions in screen pixels rather than artboard pixels means every export at a different scale is subtly wrong. Store artboard coordinates only, and treat the display scale as a view concern that never reaches the document. ## Phase 2 · Typography that behaves Text wraps, fits and renders the same in the editor as it will in the export. ### Steps 1. Measure text with the real font loaded Wait for the font to load before measuring. Measuring against a fallback and rendering with the real face is a classic source of one-word overflow. Files: `src/text-measure.ts` 2. Wrap inside the layout function Break into lines there and hand the editor positioned lines to draw. The editor never calls the browser's own wrapping. Files: `src/layout.ts` 3. Add auto-fit Binary search the font size until the wrapped height fits the box, with a floor so text never becomes unreadable. Files: `src/autofit.ts` ### Done when - [ ] An awkward string wraps to the same lines in editor and export - [ ] Auto-fit stops at a readable minimum rather than shrinking indefinitely - [ ] Changing the font changes wrapping identically in both paths - [ ] Measurement waits for the real font to load ### Watch out - Letting the browser wrap the text in the editor and letting the exporter wrap it separately guarantees they disagree, usually by one word on the last line, and always on the graphic you already published. Wrap once, in the shared layout function, and have the editor render the result rather than compute its own. ## Phase 3 · Images, cropping and a brand kit Swapping an image or a brand does not break a design. ### Steps 1. Store assets with a size limit Downscale on import to a sensible maximum. A twelve megapixel photo in a social graphic is wasted bytes and a slow editor. Files: `src/assets.ts` 2. Implement focal point cropping Fit modes of cover and contain, with a draggable focal point used by cover. Default centre, and make it visible. Files: `src/image.ts` 3. Add the brand kit with references A colour is either a literal or a reference into the brand palette. References are what make rebranding one action. Files: `src/brand.ts` ### Done when - [ ] A portrait image in a wide frame keeps its focal point visible - [ ] Changing a brand colour updates referencing layers only - [ ] Oversized images are downscaled on import - [ ] A document using no brand references is unaffected by brand changes ### Watch out - Centre-cropping a portrait into a wide frame decapitates people with great reliability. Store a focal point per image, default it to the centre, and let the user move it. This is a small feature that prevents most of the bad outputs. ## Phase 4 · Export at exact dimensions The exported file is exactly the pixel size the destination requires, and it matches the screen. ### Steps 1. Render through satori and resvg Feed the layout result to satori as element structure, then rasterise. Both run identically wherever they run, which is the property you are buying. Files: `src/export.ts` ```sh npm install satori @resvg/resvg-js ``` 2. Add presets with asserted dimensions Named sizes with a test that asserts the output dimensions rather than trusting the arithmetic. Files: `src/presets.ts`, `src/export.test.ts` 3. Handle fonts explicitly Load the font files into the renderer. Record each font's licence and whether embedding is permitted. Files: `docs/fonts.md` ### Done when - [ ] Every preset produces exactly the dimensions it names - [ ] The export matches the editor on wrapped text and a cropped image - [ ] Exporting twice produces byte-identical files - [ ] Every bundled font's licence is recorded ### Watch out - Exporting by screenshotting the editor DOM inherits the browser's font rendering, device pixel ratio and any scrollbar, and produces a different file on a different machine. Render from the document through the shared layout function instead, which is why the layout function exists. ## Phase 5 · Templates as data A design becomes a reusable template, and filling it takes seconds. ### Steps 1. Mark slots explicitly A layer can be declared a slot with a name and a kind. Everything else is fixed design. Files: `src/template.ts` 2. Build the fill flow Applying a template shows only its slots. This is the difference between a template and a starting point. Files: `src/template.ts` 3. Export and import templates as files A template plus its assets in one file. No server, and shareable by any means you already have. Files: `src/template-io.ts` ### Done when - [ ] Filling slots produces a finished graphic without canvas editing - [ ] Editing a template does not change documents already made from it - [ ] A template file imports into a fresh browser profile and applies correctly - [ ] A template with no slots still applies as a starting document ### Watch out - A template that hard-codes its own text and image assets is a copy, not a template, and updating the original never propagates. Mark slots explicitly and keep the rest as the design, so applying a template is filling in blanks rather than editing someone else's document. ## Not in this build - The template library, which is the product. - Stock photo, video and audio libraries. - Generative image tools and background removal. - Real-time collaboration and team brand governance. ## After v1, if you want it - A batch mode that fills one template from a spreadsheet row per graphic - Artboard variants so one design exports at several sizes at once - A contrast check against the brand palette before export ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Longest edge kept on import. Larger images are downscaled. VITE_MAX_IMAGE_PX=4000 # Required. The smallest font size auto-fit will produce, so text never shrinks into illegibility. VITE_AUTOFIT_MIN_PT=12 # Required. Where self-hosted font files are served from. The licence files live beside them. VITE_FONT_DIR=/fonts # Optional. Product phase only. Leave unset to keep everything local, which is a legitimate final state. VITE_SHARE_ENDPOINT=https://share.example.com
You are building a production product version of Canva. 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 ===== # Canva · product brief ## Problem A prompt-to-template design generator is buildable, but Canva's value is templates, stock assets, collaboration, brand kits, print/export workflows, and distribution. ## Product outcome An editor whose exports are exactly the size they claim and identical to what was on screen, with templates that fill rather than copy, a brand kit applied by reference, and a licence recorded for every asset it can distribute. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - canvas editor library - image generation/editing API - template assets - file export pipeline - hosted app ## Explicit non-goals for v1 - The template library, which is the product. - Stock photo, video and audio libraries. - Generative image tools and background removal. - Real-time collaboration and team brand governance. - template library - stock/licensed assets - Magic Studio depth - collaboration - brand kits - print/social integrations - mobile apps ## Success criteria - Fixture documents export byte-identically to their stored references - Every preset's dimensions are asserted by test - Editor and export wrapping agree on the awkward string fixture - Every bundled font and asset licence is recorded and permits the use - A template file round-trips into a fresh browser profile ===== BRIEF.md ===== # Build brief · Canva 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 fixed-artboard graphics editor with your own templates, brand kit and exact export presets to replace Canva. 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: Vite with TypeScript, no framework · one stateful canvas surface, where a component tree adds ceremony without buying anything - Rendering: DOM and CSS for the editor, with one layout engine shared by editor and export · the only reliable way to make what you export match what you saw - Export: satori to turn the layout into SVG, then resvg to rasterise · deterministic server-quality rendering that does not depend on the browser's own screenshot behaviour - Fonts: Self-hosted font files with the licence recorded · embedding a font in an exported file is redistribution, and not every licence allows it - Storage: IndexedDB for documents, with JSON import and export · no account, and the template format is a file you can share ### Data model (create this before Phase 1) - artboards(id, doc_id, name, width, height, background) · width and height are pixels, fixed, chosen from a preset - layers(id, artboard_id, kind, z, x, y, w, h, rotation, opacity) · kind is text, image, shape or group - text_props(layer_id, content, font_family, size, weight, line_height, letter_spacing, align, color) - image_props(layer_id, asset_id, fit, focal_x, focal_y) · focal point drives cropping when the frame changes shape - brand(id, name, palette_json, fonts_json, logo_asset_id) - templates(id, name, thumbnail, document_json) · a template is a document with placeholder slots, nothing more ### Phase 1 · Artboard, layers and one layout engine Build: A fixed-size artboard rendered at a display scale that never affects stored geometry. Add, move, resize and reorder rectangle and text layers. Write the layout as one function from document to positioned boxes, and use it for the editor from day one, because Phase 4 will use the same function for export. Trap: Storing positions in screen pixels rather than artboard pixels means every export at a different scale is subtly wrong. Store artboard coordinates only, and treat the display scale as a view concern that never reaches the document. Done when: A layer placed at a known artboard coordinate reports that same coordinate after the editor is zoomed and the window resized. Reordering changes the visual stacking. Reloading the page restores the document exactly. Do not build yet: No typography, no images, no export. ### Phase 2 · Typography that behaves Build: Real typographic controls: family, size, weight, line height, letter spacing and alignment. Wrap text within the layer's box. Add auto-fit that shrinks the size until the text fits, and make wrapping a property of the layout function rather than of CSS. Trap: Letting the browser wrap the text in the editor and letting the exporter wrap it separately guarantees they disagree, usually by one word on the last line, and always on the graphic you already published. Wrap once, in the shared layout function, and have the editor render the result rather than compute its own. Done when: A paragraph wraps to identical line breaks in the editor and in the exported image, checked on a string that breaks awkwardly. Auto-fit reduces the size until the text fits its box and stops. Changing the font family changes the wrapping in both places identically. Do not build yet: No images and no templates yet. ### Phase 3 · Images, cropping and a brand kit Build: Import images, store them as assets, and place them in frames with a fit mode and a focal point so a differently shaped frame crops sensibly rather than centring blindly. Add a brand kit of palette and fonts, applied by reference so changing a brand colour updates every layer that uses it. Trap: Centre-cropping a portrait into a wide frame decapitates people with great reliability. Store a focal point per image, default it to the centre, and let the user move it. This is a small feature that prevents most of the bad outputs. Done when: Replacing a landscape image with a portrait one in the same frame keeps the focal point visible rather than cropping it out. Changing a brand palette colour updates every layer referencing it without touching layers that use a literal colour. An image larger than a size limit is downscaled on import rather than stored at full size. Do not build yet: No export and no templates yet. ### Phase 4 · Export at exact dimensions Build: Render the layout to SVG with satori, rasterise with resvg, and offer named presets with exact pixel dimensions. Embed or outline fonts. Add PDF export for print sizes with the resolution stated rather than assumed. Trap: Exporting by screenshotting the editor DOM inherits the browser's font rendering, device pixel ratio and any scrollbar, and produces a different file on a different machine. Render from the document through the shared layout function instead, which is why the layout function exists. Done when: Each preset produces exactly the pixel dimensions it names. The exported image matches the editor rendering, compared side by side on a design with wrapped text and a cropped image. Exporting the same document twice produces byte-identical files. Do not build yet: No templates or sharing yet. ### Phase 5 · Templates as data Build: Save any document as a template with named placeholder slots. Applying a template creates a document and prompts only for the slots. Templates export and import as files so they can be shared without a server. Trap: A template that hard-codes its own text and image assets is a copy, not a template, and updating the original never propagates. Mark slots explicitly and keep the rest as the design, so applying a template is filling in blanks rather than editing someone else's document. Done when: Applying a template and filling three slots produces a finished graphic without touching the canvas. Editing the template afterwards does not alter documents already created from it. A template exported to a file applies correctly after being imported into a fresh browser profile. Do not build yet: Sharing and hosting are the product phase. ### Phase 6 · Assets, licensing and sharing Build: If you add a shared asset library or publish templates, record the licence of every font, image and icon, and surface it in the interface. Add authentication before anything is hosted. Trap: Stock imagery and fonts are the substantial part of what the subscription buys, and their licences are specific about redistribution. A template shipped with a licensed photo inside it is redistribution even when your application is free. Done when: Every bundled asset has a recorded licence and source. The interface shows the licence for any asset before it is used in a document that will be shared. Hosted sharing requires authentication. Do not build yet: Nothing after this. ### Out of scope (and why) - The template library. Tens of thousands of professionally designed templates is the product, and it is content rather than code. - The stock photo, video and audio library, which is a licensing operation with a continuing bill. - The generative image and background removal features, which are model access and infrastructure. - Real-time collaborative editing and team brand governance. ### README must contain - That the editor and the exporter share one layout function, and why that is the central design decision - That geometry is stored in artboard pixels and the display scale never reaches the document - The export presets and their exact pixel dimensions - Every bundled font and asset, with its licence and whether embedding or redistribution is permitted - That templates use explicit slots, and that editing a template does not change existing documents ===== ARCHITECTURE.md ===== # Architecture · Canva ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Vite with TypeScript, no framework | one stateful canvas surface, where a component tree adds ceremony without buying anything | | Rendering | DOM and CSS for the editor, with one layout engine shared by editor and export | the only reliable way to make what you export match what you saw | | Export | satori to turn the layout into SVG, then resvg to rasterise | deterministic server-quality rendering that does not depend on the browser's own screenshot behaviour | | Fonts | Self-hosted font files with the licence recorded | embedding a font in an exported file is redistribution, and not every licence allows it | | Storage | IndexedDB for documents, with JSON import and export | no account, and the template format is a file you can share | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Document | Artboards, layers and serialisation. | Plain data, so persistence, templates and export all read the same shape. | | Layout | Turning a document into positioned boxes and wrapped lines. | This is the single most important boundary. Both editor and exporter must keep calling it, or they will disagree. | | Render | SVG generation and rasterisation. | satori and resvg are replaceable, but not by a browser screenshot, which reintroduces machine-dependent output. | | Brand | Palette and font references. | References resolve at render, so an alternative brand model is additive. | | Templates | Slots and application. | A template is a document plus slot declarations, so the format travels with the document format. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `VITE_MAX_IMAGE_PX` · required · Longest edge kept on import. Larger images are downscaled. - `VITE_AUTOFIT_MIN_PT` · required · The smallest font size auto-fit will produce, so text never shrinks into illegibility. - `VITE_FONT_DIR` · required · Where self-hosted font files are served from. The licence files live beside them. - `VITE_SHARE_ENDPOINT` · optional · Product phase only. Leave unset to keep everything local, which is a legitimate final state. ## 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 · Canva product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Vite with TypeScript, no framework, DOM and CSS for the editor, with one layout engine shared by editor and export, satori to turn the layout into SVG, then resvg to rasterise, Self-hosted font files with the licence recorded, IndexedDB for documents, with JSON import and export. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Storing positions in screen pixels rather than artboard pixels means every export at a different scale is subtly wrong. Store artboard coordinates only, and treat the display scale as a view concern that never reaches the document. - Letting the browser wrap the text in the editor and letting the exporter wrap it separately guarantees they disagree, usually by one word on the last line, and always on the graphic you already published. Wrap once, in the shared layout function, and have the editor render the result rather than compute its own. - Centre-cropping a portrait into a wide frame decapitates people with great reliability. Store a focal point per image, default it to the centre, and let the user move it. This is a small feature that prevents most of the bad outputs. - Exporting by screenshotting the editor DOM inherits the browser's font rendering, device pixel ratio and any scrollbar, and produces a different file on a different machine. Render from the document through the shared layout function instead, which is why the layout function exists. - A template that hard-codes its own text and image assets is a copy, not a template, and updating the original never propagates. Mark slots explicitly and keep the rest as the design, so applying a template is filling in blanks rather than editing someone else's document. - Stock imagery and fonts are the substantial part of what the subscription buys, and their licences are specific about redistribution. A template shipped with a licensed photo inside it is redistribution even when your application is free. ===== MILESTONES.md ===== # Delivery milestones · Canva Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Artboard, layers and one layout engine You can place and move things on a fixed-size artboard, and the geometry is exact. ### Steps 1. Set up the artboard and view transform One transform from artboard coordinates to screen. Everything else works in artboard space. Files: `src/artboard.ts` ```sh npm create vite@latest . -- --template vanilla-ts ``` 2. Implement the document and layer model Plain serialisable objects. Selection and hover are view state and never stored. Files: `src/document.ts` 3. Write the shared layout function Document in, positioned boxes out, pure and testable. Both the editor and the exporter call it. Files: `src/layout.ts` ### Done when - [ ] A layer's stored coordinates do not change when the view is zoomed or resized - [ ] Reordering changes visual stacking - [ ] The document round-trips through JSON unchanged - [ ] The layout function is called by the editor rather than duplicated in it ### Watch out - Storing positions in screen pixels rather than artboard pixels means every export at a different scale is subtly wrong. Store artboard coordinates only, and treat the display scale as a view concern that never reaches the document. ## M2 · Typography that behaves Text wraps, fits and renders the same in the editor as it will in the export. ### Steps 1. Measure text with the real font loaded Wait for the font to load before measuring. Measuring against a fallback and rendering with the real face is a classic source of one-word overflow. Files: `src/text-measure.ts` 2. Wrap inside the layout function Break into lines there and hand the editor positioned lines to draw. The editor never calls the browser's own wrapping. Files: `src/layout.ts` 3. Add auto-fit Binary search the font size until the wrapped height fits the box, with a floor so text never becomes unreadable. Files: `src/autofit.ts` ### Done when - [ ] An awkward string wraps to the same lines in editor and export - [ ] Auto-fit stops at a readable minimum rather than shrinking indefinitely - [ ] Changing the font changes wrapping identically in both paths - [ ] Measurement waits for the real font to load ### Watch out - Letting the browser wrap the text in the editor and letting the exporter wrap it separately guarantees they disagree, usually by one word on the last line, and always on the graphic you already published. Wrap once, in the shared layout function, and have the editor render the result rather than compute its own. ## M3 · Images, cropping and a brand kit Swapping an image or a brand does not break a design. ### Steps 1. Store assets with a size limit Downscale on import to a sensible maximum. A twelve megapixel photo in a social graphic is wasted bytes and a slow editor. Files: `src/assets.ts` 2. Implement focal point cropping Fit modes of cover and contain, with a draggable focal point used by cover. Default centre, and make it visible. Files: `src/image.ts` 3. Add the brand kit with references A colour is either a literal or a reference into the brand palette. References are what make rebranding one action. Files: `src/brand.ts` ### Done when - [ ] A portrait image in a wide frame keeps its focal point visible - [ ] Changing a brand colour updates referencing layers only - [ ] Oversized images are downscaled on import - [ ] A document using no brand references is unaffected by brand changes ### Watch out - Centre-cropping a portrait into a wide frame decapitates people with great reliability. Store a focal point per image, default it to the centre, and let the user move it. This is a small feature that prevents most of the bad outputs. ## M4 · Export at exact dimensions The exported file is exactly the pixel size the destination requires, and it matches the screen. ### Steps 1. Render through satori and resvg Feed the layout result to satori as element structure, then rasterise. Both run identically wherever they run, which is the property you are buying. Files: `src/export.ts` ```sh npm install satori @resvg/resvg-js ``` 2. Add presets with asserted dimensions Named sizes with a test that asserts the output dimensions rather than trusting the arithmetic. Files: `src/presets.ts`, `src/export.test.ts` 3. Handle fonts explicitly Load the font files into the renderer. Record each font's licence and whether embedding is permitted. Files: `docs/fonts.md` ### Done when - [ ] Every preset produces exactly the dimensions it names - [ ] The export matches the editor on wrapped text and a cropped image - [ ] Exporting twice produces byte-identical files - [ ] Every bundled font's licence is recorded ### Watch out - Exporting by screenshotting the editor DOM inherits the browser's font rendering, device pixel ratio and any scrollbar, and produces a different file on a different machine. Render from the document through the shared layout function instead, which is why the layout function exists. ## M5 · Templates as data A design becomes a reusable template, and filling it takes seconds. ### Steps 1. Mark slots explicitly A layer can be declared a slot with a name and a kind. Everything else is fixed design. Files: `src/template.ts` 2. Build the fill flow Applying a template shows only its slots. This is the difference between a template and a starting point. Files: `src/template.ts` 3. Export and import templates as files A template plus its assets in one file. No server, and shareable by any means you already have. Files: `src/template-io.ts` ### Done when - [ ] Filling slots produces a finished graphic without canvas editing - [ ] Editing a template does not change documents already made from it - [ ] A template file imports into a fresh browser profile and applies correctly - [ ] A template with no slots still applies as a starting document ### Watch out - A template that hard-codes its own text and image assets is a copy, not a template, and updating the original never propagates. Mark slots explicitly and keep the rest as the design, so applying a template is filling in blanks rather than editing someone else's document. ## M6 · Assets, licensing and sharing (production only) Anything you distribute is something you are allowed to distribute. ### Steps 1. Record every asset's provenance Source, licence and whether redistribution is permitted, stored with the asset rather than in a separate document that drifts. Files: `src/assets.ts` 2. Surface licences in the interface Shown at the point of use, not buried. This is the only place it changes behaviour. Files: `src/assets.ts` 3. Authenticate anything hosted If you host templates or exports, put a login in front of it. Files: `src/auth.ts` ### Done when - [ ] Every bundled asset has a licence and source recorded - [ ] The licence is visible at the point of use - [ ] Hosted sharing requires authentication - [ ] An asset whose licence forbids redistribution cannot be included in an exported template ### Watch out - Stock imagery and fonts are the substantial part of what the subscription buys, and their licences are specific about redistribution. A template shipped with a licensed photo inside it is redistribution even when your application is free. ===== OPERATIONS.md ===== # Operations · Canva ## Backup Documents live in IndexedDB, which the browser can clear. JSON export is the backup, and templates and assets export with it. Make export prominent rather than buried. ## Restore Import the exported file into a fresh profile. The round-trip check in Phase 5 is what makes this trustworthy. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Nothing server-side. What is worth tracking is export determinism: keep fixture documents and diff their exported bytes in continuous integration, so a font or renderer change that shifts output is caught immediately. ## Incident checklist The realistic incident is a font update changing text metrics and reflowing published designs. Pin font file versions and treat a font change as a release with its own visual 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 - [ ] Fixture documents export byte-identically to their stored references - [ ] Every preset's dimensions are asserted by test - [ ] Editor and export wrapping agree on the awkward string fixture - [ ] Every bundled font and asset licence is recorded and permits the use - [ ] A template file round-trips into a fresh browser profile ## Launch constraint Do not market omitted Canva 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. Longest edge kept on import. Larger images are downscaled. VITE_MAX_IMAGE_PX=4000 # Required. The smallest font size auto-fit will produce, so text never shrinks into illegibility. VITE_AUTOFIT_MIN_PT=12 # Required. Where self-hosted font files are served from. The licence files live beside them. VITE_FONT_DIR=/fonts # Optional. Product phase only. Leave unset to keep everything local, which is a legitimate final state. VITE_SHARE_ENDPOINT=https://share.example.com
# Canva · indie build A browser editor for social graphics: fixed-size artboards, text with real typography, images with focal-point cropping, your own templates as data, a brand kit of colours and fonts, and PNG and PDF export at exactly the pixel dimensions each destination expects. 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 | Vite with TypeScript, no framework | one stateful canvas surface, where a component tree adds ceremony without buying anything | | Rendering | DOM and CSS for the editor, with one layout engine shared by editor and export | the only reliable way to make what you export match what you saw | | Export | satori to turn the layout into SVG, then resvg to rasterise | deterministic server-quality rendering that does not depend on the browser's own screenshot behaviour | | Fonts | Self-hosted font files with the licence recorded | embedding a font in an exported file is redistribution, and not every licence allows it | | Storage | IndexedDB for documents, with JSON import and export | no account, and the template format is a file you can share | ## 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 and the export pipeline. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Fonts you are licensed to embed** · free with open licences; commercial families vary widely - Why: Export embeds or outlines fonts, which is redistribution. A font licensed for web display is not automatically licensed for that. - Get it: Use openly licensed families such as Inter, Source Sans or Public Sans, and keep each licence file alongside the font. If you must use a commercial face, read its embedding clause before Phase 4. - Verify: Each bundled font has its licence file present and permits embedding - [ ] **Test images including a portrait and a very large photo** · free - Why: Phase 3's cropping and downscaling behaviour cannot be judged without them, and these are exactly the cases that produce bad output. - Get it: Any photo library. You need one tall portrait with a face near the top and one photograph over ten megapixels. - Verify: Both are saved as fixtures - [ ] **The exact pixel sizes you actually need** · free - Why: Export presets are only useful if they match real destinations, and platform requirements change. - Get it: Check the current specification for each destination you publish to, and write the numbers down with the date you checked. - Verify: Each preset's dimensions are recorded with a source and a date ## Quick start ```sh npm create vite@latest . -- --template vanilla-ts ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The template library, which is the product. - Stock photo, video and audio libraries. - Generative image tools and background removal. - Real-time collaboration and team brand governance. - template library - stock/licensed assets - Magic Studio depth - collaboration - brand kits - print/social integrations - mobile apps If one of those is essential to you, that is the reason to keep paying for Canva, and the README should say so rather than pretend.
# Build brief · Canva 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 fixed-artboard graphics editor with your own templates, brand kit and exact export presets to replace Canva. 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: Vite with TypeScript, no framework · one stateful canvas surface, where a component tree adds ceremony without buying anything - Rendering: DOM and CSS for the editor, with one layout engine shared by editor and export · the only reliable way to make what you export match what you saw - Export: satori to turn the layout into SVG, then resvg to rasterise · deterministic server-quality rendering that does not depend on the browser's own screenshot behaviour - Fonts: Self-hosted font files with the licence recorded · embedding a font in an exported file is redistribution, and not every licence allows it - Storage: IndexedDB for documents, with JSON import and export · no account, and the template format is a file you can share ### Data model (create this before Phase 1) - artboards(id, doc_id, name, width, height, background) · width and height are pixels, fixed, chosen from a preset - layers(id, artboard_id, kind, z, x, y, w, h, rotation, opacity) · kind is text, image, shape or group - text_props(layer_id, content, font_family, size, weight, line_height, letter_spacing, align, color) - image_props(layer_id, asset_id, fit, focal_x, focal_y) · focal point drives cropping when the frame changes shape - brand(id, name, palette_json, fonts_json, logo_asset_id) - templates(id, name, thumbnail, document_json) · a template is a document with placeholder slots, nothing more ### Phase 1 · Artboard, layers and one layout engine Build: A fixed-size artboard rendered at a display scale that never affects stored geometry. Add, move, resize and reorder rectangle and text layers. Write the layout as one function from document to positioned boxes, and use it for the editor from day one, because Phase 4 will use the same function for export. Trap: Storing positions in screen pixels rather than artboard pixels means every export at a different scale is subtly wrong. Store artboard coordinates only, and treat the display scale as a view concern that never reaches the document. Done when: A layer placed at a known artboard coordinate reports that same coordinate after the editor is zoomed and the window resized. Reordering changes the visual stacking. Reloading the page restores the document exactly. Do not build yet: No typography, no images, no export. ### Phase 2 · Typography that behaves Build: Real typographic controls: family, size, weight, line height, letter spacing and alignment. Wrap text within the layer's box. Add auto-fit that shrinks the size until the text fits, and make wrapping a property of the layout function rather than of CSS. Trap: Letting the browser wrap the text in the editor and letting the exporter wrap it separately guarantees they disagree, usually by one word on the last line, and always on the graphic you already published. Wrap once, in the shared layout function, and have the editor render the result rather than compute its own. Done when: A paragraph wraps to identical line breaks in the editor and in the exported image, checked on a string that breaks awkwardly. Auto-fit reduces the size until the text fits its box and stops. Changing the font family changes the wrapping in both places identically. Do not build yet: No images and no templates yet. ### Phase 3 · Images, cropping and a brand kit Build: Import images, store them as assets, and place them in frames with a fit mode and a focal point so a differently shaped frame crops sensibly rather than centring blindly. Add a brand kit of palette and fonts, applied by reference so changing a brand colour updates every layer that uses it. Trap: Centre-cropping a portrait into a wide frame decapitates people with great reliability. Store a focal point per image, default it to the centre, and let the user move it. This is a small feature that prevents most of the bad outputs. Done when: Replacing a landscape image with a portrait one in the same frame keeps the focal point visible rather than cropping it out. Changing a brand palette colour updates every layer referencing it without touching layers that use a literal colour. An image larger than a size limit is downscaled on import rather than stored at full size. Do not build yet: No export and no templates yet. ### Phase 4 · Export at exact dimensions Build: Render the layout to SVG with satori, rasterise with resvg, and offer named presets with exact pixel dimensions. Embed or outline fonts. Add PDF export for print sizes with the resolution stated rather than assumed. Trap: Exporting by screenshotting the editor DOM inherits the browser's font rendering, device pixel ratio and any scrollbar, and produces a different file on a different machine. Render from the document through the shared layout function instead, which is why the layout function exists. Done when: Each preset produces exactly the pixel dimensions it names. The exported image matches the editor rendering, compared side by side on a design with wrapped text and a cropped image. Exporting the same document twice produces byte-identical files. Do not build yet: No templates or sharing yet. ### Phase 5 · Templates as data Build: Save any document as a template with named placeholder slots. Applying a template creates a document and prompts only for the slots. Templates export and import as files so they can be shared without a server. Trap: A template that hard-codes its own text and image assets is a copy, not a template, and updating the original never propagates. Mark slots explicitly and keep the rest as the design, so applying a template is filling in blanks rather than editing someone else's document. Done when: Applying a template and filling three slots produces a finished graphic without touching the canvas. Editing the template afterwards does not alter documents already created from it. A template exported to a file applies correctly after being imported into a fresh browser profile. Do not build yet: Sharing and hosting are the product phase. ### Phase 6 · Assets, licensing and sharing Build: If you add a shared asset library or publish templates, record the licence of every font, image and icon, and surface it in the interface. Add authentication before anything is hosted. Trap: Stock imagery and fonts are the substantial part of what the subscription buys, and their licences are specific about redistribution. A template shipped with a licensed photo inside it is redistribution even when your application is free. Done when: Every bundled asset has a recorded licence and source. The interface shows the licence for any asset before it is used in a document that will be shared. Hosted sharing requires authentication. Do not build yet: Nothing after this. ### Out of scope (and why) - The template library. Tens of thousands of professionally designed templates is the product, and it is content rather than code. - The stock photo, video and audio library, which is a licensing operation with a continuing bill. - The generative image and background removal features, which are model access and infrastructure. - Real-time collaborative editing and team brand governance. ### README must contain - That the editor and the exporter share one layout function, and why that is the central design decision - That geometry is stored in artboard pixels and the display scale never reaches the document - The export presets and their exact pixel dimensions - Every bundled font and asset, with its licence and whether embedding or redistribution is permitted - That templates use explicit slots, and that editing a template does not change existing documents
# Agent instructions · Canva indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Vite with TypeScript, no framework, DOM and CSS for the editor, with one layout engine shared by editor and export, satori to turn the layout into SVG, then resvg to rasterise, Self-hosted font files with the licence recorded, IndexedDB for documents, with JSON import and export. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing positions in screen pixels rather than artboard pixels means every export at a different scale is subtly wrong. Store artboard coordinates only, and treat the display scale as a view concern that never reaches the document. - Letting the browser wrap the text in the editor and letting the exporter wrap it separately guarantees they disagree, usually by one word on the last line, and always on the graphic you already published. Wrap once, in the shared layout function, and have the editor render the result rather than compute its own. - Centre-cropping a portrait into a wide frame decapitates people with great reliability. Store a focal point per image, default it to the centre, and let the user move it. This is a small feature that prevents most of the bad outputs. - Exporting by screenshotting the editor DOM inherits the browser's font rendering, device pixel ratio and any scrollbar, and produces a different file on a different machine. Render from the document through the shared layout function instead, which is why the layout function exists. - A template that hard-codes its own text and image assets is a copy, not a template, and updating the original never propagates. Mark slots explicitly and keep the rest as the design, so applying a template is filling in blanks rather than editing someone else's document.
# Build plan · Canva A browser editor for social graphics: fixed-size artboards, text with real typography, images with focal-point cropping, your own templates as data, a brand kit of colours and fonts, and PNG and PDF export at exactly the pixel dimensions each destination expects. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Artboard, layers and one layout engine You can place and move things on a fixed-size artboard, and the geometry is exact. ### Steps 1. Set up the artboard and view transform One transform from artboard coordinates to screen. Everything else works in artboard space. Files: `src/artboard.ts` ```sh npm create vite@latest . -- --template vanilla-ts ``` 2. Implement the document and layer model Plain serialisable objects. Selection and hover are view state and never stored. Files: `src/document.ts` 3. Write the shared layout function Document in, positioned boxes out, pure and testable. Both the editor and the exporter call it. Files: `src/layout.ts` ### Done when - [ ] A layer's stored coordinates do not change when the view is zoomed or resized - [ ] Reordering changes visual stacking - [ ] The document round-trips through JSON unchanged - [ ] The layout function is called by the editor rather than duplicated in it ### Watch out - Storing positions in screen pixels rather than artboard pixels means every export at a different scale is subtly wrong. Store artboard coordinates only, and treat the display scale as a view concern that never reaches the document. ## Phase 2 · Typography that behaves Text wraps, fits and renders the same in the editor as it will in the export. ### Steps 1. Measure text with the real font loaded Wait for the font to load before measuring. Measuring against a fallback and rendering with the real face is a classic source of one-word overflow. Files: `src/text-measure.ts` 2. Wrap inside the layout function Break into lines there and hand the editor positioned lines to draw. The editor never calls the browser's own wrapping. Files: `src/layout.ts` 3. Add auto-fit Binary search the font size until the wrapped height fits the box, with a floor so text never becomes unreadable. Files: `src/autofit.ts` ### Done when - [ ] An awkward string wraps to the same lines in editor and export - [ ] Auto-fit stops at a readable minimum rather than shrinking indefinitely - [ ] Changing the font changes wrapping identically in both paths - [ ] Measurement waits for the real font to load ### Watch out - Letting the browser wrap the text in the editor and letting the exporter wrap it separately guarantees they disagree, usually by one word on the last line, and always on the graphic you already published. Wrap once, in the shared layout function, and have the editor render the result rather than compute its own. ## Phase 3 · Images, cropping and a brand kit Swapping an image or a brand does not break a design. ### Steps 1. Store assets with a size limit Downscale on import to a sensible maximum. A twelve megapixel photo in a social graphic is wasted bytes and a slow editor. Files: `src/assets.ts` 2. Implement focal point cropping Fit modes of cover and contain, with a draggable focal point used by cover. Default centre, and make it visible. Files: `src/image.ts` 3. Add the brand kit with references A colour is either a literal or a reference into the brand palette. References are what make rebranding one action. Files: `src/brand.ts` ### Done when - [ ] A portrait image in a wide frame keeps its focal point visible - [ ] Changing a brand colour updates referencing layers only - [ ] Oversized images are downscaled on import - [ ] A document using no brand references is unaffected by brand changes ### Watch out - Centre-cropping a portrait into a wide frame decapitates people with great reliability. Store a focal point per image, default it to the centre, and let the user move it. This is a small feature that prevents most of the bad outputs. ## Phase 4 · Export at exact dimensions The exported file is exactly the pixel size the destination requires, and it matches the screen. ### Steps 1. Render through satori and resvg Feed the layout result to satori as element structure, then rasterise. Both run identically wherever they run, which is the property you are buying. Files: `src/export.ts` ```sh npm install satori @resvg/resvg-js ``` 2. Add presets with asserted dimensions Named sizes with a test that asserts the output dimensions rather than trusting the arithmetic. Files: `src/presets.ts`, `src/export.test.ts` 3. Handle fonts explicitly Load the font files into the renderer. Record each font's licence and whether embedding is permitted. Files: `docs/fonts.md` ### Done when - [ ] Every preset produces exactly the dimensions it names - [ ] The export matches the editor on wrapped text and a cropped image - [ ] Exporting twice produces byte-identical files - [ ] Every bundled font's licence is recorded ### Watch out - Exporting by screenshotting the editor DOM inherits the browser's font rendering, device pixel ratio and any scrollbar, and produces a different file on a different machine. Render from the document through the shared layout function instead, which is why the layout function exists. ## Phase 5 · Templates as data A design becomes a reusable template, and filling it takes seconds. ### Steps 1. Mark slots explicitly A layer can be declared a slot with a name and a kind. Everything else is fixed design. Files: `src/template.ts` 2. Build the fill flow Applying a template shows only its slots. This is the difference between a template and a starting point. Files: `src/template.ts` 3. Export and import templates as files A template plus its assets in one file. No server, and shareable by any means you already have. Files: `src/template-io.ts` ### Done when - [ ] Filling slots produces a finished graphic without canvas editing - [ ] Editing a template does not change documents already made from it - [ ] A template file imports into a fresh browser profile and applies correctly - [ ] A template with no slots still applies as a starting document ### Watch out - A template that hard-codes its own text and image assets is a copy, not a template, and updating the original never propagates. Mark slots explicitly and keep the rest as the design, so applying a template is filling in blanks rather than editing someone else's document. ## Not in this build - The template library, which is the product. - Stock photo, video and audio libraries. - Generative image tools and background removal. - Real-time collaboration and team brand governance. ## After v1, if you want it - A batch mode that fills one template from a spreadsheet row per graphic - Artboard variants so one design exports at several sizes at once - A contrast check against the brand palette before export
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Longest edge kept on import. Larger images are downscaled. VITE_MAX_IMAGE_PX=4000 # Required. The smallest font size auto-fit will produce, so text never shrinks into illegibility. VITE_AUTOFIT_MIN_PT=12 # Required. Where self-hosted font files are served from. The licence files live beside them. VITE_FONT_DIR=/fonts # Optional. Product phase only. Leave unset to keep everything local, which is a legitimate final state. VITE_SHARE_ENDPOINT=https://share.example.com
# Canva · product brief ## Problem A prompt-to-template design generator is buildable, but Canva's value is templates, stock assets, collaboration, brand kits, print/export workflows, and distribution. ## Product outcome An editor whose exports are exactly the size they claim and identical to what was on screen, with templates that fill rather than copy, a brand kit applied by reference, and a licence recorded for every asset it can distribute. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - canvas editor library - image generation/editing API - template assets - file export pipeline - hosted app ## Explicit non-goals for v1 - The template library, which is the product. - Stock photo, video and audio libraries. - Generative image tools and background removal. - Real-time collaboration and team brand governance. - template library - stock/licensed assets - Magic Studio depth - collaboration - brand kits - print/social integrations - mobile apps ## Success criteria - Fixture documents export byte-identically to their stored references - Every preset's dimensions are asserted by test - Editor and export wrapping agree on the awkward string fixture - Every bundled font and asset licence is recorded and permits the use - A template file round-trips into a fresh browser profile
# Build brief · Canva 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 fixed-artboard graphics editor with your own templates, brand kit and exact export presets to replace Canva. 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: Vite with TypeScript, no framework · one stateful canvas surface, where a component tree adds ceremony without buying anything - Rendering: DOM and CSS for the editor, with one layout engine shared by editor and export · the only reliable way to make what you export match what you saw - Export: satori to turn the layout into SVG, then resvg to rasterise · deterministic server-quality rendering that does not depend on the browser's own screenshot behaviour - Fonts: Self-hosted font files with the licence recorded · embedding a font in an exported file is redistribution, and not every licence allows it - Storage: IndexedDB for documents, with JSON import and export · no account, and the template format is a file you can share ### Data model (create this before Phase 1) - artboards(id, doc_id, name, width, height, background) · width and height are pixels, fixed, chosen from a preset - layers(id, artboard_id, kind, z, x, y, w, h, rotation, opacity) · kind is text, image, shape or group - text_props(layer_id, content, font_family, size, weight, line_height, letter_spacing, align, color) - image_props(layer_id, asset_id, fit, focal_x, focal_y) · focal point drives cropping when the frame changes shape - brand(id, name, palette_json, fonts_json, logo_asset_id) - templates(id, name, thumbnail, document_json) · a template is a document with placeholder slots, nothing more ### Phase 1 · Artboard, layers and one layout engine Build: A fixed-size artboard rendered at a display scale that never affects stored geometry. Add, move, resize and reorder rectangle and text layers. Write the layout as one function from document to positioned boxes, and use it for the editor from day one, because Phase 4 will use the same function for export. Trap: Storing positions in screen pixels rather than artboard pixels means every export at a different scale is subtly wrong. Store artboard coordinates only, and treat the display scale as a view concern that never reaches the document. Done when: A layer placed at a known artboard coordinate reports that same coordinate after the editor is zoomed and the window resized. Reordering changes the visual stacking. Reloading the page restores the document exactly. Do not build yet: No typography, no images, no export. ### Phase 2 · Typography that behaves Build: Real typographic controls: family, size, weight, line height, letter spacing and alignment. Wrap text within the layer's box. Add auto-fit that shrinks the size until the text fits, and make wrapping a property of the layout function rather than of CSS. Trap: Letting the browser wrap the text in the editor and letting the exporter wrap it separately guarantees they disagree, usually by one word on the last line, and always on the graphic you already published. Wrap once, in the shared layout function, and have the editor render the result rather than compute its own. Done when: A paragraph wraps to identical line breaks in the editor and in the exported image, checked on a string that breaks awkwardly. Auto-fit reduces the size until the text fits its box and stops. Changing the font family changes the wrapping in both places identically. Do not build yet: No images and no templates yet. ### Phase 3 · Images, cropping and a brand kit Build: Import images, store them as assets, and place them in frames with a fit mode and a focal point so a differently shaped frame crops sensibly rather than centring blindly. Add a brand kit of palette and fonts, applied by reference so changing a brand colour updates every layer that uses it. Trap: Centre-cropping a portrait into a wide frame decapitates people with great reliability. Store a focal point per image, default it to the centre, and let the user move it. This is a small feature that prevents most of the bad outputs. Done when: Replacing a landscape image with a portrait one in the same frame keeps the focal point visible rather than cropping it out. Changing a brand palette colour updates every layer referencing it without touching layers that use a literal colour. An image larger than a size limit is downscaled on import rather than stored at full size. Do not build yet: No export and no templates yet. ### Phase 4 · Export at exact dimensions Build: Render the layout to SVG with satori, rasterise with resvg, and offer named presets with exact pixel dimensions. Embed or outline fonts. Add PDF export for print sizes with the resolution stated rather than assumed. Trap: Exporting by screenshotting the editor DOM inherits the browser's font rendering, device pixel ratio and any scrollbar, and produces a different file on a different machine. Render from the document through the shared layout function instead, which is why the layout function exists. Done when: Each preset produces exactly the pixel dimensions it names. The exported image matches the editor rendering, compared side by side on a design with wrapped text and a cropped image. Exporting the same document twice produces byte-identical files. Do not build yet: No templates or sharing yet. ### Phase 5 · Templates as data Build: Save any document as a template with named placeholder slots. Applying a template creates a document and prompts only for the slots. Templates export and import as files so they can be shared without a server. Trap: A template that hard-codes its own text and image assets is a copy, not a template, and updating the original never propagates. Mark slots explicitly and keep the rest as the design, so applying a template is filling in blanks rather than editing someone else's document. Done when: Applying a template and filling three slots produces a finished graphic without touching the canvas. Editing the template afterwards does not alter documents already created from it. A template exported to a file applies correctly after being imported into a fresh browser profile. Do not build yet: Sharing and hosting are the product phase. ### Phase 6 · Assets, licensing and sharing Build: If you add a shared asset library or publish templates, record the licence of every font, image and icon, and surface it in the interface. Add authentication before anything is hosted. Trap: Stock imagery and fonts are the substantial part of what the subscription buys, and their licences are specific about redistribution. A template shipped with a licensed photo inside it is redistribution even when your application is free. Done when: Every bundled asset has a recorded licence and source. The interface shows the licence for any asset before it is used in a document that will be shared. Hosted sharing requires authentication. Do not build yet: Nothing after this. ### Out of scope (and why) - The template library. Tens of thousands of professionally designed templates is the product, and it is content rather than code. - The stock photo, video and audio library, which is a licensing operation with a continuing bill. - The generative image and background removal features, which are model access and infrastructure. - Real-time collaborative editing and team brand governance. ### README must contain - That the editor and the exporter share one layout function, and why that is the central design decision - That geometry is stored in artboard pixels and the display scale never reaches the document - The export presets and their exact pixel dimensions - Every bundled font and asset, with its licence and whether embedding or redistribution is permitted - That templates use explicit slots, and that editing a template does not change existing documents
# Architecture · Canva ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Vite with TypeScript, no framework | one stateful canvas surface, where a component tree adds ceremony without buying anything | | Rendering | DOM and CSS for the editor, with one layout engine shared by editor and export | the only reliable way to make what you export match what you saw | | Export | satori to turn the layout into SVG, then resvg to rasterise | deterministic server-quality rendering that does not depend on the browser's own screenshot behaviour | | Fonts | Self-hosted font files with the licence recorded | embedding a font in an exported file is redistribution, and not every licence allows it | | Storage | IndexedDB for documents, with JSON import and export | no account, and the template format is a file you can share | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Document | Artboards, layers and serialisation. | Plain data, so persistence, templates and export all read the same shape. | | Layout | Turning a document into positioned boxes and wrapped lines. | This is the single most important boundary. Both editor and exporter must keep calling it, or they will disagree. | | Render | SVG generation and rasterisation. | satori and resvg are replaceable, but not by a browser screenshot, which reintroduces machine-dependent output. | | Brand | Palette and font references. | References resolve at render, so an alternative brand model is additive. | | Templates | Slots and application. | A template is a document plus slot declarations, so the format travels with the document format. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `VITE_MAX_IMAGE_PX` · required · Longest edge kept on import. Larger images are downscaled. - `VITE_AUTOFIT_MIN_PT` · required · The smallest font size auto-fit will produce, so text never shrinks into illegibility. - `VITE_FONT_DIR` · required · Where self-hosted font files are served from. The licence files live beside them. - `VITE_SHARE_ENDPOINT` · optional · Product phase only. Leave unset to keep everything local, which is a legitimate final state. ## 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 · Canva product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Vite with TypeScript, no framework, DOM and CSS for the editor, with one layout engine shared by editor and export, satori to turn the layout into SVG, then resvg to rasterise, Self-hosted font files with the licence recorded, IndexedDB for documents, with JSON import and export. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Storing positions in screen pixels rather than artboard pixels means every export at a different scale is subtly wrong. Store artboard coordinates only, and treat the display scale as a view concern that never reaches the document. - Letting the browser wrap the text in the editor and letting the exporter wrap it separately guarantees they disagree, usually by one word on the last line, and always on the graphic you already published. Wrap once, in the shared layout function, and have the editor render the result rather than compute its own. - Centre-cropping a portrait into a wide frame decapitates people with great reliability. Store a focal point per image, default it to the centre, and let the user move it. This is a small feature that prevents most of the bad outputs. - Exporting by screenshotting the editor DOM inherits the browser's font rendering, device pixel ratio and any scrollbar, and produces a different file on a different machine. Render from the document through the shared layout function instead, which is why the layout function exists. - A template that hard-codes its own text and image assets is a copy, not a template, and updating the original never propagates. Mark slots explicitly and keep the rest as the design, so applying a template is filling in blanks rather than editing someone else's document. - Stock imagery and fonts are the substantial part of what the subscription buys, and their licences are specific about redistribution. A template shipped with a licensed photo inside it is redistribution even when your application is free.
# Delivery milestones · Canva Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Artboard, layers and one layout engine You can place and move things on a fixed-size artboard, and the geometry is exact. ### Steps 1. Set up the artboard and view transform One transform from artboard coordinates to screen. Everything else works in artboard space. Files: `src/artboard.ts` ```sh npm create vite@latest . -- --template vanilla-ts ``` 2. Implement the document and layer model Plain serialisable objects. Selection and hover are view state and never stored. Files: `src/document.ts` 3. Write the shared layout function Document in, positioned boxes out, pure and testable. Both the editor and the exporter call it. Files: `src/layout.ts` ### Done when - [ ] A layer's stored coordinates do not change when the view is zoomed or resized - [ ] Reordering changes visual stacking - [ ] The document round-trips through JSON unchanged - [ ] The layout function is called by the editor rather than duplicated in it ### Watch out - Storing positions in screen pixels rather than artboard pixels means every export at a different scale is subtly wrong. Store artboard coordinates only, and treat the display scale as a view concern that never reaches the document. ## M2 · Typography that behaves Text wraps, fits and renders the same in the editor as it will in the export. ### Steps 1. Measure text with the real font loaded Wait for the font to load before measuring. Measuring against a fallback and rendering with the real face is a classic source of one-word overflow. Files: `src/text-measure.ts` 2. Wrap inside the layout function Break into lines there and hand the editor positioned lines to draw. The editor never calls the browser's own wrapping. Files: `src/layout.ts` 3. Add auto-fit Binary search the font size until the wrapped height fits the box, with a floor so text never becomes unreadable. Files: `src/autofit.ts` ### Done when - [ ] An awkward string wraps to the same lines in editor and export - [ ] Auto-fit stops at a readable minimum rather than shrinking indefinitely - [ ] Changing the font changes wrapping identically in both paths - [ ] Measurement waits for the real font to load ### Watch out - Letting the browser wrap the text in the editor and letting the exporter wrap it separately guarantees they disagree, usually by one word on the last line, and always on the graphic you already published. Wrap once, in the shared layout function, and have the editor render the result rather than compute its own. ## M3 · Images, cropping and a brand kit Swapping an image or a brand does not break a design. ### Steps 1. Store assets with a size limit Downscale on import to a sensible maximum. A twelve megapixel photo in a social graphic is wasted bytes and a slow editor. Files: `src/assets.ts` 2. Implement focal point cropping Fit modes of cover and contain, with a draggable focal point used by cover. Default centre, and make it visible. Files: `src/image.ts` 3. Add the brand kit with references A colour is either a literal or a reference into the brand palette. References are what make rebranding one action. Files: `src/brand.ts` ### Done when - [ ] A portrait image in a wide frame keeps its focal point visible - [ ] Changing a brand colour updates referencing layers only - [ ] Oversized images are downscaled on import - [ ] A document using no brand references is unaffected by brand changes ### Watch out - Centre-cropping a portrait into a wide frame decapitates people with great reliability. Store a focal point per image, default it to the centre, and let the user move it. This is a small feature that prevents most of the bad outputs. ## M4 · Export at exact dimensions The exported file is exactly the pixel size the destination requires, and it matches the screen. ### Steps 1. Render through satori and resvg Feed the layout result to satori as element structure, then rasterise. Both run identically wherever they run, which is the property you are buying. Files: `src/export.ts` ```sh npm install satori @resvg/resvg-js ``` 2. Add presets with asserted dimensions Named sizes with a test that asserts the output dimensions rather than trusting the arithmetic. Files: `src/presets.ts`, `src/export.test.ts` 3. Handle fonts explicitly Load the font files into the renderer. Record each font's licence and whether embedding is permitted. Files: `docs/fonts.md` ### Done when - [ ] Every preset produces exactly the dimensions it names - [ ] The export matches the editor on wrapped text and a cropped image - [ ] Exporting twice produces byte-identical files - [ ] Every bundled font's licence is recorded ### Watch out - Exporting by screenshotting the editor DOM inherits the browser's font rendering, device pixel ratio and any scrollbar, and produces a different file on a different machine. Render from the document through the shared layout function instead, which is why the layout function exists. ## M5 · Templates as data A design becomes a reusable template, and filling it takes seconds. ### Steps 1. Mark slots explicitly A layer can be declared a slot with a name and a kind. Everything else is fixed design. Files: `src/template.ts` 2. Build the fill flow Applying a template shows only its slots. This is the difference between a template and a starting point. Files: `src/template.ts` 3. Export and import templates as files A template plus its assets in one file. No server, and shareable by any means you already have. Files: `src/template-io.ts` ### Done when - [ ] Filling slots produces a finished graphic without canvas editing - [ ] Editing a template does not change documents already made from it - [ ] A template file imports into a fresh browser profile and applies correctly - [ ] A template with no slots still applies as a starting document ### Watch out - A template that hard-codes its own text and image assets is a copy, not a template, and updating the original never propagates. Mark slots explicitly and keep the rest as the design, so applying a template is filling in blanks rather than editing someone else's document. ## M6 · Assets, licensing and sharing (production only) Anything you distribute is something you are allowed to distribute. ### Steps 1. Record every asset's provenance Source, licence and whether redistribution is permitted, stored with the asset rather than in a separate document that drifts. Files: `src/assets.ts` 2. Surface licences in the interface Shown at the point of use, not buried. This is the only place it changes behaviour. Files: `src/assets.ts` 3. Authenticate anything hosted If you host templates or exports, put a login in front of it. Files: `src/auth.ts` ### Done when - [ ] Every bundled asset has a licence and source recorded - [ ] The licence is visible at the point of use - [ ] Hosted sharing requires authentication - [ ] An asset whose licence forbids redistribution cannot be included in an exported template ### Watch out - Stock imagery and fonts are the substantial part of what the subscription buys, and their licences are specific about redistribution. A template shipped with a licensed photo inside it is redistribution even when your application is free.
# Operations · Canva ## Backup Documents live in IndexedDB, which the browser can clear. JSON export is the backup, and templates and assets export with it. Make export prominent rather than buried. ## Restore Import the exported file into a fresh profile. The round-trip check in Phase 5 is what makes this trustworthy. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Nothing server-side. What is worth tracking is export determinism: keep fixture documents and diff their exported bytes in continuous integration, so a font or renderer change that shifts output is caught immediately. ## Incident checklist The realistic incident is a font update changing text metrics and reflowing published designs. Pin font file versions and treat a font change as a release with its own visual 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 - [ ] Fixture documents export byte-identically to their stored references - [ ] Every preset's dimensions are asserted by test - [ ] Editor and export wrapping agree on the awkward string fixture - [ ] Every bundled font and asset licence is recorded and permits the use - [ ] A template file round-trips into a fresh browser profile ## Launch constraint Do not market omitted Canva 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. Longest edge kept on import. Larger images are downscaled. VITE_MAX_IMAGE_PX=4000 # Required. The smallest font size auto-fit will produce, so text never shrinks into illegibility. VITE_AUTOFIT_MIN_PT=12 # Required. Where self-hosted font files are served from. The licence files live beside them. VITE_FONT_DIR=/fonts # Optional. Product phase only. Leave unset to keep everything local, which is a legitimate final state. VITE_SHARE_ENDPOINT=https://share.example.com
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay because design work is mostly choosing from a huge polished library and collaborating without breaking layouts.
xtemplate library
xstock/licensed assets
xMagic Studio depth
xcollaboration
xbrand kits
xprint/social integrations
xmobile apps
Don't feel like building it? These folks already made it free.
all 4 free alternatives to Canva →· no votes, no pay-to-list · just what's real
Canva pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/user | $0/user | 5 GB cloud storage; free-content library; free video exports up to 480p; paid assets remain watermarked unless licensed. |
| pro | $15/user | $10/user | 100 GB cloud storage and premium templates, assets, brand tools and AI features. |
| business | $20/user | — | 500 GB cloud storage; 20x the Free plan's AI allowance; Leonardo.Ai Essential, Flourish Presenter and team/marketing controls; no seat minimum. |
| enterprise | custom | — | Custom enterprise pricing with approximately 1–5 TB storage depending deployment; public price is not listed. |
free tier5 GB storage and free-content access; free video export is capped at 480p, while premium assets require licensing.
billingmonthly + annual; Business pricing varies by location and team size
hidden costsPremium content can require a one-off licence or paid plan, and regional pricing/tax can differ. Canva Teams is no longer sold to new sign-ups; existing Teams customers keep legacy pricing.
verified 2026-08-14 · source ↗
Vibecode Canva
Kinda. The core of Canva is buildable in a weekend with the prompt on this page, but there are real gaps: template library, stock/licensed assets. Read the honest list above before committing.
How much does Canva cost?
Canva costs about $15/month (Pro, checked 2026-08-14), which is $180 per year.
What do I lose by replacing Canva?
Honestly: template library; stock/licensed assets; Magic Studio depth; collaboration; brand kits; print/social integrations; mobile apps. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Canva?
Yes: Adobe Express Free (The closest hosted swap for social graphics, short video, documents, and templates; its best brand automation is also paid.) Canva Free (Canva without the convenient tollbooths: plenty of templates, docs, decks, and exports, but no Magic Resize or serious Brand Kit.) Desygner Free (A surprisingly complete free suite for templates, PDFs, scheduling, and small-team brand work, capped at three collaborators.) All 4 curated free alternatives are at vibecodeit.com/canva/alternatives. The prompt is for when you want it exactly your way.