Vibecode Polarr
track this build4 phases, 12 steps, beginner friendly0%The visible photo editing loop is buildable, but a credible replacement needs more than the first screen. Polarr 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 Polarr. 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 ===== # Polarr · indie build A local photo editor whose hard problems are keeping the original file untouched while edits live as a replayable recipe, choosing a colour pipeline deliberately instead of inheriting the defaults, and making the preview and the export come out of the same code. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with a React and TypeScript front end | the photographs stay on the machine and the file dialogs are the system's | | Pixels | Canvas 2D with colour space and colour type set explicitly at context creation | the defaults are eight bit sRGB and that is a decision whether or not you make it | | Catalogue | SQLite holding references and recipes, never image bytes | the originals stay where the person put them, under the names they gave them | | Recipes | A versioned JSON document per image, written atomically and mirrored to a sidecar | the edits are the product and they have to outlive this binary | | Rendering | One operation list replayed at preview size and at full size by the same code | two pipelines means the exported file does not match what was approved | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 and a Rust toolchain** · free - Why: Tauri needs both, and discovering that halfway through the first render is a bad afternoon. - Get it: nodejs.org and rustup.rs, then the Tauri prerequisites for your platform. - Verify: A fresh Tauri template builds and opens a window - [ ] **Sample photographs with known colour profiles** · free - Why: Colour handling is untestable without at least one wide gamut file and one plain sRGB file whose profiles you already know. - Get it: Export the same photograph twice from any camera application, once tagged Display P3 and once sRGB, and note which is which. - Verify: An image inspector reports a different profile for each file - [ ] **A smooth gradient test image** · free - Why: Banding from an eight bit working chain is invisible on a photograph of leaves and obvious on a gradient, which is the point of having one. - Get it: Generate a wide, very gradual grey or sky gradient at full resolution and keep it in the test fixtures. - Verify: The gradient renders smoothly before any adjustment is applied - [ ] **Your colour policy, written down first** · free - Why: Deciding what you convert after seeing which images look wrong means writing a policy that matches your bugs. - Get it: Write one paragraph: the working colour space, the colour type, what happens to a wide gamut import, and what the export writes. - Verify: The paragraph is in the README and the values it names are the ones in the code ## 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: - Video and creator effects. - Generative retouching. - Cloud sync and mobile applications. - Print output and soft proofing. - pixel-perfect professional tooling - large template and asset libraries - color-management edge cases - cloud collaboration and mobile apps - high-fidelity color, format, and export handling If one of those is essential to you, that is the reason to keep paying for Polarr, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Polarr 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 local photo editor that never writes to the original file and keeps every edit as a recipe to replace Polarr. 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. ### Rule zero Two rules. The first is the difference between an editor and a shredder. The imported file is never opened for writing. Every adjustment is a parameter in a recipe kept beside the catalogue, and an export renders that recipe onto a copy. When the recipe format changes, recipes written under the old version still open, because the photograph somebody edited last year is the one they will come back for. Second, say out loud what your colour handling is. A 2D canvas context takes its colour space and colour type from a settings dictionary whose defaults are 'srgb' and 'unorm8', so a wide gamut photograph dropped into one is converted and clipped whether or not you decided that. Choose the settings deliberately, record each image's source profile, and write in the README which conversions you perform and which you do not attempt. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with a React and TypeScript front end · the photographs stay on the machine and the file dialogs are the system's - Pixels: Canvas 2D with colour space and colour type set explicitly at context creation · the defaults are eight bit sRGB and that is a decision whether or not you make it - Catalogue: SQLite holding references and recipes, never image bytes · the originals stay where the person put them, under the names they gave them - Recipes: A versioned JSON document per image, written atomically and mirrored to a sidecar · the edits are the product and they have to outlive this binary - Rendering: One operation list replayed at preview size and at full size by the same code · two pipelines means the exported file does not match what was approved ### Data model (create this before Phase 1) - images(id, path, sha256, bytes, width, height, source_profile, orientation_tag, imported_at, missing_since) - recipes(image_id, format_version, ops_json, updated_at) · the ordered operation list, which is the whole of the edit - ops inside a recipe(ordinal, kind, params_json, mask_id, enabled) · order matters, so order is stored rather than implied - masks(id, image_id, kind, geometry_json, feather, inverted) · geometry and parameters, never a saved pixel buffer - history(image_id, ordinal, ops_json, at) · undo that survives closing the application - exports(id, image_id, recipe_version, path, format, colour_space, bytes, at) · what was rendered, from which recipe, in which space - Nothing here can be destroyed by editing a photograph, because nothing here is the photograph. ### Phase 1 · A catalogue that only points at your photographs Build: Import by reference: record the path, size, checksum, pixel dimensions, the embedded colour profile name and the orientation tag. Show a grid with thumbnails cached separately. Detect a file that has moved or changed underneath the catalogue and mark it rather than guessing. Trap: Importing by copying every file into an application folder doubles the disk use of a photo library and leaves two copies that drift apart, while importing by path with no checksum turns a moved or re-saved file into a blank frame with no explanation. Store the path and the checksum, and show a missing file as missing with the path it had. Done when: A file has the same checksum before and after import, asserted in the suite. Moving or replacing a file on disk marks the image missing or changed and names the path, rather than showing an empty frame. The embedded profile name and the orientation tag are recorded at import and visible in the interface. No code path anywhere opens an original in a writable mode, verified by review and by a test that makes the library directory read only. Do not build yet: No adjustments, masks, history or export. ### Phase 2 · Colour you can name Build: Create the drawing context with the colour space and colour type set explicitly, rather than accepting the 'srgb' and 'unorm8' defaults. Keep each image's source profile, convert on purpose, and put in the README which conversions happen and which are out of scope. Trap: Leaving the context settings alone converts every wide gamut photograph into sRGB before the first adjustment, so the saturated reds somebody bought the camera for are clipped silently on a display that could have shown them. Set the colour space and colour type at creation, keep the source profile on the image row, and name the working space in the interface. Done when: The context is created with an explicit colour space and colour type and both values appear in the README and in the interface. An image carrying a wide gamut profile is either preserved or converted with the conversion named on screen, never quietly. A long smooth gradient taken through the full operation chain is inspected for banding at the chosen colour type and the result is recorded. Every export row stores the colour space it was written in. Do not build yet: No masks, history or batch export yet. ### Phase 3 · An edit that is a list, not a bitmap Build: Model each adjustment as an operation with a kind, parameters and an ordinal. Render the preview by replaying the list at screen size and the export by replaying the same list at full size through the same code. Undo and redo move along the list and persist across a restart. Trap: Rendering the preview with a fast approximation and the export with a separate proper path produces a file that does not match what the person approved, and they find out after it is published. Keep one implementation and two resolutions, with a test that renders one recipe at two sizes and compares the downscaled results within a stated tolerance. Done when: The same recipe rendered at preview size and at full size matches within a stated tolerance after downscaling, tested. Reordering two operations changes the output and the new order is what gets exported. Undo and redo survive quitting and reopening the application. Toggling an operation off leaves it in the recipe rather than deleting it. Do not build yet: No masks, sidecars or batch export yet. ### Phase 4 · Masks that are geometry Build: Store radial, linear and brush masks as geometry with a feather and an inversion flag, in coordinates relative to the original image. Operations reference a mask by id. Editing a mask re-renders rather than accumulating, and a crop maps mask coordinates rather than baking them. Trap: Saving a mask as a rasterised alpha buffer at preview resolution gives an export with visibly blocky edges and quietly invalidates every mask the first time the image is cropped or straightened. Keep the geometry in original image coordinates and rasterise at whatever resolution the current render needs. Done when: A mask created at preview resolution exports with a clean edge at full resolution, checked visually and by an edge gradient test. Cropping an image leaves masked adjustments on the same part of the picture. No rasterised mask buffer is ever written to the recipe, verified by inspecting a saved recipe. A mask can be inverted and feathered without re-creating it. Do not build yet: No batch export, sidecars or rebuild yet. ### Phase 5 · Getting the photographs and the edits back out Build: Export with format, quality and colour space recorded on the row. Mirror every recipe to a sidecar file beside its image so another tool or a fresh catalogue can find it. Batch export as a resumable job. Provide a rebuild command that reconstructs the catalogue from files and sidecars alone, and run it in the test suite. Trap: Keeping the only copy of every recipe inside one SQLite file means a corrupt catalogue loses years of edits while every original survives perfectly, which is the most annoying possible way to lose work. Write a sidecar per image as well, and make the rebuild from sidecars a tested command rather than a paragraph in the README. Done when: Deleting the catalogue and running the rebuild command restores every recipe from sidecars, exercised in the suite. A batch export interrupted halfway resumes without re-rendering finished images and without skipping any. Each export row records format, quality and colour space, and re-exporting from the same recipe version produces the same bytes. The README states plainly which colour conversions the tool performs. Do not build yet: Nothing after this. ### Out of scope (and why) - Video filters and creator effects, which are a second product with their own codec, timeline and performance problems. - Generative retouching and sky replacement, which need models and rights you do not have and change what the photograph is evidence of. - Cloud sync and mobile applications, which is where the subscription actually goes and what a local editor deliberately trades away. - Print output and soft proofing to a press profile, which needs measured profiles and a calibrated display to mean anything. ### README must contain - That originals are never opened for writing and every edit lives in a recipe and a sidecar - Which colour space and colour type the drawing context is created with, and why they were chosen - Which colour conversions the tool performs and which it explicitly does not attempt - That preview and export share one renderer, and the tolerance at which they are tested to agree - How to rebuild the catalogue from images and sidecars after deleting it ===== AGENTS.md ===== # Agent instructions · Polarr indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with a React and TypeScript front end, Canvas 2D with colour space and colour type set explicitly at context creation, SQLite holding references and recipes, never image bytes, A versioned JSON document per image, written atomically and mirrored to a sidecar, One operation list replayed at preview size and at full size by the same code. 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 - Importing by copying every file into an application folder doubles the disk use of a photo library and leaves two copies that drift apart, while importing by path with no checksum turns a moved or re-saved file into a blank frame with no explanation. Store the path and the checksum, and show a missing file as missing with the path it had. - Leaving the context settings alone converts every wide gamut photograph into sRGB before the first adjustment, so the saturated reds somebody bought the camera for are clipped silently on a display that could have shown them. Set the colour space and colour type at creation, keep the source profile on the image row, and name the working space in the interface. - Rendering the preview with a fast approximation and the export with a separate proper path produces a file that does not match what the person approved, and they find out after it is published. Keep one implementation and two resolutions, with a test that renders one recipe at two sizes and compares the downscaled results within a stated tolerance. - Saving a mask as a rasterised alpha buffer at preview resolution gives an export with visibly blocky edges and quietly invalidates every mask the first time the image is cropped or straightened. Keep the geometry in original image coordinates and rasterise at whatever resolution the current render needs. ===== BUILD_PLAN.md ===== # Build plan · Polarr A local photo editor whose hard problems are keeping the original file untouched while edits live as a replayable recipe, choosing a colour pipeline deliberately instead of inheriting the defaults, and making the preview and the export come out of the same code. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A catalogue that only points at your photographs Importing records a reference and a checksum and writes nothing to the original. ### Steps 1. Import by reference with a checksum The checksum is how you tell moved from edited from gone. Files: `src/import.ts`, `tests/import-readonly.test.ts` 2. Cache thumbnails somewhere disposable A thumbnail cache you can delete without losing anything is the right kind of cache. Files: `src/thumbnails.ts` 3. Open originals read only, and prove it Run the suite against a read only library directory so a stray write fails loudly. Files: `src/files.ts`, `tests/import-readonly.test.ts` ### Done when - [ ] Checksums match before and after import - [ ] A moved file is marked missing with its old path - [ ] Profile name and orientation tag are recorded - [ ] The suite passes with the library directory read only ### Watch out - Importing by copying every file into an application folder doubles the disk use of a photo library and leaves two copies that drift apart, while importing by path with no checksum turns a moved or re-saved file into a blank frame with no explanation. Store the path and the checksum, and show a missing file as missing with the path it had. ## Phase 2 · Colour you can name The pipeline states its colour space instead of inheriting one. ### Steps 1. Set the context settings explicitly Colour space and colour type are arguments, not defaults you inherit. Files: `src/render/context.ts` 2. Carry the source profile on the image You cannot convert honestly from a profile you did not record. Files: `src/import.ts`, `src/render/colour.ts` 3. Test a gradient for banding Eight bits per channel through six operations is where banding appears. Files: `tests/gradient-banding.test.ts` ### Done when - [ ] Colour space and colour type are set at creation - [ ] A wide gamut image is preserved or converted with the conversion named - [ ] A gradient through the full chain is checked for banding - [ ] Exports record the colour space written ### Watch out - Leaving the context settings alone converts every wide gamut photograph into sRGB before the first adjustment, so the saturated reds somebody bought the camera for are clipped silently on a display that could have shown them. Set the colour space and colour type at creation, keep the source profile on the image row, and name the working space in the interface. ## Phase 3 · An edit that is a list, not a bitmap Adjustments are ordered parameters replayed identically at any resolution. ### Steps 1. One renderer, two resolutions The export path is the preview path with a different size argument. Files: `src/render/pipeline.ts`, `tests/preview-matches-export.test.ts` 2. Persist history with the recipe Undo that dies at quit is a worse undo than none, because people rely on it. Files: `src/history.ts` 3. Keep disabled operations in the list Turning an adjustment off is not the same as throwing it away. Files: `src/recipe.ts` ### Done when - [ ] Preview and export agree within a stated tolerance - [ ] Operation order is explicit and affects output - [ ] Undo survives a restart - [ ] Disabled operations remain in the recipe ### Watch out - Rendering the preview with a fast approximation and the export with a separate proper path produces a file that does not match what the person approved, and they find out after it is published. Keep one implementation and two resolutions, with a test that renders one recipe at two sizes and compares the downscaled results within a stated tolerance. ## Phase 4 · Masks that are geometry A local adjustment survives a crop, a re-export and a format change. ### Steps 1. Store masks in original image coordinates So a crop is a coordinate change rather than a rebuild. Files: `src/masks.ts` 2. Rasterise at render time only Whatever resolution this render needs, and nothing persisted. Files: `src/render/masks.ts`, `tests/mask-edges.test.ts` 3. Reference masks by id from operations Two operations sharing one mask should share one mask. Files: `src/recipe.ts` ### Done when - [ ] Masks export with clean edges at full resolution - [ ] A crop keeps adjustments on the same part of the image - [ ] No rasterised mask is stored in a recipe - [ ] Invert and feather work without re-creating the mask ### Watch out - Saving a mask as a rasterised alpha buffer at preview resolution gives an export with visibly blocky edges and quietly invalidates every mask the first time the image is cropped or straightened. Keep the geometry in original image coordinates and rasterise at whatever resolution the current render needs. ## Not in this build - Video and creator effects. - Generative retouching. - Cloud sync and mobile applications. - Print output and soft proofing. ## After v1, if you want it - Presets as named partial recipes, which is a filter pack without any of the licensing - A compare view against the unedited original, which costs nothing once edits are a list - Lens and sensor corrections from published profiles, once the operation list has settled ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. References and recipes. Rebuildable from sidecars, which is the whole idea. CATALOGUE_PATH=~/.local/share/photoedit/catalogue.db # Required. Written beside each image. This is the durable copy of the edit. SIDECAR_SUFFIX=.recipe.json # Required. Passed to the context at creation. Named in the interface and the README. WORKING_COLOUR_SPACE=display-p3 # Required. The other half of the same decision. Check what your runtime actually supports. WORKING_COLOUR_TYPE=float16 # Required. Preview render budget. The export ignores it and uses the same code. PREVIEW_MAX_PIXELS=4000000 # Required. Undo steps retained per image across restarts. HISTORY_KEEP=200
You are building a lean indie version of Polarr. 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 ===== # Polarr · indie build A local photo editor whose hard problems are keeping the original file untouched while edits live as a replayable recipe, choosing a colour pipeline deliberately instead of inheriting the defaults, and making the preview and the export come out of the same code. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with a React and TypeScript front end | the photographs stay on the machine and the file dialogs are the system's | | Pixels | Canvas 2D with colour space and colour type set explicitly at context creation | the defaults are eight bit sRGB and that is a decision whether or not you make it | | Catalogue | SQLite holding references and recipes, never image bytes | the originals stay where the person put them, under the names they gave them | | Recipes | A versioned JSON document per image, written atomically and mirrored to a sidecar | the edits are the product and they have to outlive this binary | | Rendering | One operation list replayed at preview size and at full size by the same code | two pipelines means the exported file does not match what was approved | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 and a Rust toolchain** · free - Why: Tauri needs both, and discovering that halfway through the first render is a bad afternoon. - Get it: nodejs.org and rustup.rs, then the Tauri prerequisites for your platform. - Verify: A fresh Tauri template builds and opens a window - [ ] **Sample photographs with known colour profiles** · free - Why: Colour handling is untestable without at least one wide gamut file and one plain sRGB file whose profiles you already know. - Get it: Export the same photograph twice from any camera application, once tagged Display P3 and once sRGB, and note which is which. - Verify: An image inspector reports a different profile for each file - [ ] **A smooth gradient test image** · free - Why: Banding from an eight bit working chain is invisible on a photograph of leaves and obvious on a gradient, which is the point of having one. - Get it: Generate a wide, very gradual grey or sky gradient at full resolution and keep it in the test fixtures. - Verify: The gradient renders smoothly before any adjustment is applied - [ ] **Your colour policy, written down first** · free - Why: Deciding what you convert after seeing which images look wrong means writing a policy that matches your bugs. - Get it: Write one paragraph: the working colour space, the colour type, what happens to a wide gamut import, and what the export writes. - Verify: The paragraph is in the README and the values it names are the ones in the code ## 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: - Video and creator effects. - Generative retouching. - Cloud sync and mobile applications. - Print output and soft proofing. - pixel-perfect professional tooling - large template and asset libraries - color-management edge cases - cloud collaboration and mobile apps - high-fidelity color, format, and export handling If one of those is essential to you, that is the reason to keep paying for Polarr, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Polarr 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 local photo editor that never writes to the original file and keeps every edit as a recipe to replace Polarr. 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. ### Rule zero Two rules. The first is the difference between an editor and a shredder. The imported file is never opened for writing. Every adjustment is a parameter in a recipe kept beside the catalogue, and an export renders that recipe onto a copy. When the recipe format changes, recipes written under the old version still open, because the photograph somebody edited last year is the one they will come back for. Second, say out loud what your colour handling is. A 2D canvas context takes its colour space and colour type from a settings dictionary whose defaults are 'srgb' and 'unorm8', so a wide gamut photograph dropped into one is converted and clipped whether or not you decided that. Choose the settings deliberately, record each image's source profile, and write in the README which conversions you perform and which you do not attempt. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with a React and TypeScript front end · the photographs stay on the machine and the file dialogs are the system's - Pixels: Canvas 2D with colour space and colour type set explicitly at context creation · the defaults are eight bit sRGB and that is a decision whether or not you make it - Catalogue: SQLite holding references and recipes, never image bytes · the originals stay where the person put them, under the names they gave them - Recipes: A versioned JSON document per image, written atomically and mirrored to a sidecar · the edits are the product and they have to outlive this binary - Rendering: One operation list replayed at preview size and at full size by the same code · two pipelines means the exported file does not match what was approved ### Data model (create this before Phase 1) - images(id, path, sha256, bytes, width, height, source_profile, orientation_tag, imported_at, missing_since) - recipes(image_id, format_version, ops_json, updated_at) · the ordered operation list, which is the whole of the edit - ops inside a recipe(ordinal, kind, params_json, mask_id, enabled) · order matters, so order is stored rather than implied - masks(id, image_id, kind, geometry_json, feather, inverted) · geometry and parameters, never a saved pixel buffer - history(image_id, ordinal, ops_json, at) · undo that survives closing the application - exports(id, image_id, recipe_version, path, format, colour_space, bytes, at) · what was rendered, from which recipe, in which space - Nothing here can be destroyed by editing a photograph, because nothing here is the photograph. ### Phase 1 · A catalogue that only points at your photographs Build: Import by reference: record the path, size, checksum, pixel dimensions, the embedded colour profile name and the orientation tag. Show a grid with thumbnails cached separately. Detect a file that has moved or changed underneath the catalogue and mark it rather than guessing. Trap: Importing by copying every file into an application folder doubles the disk use of a photo library and leaves two copies that drift apart, while importing by path with no checksum turns a moved or re-saved file into a blank frame with no explanation. Store the path and the checksum, and show a missing file as missing with the path it had. Done when: A file has the same checksum before and after import, asserted in the suite. Moving or replacing a file on disk marks the image missing or changed and names the path, rather than showing an empty frame. The embedded profile name and the orientation tag are recorded at import and visible in the interface. No code path anywhere opens an original in a writable mode, verified by review and by a test that makes the library directory read only. Do not build yet: No adjustments, masks, history or export. ### Phase 2 · Colour you can name Build: Create the drawing context with the colour space and colour type set explicitly, rather than accepting the 'srgb' and 'unorm8' defaults. Keep each image's source profile, convert on purpose, and put in the README which conversions happen and which are out of scope. Trap: Leaving the context settings alone converts every wide gamut photograph into sRGB before the first adjustment, so the saturated reds somebody bought the camera for are clipped silently on a display that could have shown them. Set the colour space and colour type at creation, keep the source profile on the image row, and name the working space in the interface. Done when: The context is created with an explicit colour space and colour type and both values appear in the README and in the interface. An image carrying a wide gamut profile is either preserved or converted with the conversion named on screen, never quietly. A long smooth gradient taken through the full operation chain is inspected for banding at the chosen colour type and the result is recorded. Every export row stores the colour space it was written in. Do not build yet: No masks, history or batch export yet. ### Phase 3 · An edit that is a list, not a bitmap Build: Model each adjustment as an operation with a kind, parameters and an ordinal. Render the preview by replaying the list at screen size and the export by replaying the same list at full size through the same code. Undo and redo move along the list and persist across a restart. Trap: Rendering the preview with a fast approximation and the export with a separate proper path produces a file that does not match what the person approved, and they find out after it is published. Keep one implementation and two resolutions, with a test that renders one recipe at two sizes and compares the downscaled results within a stated tolerance. Done when: The same recipe rendered at preview size and at full size matches within a stated tolerance after downscaling, tested. Reordering two operations changes the output and the new order is what gets exported. Undo and redo survive quitting and reopening the application. Toggling an operation off leaves it in the recipe rather than deleting it. Do not build yet: No masks, sidecars or batch export yet. ### Phase 4 · Masks that are geometry Build: Store radial, linear and brush masks as geometry with a feather and an inversion flag, in coordinates relative to the original image. Operations reference a mask by id. Editing a mask re-renders rather than accumulating, and a crop maps mask coordinates rather than baking them. Trap: Saving a mask as a rasterised alpha buffer at preview resolution gives an export with visibly blocky edges and quietly invalidates every mask the first time the image is cropped or straightened. Keep the geometry in original image coordinates and rasterise at whatever resolution the current render needs. Done when: A mask created at preview resolution exports with a clean edge at full resolution, checked visually and by an edge gradient test. Cropping an image leaves masked adjustments on the same part of the picture. No rasterised mask buffer is ever written to the recipe, verified by inspecting a saved recipe. A mask can be inverted and feathered without re-creating it. Do not build yet: No batch export, sidecars or rebuild yet. ### Phase 5 · Getting the photographs and the edits back out Build: Export with format, quality and colour space recorded on the row. Mirror every recipe to a sidecar file beside its image so another tool or a fresh catalogue can find it. Batch export as a resumable job. Provide a rebuild command that reconstructs the catalogue from files and sidecars alone, and run it in the test suite. Trap: Keeping the only copy of every recipe inside one SQLite file means a corrupt catalogue loses years of edits while every original survives perfectly, which is the most annoying possible way to lose work. Write a sidecar per image as well, and make the rebuild from sidecars a tested command rather than a paragraph in the README. Done when: Deleting the catalogue and running the rebuild command restores every recipe from sidecars, exercised in the suite. A batch export interrupted halfway resumes without re-rendering finished images and without skipping any. Each export row records format, quality and colour space, and re-exporting from the same recipe version produces the same bytes. The README states plainly which colour conversions the tool performs. Do not build yet: Nothing after this. ### Out of scope (and why) - Video filters and creator effects, which are a second product with their own codec, timeline and performance problems. - Generative retouching and sky replacement, which need models and rights you do not have and change what the photograph is evidence of. - Cloud sync and mobile applications, which is where the subscription actually goes and what a local editor deliberately trades away. - Print output and soft proofing to a press profile, which needs measured profiles and a calibrated display to mean anything. ### README must contain - That originals are never opened for writing and every edit lives in a recipe and a sidecar - Which colour space and colour type the drawing context is created with, and why they were chosen - Which colour conversions the tool performs and which it explicitly does not attempt - That preview and export share one renderer, and the tolerance at which they are tested to agree - How to rebuild the catalogue from images and sidecars after deleting it ===== AGENTS.md ===== # Agent instructions · Polarr indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with a React and TypeScript front end, Canvas 2D with colour space and colour type set explicitly at context creation, SQLite holding references and recipes, never image bytes, A versioned JSON document per image, written atomically and mirrored to a sidecar, One operation list replayed at preview size and at full size by the same code. 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 - Importing by copying every file into an application folder doubles the disk use of a photo library and leaves two copies that drift apart, while importing by path with no checksum turns a moved or re-saved file into a blank frame with no explanation. Store the path and the checksum, and show a missing file as missing with the path it had. - Leaving the context settings alone converts every wide gamut photograph into sRGB before the first adjustment, so the saturated reds somebody bought the camera for are clipped silently on a display that could have shown them. Set the colour space and colour type at creation, keep the source profile on the image row, and name the working space in the interface. - Rendering the preview with a fast approximation and the export with a separate proper path produces a file that does not match what the person approved, and they find out after it is published. Keep one implementation and two resolutions, with a test that renders one recipe at two sizes and compares the downscaled results within a stated tolerance. - Saving a mask as a rasterised alpha buffer at preview resolution gives an export with visibly blocky edges and quietly invalidates every mask the first time the image is cropped or straightened. Keep the geometry in original image coordinates and rasterise at whatever resolution the current render needs. ===== BUILD_PLAN.md ===== # Build plan · Polarr A local photo editor whose hard problems are keeping the original file untouched while edits live as a replayable recipe, choosing a colour pipeline deliberately instead of inheriting the defaults, and making the preview and the export come out of the same code. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A catalogue that only points at your photographs Importing records a reference and a checksum and writes nothing to the original. ### Steps 1. Import by reference with a checksum The checksum is how you tell moved from edited from gone. Files: `src/import.ts`, `tests/import-readonly.test.ts` 2. Cache thumbnails somewhere disposable A thumbnail cache you can delete without losing anything is the right kind of cache. Files: `src/thumbnails.ts` 3. Open originals read only, and prove it Run the suite against a read only library directory so a stray write fails loudly. Files: `src/files.ts`, `tests/import-readonly.test.ts` ### Done when - [ ] Checksums match before and after import - [ ] A moved file is marked missing with its old path - [ ] Profile name and orientation tag are recorded - [ ] The suite passes with the library directory read only ### Watch out - Importing by copying every file into an application folder doubles the disk use of a photo library and leaves two copies that drift apart, while importing by path with no checksum turns a moved or re-saved file into a blank frame with no explanation. Store the path and the checksum, and show a missing file as missing with the path it had. ## Phase 2 · Colour you can name The pipeline states its colour space instead of inheriting one. ### Steps 1. Set the context settings explicitly Colour space and colour type are arguments, not defaults you inherit. Files: `src/render/context.ts` 2. Carry the source profile on the image You cannot convert honestly from a profile you did not record. Files: `src/import.ts`, `src/render/colour.ts` 3. Test a gradient for banding Eight bits per channel through six operations is where banding appears. Files: `tests/gradient-banding.test.ts` ### Done when - [ ] Colour space and colour type are set at creation - [ ] A wide gamut image is preserved or converted with the conversion named - [ ] A gradient through the full chain is checked for banding - [ ] Exports record the colour space written ### Watch out - Leaving the context settings alone converts every wide gamut photograph into sRGB before the first adjustment, so the saturated reds somebody bought the camera for are clipped silently on a display that could have shown them. Set the colour space and colour type at creation, keep the source profile on the image row, and name the working space in the interface. ## Phase 3 · An edit that is a list, not a bitmap Adjustments are ordered parameters replayed identically at any resolution. ### Steps 1. One renderer, two resolutions The export path is the preview path with a different size argument. Files: `src/render/pipeline.ts`, `tests/preview-matches-export.test.ts` 2. Persist history with the recipe Undo that dies at quit is a worse undo than none, because people rely on it. Files: `src/history.ts` 3. Keep disabled operations in the list Turning an adjustment off is not the same as throwing it away. Files: `src/recipe.ts` ### Done when - [ ] Preview and export agree within a stated tolerance - [ ] Operation order is explicit and affects output - [ ] Undo survives a restart - [ ] Disabled operations remain in the recipe ### Watch out - Rendering the preview with a fast approximation and the export with a separate proper path produces a file that does not match what the person approved, and they find out after it is published. Keep one implementation and two resolutions, with a test that renders one recipe at two sizes and compares the downscaled results within a stated tolerance. ## Phase 4 · Masks that are geometry A local adjustment survives a crop, a re-export and a format change. ### Steps 1. Store masks in original image coordinates So a crop is a coordinate change rather than a rebuild. Files: `src/masks.ts` 2. Rasterise at render time only Whatever resolution this render needs, and nothing persisted. Files: `src/render/masks.ts`, `tests/mask-edges.test.ts` 3. Reference masks by id from operations Two operations sharing one mask should share one mask. Files: `src/recipe.ts` ### Done when - [ ] Masks export with clean edges at full resolution - [ ] A crop keeps adjustments on the same part of the image - [ ] No rasterised mask is stored in a recipe - [ ] Invert and feather work without re-creating the mask ### Watch out - Saving a mask as a rasterised alpha buffer at preview resolution gives an export with visibly blocky edges and quietly invalidates every mask the first time the image is cropped or straightened. Keep the geometry in original image coordinates and rasterise at whatever resolution the current render needs. ## Not in this build - Video and creator effects. - Generative retouching. - Cloud sync and mobile applications. - Print output and soft proofing. ## After v1, if you want it - Presets as named partial recipes, which is a filter pack without any of the licensing - A compare view against the unedited original, which costs nothing once edits are a list - Lens and sensor corrections from published profiles, once the operation list has settled ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. References and recipes. Rebuildable from sidecars, which is the whole idea. CATALOGUE_PATH=~/.local/share/photoedit/catalogue.db # Required. Written beside each image. This is the durable copy of the edit. SIDECAR_SUFFIX=.recipe.json # Required. Passed to the context at creation. Named in the interface and the README. WORKING_COLOUR_SPACE=display-p3 # Required. The other half of the same decision. Check what your runtime actually supports. WORKING_COLOUR_TYPE=float16 # Required. Preview render budget. The export ignores it and uses the same code. PREVIEW_MAX_PIXELS=4000000 # Required. Undo steps retained per image across restarts. HISTORY_KEEP=200
You are building a production product version of Polarr. 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 ===== # Polarr · product brief ## Problem The visible photo editing loop is buildable, but a credible replacement needs more than the first screen. Polarr earns its keep through editor polish, assets, algorithms, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome An editor that cannot damage a photograph, states which colour space it works in rather than inheriting one, exports exactly what the preview showed, and can be rebuilt from the files on disk after you delete its database. ## 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 - Video and creator effects. - Generative retouching. - Cloud sync and mobile applications. - Print output and soft proofing. - pixel-perfect professional tooling - large template and asset libraries - color-management edge cases - cloud collaboration and mobile apps - high-fidelity color, format, and export handling ## Success criteria - Originals are confirmed never opened for writing - The context colour space and colour type are confirmed explicit - Preview and export are confirmed to agree within tolerance - Masks are confirmed stored as geometry only - Rebuild from sidecars is confirmed in the suite ===== BRIEF.md ===== # Build brief · Polarr 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 local photo editor that never writes to the original file and keeps every edit as a recipe to replace Polarr. 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. ### Rule zero Two rules. The first is the difference between an editor and a shredder. The imported file is never opened for writing. Every adjustment is a parameter in a recipe kept beside the catalogue, and an export renders that recipe onto a copy. When the recipe format changes, recipes written under the old version still open, because the photograph somebody edited last year is the one they will come back for. Second, say out loud what your colour handling is. A 2D canvas context takes its colour space and colour type from a settings dictionary whose defaults are 'srgb' and 'unorm8', so a wide gamut photograph dropped into one is converted and clipped whether or not you decided that. Choose the settings deliberately, record each image's source profile, and write in the README which conversions you perform and which you do not attempt. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with a React and TypeScript front end · the photographs stay on the machine and the file dialogs are the system's - Pixels: Canvas 2D with colour space and colour type set explicitly at context creation · the defaults are eight bit sRGB and that is a decision whether or not you make it - Catalogue: SQLite holding references and recipes, never image bytes · the originals stay where the person put them, under the names they gave them - Recipes: A versioned JSON document per image, written atomically and mirrored to a sidecar · the edits are the product and they have to outlive this binary - Rendering: One operation list replayed at preview size and at full size by the same code · two pipelines means the exported file does not match what was approved ### Data model (create this before Phase 1) - images(id, path, sha256, bytes, width, height, source_profile, orientation_tag, imported_at, missing_since) - recipes(image_id, format_version, ops_json, updated_at) · the ordered operation list, which is the whole of the edit - ops inside a recipe(ordinal, kind, params_json, mask_id, enabled) · order matters, so order is stored rather than implied - masks(id, image_id, kind, geometry_json, feather, inverted) · geometry and parameters, never a saved pixel buffer - history(image_id, ordinal, ops_json, at) · undo that survives closing the application - exports(id, image_id, recipe_version, path, format, colour_space, bytes, at) · what was rendered, from which recipe, in which space - Nothing here can be destroyed by editing a photograph, because nothing here is the photograph. ### Phase 1 · A catalogue that only points at your photographs Build: Import by reference: record the path, size, checksum, pixel dimensions, the embedded colour profile name and the orientation tag. Show a grid with thumbnails cached separately. Detect a file that has moved or changed underneath the catalogue and mark it rather than guessing. Trap: Importing by copying every file into an application folder doubles the disk use of a photo library and leaves two copies that drift apart, while importing by path with no checksum turns a moved or re-saved file into a blank frame with no explanation. Store the path and the checksum, and show a missing file as missing with the path it had. Done when: A file has the same checksum before and after import, asserted in the suite. Moving or replacing a file on disk marks the image missing or changed and names the path, rather than showing an empty frame. The embedded profile name and the orientation tag are recorded at import and visible in the interface. No code path anywhere opens an original in a writable mode, verified by review and by a test that makes the library directory read only. Do not build yet: No adjustments, masks, history or export. ### Phase 2 · Colour you can name Build: Create the drawing context with the colour space and colour type set explicitly, rather than accepting the 'srgb' and 'unorm8' defaults. Keep each image's source profile, convert on purpose, and put in the README which conversions happen and which are out of scope. Trap: Leaving the context settings alone converts every wide gamut photograph into sRGB before the first adjustment, so the saturated reds somebody bought the camera for are clipped silently on a display that could have shown them. Set the colour space and colour type at creation, keep the source profile on the image row, and name the working space in the interface. Done when: The context is created with an explicit colour space and colour type and both values appear in the README and in the interface. An image carrying a wide gamut profile is either preserved or converted with the conversion named on screen, never quietly. A long smooth gradient taken through the full operation chain is inspected for banding at the chosen colour type and the result is recorded. Every export row stores the colour space it was written in. Do not build yet: No masks, history or batch export yet. ### Phase 3 · An edit that is a list, not a bitmap Build: Model each adjustment as an operation with a kind, parameters and an ordinal. Render the preview by replaying the list at screen size and the export by replaying the same list at full size through the same code. Undo and redo move along the list and persist across a restart. Trap: Rendering the preview with a fast approximation and the export with a separate proper path produces a file that does not match what the person approved, and they find out after it is published. Keep one implementation and two resolutions, with a test that renders one recipe at two sizes and compares the downscaled results within a stated tolerance. Done when: The same recipe rendered at preview size and at full size matches within a stated tolerance after downscaling, tested. Reordering two operations changes the output and the new order is what gets exported. Undo and redo survive quitting and reopening the application. Toggling an operation off leaves it in the recipe rather than deleting it. Do not build yet: No masks, sidecars or batch export yet. ### Phase 4 · Masks that are geometry Build: Store radial, linear and brush masks as geometry with a feather and an inversion flag, in coordinates relative to the original image. Operations reference a mask by id. Editing a mask re-renders rather than accumulating, and a crop maps mask coordinates rather than baking them. Trap: Saving a mask as a rasterised alpha buffer at preview resolution gives an export with visibly blocky edges and quietly invalidates every mask the first time the image is cropped or straightened. Keep the geometry in original image coordinates and rasterise at whatever resolution the current render needs. Done when: A mask created at preview resolution exports with a clean edge at full resolution, checked visually and by an edge gradient test. Cropping an image leaves masked adjustments on the same part of the picture. No rasterised mask buffer is ever written to the recipe, verified by inspecting a saved recipe. A mask can be inverted and feathered without re-creating it. Do not build yet: No batch export, sidecars or rebuild yet. ### Phase 5 · Getting the photographs and the edits back out Build: Export with format, quality and colour space recorded on the row. Mirror every recipe to a sidecar file beside its image so another tool or a fresh catalogue can find it. Batch export as a resumable job. Provide a rebuild command that reconstructs the catalogue from files and sidecars alone, and run it in the test suite. Trap: Keeping the only copy of every recipe inside one SQLite file means a corrupt catalogue loses years of edits while every original survives perfectly, which is the most annoying possible way to lose work. Write a sidecar per image as well, and make the rebuild from sidecars a tested command rather than a paragraph in the README. Done when: Deleting the catalogue and running the rebuild command restores every recipe from sidecars, exercised in the suite. A batch export interrupted halfway resumes without re-rendering finished images and without skipping any. Each export row records format, quality and colour space, and re-exporting from the same recipe version produces the same bytes. The README states plainly which colour conversions the tool performs. Do not build yet: Nothing after this. ### Out of scope (and why) - Video filters and creator effects, which are a second product with their own codec, timeline and performance problems. - Generative retouching and sky replacement, which need models and rights you do not have and change what the photograph is evidence of. - Cloud sync and mobile applications, which is where the subscription actually goes and what a local editor deliberately trades away. - Print output and soft proofing to a press profile, which needs measured profiles and a calibrated display to mean anything. ### README must contain - That originals are never opened for writing and every edit lives in a recipe and a sidecar - Which colour space and colour type the drawing context is created with, and why they were chosen - Which colour conversions the tool performs and which it explicitly does not attempt - That preview and export share one renderer, and the tolerance at which they are tested to agree - How to rebuild the catalogue from images and sidecars after deleting it ===== ARCHITECTURE.md ===== # Architecture · Polarr ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with a React and TypeScript front end | the photographs stay on the machine and the file dialogs are the system's | | Pixels | Canvas 2D with colour space and colour type set explicitly at context creation | the defaults are eight bit sRGB and that is a decision whether or not you make it | | Catalogue | SQLite holding references and recipes, never image bytes | the originals stay where the person put them, under the names they gave them | | Recipes | A versioned JSON document per image, written atomically and mirrored to a sidecar | the edits are the product and they have to outlive this binary | | Rendering | One operation list replayed at preview size and at full size by the same code | two pipelines means the exported file does not match what was approved | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Catalogue | References, checksums and missing file detection. | Any store, provided it never holds image bytes and can be rebuilt from sidecars. | | Colour | Context settings, source profiles and conversions. | Any implementation that still names its working space on screen. | | Recipe | The ordered operation list and history. | Any format, provided old versions still open and a sidecar is written. | | Renderer | One replay path used at both resolutions. | Not swappable for a separate export renderer, which is the mismatch bug. | | Masks | Geometry in original image coordinates. | Any mask kinds, provided nothing rasterised is persisted. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `CATALOGUE_PATH` · required · References and recipes. Rebuildable from sidecars, which is the whole idea. - `SIDECAR_SUFFIX` · required · Written beside each image. This is the durable copy of the edit. - `WORKING_COLOUR_SPACE` · required · Passed to the context at creation. Named in the interface and the README. - `WORKING_COLOUR_TYPE` · required · The other half of the same decision. Check what your runtime actually supports. - `PREVIEW_MAX_PIXELS` · required · Preview render budget. The export ignores it and uses the same code. - `HISTORY_KEEP` · required · Undo steps retained per image across restarts. ## 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 · Polarr product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with a React and TypeScript front end, Canvas 2D with colour space and colour type set explicitly at context creation, SQLite holding references and recipes, never image bytes, A versioned JSON document per image, written atomically and mirrored to a sidecar, One operation list replayed at preview size and at full size by the same code. - 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 - Importing by copying every file into an application folder doubles the disk use of a photo library and leaves two copies that drift apart, while importing by path with no checksum turns a moved or re-saved file into a blank frame with no explanation. Store the path and the checksum, and show a missing file as missing with the path it had. - Leaving the context settings alone converts every wide gamut photograph into sRGB before the first adjustment, so the saturated reds somebody bought the camera for are clipped silently on a display that could have shown them. Set the colour space and colour type at creation, keep the source profile on the image row, and name the working space in the interface. - Rendering the preview with a fast approximation and the export with a separate proper path produces a file that does not match what the person approved, and they find out after it is published. Keep one implementation and two resolutions, with a test that renders one recipe at two sizes and compares the downscaled results within a stated tolerance. - Saving a mask as a rasterised alpha buffer at preview resolution gives an export with visibly blocky edges and quietly invalidates every mask the first time the image is cropped or straightened. Keep the geometry in original image coordinates and rasterise at whatever resolution the current render needs. - Keeping the only copy of every recipe inside one SQLite file means a corrupt catalogue loses years of edits while every original survives perfectly, which is the most annoying possible way to lose work. Write a sidecar per image as well, and make the rebuild from sidecars a tested command rather than a paragraph in the README. ===== MILESTONES.md ===== # Delivery milestones · Polarr Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A catalogue that only points at your photographs Importing records a reference and a checksum and writes nothing to the original. ### Steps 1. Import by reference with a checksum The checksum is how you tell moved from edited from gone. Files: `src/import.ts`, `tests/import-readonly.test.ts` 2. Cache thumbnails somewhere disposable A thumbnail cache you can delete without losing anything is the right kind of cache. Files: `src/thumbnails.ts` 3. Open originals read only, and prove it Run the suite against a read only library directory so a stray write fails loudly. Files: `src/files.ts`, `tests/import-readonly.test.ts` ### Done when - [ ] Checksums match before and after import - [ ] A moved file is marked missing with its old path - [ ] Profile name and orientation tag are recorded - [ ] The suite passes with the library directory read only ### Watch out - Importing by copying every file into an application folder doubles the disk use of a photo library and leaves two copies that drift apart, while importing by path with no checksum turns a moved or re-saved file into a blank frame with no explanation. Store the path and the checksum, and show a missing file as missing with the path it had. ## M2 · Colour you can name The pipeline states its colour space instead of inheriting one. ### Steps 1. Set the context settings explicitly Colour space and colour type are arguments, not defaults you inherit. Files: `src/render/context.ts` 2. Carry the source profile on the image You cannot convert honestly from a profile you did not record. Files: `src/import.ts`, `src/render/colour.ts` 3. Test a gradient for banding Eight bits per channel through six operations is where banding appears. Files: `tests/gradient-banding.test.ts` ### Done when - [ ] Colour space and colour type are set at creation - [ ] A wide gamut image is preserved or converted with the conversion named - [ ] A gradient through the full chain is checked for banding - [ ] Exports record the colour space written ### Watch out - Leaving the context settings alone converts every wide gamut photograph into sRGB before the first adjustment, so the saturated reds somebody bought the camera for are clipped silently on a display that could have shown them. Set the colour space and colour type at creation, keep the source profile on the image row, and name the working space in the interface. ## M3 · An edit that is a list, not a bitmap Adjustments are ordered parameters replayed identically at any resolution. ### Steps 1. One renderer, two resolutions The export path is the preview path with a different size argument. Files: `src/render/pipeline.ts`, `tests/preview-matches-export.test.ts` 2. Persist history with the recipe Undo that dies at quit is a worse undo than none, because people rely on it. Files: `src/history.ts` 3. Keep disabled operations in the list Turning an adjustment off is not the same as throwing it away. Files: `src/recipe.ts` ### Done when - [ ] Preview and export agree within a stated tolerance - [ ] Operation order is explicit and affects output - [ ] Undo survives a restart - [ ] Disabled operations remain in the recipe ### Watch out - Rendering the preview with a fast approximation and the export with a separate proper path produces a file that does not match what the person approved, and they find out after it is published. Keep one implementation and two resolutions, with a test that renders one recipe at two sizes and compares the downscaled results within a stated tolerance. ## M4 · Masks that are geometry A local adjustment survives a crop, a re-export and a format change. ### Steps 1. Store masks in original image coordinates So a crop is a coordinate change rather than a rebuild. Files: `src/masks.ts` 2. Rasterise at render time only Whatever resolution this render needs, and nothing persisted. Files: `src/render/masks.ts`, `tests/mask-edges.test.ts` 3. Reference masks by id from operations Two operations sharing one mask should share one mask. Files: `src/recipe.ts` ### Done when - [ ] Masks export with clean edges at full resolution - [ ] A crop keeps adjustments on the same part of the image - [ ] No rasterised mask is stored in a recipe - [ ] Invert and feather work without re-creating the mask ### Watch out - Saving a mask as a rasterised alpha buffer at preview resolution gives an export with visibly blocky edges and quietly invalidates every mask the first time the image is cropped or straightened. Keep the geometry in original image coordinates and rasterise at whatever resolution the current render needs. ## M5 · Getting the photographs and the edits back out (production only) Exports are reproducible and the catalogue can be rebuilt or abandoned. ### Steps 1. Mirror every recipe to a sidecar The catalogue becomes a cache, which is the right thing for it to be. Files: `src/sidecar.ts` 2. Make rebuild a tested command A recovery path nobody has run is a recovery path that does not exist. Files: `src/rebuild.ts`, `tests/rebuild.test.ts` 3. Make batch export resumable Four hundred images and a closed laptop is the normal case, not the edge case. Files: `src/export/batch.ts` ### Done when - [ ] Rebuild from sidecars restores every recipe - [ ] An interrupted batch export resumes correctly - [ ] Exports record format, quality and colour space - [ ] Re-export from one recipe version is byte identical ### Watch out - Keeping the only copy of every recipe inside one SQLite file means a corrupt catalogue loses years of edits while every original survives perfectly, which is the most annoying possible way to lose work. Write a sidecar per image as well, and make the rebuild from sidecars a tested command rather than a paragraph in the README. ===== OPERATIONS.md ===== # Operations · Polarr ## Backup Back up the sidecars with the photographs, since together they are the work; the catalogue is a cache and can be rebuilt. State this in the README so nobody backs up the database and loses the sidecars. ## Restore Restore images and sidecars, run the rebuild command, and confirm the recipe count matches the sidecar count. A restore that produces images with no edits means sidecars were left out of the backup. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool this is the suite: originals never opened writable, preview matching export within tolerance, rebuild from sidecars, and the gradient banding check at the chosen colour type. ## Incident checklist The two incidents are a damaged original, which the read only test exists to make impossible, and an export that does not match the screen, which is always two render paths having drifted apart. 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 - [ ] Originals are confirmed never opened for writing - [ ] The context colour space and colour type are confirmed explicit - [ ] Preview and export are confirmed to agree within tolerance - [ ] Masks are confirmed stored as geometry only - [ ] Rebuild from sidecars is confirmed in the suite ## Launch constraint Do not market omitted Polarr 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. References and recipes. Rebuildable from sidecars, which is the whole idea. CATALOGUE_PATH=~/.local/share/photoedit/catalogue.db # Required. Written beside each image. This is the durable copy of the edit. SIDECAR_SUFFIX=.recipe.json # Required. Passed to the context at creation. Named in the interface and the README. WORKING_COLOUR_SPACE=display-p3 # Required. The other half of the same decision. Check what your runtime actually supports. WORKING_COLOUR_TYPE=float16 # Required. Preview render budget. The export ignores it and uses the same code. PREVIEW_MAX_PIXELS=4000000 # Required. Undo steps retained per image across restarts. HISTORY_KEEP=200
# Polarr · indie build A local photo editor whose hard problems are keeping the original file untouched while edits live as a replayable recipe, choosing a colour pipeline deliberately instead of inheriting the defaults, and making the preview and the export come out of the same code. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with a React and TypeScript front end | the photographs stay on the machine and the file dialogs are the system's | | Pixels | Canvas 2D with colour space and colour type set explicitly at context creation | the defaults are eight bit sRGB and that is a decision whether or not you make it | | Catalogue | SQLite holding references and recipes, never image bytes | the originals stay where the person put them, under the names they gave them | | Recipes | A versioned JSON document per image, written atomically and mirrored to a sidecar | the edits are the product and they have to outlive this binary | | Rendering | One operation list replayed at preview size and at full size by the same code | two pipelines means the exported file does not match what was approved | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 and a Rust toolchain** · free - Why: Tauri needs both, and discovering that halfway through the first render is a bad afternoon. - Get it: nodejs.org and rustup.rs, then the Tauri prerequisites for your platform. - Verify: A fresh Tauri template builds and opens a window - [ ] **Sample photographs with known colour profiles** · free - Why: Colour handling is untestable without at least one wide gamut file and one plain sRGB file whose profiles you already know. - Get it: Export the same photograph twice from any camera application, once tagged Display P3 and once sRGB, and note which is which. - Verify: An image inspector reports a different profile for each file - [ ] **A smooth gradient test image** · free - Why: Banding from an eight bit working chain is invisible on a photograph of leaves and obvious on a gradient, which is the point of having one. - Get it: Generate a wide, very gradual grey or sky gradient at full resolution and keep it in the test fixtures. - Verify: The gradient renders smoothly before any adjustment is applied - [ ] **Your colour policy, written down first** · free - Why: Deciding what you convert after seeing which images look wrong means writing a policy that matches your bugs. - Get it: Write one paragraph: the working colour space, the colour type, what happens to a wide gamut import, and what the export writes. - Verify: The paragraph is in the README and the values it names are the ones in the code ## 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: - Video and creator effects. - Generative retouching. - Cloud sync and mobile applications. - Print output and soft proofing. - pixel-perfect professional tooling - large template and asset libraries - color-management edge cases - cloud collaboration and mobile apps - high-fidelity color, format, and export handling If one of those is essential to you, that is the reason to keep paying for Polarr, and the README should say so rather than pretend.
# Build brief · Polarr 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 local photo editor that never writes to the original file and keeps every edit as a recipe to replace Polarr. 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. ### Rule zero Two rules. The first is the difference between an editor and a shredder. The imported file is never opened for writing. Every adjustment is a parameter in a recipe kept beside the catalogue, and an export renders that recipe onto a copy. When the recipe format changes, recipes written under the old version still open, because the photograph somebody edited last year is the one they will come back for. Second, say out loud what your colour handling is. A 2D canvas context takes its colour space and colour type from a settings dictionary whose defaults are 'srgb' and 'unorm8', so a wide gamut photograph dropped into one is converted and clipped whether or not you decided that. Choose the settings deliberately, record each image's source profile, and write in the README which conversions you perform and which you do not attempt. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with a React and TypeScript front end · the photographs stay on the machine and the file dialogs are the system's - Pixels: Canvas 2D with colour space and colour type set explicitly at context creation · the defaults are eight bit sRGB and that is a decision whether or not you make it - Catalogue: SQLite holding references and recipes, never image bytes · the originals stay where the person put them, under the names they gave them - Recipes: A versioned JSON document per image, written atomically and mirrored to a sidecar · the edits are the product and they have to outlive this binary - Rendering: One operation list replayed at preview size and at full size by the same code · two pipelines means the exported file does not match what was approved ### Data model (create this before Phase 1) - images(id, path, sha256, bytes, width, height, source_profile, orientation_tag, imported_at, missing_since) - recipes(image_id, format_version, ops_json, updated_at) · the ordered operation list, which is the whole of the edit - ops inside a recipe(ordinal, kind, params_json, mask_id, enabled) · order matters, so order is stored rather than implied - masks(id, image_id, kind, geometry_json, feather, inverted) · geometry and parameters, never a saved pixel buffer - history(image_id, ordinal, ops_json, at) · undo that survives closing the application - exports(id, image_id, recipe_version, path, format, colour_space, bytes, at) · what was rendered, from which recipe, in which space - Nothing here can be destroyed by editing a photograph, because nothing here is the photograph. ### Phase 1 · A catalogue that only points at your photographs Build: Import by reference: record the path, size, checksum, pixel dimensions, the embedded colour profile name and the orientation tag. Show a grid with thumbnails cached separately. Detect a file that has moved or changed underneath the catalogue and mark it rather than guessing. Trap: Importing by copying every file into an application folder doubles the disk use of a photo library and leaves two copies that drift apart, while importing by path with no checksum turns a moved or re-saved file into a blank frame with no explanation. Store the path and the checksum, and show a missing file as missing with the path it had. Done when: A file has the same checksum before and after import, asserted in the suite. Moving or replacing a file on disk marks the image missing or changed and names the path, rather than showing an empty frame. The embedded profile name and the orientation tag are recorded at import and visible in the interface. No code path anywhere opens an original in a writable mode, verified by review and by a test that makes the library directory read only. Do not build yet: No adjustments, masks, history or export. ### Phase 2 · Colour you can name Build: Create the drawing context with the colour space and colour type set explicitly, rather than accepting the 'srgb' and 'unorm8' defaults. Keep each image's source profile, convert on purpose, and put in the README which conversions happen and which are out of scope. Trap: Leaving the context settings alone converts every wide gamut photograph into sRGB before the first adjustment, so the saturated reds somebody bought the camera for are clipped silently on a display that could have shown them. Set the colour space and colour type at creation, keep the source profile on the image row, and name the working space in the interface. Done when: The context is created with an explicit colour space and colour type and both values appear in the README and in the interface. An image carrying a wide gamut profile is either preserved or converted with the conversion named on screen, never quietly. A long smooth gradient taken through the full operation chain is inspected for banding at the chosen colour type and the result is recorded. Every export row stores the colour space it was written in. Do not build yet: No masks, history or batch export yet. ### Phase 3 · An edit that is a list, not a bitmap Build: Model each adjustment as an operation with a kind, parameters and an ordinal. Render the preview by replaying the list at screen size and the export by replaying the same list at full size through the same code. Undo and redo move along the list and persist across a restart. Trap: Rendering the preview with a fast approximation and the export with a separate proper path produces a file that does not match what the person approved, and they find out after it is published. Keep one implementation and two resolutions, with a test that renders one recipe at two sizes and compares the downscaled results within a stated tolerance. Done when: The same recipe rendered at preview size and at full size matches within a stated tolerance after downscaling, tested. Reordering two operations changes the output and the new order is what gets exported. Undo and redo survive quitting and reopening the application. Toggling an operation off leaves it in the recipe rather than deleting it. Do not build yet: No masks, sidecars or batch export yet. ### Phase 4 · Masks that are geometry Build: Store radial, linear and brush masks as geometry with a feather and an inversion flag, in coordinates relative to the original image. Operations reference a mask by id. Editing a mask re-renders rather than accumulating, and a crop maps mask coordinates rather than baking them. Trap: Saving a mask as a rasterised alpha buffer at preview resolution gives an export with visibly blocky edges and quietly invalidates every mask the first time the image is cropped or straightened. Keep the geometry in original image coordinates and rasterise at whatever resolution the current render needs. Done when: A mask created at preview resolution exports with a clean edge at full resolution, checked visually and by an edge gradient test. Cropping an image leaves masked adjustments on the same part of the picture. No rasterised mask buffer is ever written to the recipe, verified by inspecting a saved recipe. A mask can be inverted and feathered without re-creating it. Do not build yet: No batch export, sidecars or rebuild yet. ### Phase 5 · Getting the photographs and the edits back out Build: Export with format, quality and colour space recorded on the row. Mirror every recipe to a sidecar file beside its image so another tool or a fresh catalogue can find it. Batch export as a resumable job. Provide a rebuild command that reconstructs the catalogue from files and sidecars alone, and run it in the test suite. Trap: Keeping the only copy of every recipe inside one SQLite file means a corrupt catalogue loses years of edits while every original survives perfectly, which is the most annoying possible way to lose work. Write a sidecar per image as well, and make the rebuild from sidecars a tested command rather than a paragraph in the README. Done when: Deleting the catalogue and running the rebuild command restores every recipe from sidecars, exercised in the suite. A batch export interrupted halfway resumes without re-rendering finished images and without skipping any. Each export row records format, quality and colour space, and re-exporting from the same recipe version produces the same bytes. The README states plainly which colour conversions the tool performs. Do not build yet: Nothing after this. ### Out of scope (and why) - Video filters and creator effects, which are a second product with their own codec, timeline and performance problems. - Generative retouching and sky replacement, which need models and rights you do not have and change what the photograph is evidence of. - Cloud sync and mobile applications, which is where the subscription actually goes and what a local editor deliberately trades away. - Print output and soft proofing to a press profile, which needs measured profiles and a calibrated display to mean anything. ### README must contain - That originals are never opened for writing and every edit lives in a recipe and a sidecar - Which colour space and colour type the drawing context is created with, and why they were chosen - Which colour conversions the tool performs and which it explicitly does not attempt - That preview and export share one renderer, and the tolerance at which they are tested to agree - How to rebuild the catalogue from images and sidecars after deleting it
# Agent instructions · Polarr indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with a React and TypeScript front end, Canvas 2D with colour space and colour type set explicitly at context creation, SQLite holding references and recipes, never image bytes, A versioned JSON document per image, written atomically and mirrored to a sidecar, One operation list replayed at preview size and at full size by the same code. 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 - Importing by copying every file into an application folder doubles the disk use of a photo library and leaves two copies that drift apart, while importing by path with no checksum turns a moved or re-saved file into a blank frame with no explanation. Store the path and the checksum, and show a missing file as missing with the path it had. - Leaving the context settings alone converts every wide gamut photograph into sRGB before the first adjustment, so the saturated reds somebody bought the camera for are clipped silently on a display that could have shown them. Set the colour space and colour type at creation, keep the source profile on the image row, and name the working space in the interface. - Rendering the preview with a fast approximation and the export with a separate proper path produces a file that does not match what the person approved, and they find out after it is published. Keep one implementation and two resolutions, with a test that renders one recipe at two sizes and compares the downscaled results within a stated tolerance. - Saving a mask as a rasterised alpha buffer at preview resolution gives an export with visibly blocky edges and quietly invalidates every mask the first time the image is cropped or straightened. Keep the geometry in original image coordinates and rasterise at whatever resolution the current render needs.
# Build plan · Polarr A local photo editor whose hard problems are keeping the original file untouched while edits live as a replayable recipe, choosing a colour pipeline deliberately instead of inheriting the defaults, and making the preview and the export come out of the same code. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A catalogue that only points at your photographs Importing records a reference and a checksum and writes nothing to the original. ### Steps 1. Import by reference with a checksum The checksum is how you tell moved from edited from gone. Files: `src/import.ts`, `tests/import-readonly.test.ts` 2. Cache thumbnails somewhere disposable A thumbnail cache you can delete without losing anything is the right kind of cache. Files: `src/thumbnails.ts` 3. Open originals read only, and prove it Run the suite against a read only library directory so a stray write fails loudly. Files: `src/files.ts`, `tests/import-readonly.test.ts` ### Done when - [ ] Checksums match before and after import - [ ] A moved file is marked missing with its old path - [ ] Profile name and orientation tag are recorded - [ ] The suite passes with the library directory read only ### Watch out - Importing by copying every file into an application folder doubles the disk use of a photo library and leaves two copies that drift apart, while importing by path with no checksum turns a moved or re-saved file into a blank frame with no explanation. Store the path and the checksum, and show a missing file as missing with the path it had. ## Phase 2 · Colour you can name The pipeline states its colour space instead of inheriting one. ### Steps 1. Set the context settings explicitly Colour space and colour type are arguments, not defaults you inherit. Files: `src/render/context.ts` 2. Carry the source profile on the image You cannot convert honestly from a profile you did not record. Files: `src/import.ts`, `src/render/colour.ts` 3. Test a gradient for banding Eight bits per channel through six operations is where banding appears. Files: `tests/gradient-banding.test.ts` ### Done when - [ ] Colour space and colour type are set at creation - [ ] A wide gamut image is preserved or converted with the conversion named - [ ] A gradient through the full chain is checked for banding - [ ] Exports record the colour space written ### Watch out - Leaving the context settings alone converts every wide gamut photograph into sRGB before the first adjustment, so the saturated reds somebody bought the camera for are clipped silently on a display that could have shown them. Set the colour space and colour type at creation, keep the source profile on the image row, and name the working space in the interface. ## Phase 3 · An edit that is a list, not a bitmap Adjustments are ordered parameters replayed identically at any resolution. ### Steps 1. One renderer, two resolutions The export path is the preview path with a different size argument. Files: `src/render/pipeline.ts`, `tests/preview-matches-export.test.ts` 2. Persist history with the recipe Undo that dies at quit is a worse undo than none, because people rely on it. Files: `src/history.ts` 3. Keep disabled operations in the list Turning an adjustment off is not the same as throwing it away. Files: `src/recipe.ts` ### Done when - [ ] Preview and export agree within a stated tolerance - [ ] Operation order is explicit and affects output - [ ] Undo survives a restart - [ ] Disabled operations remain in the recipe ### Watch out - Rendering the preview with a fast approximation and the export with a separate proper path produces a file that does not match what the person approved, and they find out after it is published. Keep one implementation and two resolutions, with a test that renders one recipe at two sizes and compares the downscaled results within a stated tolerance. ## Phase 4 · Masks that are geometry A local adjustment survives a crop, a re-export and a format change. ### Steps 1. Store masks in original image coordinates So a crop is a coordinate change rather than a rebuild. Files: `src/masks.ts` 2. Rasterise at render time only Whatever resolution this render needs, and nothing persisted. Files: `src/render/masks.ts`, `tests/mask-edges.test.ts` 3. Reference masks by id from operations Two operations sharing one mask should share one mask. Files: `src/recipe.ts` ### Done when - [ ] Masks export with clean edges at full resolution - [ ] A crop keeps adjustments on the same part of the image - [ ] No rasterised mask is stored in a recipe - [ ] Invert and feather work without re-creating the mask ### Watch out - Saving a mask as a rasterised alpha buffer at preview resolution gives an export with visibly blocky edges and quietly invalidates every mask the first time the image is cropped or straightened. Keep the geometry in original image coordinates and rasterise at whatever resolution the current render needs. ## Not in this build - Video and creator effects. - Generative retouching. - Cloud sync and mobile applications. - Print output and soft proofing. ## After v1, if you want it - Presets as named partial recipes, which is a filter pack without any of the licensing - A compare view against the unedited original, which costs nothing once edits are a list - Lens and sensor corrections from published profiles, once the operation list has settled
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. References and recipes. Rebuildable from sidecars, which is the whole idea. CATALOGUE_PATH=~/.local/share/photoedit/catalogue.db # Required. Written beside each image. This is the durable copy of the edit. SIDECAR_SUFFIX=.recipe.json # Required. Passed to the context at creation. Named in the interface and the README. WORKING_COLOUR_SPACE=display-p3 # Required. The other half of the same decision. Check what your runtime actually supports. WORKING_COLOUR_TYPE=float16 # Required. Preview render budget. The export ignores it and uses the same code. PREVIEW_MAX_PIXELS=4000000 # Required. Undo steps retained per image across restarts. HISTORY_KEEP=200
# Polarr · product brief ## Problem The visible photo editing loop is buildable, but a credible replacement needs more than the first screen. Polarr earns its keep through editor polish, assets, algorithms, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome An editor that cannot damage a photograph, states which colour space it works in rather than inheriting one, exports exactly what the preview showed, and can be rebuilt from the files on disk after you delete its database. ## 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 - Video and creator effects. - Generative retouching. - Cloud sync and mobile applications. - Print output and soft proofing. - pixel-perfect professional tooling - large template and asset libraries - color-management edge cases - cloud collaboration and mobile apps - high-fidelity color, format, and export handling ## Success criteria - Originals are confirmed never opened for writing - The context colour space and colour type are confirmed explicit - Preview and export are confirmed to agree within tolerance - Masks are confirmed stored as geometry only - Rebuild from sidecars is confirmed in the suite
# Build brief · Polarr 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 local photo editor that never writes to the original file and keeps every edit as a recipe to replace Polarr. 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. ### Rule zero Two rules. The first is the difference between an editor and a shredder. The imported file is never opened for writing. Every adjustment is a parameter in a recipe kept beside the catalogue, and an export renders that recipe onto a copy. When the recipe format changes, recipes written under the old version still open, because the photograph somebody edited last year is the one they will come back for. Second, say out loud what your colour handling is. A 2D canvas context takes its colour space and colour type from a settings dictionary whose defaults are 'srgb' and 'unorm8', so a wide gamut photograph dropped into one is converted and clipped whether or not you decided that. Choose the settings deliberately, record each image's source profile, and write in the README which conversions you perform and which you do not attempt. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with a React and TypeScript front end · the photographs stay on the machine and the file dialogs are the system's - Pixels: Canvas 2D with colour space and colour type set explicitly at context creation · the defaults are eight bit sRGB and that is a decision whether or not you make it - Catalogue: SQLite holding references and recipes, never image bytes · the originals stay where the person put them, under the names they gave them - Recipes: A versioned JSON document per image, written atomically and mirrored to a sidecar · the edits are the product and they have to outlive this binary - Rendering: One operation list replayed at preview size and at full size by the same code · two pipelines means the exported file does not match what was approved ### Data model (create this before Phase 1) - images(id, path, sha256, bytes, width, height, source_profile, orientation_tag, imported_at, missing_since) - recipes(image_id, format_version, ops_json, updated_at) · the ordered operation list, which is the whole of the edit - ops inside a recipe(ordinal, kind, params_json, mask_id, enabled) · order matters, so order is stored rather than implied - masks(id, image_id, kind, geometry_json, feather, inverted) · geometry and parameters, never a saved pixel buffer - history(image_id, ordinal, ops_json, at) · undo that survives closing the application - exports(id, image_id, recipe_version, path, format, colour_space, bytes, at) · what was rendered, from which recipe, in which space - Nothing here can be destroyed by editing a photograph, because nothing here is the photograph. ### Phase 1 · A catalogue that only points at your photographs Build: Import by reference: record the path, size, checksum, pixel dimensions, the embedded colour profile name and the orientation tag. Show a grid with thumbnails cached separately. Detect a file that has moved or changed underneath the catalogue and mark it rather than guessing. Trap: Importing by copying every file into an application folder doubles the disk use of a photo library and leaves two copies that drift apart, while importing by path with no checksum turns a moved or re-saved file into a blank frame with no explanation. Store the path and the checksum, and show a missing file as missing with the path it had. Done when: A file has the same checksum before and after import, asserted in the suite. Moving or replacing a file on disk marks the image missing or changed and names the path, rather than showing an empty frame. The embedded profile name and the orientation tag are recorded at import and visible in the interface. No code path anywhere opens an original in a writable mode, verified by review and by a test that makes the library directory read only. Do not build yet: No adjustments, masks, history or export. ### Phase 2 · Colour you can name Build: Create the drawing context with the colour space and colour type set explicitly, rather than accepting the 'srgb' and 'unorm8' defaults. Keep each image's source profile, convert on purpose, and put in the README which conversions happen and which are out of scope. Trap: Leaving the context settings alone converts every wide gamut photograph into sRGB before the first adjustment, so the saturated reds somebody bought the camera for are clipped silently on a display that could have shown them. Set the colour space and colour type at creation, keep the source profile on the image row, and name the working space in the interface. Done when: The context is created with an explicit colour space and colour type and both values appear in the README and in the interface. An image carrying a wide gamut profile is either preserved or converted with the conversion named on screen, never quietly. A long smooth gradient taken through the full operation chain is inspected for banding at the chosen colour type and the result is recorded. Every export row stores the colour space it was written in. Do not build yet: No masks, history or batch export yet. ### Phase 3 · An edit that is a list, not a bitmap Build: Model each adjustment as an operation with a kind, parameters and an ordinal. Render the preview by replaying the list at screen size and the export by replaying the same list at full size through the same code. Undo and redo move along the list and persist across a restart. Trap: Rendering the preview with a fast approximation and the export with a separate proper path produces a file that does not match what the person approved, and they find out after it is published. Keep one implementation and two resolutions, with a test that renders one recipe at two sizes and compares the downscaled results within a stated tolerance. Done when: The same recipe rendered at preview size and at full size matches within a stated tolerance after downscaling, tested. Reordering two operations changes the output and the new order is what gets exported. Undo and redo survive quitting and reopening the application. Toggling an operation off leaves it in the recipe rather than deleting it. Do not build yet: No masks, sidecars or batch export yet. ### Phase 4 · Masks that are geometry Build: Store radial, linear and brush masks as geometry with a feather and an inversion flag, in coordinates relative to the original image. Operations reference a mask by id. Editing a mask re-renders rather than accumulating, and a crop maps mask coordinates rather than baking them. Trap: Saving a mask as a rasterised alpha buffer at preview resolution gives an export with visibly blocky edges and quietly invalidates every mask the first time the image is cropped or straightened. Keep the geometry in original image coordinates and rasterise at whatever resolution the current render needs. Done when: A mask created at preview resolution exports with a clean edge at full resolution, checked visually and by an edge gradient test. Cropping an image leaves masked adjustments on the same part of the picture. No rasterised mask buffer is ever written to the recipe, verified by inspecting a saved recipe. A mask can be inverted and feathered without re-creating it. Do not build yet: No batch export, sidecars or rebuild yet. ### Phase 5 · Getting the photographs and the edits back out Build: Export with format, quality and colour space recorded on the row. Mirror every recipe to a sidecar file beside its image so another tool or a fresh catalogue can find it. Batch export as a resumable job. Provide a rebuild command that reconstructs the catalogue from files and sidecars alone, and run it in the test suite. Trap: Keeping the only copy of every recipe inside one SQLite file means a corrupt catalogue loses years of edits while every original survives perfectly, which is the most annoying possible way to lose work. Write a sidecar per image as well, and make the rebuild from sidecars a tested command rather than a paragraph in the README. Done when: Deleting the catalogue and running the rebuild command restores every recipe from sidecars, exercised in the suite. A batch export interrupted halfway resumes without re-rendering finished images and without skipping any. Each export row records format, quality and colour space, and re-exporting from the same recipe version produces the same bytes. The README states plainly which colour conversions the tool performs. Do not build yet: Nothing after this. ### Out of scope (and why) - Video filters and creator effects, which are a second product with their own codec, timeline and performance problems. - Generative retouching and sky replacement, which need models and rights you do not have and change what the photograph is evidence of. - Cloud sync and mobile applications, which is where the subscription actually goes and what a local editor deliberately trades away. - Print output and soft proofing to a press profile, which needs measured profiles and a calibrated display to mean anything. ### README must contain - That originals are never opened for writing and every edit lives in a recipe and a sidecar - Which colour space and colour type the drawing context is created with, and why they were chosen - Which colour conversions the tool performs and which it explicitly does not attempt - That preview and export share one renderer, and the tolerance at which they are tested to agree - How to rebuild the catalogue from images and sidecars after deleting it
# Architecture · Polarr ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with a React and TypeScript front end | the photographs stay on the machine and the file dialogs are the system's | | Pixels | Canvas 2D with colour space and colour type set explicitly at context creation | the defaults are eight bit sRGB and that is a decision whether or not you make it | | Catalogue | SQLite holding references and recipes, never image bytes | the originals stay where the person put them, under the names they gave them | | Recipes | A versioned JSON document per image, written atomically and mirrored to a sidecar | the edits are the product and they have to outlive this binary | | Rendering | One operation list replayed at preview size and at full size by the same code | two pipelines means the exported file does not match what was approved | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Catalogue | References, checksums and missing file detection. | Any store, provided it never holds image bytes and can be rebuilt from sidecars. | | Colour | Context settings, source profiles and conversions. | Any implementation that still names its working space on screen. | | Recipe | The ordered operation list and history. | Any format, provided old versions still open and a sidecar is written. | | Renderer | One replay path used at both resolutions. | Not swappable for a separate export renderer, which is the mismatch bug. | | Masks | Geometry in original image coordinates. | Any mask kinds, provided nothing rasterised is persisted. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `CATALOGUE_PATH` · required · References and recipes. Rebuildable from sidecars, which is the whole idea. - `SIDECAR_SUFFIX` · required · Written beside each image. This is the durable copy of the edit. - `WORKING_COLOUR_SPACE` · required · Passed to the context at creation. Named in the interface and the README. - `WORKING_COLOUR_TYPE` · required · The other half of the same decision. Check what your runtime actually supports. - `PREVIEW_MAX_PIXELS` · required · Preview render budget. The export ignores it and uses the same code. - `HISTORY_KEEP` · required · Undo steps retained per image across restarts. ## 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 · Polarr product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with a React and TypeScript front end, Canvas 2D with colour space and colour type set explicitly at context creation, SQLite holding references and recipes, never image bytes, A versioned JSON document per image, written atomically and mirrored to a sidecar, One operation list replayed at preview size and at full size by the same code. - 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 - Importing by copying every file into an application folder doubles the disk use of a photo library and leaves two copies that drift apart, while importing by path with no checksum turns a moved or re-saved file into a blank frame with no explanation. Store the path and the checksum, and show a missing file as missing with the path it had. - Leaving the context settings alone converts every wide gamut photograph into sRGB before the first adjustment, so the saturated reds somebody bought the camera for are clipped silently on a display that could have shown them. Set the colour space and colour type at creation, keep the source profile on the image row, and name the working space in the interface. - Rendering the preview with a fast approximation and the export with a separate proper path produces a file that does not match what the person approved, and they find out after it is published. Keep one implementation and two resolutions, with a test that renders one recipe at two sizes and compares the downscaled results within a stated tolerance. - Saving a mask as a rasterised alpha buffer at preview resolution gives an export with visibly blocky edges and quietly invalidates every mask the first time the image is cropped or straightened. Keep the geometry in original image coordinates and rasterise at whatever resolution the current render needs. - Keeping the only copy of every recipe inside one SQLite file means a corrupt catalogue loses years of edits while every original survives perfectly, which is the most annoying possible way to lose work. Write a sidecar per image as well, and make the rebuild from sidecars a tested command rather than a paragraph in the README.
# Delivery milestones · Polarr Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A catalogue that only points at your photographs Importing records a reference and a checksum and writes nothing to the original. ### Steps 1. Import by reference with a checksum The checksum is how you tell moved from edited from gone. Files: `src/import.ts`, `tests/import-readonly.test.ts` 2. Cache thumbnails somewhere disposable A thumbnail cache you can delete without losing anything is the right kind of cache. Files: `src/thumbnails.ts` 3. Open originals read only, and prove it Run the suite against a read only library directory so a stray write fails loudly. Files: `src/files.ts`, `tests/import-readonly.test.ts` ### Done when - [ ] Checksums match before and after import - [ ] A moved file is marked missing with its old path - [ ] Profile name and orientation tag are recorded - [ ] The suite passes with the library directory read only ### Watch out - Importing by copying every file into an application folder doubles the disk use of a photo library and leaves two copies that drift apart, while importing by path with no checksum turns a moved or re-saved file into a blank frame with no explanation. Store the path and the checksum, and show a missing file as missing with the path it had. ## M2 · Colour you can name The pipeline states its colour space instead of inheriting one. ### Steps 1. Set the context settings explicitly Colour space and colour type are arguments, not defaults you inherit. Files: `src/render/context.ts` 2. Carry the source profile on the image You cannot convert honestly from a profile you did not record. Files: `src/import.ts`, `src/render/colour.ts` 3. Test a gradient for banding Eight bits per channel through six operations is where banding appears. Files: `tests/gradient-banding.test.ts` ### Done when - [ ] Colour space and colour type are set at creation - [ ] A wide gamut image is preserved or converted with the conversion named - [ ] A gradient through the full chain is checked for banding - [ ] Exports record the colour space written ### Watch out - Leaving the context settings alone converts every wide gamut photograph into sRGB before the first adjustment, so the saturated reds somebody bought the camera for are clipped silently on a display that could have shown them. Set the colour space and colour type at creation, keep the source profile on the image row, and name the working space in the interface. ## M3 · An edit that is a list, not a bitmap Adjustments are ordered parameters replayed identically at any resolution. ### Steps 1. One renderer, two resolutions The export path is the preview path with a different size argument. Files: `src/render/pipeline.ts`, `tests/preview-matches-export.test.ts` 2. Persist history with the recipe Undo that dies at quit is a worse undo than none, because people rely on it. Files: `src/history.ts` 3. Keep disabled operations in the list Turning an adjustment off is not the same as throwing it away. Files: `src/recipe.ts` ### Done when - [ ] Preview and export agree within a stated tolerance - [ ] Operation order is explicit and affects output - [ ] Undo survives a restart - [ ] Disabled operations remain in the recipe ### Watch out - Rendering the preview with a fast approximation and the export with a separate proper path produces a file that does not match what the person approved, and they find out after it is published. Keep one implementation and two resolutions, with a test that renders one recipe at two sizes and compares the downscaled results within a stated tolerance. ## M4 · Masks that are geometry A local adjustment survives a crop, a re-export and a format change. ### Steps 1. Store masks in original image coordinates So a crop is a coordinate change rather than a rebuild. Files: `src/masks.ts` 2. Rasterise at render time only Whatever resolution this render needs, and nothing persisted. Files: `src/render/masks.ts`, `tests/mask-edges.test.ts` 3. Reference masks by id from operations Two operations sharing one mask should share one mask. Files: `src/recipe.ts` ### Done when - [ ] Masks export with clean edges at full resolution - [ ] A crop keeps adjustments on the same part of the image - [ ] No rasterised mask is stored in a recipe - [ ] Invert and feather work without re-creating the mask ### Watch out - Saving a mask as a rasterised alpha buffer at preview resolution gives an export with visibly blocky edges and quietly invalidates every mask the first time the image is cropped or straightened. Keep the geometry in original image coordinates and rasterise at whatever resolution the current render needs. ## M5 · Getting the photographs and the edits back out (production only) Exports are reproducible and the catalogue can be rebuilt or abandoned. ### Steps 1. Mirror every recipe to a sidecar The catalogue becomes a cache, which is the right thing for it to be. Files: `src/sidecar.ts` 2. Make rebuild a tested command A recovery path nobody has run is a recovery path that does not exist. Files: `src/rebuild.ts`, `tests/rebuild.test.ts` 3. Make batch export resumable Four hundred images and a closed laptop is the normal case, not the edge case. Files: `src/export/batch.ts` ### Done when - [ ] Rebuild from sidecars restores every recipe - [ ] An interrupted batch export resumes correctly - [ ] Exports record format, quality and colour space - [ ] Re-export from one recipe version is byte identical ### Watch out - Keeping the only copy of every recipe inside one SQLite file means a corrupt catalogue loses years of edits while every original survives perfectly, which is the most annoying possible way to lose work. Write a sidecar per image as well, and make the rebuild from sidecars a tested command rather than a paragraph in the README.
# Operations · Polarr ## Backup Back up the sidecars with the photographs, since together they are the work; the catalogue is a cache and can be rebuilt. State this in the README so nobody backs up the database and loses the sidecars. ## Restore Restore images and sidecars, run the rebuild command, and confirm the recipe count matches the sidecar count. A restore that produces images with no edits means sidecars were left out of the backup. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool this is the suite: originals never opened writable, preview matching export within tolerance, rebuild from sidecars, and the gradient banding check at the chosen colour type. ## Incident checklist The two incidents are a damaged original, which the read only test exists to make impossible, and an export that does not match the screen, which is always two render paths having drifted apart. 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 - [ ] Originals are confirmed never opened for writing - [ ] The context colour space and colour type are confirmed explicit - [ ] Preview and export are confirmed to agree within tolerance - [ ] Masks are confirmed stored as geometry only - [ ] Rebuild from sidecars is confirmed in the suite ## Launch constraint Do not market omitted Polarr 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. References and recipes. Rebuildable from sidecars, which is the whole idea. CATALOGUE_PATH=~/.local/share/photoedit/catalogue.db # Required. Written beside each image. This is the durable copy of the edit. SIDECAR_SUFFIX=.recipe.json # Required. Passed to the context at creation. Named in the interface and the README. WORKING_COLOUR_SPACE=display-p3 # Required. The other half of the same decision. Check what your runtime actually supports. WORKING_COLOUR_TYPE=float16 # Required. Preview render budget. The export ignores it and uses the same code. PREVIEW_MAX_PIXELS=4000000 # Required. Undo steps retained per image across restarts. HISTORY_KEEP=200
$ choose a build depth, inspect the files, then open the complete pack in your agent
Polarr: Creative users pay for speed, output fidelity, nondestructive editing, and years of edge-case work hidden behind direct manipulation.
xpixel-perfect professional tooling
xlarge template and asset libraries
xcolor-management edge cases
xcloud collaboration and mobile apps
xhigh-fidelity color, format, and export handling
Polarr pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0 | $0 | 14 basic preset groups plus core adjustments, layers, retouching and unlimited undo; no masks, batch export or Pro filters. |
| pro | $7.99 | $3.99 | Pro filters, masks and batch features across up to 5 users/devices. |
free tier14 basic preset groups, core adjustments, layers, retouching and unlimited undo; no masks or batch tools.
billingmonthly + annual
hidden costsApp Store and regional prices can differ from the web price; the monthly offer includes a promotional first month free.
verified 2026-08-14 · source ↗
Vibecode Polarr
Kinda. The core of Polarr is buildable in a weekend with the prompt on this page, but there are real gaps: pixel-perfect professional tooling, large template and asset libraries. Read the honest list above before committing.
How much does Polarr cost?
Polarr'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 Polarr?
Honestly: pixel-perfect professional tooling; large template and asset libraries; color-management edge cases; cloud collaboration and mobile apps; high-fidelity color, format, and export handling. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Polarr?
Yes: Penpot (Open-source collaborative design and prototyping platform.), PhotoPrism (Self-hosted photo organization and search.). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.