Vibecode RelayThat
track this build4 phases, 12 steps, beginner friendly0%The visible brand design automation loop is buildable, but a credible replacement needs more than the first screen. RelayThat earns its keep through editor polish, assets, algorithms, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of RelayThat. 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 ===== # RelayThat · indie build A local brand design tool whose hard problems are one layout resolving correctly into a dozen aspect ratios, text and fonts that render identically on two machines, and exports whose pixel dimensions are the ones you asked for. Estimated effort: **weekend**. 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 | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript | the fonts, the photographs and the exports stay on your disk | | Layout | Slots anchored to a per-size safe area, never coordinates multiplied by a scale factor | a square design stretched into a portrait one puts the headline off the canvas | | Rendering | One renderer for both the preview and the export, run at the export's real pixel size | two renderers means the preview is a guess | | Text | Font files loaded by path, measured by the same engine that draws them | naming a family and hoping is how two machines produce different line breaks | | Storage | SQLite beside a folder of fonts and assets, with a manifest written per render | an export you cannot reproduce next month is a screenshot | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 and the Rust toolchain** · free - Why: Tauri 2 needs both, and the desktop shell is what gives you the font folder and the export folder. - Get it: nodejs.org and rustup.rs, then the Tauri prerequisites for your platform. - Verify: node --version prints v22 or higher and cargo --version succeeds - [ ] **Font files you are licensed to use** · free for open licensed families, otherwise whatever you paid - Why: The whole rendering model loads faces by file, and the licence of each face decides whether it may travel in an export. - Get it: Your own purchased faces, or open licensed families whose licence text you have actually read. - Verify: Every face in the folder has a recorded licence name and a redistributable flag - [ ] **Your logo files and a few photographs** · free - Why: Contrast over a photograph behaves nothing like contrast over a flat colour, and it is the case that breaks first. - Get it: Export your logo with transparency at a generous size, and pick two photographs with busy areas. - Verify: A headline over the busy photograph is flagged at some sizes and not others - [ ] **The current contrast guidance** · free - Why: The minimum ratio is copied from published guidance rather than chosen, and it differs by text size. - Get it: Read the current accessibility guidance for contrast, note the ratios and the date, and record both in your configuration. - Verify: A note records the ratios used and the guidance version - [ ] **A high density and a standard display** · free - Why: The export dimension bug only appears when the display scale factor differs from one, and it is the most common way a render comes out the wrong size. - Get it: A laptop with a high density panel plus any ordinary external monitor. - Verify: The same export produces identical dimensions on both ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Font files inside exports. - Print colour profiles. - A stock template and photograph library. - Cloud collaboration and a mobile editor. - color-management edge cases - cloud collaboration and mobile apps - pixel-perfect professional tooling - large template and asset libraries If one of those is essential to you, that is the reason to keep paying for RelayThat, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · RelayThat 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 brand kit that lays a message out once and renders it into every size you actually post to replace RelayThat. 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) - Shell: Tauri 2 with React and TypeScript · the fonts, the photographs and the exports stay on your disk - Layout: Slots anchored to a per-size safe area, never coordinates multiplied by a scale factor · a square design stretched into a portrait one puts the headline off the canvas - Rendering: One renderer for both the preview and the export, run at the export's real pixel size · two renderers means the preview is a guess - Text: Font files loaded by path, measured by the same engine that draws them · naming a family and hoping is how two machines produce different line breaks - Storage: SQLite beside a folder of fonts and assets, with a manifest written per render · an export you cannot reproduce next month is a screenshot ### Data model (create this before Phase 1) - brands(id, name, version, palette_json, logo_asset_ids, font_ids, created_at) · versioned, because a render has to say which brand it used - fonts(id, family, style, file_path, licence_name, licence_url, redistributable, added_at) · the licence is recorded per face, not per project - assets(id, kind, path, width, height, sha256, has_alpha) · logos and photographs copied in, never referenced from somewhere else on disk - sizes(id, name, width_px, height_px, safe_top, safe_right, safe_bottom, safe_left) · every output shape carries its own safe area - layouts(id, name, version, slots_json) · a slot has an anchor, a minimum and maximum type size, and a stated behaviour when the text still does not fit - designs(id, brand_id, layout_id, headline, subhead, call_to_action, asset_ids_json) - renders(id, design_id, size_id, path, manifest_json, rendered_at) · the manifest holds font ids, asset hashes, brand version and layout version - A design is stored once; a render is a design plus a size plus the manifest that lets you rebuild that exact file a year later. ### Phase 1 · One design, many shapes Build: Define each output size with its own safe area. Lay a design out as slots anchored to that safe area, and resolve every slot at the target size rather than at a reference size. Render three very different aspect ratios from one design, side by side, and compare them. Trap: Producing the other sizes by multiplying the square design's coordinates by a scale factor pushes the headline off a tall canvas and drops the call to action to nine pixels on a wide banner, and both look acceptable in a thumbnail. Anchor each slot to the target size's safe area and resolve its type size for that size alone. Done when: One design renders into a wide, a square and a tall size with every slot inside its safe area, asserted by comparing slot bounding boxes against the safe rectangle. No slot position is derived by scaling another size's coordinates, verified by review. Adding a new size requires no change to the design. Each rendered size is shown at its real aspect ratio in the preview, not letterboxed into a square. Do not build yet: No fonts of your own, no brand kit, no batch export. ### Phase 2 · Fonts that render the same on two machines Build: Load each font face from a file path and record its family, style, licence name and whether the file may be redistributed. Measure text with the same engine that draws it. When a face is missing, fail the render and name the family rather than falling back to something else. Trap: Naming a font family and letting the system pick means the machine without that family quietly substitutes a fallback, so the same design exports with different line breaks on your laptop and on the machine you render the batch from, and nobody notices until a headline wraps onto three lines in the one size you did not check. Load the file, measure with the engine that draws, and treat a missing face as a failure. Done when: Every font face in the kit records a licence name and a redistributable flag, and the export folder contains no font file marked as not redistributable. Rendering with a face removed from the folder fails with the family and style named, tested by deleting one. Text measurement and text drawing use the same engine, verified by rendering a known string and comparing measured width to drawn width. Two machines with the same font folder produce byte-identical line breaks for the same design. Do not build yet: No brand kit, autofit or export yet. ### Phase 3 · Text that fits and colour you can read Build: Give each slot a minimum and maximum type size and a stated overflow behaviour: shrink to the minimum, then wrap, then mark the render failed. Never clip silently. Compute the contrast between each text slot and whatever sits behind it at that size, against the published minimum ratio for that text size, and show the failing size before export. Trap: Shrinking type until the copy fits gives you a six point headline on the tall size, and clipping gives you a caption that ends mid-word, and both export without complaint because the code only asked whether the text fitted. Set a floor, and when the copy still does not fit, fail that render with the size name and the character count so the copy gets shortened instead. Trap: Contrast checked once against the brand's background colour passes, then the same headline goes over a photograph and becomes invisible on half the sizes because the crop differs per aspect ratio. Sample what is actually behind the text at each size. Done when: Copy too long for a slot at the minimum type size fails that render with the size name and the character count, tested with a deliberately long headline. No render clips text without reporting it. Contrast is computed per size against what is actually behind the text, including a photograph, and a failing size is listed before export. The published ratio used is recorded in the configuration rather than hard-coded in a component. Do not build yet: No batch export or manifests yet. ### Phase 4 · Exports at the size you asked for Build: Render off-screen at the target pixel dimensions, independent of the display's scale factor. Write sRGB. Name files by design and size. Batch every size of a design in one run and assert each output file's dimensions after writing it. Trap: Exporting by reading pixels back from the canvas that is on screen gives you a file at the display's device pixel ratio, so the same button produces a 2160 pixel square on a high density laptop and a 1080 pixel one on an external monitor, and the platform rejects one of them. Render off-screen at the requested dimensions and assert the written file's size. Done when: An exported file's pixel dimensions equal the requested size on both a high density and a standard display, asserted in the suite by reading the written file. Every export declares sRGB rather than inheriting whatever the display profile was. A batch of eight sizes writes eight files with predictable names and no partial file is left behind on failure. Rendering the same design twice produces identical bytes. Do not build yet: No manifests, re-render or kit export yet. ### Phase 5 · Renders you can reproduce next year Build: Write a manifest beside every render recording font ids, asset hashes, brand version, layout version and the size. Add a re-render command that takes a manifest and reproduces the file. Export the whole brand kit as plain folders of images, colour values and a licence list, and bound the export folder with a stated retention. Trap: Editing a brand colour in place rewrites the brand for every past render, so the file a client approved in March can no longer be reproduced and nobody can say which logo version they actually saw. Version the brand kit, and record the version in each render's manifest. Done when: Every render has a manifest naming font ids, asset hashes, brand version and layout version. Re-rendering from a manifest after the brand has been edited reproduces the original file byte for byte, tested by editing a colour and re-rendering. The kit export is plain folders and files readable without this application, including a list of every font face and its licence. The export folder is bounded by a stated retention and the deletion is logged. Do not build yet: Nothing after this. ### Out of scope (and why) - Bundling font files into an export, because permission to embed a face in a document is not the same as permission to pass the file on. The SIL Open Font License, for one, allows a font to be bundled and redistributed with software but forbids selling the font software by itself, while placing no restriction at all on documents you create with it; read each face's licence before you ship its file. - Colour management beyond sRGB. Print profiles are a specialist problem and a wrongly applied profile is worse than none. - A stock template and photograph library, which is a licensing operation rather than a feature. - Cloud collaboration and a mobile editor, which turn a local renderer into a service with accounts. ### README must contain - That every output size carries its own safe area and slots anchor to it, rather than being scaled from a square - That fonts load from files you hold, each face records its licence, and a missing face fails the render - That overlong copy fails a render with the size and character count rather than shrinking or clipping - That exports are rendered off-screen at the requested dimensions, so the display's scale factor never reaches the file - That every render has a manifest and can be reproduced after the brand kit changes ===== AGENTS.md ===== # Agent instructions · RelayThat indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, Slots anchored to a per-size safe area, never coordinates multiplied by a scale factor, One renderer for both the preview and the export, run at the export's real pixel size, Font files loaded by path, measured by the same engine that draws them, SQLite beside a folder of fonts and assets, with a manifest written per render. 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 - Producing the other sizes by multiplying the square design's coordinates by a scale factor pushes the headline off a tall canvas and drops the call to action to nine pixels on a wide banner, and both look acceptable in a thumbnail. Anchor each slot to the target size's safe area and resolve its type size for that size alone. - Naming a font family and letting the system pick means the machine without that family quietly substitutes a fallback, so the same design exports with different line breaks on your laptop and on the machine you render the batch from, and nobody notices until a headline wraps onto three lines in the one size you did not check. Load the file, measure with the engine that draws, and treat a missing face as a failure. - Shrinking type until the copy fits gives you a six point headline on the tall size, and clipping gives you a caption that ends mid-word, and both export without complaint because the code only asked whether the text fitted. Set a floor, and when the copy still does not fit, fail that render with the size name and the character count so the copy gets shortened instead. - Contrast checked once against the brand's background colour passes, then the same headline goes over a photograph and becomes invisible on half the sizes because the crop differs per aspect ratio. Sample what is actually behind the text at each size. - Exporting by reading pixels back from the canvas that is on screen gives you a file at the display's device pixel ratio, so the same button produces a 2160 pixel square on a high density laptop and a 1080 pixel one on an external monitor, and the platform rejects one of them. Render off-screen at the requested dimensions and assert the written file's size. ===== BUILD_PLAN.md ===== # Build plan · RelayThat A local brand design tool whose hard problems are one layout resolving correctly into a dozen aspect ratios, text and fonts that render identically on two machines, and exports whose pixel dimensions are the ones you asked for. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One design, many shapes A design laid out once renders into a wide, a square and a tall size without anything falling off the canvas. ### Steps 1. Give every size a safe area The safe area is what the layout anchors to, so it is part of the size definition. Files: `src/sizes.ts`, `src/layout/anchors.ts` 2. Resolve slots per size, never by scaling Scaling is the bug this whole phase exists to avoid. Files: `src/layout/resolve.ts`, `src/layout/resolve.test.ts` 3. Assert every slot lands inside the safe rectangle A bounding box check catches the failure the eye misses at thumbnail size. Files: `tests/safe-area.test.ts` ### Done when - [ ] Three aspect ratios render from one design - [ ] Every slot lands inside its safe rectangle - [ ] No position is derived by scaling another size - [ ] A new size needs no design change ### Watch out - Producing the other sizes by multiplying the square design's coordinates by a scale factor pushes the headline off a tall canvas and drops the call to action to nine pixels on a wide banner, and both look acceptable in a thumbnail. Anchor each slot to the target size's safe area and resolve its type size for that size alone. ## Phase 2 · Fonts that render the same on two machines Type is drawn from a font file you hold, with its licence recorded, and a missing face fails loudly. ### Steps 1. Load faces by file path and record the licence The licence belongs to the face, and you will want it when you export a kit. Files: `src/fonts/registry.ts`, `src/fonts/licence.ts` 2. Measure with the engine that draws Any other measurement is an estimate that diverges on the long strings. Files: `src/text/measure.ts`, `src/text/measure.test.ts` 3. Fail on a missing face, never substitute A silent fallback is a wrong export you will ship. Files: `src/fonts/registry.ts`, `tests/missing-face.test.ts` ### Done when - [ ] Each face records a licence and a redistributable flag - [ ] A missing face fails with the family named - [ ] Measuring and drawing use one engine - [ ] Two machines produce the same line breaks ### Watch out - Naming a font family and letting the system pick means the machine without that family quietly substitutes a fallback, so the same design exports with different line breaks on your laptop and on the machine you render the batch from, and nobody notices until a headline wraps onto three lines in the one size you did not check. Load the file, measure with the engine that draws, and treat a missing face as a failure. ## Phase 3 · Text that fits and colour you can read Long copy on a narrow size fails visibly rather than exporting something unreadable. ### Steps 1. Floor the type size, then fail A readable failure beats an unreadable export. Files: `src/text/autofit.ts`, `src/text/autofit.test.ts` 2. Sample what is behind the text at each size The crop changes per aspect ratio, so the background does too. Files: `src/contrast/sample.ts` 3. Record the ratio you are checking against Copied from the current guidance, with the date, not invented. Files: `src/contrast/thresholds.ts`, `docs/contrast.md` ### Done when - [ ] Overlong copy fails with size and character count - [ ] No render clips text silently - [ ] Contrast is sampled per size over photographs - [ ] The threshold is recorded, not hard-coded inline ### Watch out - Shrinking type until the copy fits gives you a six point headline on the tall size, and clipping gives you a caption that ends mid-word, and both export without complaint because the code only asked whether the text fitted. Set a floor, and when the copy still does not fit, fail that render with the size name and the character count so the copy gets shortened instead. - Contrast checked once against the brand's background colour passes, then the same headline goes over a photograph and becomes invisible on half the sizes because the crop differs per aspect ratio. Sample what is actually behind the text at each size. ## Phase 4 · Exports at the size you asked for A file requested at 1080 by 1080 is 1080 by 1080 on every display. ### Steps 1. Render off-screen at the target dimensions The display's scale factor must not reach the output. Files: `src/render/offscreen.ts` 2. Assert the written file's dimensions Read the file back; do not trust the call that wrote it. Files: `tests/export-size.test.ts` 3. Write batches atomically A failed size should not leave half a PNG in the folder. Files: `src/render/batch.ts` ### Done when - [ ] Output dimensions match the request on both displays - [ ] Exports declare sRGB - [ ] A failed batch leaves no partial files - [ ] The same design renders to identical bytes twice ### Watch out - Exporting by reading pixels back from the canvas that is on screen gives you a file at the display's device pixel ratio, so the same button produces a 2160 pixel square on a high density laptop and a 1080 pixel one on an external monitor, and the platform rejects one of them. Render off-screen at the requested dimensions and assert the written file's size. ## Not in this build - Font files inside exports. - Print colour profiles. - A stock template and photograph library. - Cloud collaboration and a mobile editor. ## After v1, if you want it - Animated sizes, once static renders have been reproducible for a month - A copy length budget shown while typing, computed from the tightest size - Per-platform size presets imported from a file you maintain rather than hard-coded ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. SQLite plus the copied assets. Back it up with the fonts folder. DATA_DIR=~/.local/share/brandkit # Required. Font files loaded by path. Each face records its licence. FONTS_DIR=~/.local/share/brandkit/fonts # Required. Renders and their manifests, side by side. EXPORT_DIR=~/Pictures/brandkit-exports # Required. How long renders are kept. Manifests let you rebuild after that. EXPORT_RETENTION_DAYS=180 # Required. A ceiling on width times height, so a mistyped size cannot exhaust memory. MAX_RENDER_PIXELS=40000000
You are building a lean indie version of RelayThat. 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 ===== # RelayThat · indie build A local brand design tool whose hard problems are one layout resolving correctly into a dozen aspect ratios, text and fonts that render identically on two machines, and exports whose pixel dimensions are the ones you asked for. Estimated effort: **weekend**. 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 | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript | the fonts, the photographs and the exports stay on your disk | | Layout | Slots anchored to a per-size safe area, never coordinates multiplied by a scale factor | a square design stretched into a portrait one puts the headline off the canvas | | Rendering | One renderer for both the preview and the export, run at the export's real pixel size | two renderers means the preview is a guess | | Text | Font files loaded by path, measured by the same engine that draws them | naming a family and hoping is how two machines produce different line breaks | | Storage | SQLite beside a folder of fonts and assets, with a manifest written per render | an export you cannot reproduce next month is a screenshot | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 and the Rust toolchain** · free - Why: Tauri 2 needs both, and the desktop shell is what gives you the font folder and the export folder. - Get it: nodejs.org and rustup.rs, then the Tauri prerequisites for your platform. - Verify: node --version prints v22 or higher and cargo --version succeeds - [ ] **Font files you are licensed to use** · free for open licensed families, otherwise whatever you paid - Why: The whole rendering model loads faces by file, and the licence of each face decides whether it may travel in an export. - Get it: Your own purchased faces, or open licensed families whose licence text you have actually read. - Verify: Every face in the folder has a recorded licence name and a redistributable flag - [ ] **Your logo files and a few photographs** · free - Why: Contrast over a photograph behaves nothing like contrast over a flat colour, and it is the case that breaks first. - Get it: Export your logo with transparency at a generous size, and pick two photographs with busy areas. - Verify: A headline over the busy photograph is flagged at some sizes and not others - [ ] **The current contrast guidance** · free - Why: The minimum ratio is copied from published guidance rather than chosen, and it differs by text size. - Get it: Read the current accessibility guidance for contrast, note the ratios and the date, and record both in your configuration. - Verify: A note records the ratios used and the guidance version - [ ] **A high density and a standard display** · free - Why: The export dimension bug only appears when the display scale factor differs from one, and it is the most common way a render comes out the wrong size. - Get it: A laptop with a high density panel plus any ordinary external monitor. - Verify: The same export produces identical dimensions on both ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Font files inside exports. - Print colour profiles. - A stock template and photograph library. - Cloud collaboration and a mobile editor. - color-management edge cases - cloud collaboration and mobile apps - pixel-perfect professional tooling - large template and asset libraries If one of those is essential to you, that is the reason to keep paying for RelayThat, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · RelayThat 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 brand kit that lays a message out once and renders it into every size you actually post to replace RelayThat. 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) - Shell: Tauri 2 with React and TypeScript · the fonts, the photographs and the exports stay on your disk - Layout: Slots anchored to a per-size safe area, never coordinates multiplied by a scale factor · a square design stretched into a portrait one puts the headline off the canvas - Rendering: One renderer for both the preview and the export, run at the export's real pixel size · two renderers means the preview is a guess - Text: Font files loaded by path, measured by the same engine that draws them · naming a family and hoping is how two machines produce different line breaks - Storage: SQLite beside a folder of fonts and assets, with a manifest written per render · an export you cannot reproduce next month is a screenshot ### Data model (create this before Phase 1) - brands(id, name, version, palette_json, logo_asset_ids, font_ids, created_at) · versioned, because a render has to say which brand it used - fonts(id, family, style, file_path, licence_name, licence_url, redistributable, added_at) · the licence is recorded per face, not per project - assets(id, kind, path, width, height, sha256, has_alpha) · logos and photographs copied in, never referenced from somewhere else on disk - sizes(id, name, width_px, height_px, safe_top, safe_right, safe_bottom, safe_left) · every output shape carries its own safe area - layouts(id, name, version, slots_json) · a slot has an anchor, a minimum and maximum type size, and a stated behaviour when the text still does not fit - designs(id, brand_id, layout_id, headline, subhead, call_to_action, asset_ids_json) - renders(id, design_id, size_id, path, manifest_json, rendered_at) · the manifest holds font ids, asset hashes, brand version and layout version - A design is stored once; a render is a design plus a size plus the manifest that lets you rebuild that exact file a year later. ### Phase 1 · One design, many shapes Build: Define each output size with its own safe area. Lay a design out as slots anchored to that safe area, and resolve every slot at the target size rather than at a reference size. Render three very different aspect ratios from one design, side by side, and compare them. Trap: Producing the other sizes by multiplying the square design's coordinates by a scale factor pushes the headline off a tall canvas and drops the call to action to nine pixels on a wide banner, and both look acceptable in a thumbnail. Anchor each slot to the target size's safe area and resolve its type size for that size alone. Done when: One design renders into a wide, a square and a tall size with every slot inside its safe area, asserted by comparing slot bounding boxes against the safe rectangle. No slot position is derived by scaling another size's coordinates, verified by review. Adding a new size requires no change to the design. Each rendered size is shown at its real aspect ratio in the preview, not letterboxed into a square. Do not build yet: No fonts of your own, no brand kit, no batch export. ### Phase 2 · Fonts that render the same on two machines Build: Load each font face from a file path and record its family, style, licence name and whether the file may be redistributed. Measure text with the same engine that draws it. When a face is missing, fail the render and name the family rather than falling back to something else. Trap: Naming a font family and letting the system pick means the machine without that family quietly substitutes a fallback, so the same design exports with different line breaks on your laptop and on the machine you render the batch from, and nobody notices until a headline wraps onto three lines in the one size you did not check. Load the file, measure with the engine that draws, and treat a missing face as a failure. Done when: Every font face in the kit records a licence name and a redistributable flag, and the export folder contains no font file marked as not redistributable. Rendering with a face removed from the folder fails with the family and style named, tested by deleting one. Text measurement and text drawing use the same engine, verified by rendering a known string and comparing measured width to drawn width. Two machines with the same font folder produce byte-identical line breaks for the same design. Do not build yet: No brand kit, autofit or export yet. ### Phase 3 · Text that fits and colour you can read Build: Give each slot a minimum and maximum type size and a stated overflow behaviour: shrink to the minimum, then wrap, then mark the render failed. Never clip silently. Compute the contrast between each text slot and whatever sits behind it at that size, against the published minimum ratio for that text size, and show the failing size before export. Trap: Shrinking type until the copy fits gives you a six point headline on the tall size, and clipping gives you a caption that ends mid-word, and both export without complaint because the code only asked whether the text fitted. Set a floor, and when the copy still does not fit, fail that render with the size name and the character count so the copy gets shortened instead. Trap: Contrast checked once against the brand's background colour passes, then the same headline goes over a photograph and becomes invisible on half the sizes because the crop differs per aspect ratio. Sample what is actually behind the text at each size. Done when: Copy too long for a slot at the minimum type size fails that render with the size name and the character count, tested with a deliberately long headline. No render clips text without reporting it. Contrast is computed per size against what is actually behind the text, including a photograph, and a failing size is listed before export. The published ratio used is recorded in the configuration rather than hard-coded in a component. Do not build yet: No batch export or manifests yet. ### Phase 4 · Exports at the size you asked for Build: Render off-screen at the target pixel dimensions, independent of the display's scale factor. Write sRGB. Name files by design and size. Batch every size of a design in one run and assert each output file's dimensions after writing it. Trap: Exporting by reading pixels back from the canvas that is on screen gives you a file at the display's device pixel ratio, so the same button produces a 2160 pixel square on a high density laptop and a 1080 pixel one on an external monitor, and the platform rejects one of them. Render off-screen at the requested dimensions and assert the written file's size. Done when: An exported file's pixel dimensions equal the requested size on both a high density and a standard display, asserted in the suite by reading the written file. Every export declares sRGB rather than inheriting whatever the display profile was. A batch of eight sizes writes eight files with predictable names and no partial file is left behind on failure. Rendering the same design twice produces identical bytes. Do not build yet: No manifests, re-render or kit export yet. ### Phase 5 · Renders you can reproduce next year Build: Write a manifest beside every render recording font ids, asset hashes, brand version, layout version and the size. Add a re-render command that takes a manifest and reproduces the file. Export the whole brand kit as plain folders of images, colour values and a licence list, and bound the export folder with a stated retention. Trap: Editing a brand colour in place rewrites the brand for every past render, so the file a client approved in March can no longer be reproduced and nobody can say which logo version they actually saw. Version the brand kit, and record the version in each render's manifest. Done when: Every render has a manifest naming font ids, asset hashes, brand version and layout version. Re-rendering from a manifest after the brand has been edited reproduces the original file byte for byte, tested by editing a colour and re-rendering. The kit export is plain folders and files readable without this application, including a list of every font face and its licence. The export folder is bounded by a stated retention and the deletion is logged. Do not build yet: Nothing after this. ### Out of scope (and why) - Bundling font files into an export, because permission to embed a face in a document is not the same as permission to pass the file on. The SIL Open Font License, for one, allows a font to be bundled and redistributed with software but forbids selling the font software by itself, while placing no restriction at all on documents you create with it; read each face's licence before you ship its file. - Colour management beyond sRGB. Print profiles are a specialist problem and a wrongly applied profile is worse than none. - A stock template and photograph library, which is a licensing operation rather than a feature. - Cloud collaboration and a mobile editor, which turn a local renderer into a service with accounts. ### README must contain - That every output size carries its own safe area and slots anchor to it, rather than being scaled from a square - That fonts load from files you hold, each face records its licence, and a missing face fails the render - That overlong copy fails a render with the size and character count rather than shrinking or clipping - That exports are rendered off-screen at the requested dimensions, so the display's scale factor never reaches the file - That every render has a manifest and can be reproduced after the brand kit changes ===== AGENTS.md ===== # Agent instructions · RelayThat indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, Slots anchored to a per-size safe area, never coordinates multiplied by a scale factor, One renderer for both the preview and the export, run at the export's real pixel size, Font files loaded by path, measured by the same engine that draws them, SQLite beside a folder of fonts and assets, with a manifest written per render. 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 - Producing the other sizes by multiplying the square design's coordinates by a scale factor pushes the headline off a tall canvas and drops the call to action to nine pixels on a wide banner, and both look acceptable in a thumbnail. Anchor each slot to the target size's safe area and resolve its type size for that size alone. - Naming a font family and letting the system pick means the machine without that family quietly substitutes a fallback, so the same design exports with different line breaks on your laptop and on the machine you render the batch from, and nobody notices until a headline wraps onto three lines in the one size you did not check. Load the file, measure with the engine that draws, and treat a missing face as a failure. - Shrinking type until the copy fits gives you a six point headline on the tall size, and clipping gives you a caption that ends mid-word, and both export without complaint because the code only asked whether the text fitted. Set a floor, and when the copy still does not fit, fail that render with the size name and the character count so the copy gets shortened instead. - Contrast checked once against the brand's background colour passes, then the same headline goes over a photograph and becomes invisible on half the sizes because the crop differs per aspect ratio. Sample what is actually behind the text at each size. - Exporting by reading pixels back from the canvas that is on screen gives you a file at the display's device pixel ratio, so the same button produces a 2160 pixel square on a high density laptop and a 1080 pixel one on an external monitor, and the platform rejects one of them. Render off-screen at the requested dimensions and assert the written file's size. ===== BUILD_PLAN.md ===== # Build plan · RelayThat A local brand design tool whose hard problems are one layout resolving correctly into a dozen aspect ratios, text and fonts that render identically on two machines, and exports whose pixel dimensions are the ones you asked for. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One design, many shapes A design laid out once renders into a wide, a square and a tall size without anything falling off the canvas. ### Steps 1. Give every size a safe area The safe area is what the layout anchors to, so it is part of the size definition. Files: `src/sizes.ts`, `src/layout/anchors.ts` 2. Resolve slots per size, never by scaling Scaling is the bug this whole phase exists to avoid. Files: `src/layout/resolve.ts`, `src/layout/resolve.test.ts` 3. Assert every slot lands inside the safe rectangle A bounding box check catches the failure the eye misses at thumbnail size. Files: `tests/safe-area.test.ts` ### Done when - [ ] Three aspect ratios render from one design - [ ] Every slot lands inside its safe rectangle - [ ] No position is derived by scaling another size - [ ] A new size needs no design change ### Watch out - Producing the other sizes by multiplying the square design's coordinates by a scale factor pushes the headline off a tall canvas and drops the call to action to nine pixels on a wide banner, and both look acceptable in a thumbnail. Anchor each slot to the target size's safe area and resolve its type size for that size alone. ## Phase 2 · Fonts that render the same on two machines Type is drawn from a font file you hold, with its licence recorded, and a missing face fails loudly. ### Steps 1. Load faces by file path and record the licence The licence belongs to the face, and you will want it when you export a kit. Files: `src/fonts/registry.ts`, `src/fonts/licence.ts` 2. Measure with the engine that draws Any other measurement is an estimate that diverges on the long strings. Files: `src/text/measure.ts`, `src/text/measure.test.ts` 3. Fail on a missing face, never substitute A silent fallback is a wrong export you will ship. Files: `src/fonts/registry.ts`, `tests/missing-face.test.ts` ### Done when - [ ] Each face records a licence and a redistributable flag - [ ] A missing face fails with the family named - [ ] Measuring and drawing use one engine - [ ] Two machines produce the same line breaks ### Watch out - Naming a font family and letting the system pick means the machine without that family quietly substitutes a fallback, so the same design exports with different line breaks on your laptop and on the machine you render the batch from, and nobody notices until a headline wraps onto three lines in the one size you did not check. Load the file, measure with the engine that draws, and treat a missing face as a failure. ## Phase 3 · Text that fits and colour you can read Long copy on a narrow size fails visibly rather than exporting something unreadable. ### Steps 1. Floor the type size, then fail A readable failure beats an unreadable export. Files: `src/text/autofit.ts`, `src/text/autofit.test.ts` 2. Sample what is behind the text at each size The crop changes per aspect ratio, so the background does too. Files: `src/contrast/sample.ts` 3. Record the ratio you are checking against Copied from the current guidance, with the date, not invented. Files: `src/contrast/thresholds.ts`, `docs/contrast.md` ### Done when - [ ] Overlong copy fails with size and character count - [ ] No render clips text silently - [ ] Contrast is sampled per size over photographs - [ ] The threshold is recorded, not hard-coded inline ### Watch out - Shrinking type until the copy fits gives you a six point headline on the tall size, and clipping gives you a caption that ends mid-word, and both export without complaint because the code only asked whether the text fitted. Set a floor, and when the copy still does not fit, fail that render with the size name and the character count so the copy gets shortened instead. - Contrast checked once against the brand's background colour passes, then the same headline goes over a photograph and becomes invisible on half the sizes because the crop differs per aspect ratio. Sample what is actually behind the text at each size. ## Phase 4 · Exports at the size you asked for A file requested at 1080 by 1080 is 1080 by 1080 on every display. ### Steps 1. Render off-screen at the target dimensions The display's scale factor must not reach the output. Files: `src/render/offscreen.ts` 2. Assert the written file's dimensions Read the file back; do not trust the call that wrote it. Files: `tests/export-size.test.ts` 3. Write batches atomically A failed size should not leave half a PNG in the folder. Files: `src/render/batch.ts` ### Done when - [ ] Output dimensions match the request on both displays - [ ] Exports declare sRGB - [ ] A failed batch leaves no partial files - [ ] The same design renders to identical bytes twice ### Watch out - Exporting by reading pixels back from the canvas that is on screen gives you a file at the display's device pixel ratio, so the same button produces a 2160 pixel square on a high density laptop and a 1080 pixel one on an external monitor, and the platform rejects one of them. Render off-screen at the requested dimensions and assert the written file's size. ## Not in this build - Font files inside exports. - Print colour profiles. - A stock template and photograph library. - Cloud collaboration and a mobile editor. ## After v1, if you want it - Animated sizes, once static renders have been reproducible for a month - A copy length budget shown while typing, computed from the tightest size - Per-platform size presets imported from a file you maintain rather than hard-coded ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. SQLite plus the copied assets. Back it up with the fonts folder. DATA_DIR=~/.local/share/brandkit # Required. Font files loaded by path. Each face records its licence. FONTS_DIR=~/.local/share/brandkit/fonts # Required. Renders and their manifests, side by side. EXPORT_DIR=~/Pictures/brandkit-exports # Required. How long renders are kept. Manifests let you rebuild after that. EXPORT_RETENTION_DAYS=180 # Required. A ceiling on width times height, so a mistyped size cannot exhaust memory. MAX_RENDER_PIXELS=40000000
You are building a production product version of RelayThat. 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 ===== # RelayThat · product brief ## Problem The visible brand design automation loop is buildable, but a credible replacement needs more than the first screen. RelayThat earns its keep through editor polish, assets, algorithms, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A brand kit that turns one message into every size you post, fails loudly when the copy is too long or the contrast too low, exports files at exactly the dimensions requested, and can rebuild any past render from its manifest. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop development machine - Node.js 22 and Rust toolchain - sample user-owned assets ## Explicit non-goals for v1 - Font files inside exports. - Print colour profiles. - A stock template and photograph library. - Cloud collaboration and a mobile editor. - color-management edge cases - cloud collaboration and mobile apps - pixel-perfect professional tooling - large template and asset libraries ## Success criteria - Three aspect ratios are confirmed to render from one design inside their safe areas - A missing font face is confirmed to fail rather than substitute - Overlong copy is confirmed to fail with size and character count - Export dimensions are confirmed correct on a high density display - A re-render from a manifest after a brand edit is confirmed byte identical ===== BRIEF.md ===== # Build brief · RelayThat 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 brand kit that lays a message out once and renders it into every size you actually post to replace RelayThat. 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) - Shell: Tauri 2 with React and TypeScript · the fonts, the photographs and the exports stay on your disk - Layout: Slots anchored to a per-size safe area, never coordinates multiplied by a scale factor · a square design stretched into a portrait one puts the headline off the canvas - Rendering: One renderer for both the preview and the export, run at the export's real pixel size · two renderers means the preview is a guess - Text: Font files loaded by path, measured by the same engine that draws them · naming a family and hoping is how two machines produce different line breaks - Storage: SQLite beside a folder of fonts and assets, with a manifest written per render · an export you cannot reproduce next month is a screenshot ### Data model (create this before Phase 1) - brands(id, name, version, palette_json, logo_asset_ids, font_ids, created_at) · versioned, because a render has to say which brand it used - fonts(id, family, style, file_path, licence_name, licence_url, redistributable, added_at) · the licence is recorded per face, not per project - assets(id, kind, path, width, height, sha256, has_alpha) · logos and photographs copied in, never referenced from somewhere else on disk - sizes(id, name, width_px, height_px, safe_top, safe_right, safe_bottom, safe_left) · every output shape carries its own safe area - layouts(id, name, version, slots_json) · a slot has an anchor, a minimum and maximum type size, and a stated behaviour when the text still does not fit - designs(id, brand_id, layout_id, headline, subhead, call_to_action, asset_ids_json) - renders(id, design_id, size_id, path, manifest_json, rendered_at) · the manifest holds font ids, asset hashes, brand version and layout version - A design is stored once; a render is a design plus a size plus the manifest that lets you rebuild that exact file a year later. ### Phase 1 · One design, many shapes Build: Define each output size with its own safe area. Lay a design out as slots anchored to that safe area, and resolve every slot at the target size rather than at a reference size. Render three very different aspect ratios from one design, side by side, and compare them. Trap: Producing the other sizes by multiplying the square design's coordinates by a scale factor pushes the headline off a tall canvas and drops the call to action to nine pixels on a wide banner, and both look acceptable in a thumbnail. Anchor each slot to the target size's safe area and resolve its type size for that size alone. Done when: One design renders into a wide, a square and a tall size with every slot inside its safe area, asserted by comparing slot bounding boxes against the safe rectangle. No slot position is derived by scaling another size's coordinates, verified by review. Adding a new size requires no change to the design. Each rendered size is shown at its real aspect ratio in the preview, not letterboxed into a square. Do not build yet: No fonts of your own, no brand kit, no batch export. ### Phase 2 · Fonts that render the same on two machines Build: Load each font face from a file path and record its family, style, licence name and whether the file may be redistributed. Measure text with the same engine that draws it. When a face is missing, fail the render and name the family rather than falling back to something else. Trap: Naming a font family and letting the system pick means the machine without that family quietly substitutes a fallback, so the same design exports with different line breaks on your laptop and on the machine you render the batch from, and nobody notices until a headline wraps onto three lines in the one size you did not check. Load the file, measure with the engine that draws, and treat a missing face as a failure. Done when: Every font face in the kit records a licence name and a redistributable flag, and the export folder contains no font file marked as not redistributable. Rendering with a face removed from the folder fails with the family and style named, tested by deleting one. Text measurement and text drawing use the same engine, verified by rendering a known string and comparing measured width to drawn width. Two machines with the same font folder produce byte-identical line breaks for the same design. Do not build yet: No brand kit, autofit or export yet. ### Phase 3 · Text that fits and colour you can read Build: Give each slot a minimum and maximum type size and a stated overflow behaviour: shrink to the minimum, then wrap, then mark the render failed. Never clip silently. Compute the contrast between each text slot and whatever sits behind it at that size, against the published minimum ratio for that text size, and show the failing size before export. Trap: Shrinking type until the copy fits gives you a six point headline on the tall size, and clipping gives you a caption that ends mid-word, and both export without complaint because the code only asked whether the text fitted. Set a floor, and when the copy still does not fit, fail that render with the size name and the character count so the copy gets shortened instead. Trap: Contrast checked once against the brand's background colour passes, then the same headline goes over a photograph and becomes invisible on half the sizes because the crop differs per aspect ratio. Sample what is actually behind the text at each size. Done when: Copy too long for a slot at the minimum type size fails that render with the size name and the character count, tested with a deliberately long headline. No render clips text without reporting it. Contrast is computed per size against what is actually behind the text, including a photograph, and a failing size is listed before export. The published ratio used is recorded in the configuration rather than hard-coded in a component. Do not build yet: No batch export or manifests yet. ### Phase 4 · Exports at the size you asked for Build: Render off-screen at the target pixel dimensions, independent of the display's scale factor. Write sRGB. Name files by design and size. Batch every size of a design in one run and assert each output file's dimensions after writing it. Trap: Exporting by reading pixels back from the canvas that is on screen gives you a file at the display's device pixel ratio, so the same button produces a 2160 pixel square on a high density laptop and a 1080 pixel one on an external monitor, and the platform rejects one of them. Render off-screen at the requested dimensions and assert the written file's size. Done when: An exported file's pixel dimensions equal the requested size on both a high density and a standard display, asserted in the suite by reading the written file. Every export declares sRGB rather than inheriting whatever the display profile was. A batch of eight sizes writes eight files with predictable names and no partial file is left behind on failure. Rendering the same design twice produces identical bytes. Do not build yet: No manifests, re-render or kit export yet. ### Phase 5 · Renders you can reproduce next year Build: Write a manifest beside every render recording font ids, asset hashes, brand version, layout version and the size. Add a re-render command that takes a manifest and reproduces the file. Export the whole brand kit as plain folders of images, colour values and a licence list, and bound the export folder with a stated retention. Trap: Editing a brand colour in place rewrites the brand for every past render, so the file a client approved in March can no longer be reproduced and nobody can say which logo version they actually saw. Version the brand kit, and record the version in each render's manifest. Done when: Every render has a manifest naming font ids, asset hashes, brand version and layout version. Re-rendering from a manifest after the brand has been edited reproduces the original file byte for byte, tested by editing a colour and re-rendering. The kit export is plain folders and files readable without this application, including a list of every font face and its licence. The export folder is bounded by a stated retention and the deletion is logged. Do not build yet: Nothing after this. ### Out of scope (and why) - Bundling font files into an export, because permission to embed a face in a document is not the same as permission to pass the file on. The SIL Open Font License, for one, allows a font to be bundled and redistributed with software but forbids selling the font software by itself, while placing no restriction at all on documents you create with it; read each face's licence before you ship its file. - Colour management beyond sRGB. Print profiles are a specialist problem and a wrongly applied profile is worse than none. - A stock template and photograph library, which is a licensing operation rather than a feature. - Cloud collaboration and a mobile editor, which turn a local renderer into a service with accounts. ### README must contain - That every output size carries its own safe area and slots anchor to it, rather than being scaled from a square - That fonts load from files you hold, each face records its licence, and a missing face fails the render - That overlong copy fails a render with the size and character count rather than shrinking or clipping - That exports are rendered off-screen at the requested dimensions, so the display's scale factor never reaches the file - That every render has a manifest and can be reproduced after the brand kit changes ===== ARCHITECTURE.md ===== # Architecture · RelayThat ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript | the fonts, the photographs and the exports stay on your disk | | Layout | Slots anchored to a per-size safe area, never coordinates multiplied by a scale factor | a square design stretched into a portrait one puts the headline off the canvas | | Rendering | One renderer for both the preview and the export, run at the export's real pixel size | two renderers means the preview is a guess | | Text | Font files loaded by path, measured by the same engine that draws them | naming a family and hoping is how two machines produce different line breaks | | Storage | SQLite beside a folder of fonts and assets, with a manifest written per render | an export you cannot reproduce next month is a screenshot | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Sizes | Output dimensions and safe areas. | Any list of sizes, provided each one carries its own safe area. | | Layout | Slot resolution per target size. | Any engine, provided nothing is derived by scaling another size. | | Fonts | Faces loaded by path with recorded licences. | Any loader that fails on a missing face rather than substituting. | | Render | Off-screen rendering at real dimensions. | Any renderer whose output dimensions are asserted after writing. | | Manifest | The record that makes a render reproducible. | Not swappable for nothing, which is what a screenshot is. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATA_DIR` · required · SQLite plus the copied assets. Back it up with the fonts folder. - `FONTS_DIR` · required · Font files loaded by path. Each face records its licence. - `EXPORT_DIR` · required · Renders and their manifests, side by side. - `EXPORT_RETENTION_DAYS` · required · How long renders are kept. Manifests let you rebuild after that. - `MAX_RENDER_PIXELS` · required · A ceiling on width times height, so a mistyped size cannot exhaust memory. ## 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 · RelayThat product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with React and TypeScript, Slots anchored to a per-size safe area, never coordinates multiplied by a scale factor, One renderer for both the preview and the export, run at the export's real pixel size, Font files loaded by path, measured by the same engine that draws them, SQLite beside a folder of fonts and assets, with a manifest written per render. - 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 - Producing the other sizes by multiplying the square design's coordinates by a scale factor pushes the headline off a tall canvas and drops the call to action to nine pixels on a wide banner, and both look acceptable in a thumbnail. Anchor each slot to the target size's safe area and resolve its type size for that size alone. - Naming a font family and letting the system pick means the machine without that family quietly substitutes a fallback, so the same design exports with different line breaks on your laptop and on the machine you render the batch from, and nobody notices until a headline wraps onto three lines in the one size you did not check. Load the file, measure with the engine that draws, and treat a missing face as a failure. - Shrinking type until the copy fits gives you a six point headline on the tall size, and clipping gives you a caption that ends mid-word, and both export without complaint because the code only asked whether the text fitted. Set a floor, and when the copy still does not fit, fail that render with the size name and the character count so the copy gets shortened instead. - Contrast checked once against the brand's background colour passes, then the same headline goes over a photograph and becomes invisible on half the sizes because the crop differs per aspect ratio. Sample what is actually behind the text at each size. - Exporting by reading pixels back from the canvas that is on screen gives you a file at the display's device pixel ratio, so the same button produces a 2160 pixel square on a high density laptop and a 1080 pixel one on an external monitor, and the platform rejects one of them. Render off-screen at the requested dimensions and assert the written file's size. - Editing a brand colour in place rewrites the brand for every past render, so the file a client approved in March can no longer be reproduced and nobody can say which logo version they actually saw. Version the brand kit, and record the version in each render's manifest. ===== MILESTONES.md ===== # Delivery milestones · RelayThat Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · One design, many shapes A design laid out once renders into a wide, a square and a tall size without anything falling off the canvas. ### Steps 1. Give every size a safe area The safe area is what the layout anchors to, so it is part of the size definition. Files: `src/sizes.ts`, `src/layout/anchors.ts` 2. Resolve slots per size, never by scaling Scaling is the bug this whole phase exists to avoid. Files: `src/layout/resolve.ts`, `src/layout/resolve.test.ts` 3. Assert every slot lands inside the safe rectangle A bounding box check catches the failure the eye misses at thumbnail size. Files: `tests/safe-area.test.ts` ### Done when - [ ] Three aspect ratios render from one design - [ ] Every slot lands inside its safe rectangle - [ ] No position is derived by scaling another size - [ ] A new size needs no design change ### Watch out - Producing the other sizes by multiplying the square design's coordinates by a scale factor pushes the headline off a tall canvas and drops the call to action to nine pixels on a wide banner, and both look acceptable in a thumbnail. Anchor each slot to the target size's safe area and resolve its type size for that size alone. ## M2 · Fonts that render the same on two machines Type is drawn from a font file you hold, with its licence recorded, and a missing face fails loudly. ### Steps 1. Load faces by file path and record the licence The licence belongs to the face, and you will want it when you export a kit. Files: `src/fonts/registry.ts`, `src/fonts/licence.ts` 2. Measure with the engine that draws Any other measurement is an estimate that diverges on the long strings. Files: `src/text/measure.ts`, `src/text/measure.test.ts` 3. Fail on a missing face, never substitute A silent fallback is a wrong export you will ship. Files: `src/fonts/registry.ts`, `tests/missing-face.test.ts` ### Done when - [ ] Each face records a licence and a redistributable flag - [ ] A missing face fails with the family named - [ ] Measuring and drawing use one engine - [ ] Two machines produce the same line breaks ### Watch out - Naming a font family and letting the system pick means the machine without that family quietly substitutes a fallback, so the same design exports with different line breaks on your laptop and on the machine you render the batch from, and nobody notices until a headline wraps onto three lines in the one size you did not check. Load the file, measure with the engine that draws, and treat a missing face as a failure. ## M3 · Text that fits and colour you can read Long copy on a narrow size fails visibly rather than exporting something unreadable. ### Steps 1. Floor the type size, then fail A readable failure beats an unreadable export. Files: `src/text/autofit.ts`, `src/text/autofit.test.ts` 2. Sample what is behind the text at each size The crop changes per aspect ratio, so the background does too. Files: `src/contrast/sample.ts` 3. Record the ratio you are checking against Copied from the current guidance, with the date, not invented. Files: `src/contrast/thresholds.ts`, `docs/contrast.md` ### Done when - [ ] Overlong copy fails with size and character count - [ ] No render clips text silently - [ ] Contrast is sampled per size over photographs - [ ] The threshold is recorded, not hard-coded inline ### Watch out - Shrinking type until the copy fits gives you a six point headline on the tall size, and clipping gives you a caption that ends mid-word, and both export without complaint because the code only asked whether the text fitted. Set a floor, and when the copy still does not fit, fail that render with the size name and the character count so the copy gets shortened instead. - Contrast checked once against the brand's background colour passes, then the same headline goes over a photograph and becomes invisible on half the sizes because the crop differs per aspect ratio. Sample what is actually behind the text at each size. ## M4 · Exports at the size you asked for A file requested at 1080 by 1080 is 1080 by 1080 on every display. ### Steps 1. Render off-screen at the target dimensions The display's scale factor must not reach the output. Files: `src/render/offscreen.ts` 2. Assert the written file's dimensions Read the file back; do not trust the call that wrote it. Files: `tests/export-size.test.ts` 3. Write batches atomically A failed size should not leave half a PNG in the folder. Files: `src/render/batch.ts` ### Done when - [ ] Output dimensions match the request on both displays - [ ] Exports declare sRGB - [ ] A failed batch leaves no partial files - [ ] The same design renders to identical bytes twice ### Watch out - Exporting by reading pixels back from the canvas that is on screen gives you a file at the display's device pixel ratio, so the same button produces a 2160 pixel square on a high density laptop and a 1080 pixel one on an external monitor, and the platform rejects one of them. Render off-screen at the requested dimensions and assert the written file's size. ## M5 · Renders you can reproduce next year (production only) Any file in the export folder can be traced to the exact inputs that made it, and rebuilt. ### Steps 1. Write a manifest with every render Font ids, asset hashes, brand and layout versions, size. Files: `src/render/manifest.ts` 2. Version the brand kit rather than editing it An edited brand makes every past render unreproducible. Files: `src/brands/versions.ts`, `tests/rerender.test.ts` 3. Export the kit as plain files Folders of images and a text list of colours and font licences. Files: `src/export/kit.ts` ### Done when - [ ] Every render has a manifest with versions and hashes - [ ] Re-render after a brand edit reproduces the original - [ ] The kit exports as plain readable files - [ ] The export folder honours a stated retention ### Watch out - Editing a brand colour in place rewrites the brand for every past render, so the file a client approved in March can no longer be reproduced and nobody can say which logo version they actually saw. Version the brand kit, and record the version in each render's manifest. ===== OPERATIONS.md ===== # Operations · RelayThat ## Backup The data folder and the fonts folder together. A backup of the database without the fonts restores designs that cannot be rendered, and a backup of the exports without the manifests restores files nobody can rebuild. ## Restore Restore both folders, then re-render one manifest from before the backup and compare bytes. That single check proves the fonts, the assets and the brand versions all came back. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the checks live in the suite: output dimensions on both display types, a missing face failing, overlong copy failing, and a re-render after a brand edit matching the original. ## Incident checklist A wrong-sized export means the display scale factor reached the renderer. A design that suddenly wraps differently means a font face changed or went missing. Both are answered by the manifest rather than by memory. 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 - [ ] Three aspect ratios are confirmed to render from one design inside their safe areas - [ ] A missing font face is confirmed to fail rather than substitute - [ ] Overlong copy is confirmed to fail with size and character count - [ ] Export dimensions are confirmed correct on a high density display - [ ] A re-render from a manifest after a brand edit is confirmed byte identical ## Launch constraint Do not market omitted RelayThat 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. SQLite plus the copied assets. Back it up with the fonts folder. DATA_DIR=~/.local/share/brandkit # Required. Font files loaded by path. Each face records its licence. FONTS_DIR=~/.local/share/brandkit/fonts # Required. Renders and their manifests, side by side. EXPORT_DIR=~/Pictures/brandkit-exports # Required. How long renders are kept. Manifests let you rebuild after that. EXPORT_RETENTION_DAYS=180 # Required. A ceiling on width times height, so a mistyped size cannot exhaust memory. MAX_RENDER_PIXELS=40000000
# RelayThat · indie build A local brand design tool whose hard problems are one layout resolving correctly into a dozen aspect ratios, text and fonts that render identically on two machines, and exports whose pixel dimensions are the ones you asked for. Estimated effort: **weekend**. 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 | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript | the fonts, the photographs and the exports stay on your disk | | Layout | Slots anchored to a per-size safe area, never coordinates multiplied by a scale factor | a square design stretched into a portrait one puts the headline off the canvas | | Rendering | One renderer for both the preview and the export, run at the export's real pixel size | two renderers means the preview is a guess | | Text | Font files loaded by path, measured by the same engine that draws them | naming a family and hoping is how two machines produce different line breaks | | Storage | SQLite beside a folder of fonts and assets, with a manifest written per render | an export you cannot reproduce next month is a screenshot | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 and the Rust toolchain** · free - Why: Tauri 2 needs both, and the desktop shell is what gives you the font folder and the export folder. - Get it: nodejs.org and rustup.rs, then the Tauri prerequisites for your platform. - Verify: node --version prints v22 or higher and cargo --version succeeds - [ ] **Font files you are licensed to use** · free for open licensed families, otherwise whatever you paid - Why: The whole rendering model loads faces by file, and the licence of each face decides whether it may travel in an export. - Get it: Your own purchased faces, or open licensed families whose licence text you have actually read. - Verify: Every face in the folder has a recorded licence name and a redistributable flag - [ ] **Your logo files and a few photographs** · free - Why: Contrast over a photograph behaves nothing like contrast over a flat colour, and it is the case that breaks first. - Get it: Export your logo with transparency at a generous size, and pick two photographs with busy areas. - Verify: A headline over the busy photograph is flagged at some sizes and not others - [ ] **The current contrast guidance** · free - Why: The minimum ratio is copied from published guidance rather than chosen, and it differs by text size. - Get it: Read the current accessibility guidance for contrast, note the ratios and the date, and record both in your configuration. - Verify: A note records the ratios used and the guidance version - [ ] **A high density and a standard display** · free - Why: The export dimension bug only appears when the display scale factor differs from one, and it is the most common way a render comes out the wrong size. - Get it: A laptop with a high density panel plus any ordinary external monitor. - Verify: The same export produces identical dimensions on both ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Font files inside exports. - Print colour profiles. - A stock template and photograph library. - Cloud collaboration and a mobile editor. - color-management edge cases - cloud collaboration and mobile apps - pixel-perfect professional tooling - large template and asset libraries If one of those is essential to you, that is the reason to keep paying for RelayThat, and the README should say so rather than pretend.
# Build brief · RelayThat 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 brand kit that lays a message out once and renders it into every size you actually post to replace RelayThat. 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) - Shell: Tauri 2 with React and TypeScript · the fonts, the photographs and the exports stay on your disk - Layout: Slots anchored to a per-size safe area, never coordinates multiplied by a scale factor · a square design stretched into a portrait one puts the headline off the canvas - Rendering: One renderer for both the preview and the export, run at the export's real pixel size · two renderers means the preview is a guess - Text: Font files loaded by path, measured by the same engine that draws them · naming a family and hoping is how two machines produce different line breaks - Storage: SQLite beside a folder of fonts and assets, with a manifest written per render · an export you cannot reproduce next month is a screenshot ### Data model (create this before Phase 1) - brands(id, name, version, palette_json, logo_asset_ids, font_ids, created_at) · versioned, because a render has to say which brand it used - fonts(id, family, style, file_path, licence_name, licence_url, redistributable, added_at) · the licence is recorded per face, not per project - assets(id, kind, path, width, height, sha256, has_alpha) · logos and photographs copied in, never referenced from somewhere else on disk - sizes(id, name, width_px, height_px, safe_top, safe_right, safe_bottom, safe_left) · every output shape carries its own safe area - layouts(id, name, version, slots_json) · a slot has an anchor, a minimum and maximum type size, and a stated behaviour when the text still does not fit - designs(id, brand_id, layout_id, headline, subhead, call_to_action, asset_ids_json) - renders(id, design_id, size_id, path, manifest_json, rendered_at) · the manifest holds font ids, asset hashes, brand version and layout version - A design is stored once; a render is a design plus a size plus the manifest that lets you rebuild that exact file a year later. ### Phase 1 · One design, many shapes Build: Define each output size with its own safe area. Lay a design out as slots anchored to that safe area, and resolve every slot at the target size rather than at a reference size. Render three very different aspect ratios from one design, side by side, and compare them. Trap: Producing the other sizes by multiplying the square design's coordinates by a scale factor pushes the headline off a tall canvas and drops the call to action to nine pixels on a wide banner, and both look acceptable in a thumbnail. Anchor each slot to the target size's safe area and resolve its type size for that size alone. Done when: One design renders into a wide, a square and a tall size with every slot inside its safe area, asserted by comparing slot bounding boxes against the safe rectangle. No slot position is derived by scaling another size's coordinates, verified by review. Adding a new size requires no change to the design. Each rendered size is shown at its real aspect ratio in the preview, not letterboxed into a square. Do not build yet: No fonts of your own, no brand kit, no batch export. ### Phase 2 · Fonts that render the same on two machines Build: Load each font face from a file path and record its family, style, licence name and whether the file may be redistributed. Measure text with the same engine that draws it. When a face is missing, fail the render and name the family rather than falling back to something else. Trap: Naming a font family and letting the system pick means the machine without that family quietly substitutes a fallback, so the same design exports with different line breaks on your laptop and on the machine you render the batch from, and nobody notices until a headline wraps onto three lines in the one size you did not check. Load the file, measure with the engine that draws, and treat a missing face as a failure. Done when: Every font face in the kit records a licence name and a redistributable flag, and the export folder contains no font file marked as not redistributable. Rendering with a face removed from the folder fails with the family and style named, tested by deleting one. Text measurement and text drawing use the same engine, verified by rendering a known string and comparing measured width to drawn width. Two machines with the same font folder produce byte-identical line breaks for the same design. Do not build yet: No brand kit, autofit or export yet. ### Phase 3 · Text that fits and colour you can read Build: Give each slot a minimum and maximum type size and a stated overflow behaviour: shrink to the minimum, then wrap, then mark the render failed. Never clip silently. Compute the contrast between each text slot and whatever sits behind it at that size, against the published minimum ratio for that text size, and show the failing size before export. Trap: Shrinking type until the copy fits gives you a six point headline on the tall size, and clipping gives you a caption that ends mid-word, and both export without complaint because the code only asked whether the text fitted. Set a floor, and when the copy still does not fit, fail that render with the size name and the character count so the copy gets shortened instead. Trap: Contrast checked once against the brand's background colour passes, then the same headline goes over a photograph and becomes invisible on half the sizes because the crop differs per aspect ratio. Sample what is actually behind the text at each size. Done when: Copy too long for a slot at the minimum type size fails that render with the size name and the character count, tested with a deliberately long headline. No render clips text without reporting it. Contrast is computed per size against what is actually behind the text, including a photograph, and a failing size is listed before export. The published ratio used is recorded in the configuration rather than hard-coded in a component. Do not build yet: No batch export or manifests yet. ### Phase 4 · Exports at the size you asked for Build: Render off-screen at the target pixel dimensions, independent of the display's scale factor. Write sRGB. Name files by design and size. Batch every size of a design in one run and assert each output file's dimensions after writing it. Trap: Exporting by reading pixels back from the canvas that is on screen gives you a file at the display's device pixel ratio, so the same button produces a 2160 pixel square on a high density laptop and a 1080 pixel one on an external monitor, and the platform rejects one of them. Render off-screen at the requested dimensions and assert the written file's size. Done when: An exported file's pixel dimensions equal the requested size on both a high density and a standard display, asserted in the suite by reading the written file. Every export declares sRGB rather than inheriting whatever the display profile was. A batch of eight sizes writes eight files with predictable names and no partial file is left behind on failure. Rendering the same design twice produces identical bytes. Do not build yet: No manifests, re-render or kit export yet. ### Phase 5 · Renders you can reproduce next year Build: Write a manifest beside every render recording font ids, asset hashes, brand version, layout version and the size. Add a re-render command that takes a manifest and reproduces the file. Export the whole brand kit as plain folders of images, colour values and a licence list, and bound the export folder with a stated retention. Trap: Editing a brand colour in place rewrites the brand for every past render, so the file a client approved in March can no longer be reproduced and nobody can say which logo version they actually saw. Version the brand kit, and record the version in each render's manifest. Done when: Every render has a manifest naming font ids, asset hashes, brand version and layout version. Re-rendering from a manifest after the brand has been edited reproduces the original file byte for byte, tested by editing a colour and re-rendering. The kit export is plain folders and files readable without this application, including a list of every font face and its licence. The export folder is bounded by a stated retention and the deletion is logged. Do not build yet: Nothing after this. ### Out of scope (and why) - Bundling font files into an export, because permission to embed a face in a document is not the same as permission to pass the file on. The SIL Open Font License, for one, allows a font to be bundled and redistributed with software but forbids selling the font software by itself, while placing no restriction at all on documents you create with it; read each face's licence before you ship its file. - Colour management beyond sRGB. Print profiles are a specialist problem and a wrongly applied profile is worse than none. - A stock template and photograph library, which is a licensing operation rather than a feature. - Cloud collaboration and a mobile editor, which turn a local renderer into a service with accounts. ### README must contain - That every output size carries its own safe area and slots anchor to it, rather than being scaled from a square - That fonts load from files you hold, each face records its licence, and a missing face fails the render - That overlong copy fails a render with the size and character count rather than shrinking or clipping - That exports are rendered off-screen at the requested dimensions, so the display's scale factor never reaches the file - That every render has a manifest and can be reproduced after the brand kit changes
# Agent instructions · RelayThat indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, Slots anchored to a per-size safe area, never coordinates multiplied by a scale factor, One renderer for both the preview and the export, run at the export's real pixel size, Font files loaded by path, measured by the same engine that draws them, SQLite beside a folder of fonts and assets, with a manifest written per render. 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 - Producing the other sizes by multiplying the square design's coordinates by a scale factor pushes the headline off a tall canvas and drops the call to action to nine pixels on a wide banner, and both look acceptable in a thumbnail. Anchor each slot to the target size's safe area and resolve its type size for that size alone. - Naming a font family and letting the system pick means the machine without that family quietly substitutes a fallback, so the same design exports with different line breaks on your laptop and on the machine you render the batch from, and nobody notices until a headline wraps onto three lines in the one size you did not check. Load the file, measure with the engine that draws, and treat a missing face as a failure. - Shrinking type until the copy fits gives you a six point headline on the tall size, and clipping gives you a caption that ends mid-word, and both export without complaint because the code only asked whether the text fitted. Set a floor, and when the copy still does not fit, fail that render with the size name and the character count so the copy gets shortened instead. - Contrast checked once against the brand's background colour passes, then the same headline goes over a photograph and becomes invisible on half the sizes because the crop differs per aspect ratio. Sample what is actually behind the text at each size. - Exporting by reading pixels back from the canvas that is on screen gives you a file at the display's device pixel ratio, so the same button produces a 2160 pixel square on a high density laptop and a 1080 pixel one on an external monitor, and the platform rejects one of them. Render off-screen at the requested dimensions and assert the written file's size.
# Build plan · RelayThat A local brand design tool whose hard problems are one layout resolving correctly into a dozen aspect ratios, text and fonts that render identically on two machines, and exports whose pixel dimensions are the ones you asked for. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One design, many shapes A design laid out once renders into a wide, a square and a tall size without anything falling off the canvas. ### Steps 1. Give every size a safe area The safe area is what the layout anchors to, so it is part of the size definition. Files: `src/sizes.ts`, `src/layout/anchors.ts` 2. Resolve slots per size, never by scaling Scaling is the bug this whole phase exists to avoid. Files: `src/layout/resolve.ts`, `src/layout/resolve.test.ts` 3. Assert every slot lands inside the safe rectangle A bounding box check catches the failure the eye misses at thumbnail size. Files: `tests/safe-area.test.ts` ### Done when - [ ] Three aspect ratios render from one design - [ ] Every slot lands inside its safe rectangle - [ ] No position is derived by scaling another size - [ ] A new size needs no design change ### Watch out - Producing the other sizes by multiplying the square design's coordinates by a scale factor pushes the headline off a tall canvas and drops the call to action to nine pixels on a wide banner, and both look acceptable in a thumbnail. Anchor each slot to the target size's safe area and resolve its type size for that size alone. ## Phase 2 · Fonts that render the same on two machines Type is drawn from a font file you hold, with its licence recorded, and a missing face fails loudly. ### Steps 1. Load faces by file path and record the licence The licence belongs to the face, and you will want it when you export a kit. Files: `src/fonts/registry.ts`, `src/fonts/licence.ts` 2. Measure with the engine that draws Any other measurement is an estimate that diverges on the long strings. Files: `src/text/measure.ts`, `src/text/measure.test.ts` 3. Fail on a missing face, never substitute A silent fallback is a wrong export you will ship. Files: `src/fonts/registry.ts`, `tests/missing-face.test.ts` ### Done when - [ ] Each face records a licence and a redistributable flag - [ ] A missing face fails with the family named - [ ] Measuring and drawing use one engine - [ ] Two machines produce the same line breaks ### Watch out - Naming a font family and letting the system pick means the machine without that family quietly substitutes a fallback, so the same design exports with different line breaks on your laptop and on the machine you render the batch from, and nobody notices until a headline wraps onto three lines in the one size you did not check. Load the file, measure with the engine that draws, and treat a missing face as a failure. ## Phase 3 · Text that fits and colour you can read Long copy on a narrow size fails visibly rather than exporting something unreadable. ### Steps 1. Floor the type size, then fail A readable failure beats an unreadable export. Files: `src/text/autofit.ts`, `src/text/autofit.test.ts` 2. Sample what is behind the text at each size The crop changes per aspect ratio, so the background does too. Files: `src/contrast/sample.ts` 3. Record the ratio you are checking against Copied from the current guidance, with the date, not invented. Files: `src/contrast/thresholds.ts`, `docs/contrast.md` ### Done when - [ ] Overlong copy fails with size and character count - [ ] No render clips text silently - [ ] Contrast is sampled per size over photographs - [ ] The threshold is recorded, not hard-coded inline ### Watch out - Shrinking type until the copy fits gives you a six point headline on the tall size, and clipping gives you a caption that ends mid-word, and both export without complaint because the code only asked whether the text fitted. Set a floor, and when the copy still does not fit, fail that render with the size name and the character count so the copy gets shortened instead. - Contrast checked once against the brand's background colour passes, then the same headline goes over a photograph and becomes invisible on half the sizes because the crop differs per aspect ratio. Sample what is actually behind the text at each size. ## Phase 4 · Exports at the size you asked for A file requested at 1080 by 1080 is 1080 by 1080 on every display. ### Steps 1. Render off-screen at the target dimensions The display's scale factor must not reach the output. Files: `src/render/offscreen.ts` 2. Assert the written file's dimensions Read the file back; do not trust the call that wrote it. Files: `tests/export-size.test.ts` 3. Write batches atomically A failed size should not leave half a PNG in the folder. Files: `src/render/batch.ts` ### Done when - [ ] Output dimensions match the request on both displays - [ ] Exports declare sRGB - [ ] A failed batch leaves no partial files - [ ] The same design renders to identical bytes twice ### Watch out - Exporting by reading pixels back from the canvas that is on screen gives you a file at the display's device pixel ratio, so the same button produces a 2160 pixel square on a high density laptop and a 1080 pixel one on an external monitor, and the platform rejects one of them. Render off-screen at the requested dimensions and assert the written file's size. ## Not in this build - Font files inside exports. - Print colour profiles. - A stock template and photograph library. - Cloud collaboration and a mobile editor. ## After v1, if you want it - Animated sizes, once static renders have been reproducible for a month - A copy length budget shown while typing, computed from the tightest size - Per-platform size presets imported from a file you maintain rather than hard-coded
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. SQLite plus the copied assets. Back it up with the fonts folder. DATA_DIR=~/.local/share/brandkit # Required. Font files loaded by path. Each face records its licence. FONTS_DIR=~/.local/share/brandkit/fonts # Required. Renders and their manifests, side by side. EXPORT_DIR=~/Pictures/brandkit-exports # Required. How long renders are kept. Manifests let you rebuild after that. EXPORT_RETENTION_DAYS=180 # Required. A ceiling on width times height, so a mistyped size cannot exhaust memory. MAX_RENDER_PIXELS=40000000
# RelayThat · product brief ## Problem The visible brand design automation loop is buildable, but a credible replacement needs more than the first screen. RelayThat earns its keep through editor polish, assets, algorithms, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A brand kit that turns one message into every size you post, fails loudly when the copy is too long or the contrast too low, exports files at exactly the dimensions requested, and can rebuild any past render from its manifest. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop development machine - Node.js 22 and Rust toolchain - sample user-owned assets ## Explicit non-goals for v1 - Font files inside exports. - Print colour profiles. - A stock template and photograph library. - Cloud collaboration and a mobile editor. - color-management edge cases - cloud collaboration and mobile apps - pixel-perfect professional tooling - large template and asset libraries ## Success criteria - Three aspect ratios are confirmed to render from one design inside their safe areas - A missing font face is confirmed to fail rather than substitute - Overlong copy is confirmed to fail with size and character count - Export dimensions are confirmed correct on a high density display - A re-render from a manifest after a brand edit is confirmed byte identical
# Build brief · RelayThat 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 brand kit that lays a message out once and renders it into every size you actually post to replace RelayThat. 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) - Shell: Tauri 2 with React and TypeScript · the fonts, the photographs and the exports stay on your disk - Layout: Slots anchored to a per-size safe area, never coordinates multiplied by a scale factor · a square design stretched into a portrait one puts the headline off the canvas - Rendering: One renderer for both the preview and the export, run at the export's real pixel size · two renderers means the preview is a guess - Text: Font files loaded by path, measured by the same engine that draws them · naming a family and hoping is how two machines produce different line breaks - Storage: SQLite beside a folder of fonts and assets, with a manifest written per render · an export you cannot reproduce next month is a screenshot ### Data model (create this before Phase 1) - brands(id, name, version, palette_json, logo_asset_ids, font_ids, created_at) · versioned, because a render has to say which brand it used - fonts(id, family, style, file_path, licence_name, licence_url, redistributable, added_at) · the licence is recorded per face, not per project - assets(id, kind, path, width, height, sha256, has_alpha) · logos and photographs copied in, never referenced from somewhere else on disk - sizes(id, name, width_px, height_px, safe_top, safe_right, safe_bottom, safe_left) · every output shape carries its own safe area - layouts(id, name, version, slots_json) · a slot has an anchor, a minimum and maximum type size, and a stated behaviour when the text still does not fit - designs(id, brand_id, layout_id, headline, subhead, call_to_action, asset_ids_json) - renders(id, design_id, size_id, path, manifest_json, rendered_at) · the manifest holds font ids, asset hashes, brand version and layout version - A design is stored once; a render is a design plus a size plus the manifest that lets you rebuild that exact file a year later. ### Phase 1 · One design, many shapes Build: Define each output size with its own safe area. Lay a design out as slots anchored to that safe area, and resolve every slot at the target size rather than at a reference size. Render three very different aspect ratios from one design, side by side, and compare them. Trap: Producing the other sizes by multiplying the square design's coordinates by a scale factor pushes the headline off a tall canvas and drops the call to action to nine pixels on a wide banner, and both look acceptable in a thumbnail. Anchor each slot to the target size's safe area and resolve its type size for that size alone. Done when: One design renders into a wide, a square and a tall size with every slot inside its safe area, asserted by comparing slot bounding boxes against the safe rectangle. No slot position is derived by scaling another size's coordinates, verified by review. Adding a new size requires no change to the design. Each rendered size is shown at its real aspect ratio in the preview, not letterboxed into a square. Do not build yet: No fonts of your own, no brand kit, no batch export. ### Phase 2 · Fonts that render the same on two machines Build: Load each font face from a file path and record its family, style, licence name and whether the file may be redistributed. Measure text with the same engine that draws it. When a face is missing, fail the render and name the family rather than falling back to something else. Trap: Naming a font family and letting the system pick means the machine without that family quietly substitutes a fallback, so the same design exports with different line breaks on your laptop and on the machine you render the batch from, and nobody notices until a headline wraps onto three lines in the one size you did not check. Load the file, measure with the engine that draws, and treat a missing face as a failure. Done when: Every font face in the kit records a licence name and a redistributable flag, and the export folder contains no font file marked as not redistributable. Rendering with a face removed from the folder fails with the family and style named, tested by deleting one. Text measurement and text drawing use the same engine, verified by rendering a known string and comparing measured width to drawn width. Two machines with the same font folder produce byte-identical line breaks for the same design. Do not build yet: No brand kit, autofit or export yet. ### Phase 3 · Text that fits and colour you can read Build: Give each slot a minimum and maximum type size and a stated overflow behaviour: shrink to the minimum, then wrap, then mark the render failed. Never clip silently. Compute the contrast between each text slot and whatever sits behind it at that size, against the published minimum ratio for that text size, and show the failing size before export. Trap: Shrinking type until the copy fits gives you a six point headline on the tall size, and clipping gives you a caption that ends mid-word, and both export without complaint because the code only asked whether the text fitted. Set a floor, and when the copy still does not fit, fail that render with the size name and the character count so the copy gets shortened instead. Trap: Contrast checked once against the brand's background colour passes, then the same headline goes over a photograph and becomes invisible on half the sizes because the crop differs per aspect ratio. Sample what is actually behind the text at each size. Done when: Copy too long for a slot at the minimum type size fails that render with the size name and the character count, tested with a deliberately long headline. No render clips text without reporting it. Contrast is computed per size against what is actually behind the text, including a photograph, and a failing size is listed before export. The published ratio used is recorded in the configuration rather than hard-coded in a component. Do not build yet: No batch export or manifests yet. ### Phase 4 · Exports at the size you asked for Build: Render off-screen at the target pixel dimensions, independent of the display's scale factor. Write sRGB. Name files by design and size. Batch every size of a design in one run and assert each output file's dimensions after writing it. Trap: Exporting by reading pixels back from the canvas that is on screen gives you a file at the display's device pixel ratio, so the same button produces a 2160 pixel square on a high density laptop and a 1080 pixel one on an external monitor, and the platform rejects one of them. Render off-screen at the requested dimensions and assert the written file's size. Done when: An exported file's pixel dimensions equal the requested size on both a high density and a standard display, asserted in the suite by reading the written file. Every export declares sRGB rather than inheriting whatever the display profile was. A batch of eight sizes writes eight files with predictable names and no partial file is left behind on failure. Rendering the same design twice produces identical bytes. Do not build yet: No manifests, re-render or kit export yet. ### Phase 5 · Renders you can reproduce next year Build: Write a manifest beside every render recording font ids, asset hashes, brand version, layout version and the size. Add a re-render command that takes a manifest and reproduces the file. Export the whole brand kit as plain folders of images, colour values and a licence list, and bound the export folder with a stated retention. Trap: Editing a brand colour in place rewrites the brand for every past render, so the file a client approved in March can no longer be reproduced and nobody can say which logo version they actually saw. Version the brand kit, and record the version in each render's manifest. Done when: Every render has a manifest naming font ids, asset hashes, brand version and layout version. Re-rendering from a manifest after the brand has been edited reproduces the original file byte for byte, tested by editing a colour and re-rendering. The kit export is plain folders and files readable without this application, including a list of every font face and its licence. The export folder is bounded by a stated retention and the deletion is logged. Do not build yet: Nothing after this. ### Out of scope (and why) - Bundling font files into an export, because permission to embed a face in a document is not the same as permission to pass the file on. The SIL Open Font License, for one, allows a font to be bundled and redistributed with software but forbids selling the font software by itself, while placing no restriction at all on documents you create with it; read each face's licence before you ship its file. - Colour management beyond sRGB. Print profiles are a specialist problem and a wrongly applied profile is worse than none. - A stock template and photograph library, which is a licensing operation rather than a feature. - Cloud collaboration and a mobile editor, which turn a local renderer into a service with accounts. ### README must contain - That every output size carries its own safe area and slots anchor to it, rather than being scaled from a square - That fonts load from files you hold, each face records its licence, and a missing face fails the render - That overlong copy fails a render with the size and character count rather than shrinking or clipping - That exports are rendered off-screen at the requested dimensions, so the display's scale factor never reaches the file - That every render has a manifest and can be reproduced after the brand kit changes
# Architecture · RelayThat ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript | the fonts, the photographs and the exports stay on your disk | | Layout | Slots anchored to a per-size safe area, never coordinates multiplied by a scale factor | a square design stretched into a portrait one puts the headline off the canvas | | Rendering | One renderer for both the preview and the export, run at the export's real pixel size | two renderers means the preview is a guess | | Text | Font files loaded by path, measured by the same engine that draws them | naming a family and hoping is how two machines produce different line breaks | | Storage | SQLite beside a folder of fonts and assets, with a manifest written per render | an export you cannot reproduce next month is a screenshot | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Sizes | Output dimensions and safe areas. | Any list of sizes, provided each one carries its own safe area. | | Layout | Slot resolution per target size. | Any engine, provided nothing is derived by scaling another size. | | Fonts | Faces loaded by path with recorded licences. | Any loader that fails on a missing face rather than substituting. | | Render | Off-screen rendering at real dimensions. | Any renderer whose output dimensions are asserted after writing. | | Manifest | The record that makes a render reproducible. | Not swappable for nothing, which is what a screenshot is. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATA_DIR` · required · SQLite plus the copied assets. Back it up with the fonts folder. - `FONTS_DIR` · required · Font files loaded by path. Each face records its licence. - `EXPORT_DIR` · required · Renders and their manifests, side by side. - `EXPORT_RETENTION_DAYS` · required · How long renders are kept. Manifests let you rebuild after that. - `MAX_RENDER_PIXELS` · required · A ceiling on width times height, so a mistyped size cannot exhaust memory. ## 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 · RelayThat product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with React and TypeScript, Slots anchored to a per-size safe area, never coordinates multiplied by a scale factor, One renderer for both the preview and the export, run at the export's real pixel size, Font files loaded by path, measured by the same engine that draws them, SQLite beside a folder of fonts and assets, with a manifest written per render. - 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 - Producing the other sizes by multiplying the square design's coordinates by a scale factor pushes the headline off a tall canvas and drops the call to action to nine pixels on a wide banner, and both look acceptable in a thumbnail. Anchor each slot to the target size's safe area and resolve its type size for that size alone. - Naming a font family and letting the system pick means the machine without that family quietly substitutes a fallback, so the same design exports with different line breaks on your laptop and on the machine you render the batch from, and nobody notices until a headline wraps onto three lines in the one size you did not check. Load the file, measure with the engine that draws, and treat a missing face as a failure. - Shrinking type until the copy fits gives you a six point headline on the tall size, and clipping gives you a caption that ends mid-word, and both export without complaint because the code only asked whether the text fitted. Set a floor, and when the copy still does not fit, fail that render with the size name and the character count so the copy gets shortened instead. - Contrast checked once against the brand's background colour passes, then the same headline goes over a photograph and becomes invisible on half the sizes because the crop differs per aspect ratio. Sample what is actually behind the text at each size. - Exporting by reading pixels back from the canvas that is on screen gives you a file at the display's device pixel ratio, so the same button produces a 2160 pixel square on a high density laptop and a 1080 pixel one on an external monitor, and the platform rejects one of them. Render off-screen at the requested dimensions and assert the written file's size. - Editing a brand colour in place rewrites the brand for every past render, so the file a client approved in March can no longer be reproduced and nobody can say which logo version they actually saw. Version the brand kit, and record the version in each render's manifest.
# Delivery milestones · RelayThat Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · One design, many shapes A design laid out once renders into a wide, a square and a tall size without anything falling off the canvas. ### Steps 1. Give every size a safe area The safe area is what the layout anchors to, so it is part of the size definition. Files: `src/sizes.ts`, `src/layout/anchors.ts` 2. Resolve slots per size, never by scaling Scaling is the bug this whole phase exists to avoid. Files: `src/layout/resolve.ts`, `src/layout/resolve.test.ts` 3. Assert every slot lands inside the safe rectangle A bounding box check catches the failure the eye misses at thumbnail size. Files: `tests/safe-area.test.ts` ### Done when - [ ] Three aspect ratios render from one design - [ ] Every slot lands inside its safe rectangle - [ ] No position is derived by scaling another size - [ ] A new size needs no design change ### Watch out - Producing the other sizes by multiplying the square design's coordinates by a scale factor pushes the headline off a tall canvas and drops the call to action to nine pixels on a wide banner, and both look acceptable in a thumbnail. Anchor each slot to the target size's safe area and resolve its type size for that size alone. ## M2 · Fonts that render the same on two machines Type is drawn from a font file you hold, with its licence recorded, and a missing face fails loudly. ### Steps 1. Load faces by file path and record the licence The licence belongs to the face, and you will want it when you export a kit. Files: `src/fonts/registry.ts`, `src/fonts/licence.ts` 2. Measure with the engine that draws Any other measurement is an estimate that diverges on the long strings. Files: `src/text/measure.ts`, `src/text/measure.test.ts` 3. Fail on a missing face, never substitute A silent fallback is a wrong export you will ship. Files: `src/fonts/registry.ts`, `tests/missing-face.test.ts` ### Done when - [ ] Each face records a licence and a redistributable flag - [ ] A missing face fails with the family named - [ ] Measuring and drawing use one engine - [ ] Two machines produce the same line breaks ### Watch out - Naming a font family and letting the system pick means the machine without that family quietly substitutes a fallback, so the same design exports with different line breaks on your laptop and on the machine you render the batch from, and nobody notices until a headline wraps onto three lines in the one size you did not check. Load the file, measure with the engine that draws, and treat a missing face as a failure. ## M3 · Text that fits and colour you can read Long copy on a narrow size fails visibly rather than exporting something unreadable. ### Steps 1. Floor the type size, then fail A readable failure beats an unreadable export. Files: `src/text/autofit.ts`, `src/text/autofit.test.ts` 2. Sample what is behind the text at each size The crop changes per aspect ratio, so the background does too. Files: `src/contrast/sample.ts` 3. Record the ratio you are checking against Copied from the current guidance, with the date, not invented. Files: `src/contrast/thresholds.ts`, `docs/contrast.md` ### Done when - [ ] Overlong copy fails with size and character count - [ ] No render clips text silently - [ ] Contrast is sampled per size over photographs - [ ] The threshold is recorded, not hard-coded inline ### Watch out - Shrinking type until the copy fits gives you a six point headline on the tall size, and clipping gives you a caption that ends mid-word, and both export without complaint because the code only asked whether the text fitted. Set a floor, and when the copy still does not fit, fail that render with the size name and the character count so the copy gets shortened instead. - Contrast checked once against the brand's background colour passes, then the same headline goes over a photograph and becomes invisible on half the sizes because the crop differs per aspect ratio. Sample what is actually behind the text at each size. ## M4 · Exports at the size you asked for A file requested at 1080 by 1080 is 1080 by 1080 on every display. ### Steps 1. Render off-screen at the target dimensions The display's scale factor must not reach the output. Files: `src/render/offscreen.ts` 2. Assert the written file's dimensions Read the file back; do not trust the call that wrote it. Files: `tests/export-size.test.ts` 3. Write batches atomically A failed size should not leave half a PNG in the folder. Files: `src/render/batch.ts` ### Done when - [ ] Output dimensions match the request on both displays - [ ] Exports declare sRGB - [ ] A failed batch leaves no partial files - [ ] The same design renders to identical bytes twice ### Watch out - Exporting by reading pixels back from the canvas that is on screen gives you a file at the display's device pixel ratio, so the same button produces a 2160 pixel square on a high density laptop and a 1080 pixel one on an external monitor, and the platform rejects one of them. Render off-screen at the requested dimensions and assert the written file's size. ## M5 · Renders you can reproduce next year (production only) Any file in the export folder can be traced to the exact inputs that made it, and rebuilt. ### Steps 1. Write a manifest with every render Font ids, asset hashes, brand and layout versions, size. Files: `src/render/manifest.ts` 2. Version the brand kit rather than editing it An edited brand makes every past render unreproducible. Files: `src/brands/versions.ts`, `tests/rerender.test.ts` 3. Export the kit as plain files Folders of images and a text list of colours and font licences. Files: `src/export/kit.ts` ### Done when - [ ] Every render has a manifest with versions and hashes - [ ] Re-render after a brand edit reproduces the original - [ ] The kit exports as plain readable files - [ ] The export folder honours a stated retention ### Watch out - Editing a brand colour in place rewrites the brand for every past render, so the file a client approved in March can no longer be reproduced and nobody can say which logo version they actually saw. Version the brand kit, and record the version in each render's manifest.
# Operations · RelayThat ## Backup The data folder and the fonts folder together. A backup of the database without the fonts restores designs that cannot be rendered, and a backup of the exports without the manifests restores files nobody can rebuild. ## Restore Restore both folders, then re-render one manifest from before the backup and compare bytes. That single check proves the fonts, the assets and the brand versions all came back. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the checks live in the suite: output dimensions on both display types, a missing face failing, overlong copy failing, and a re-render after a brand edit matching the original. ## Incident checklist A wrong-sized export means the display scale factor reached the renderer. A design that suddenly wraps differently means a font face changed or went missing. Both are answered by the manifest rather than by memory. 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 - [ ] Three aspect ratios are confirmed to render from one design inside their safe areas - [ ] A missing font face is confirmed to fail rather than substitute - [ ] Overlong copy is confirmed to fail with size and character count - [ ] Export dimensions are confirmed correct on a high density display - [ ] A re-render from a manifest after a brand edit is confirmed byte identical ## Launch constraint Do not market omitted RelayThat 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. SQLite plus the copied assets. Back it up with the fonts folder. DATA_DIR=~/.local/share/brandkit # Required. Font files loaded by path. Each face records its licence. FONTS_DIR=~/.local/share/brandkit/fonts # Required. Renders and their manifests, side by side. EXPORT_DIR=~/Pictures/brandkit-exports # Required. How long renders are kept. Manifests let you rebuild after that. EXPORT_RETENTION_DAYS=180 # Required. A ceiling on width times height, so a mistyped size cannot exhaust memory. MAX_RENDER_PIXELS=40000000
$ choose a build depth, inspect the files, then open the complete pack in your agent
RelayThat: Creative users pay for speed, output fidelity, nondestructive editing, and years of edge-case work hidden behind direct manipulation.
xcolor-management edge cases
xcloud collaboration and mobile apps
xpixel-perfect professional tooling
xlarge template and asset libraries
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
RelayThat pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| pro | $15/user | — | 1 user, 5 workspaces/brands and unlimited downloads. |
| pro+ | $25/workspace | — | 2 users, 20 workspaces/brands and unlimited downloads. |
| enterprise | custom | — | Custom users, workspaces and support; public price is not listed. |
free tierno free tier
billingmonthly only; no annual plan is displayed
hidden costsPro+ includes only 2 users; larger teams and additional enterprise capacity require a custom quote.
verified 2026-08-14 · source ↗
Vibecode RelayThat
Kinda. The core of RelayThat is buildable in a weekend with the prompt on this page, but there are real gaps: color-management edge cases, cloud collaboration and mobile apps. Read the honest list above before committing.
How much does RelayThat cost?
RelayThat's pricing is usage-based or varies by plan · The official pricing URL is recorded, but a stable current amount was not safely recoverable in this pass. Do not publish a number until rechecked..
What do I lose by replacing RelayThat?
Honestly: color-management edge cases; cloud collaboration and mobile apps; pixel-perfect professional tooling; large template and asset libraries. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to RelayThat?
Yes: Desygner Free (The rare free plan that actually includes brand assets, automatic resizing, and scheduling; less automatic than RelayThat, but not a bait tier.) The prompt is for when you want it exactly your way.