You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Live roadmap state:canonical current-state comment. Conductors should edit that comment in place; child issues and PRs hold the historical evidence.
Source: codebase audit (CODEBASE_AUDIT.md, §5–6) + owner feedback
Purpose
This is a meta-PRD: a prioritized roadmap where each feature section is written to be lifted into its own full PRD (or broken directly into issues with $to-issues). Long-term product goal: a backend that stores and syncs pixel-art drawings, with accounts and a low monthly subscription for more sync.
Guiding constraints
The client stays local-first: everything works offline; sync is a background reconciler.
Entitlements are enforced server-side at the sync API, never in client-only code.
The palette is the core tool — it must be strong, resilient, easy to use and understand (see F11).
Problem: the app persists exactly one project ('current-project' IndexedDB slot). Users cannot keep multiple works in progress. Solution: new/open/duplicate/rename/delete with thumbnails, stored in the existing IndexedDB sprites store keyed by project UUID. Entry point: File menu + a start screen / project browser. Why first: forces the project-identity model and repository interface (#65) that cloud sync requires; big UX win on its own. Scope notes: thumbnail = downscaled composite of frame 1; store lastOpenedProjectId in settings; migrate the legacy single slot into a normal project record on first run. Acceptance sketch: create ≥2 projects, switch between them with full undo isolation, reload restores the last-opened one, deleting prompts for confirmation. Depends on:#65.
F1b — Multiple projects open at once
Problem: switching projects (F1) still means one project at a time; artists want to work across pieces (compare, copy pixels/palettes). Candidate directions:
Tabs (recommended first): one active project, editor chrome rebinds to it. Simplest mental model, cheapest rendering (one live canvas), and covers most of the value.
Split view (second step): two tabs visible side by side; only one has input focus, panels follow focus. Main payoff: copy/paste and palette reuse across projects.
Free-floating windows in workspace: most flexible, most expensive (N live viewports, window management, per-window rendering). Recommend deferring indefinitely unless split view proves insufficient.
The real work is not the UI — it is state. Every store is currently a module singleton; "which project is selected" must become a first-class concept:
An activeProjectContext signal; all panels (palette panel notably, timeline, layers, context bar) read through it and rebind on switch.
Per-project undo stacks and auto-save (history context machinery already exists and helps).
Cross-project clipboard is the killer feature of multi-open — design it explicitly (palette-index remap on paste between projects with different palettes).
Acceptance sketch (tabs): two projects open in tabs; switching tabs swaps canvas, palette, timeline, layers, and undo history correctly; copying a selection from tab A pastes into tab B with sensible color handling. Depends on: F1, #63. Sizing: large — plan as its own PRD with the ProjectContext refactor as slice 1.
F2 — Accounts + cloud sync MVP (the backend)
Problem: work lives in one browser's IndexedDB; clearing site data destroys it; no cross-device access. Solution: email magic-link or OAuth login; sync the project library as whole-project blobs, last-write-wins per project; conflict = "keep both copies". Client stays local-first — sync is a background reconciler behind the ProjectRepository interface. Suggested stack (boring on purpose): Postgres (accounts, project metadata, entitlements) + S3-compatible object storage for project blobs (already on Scaleway) + a thin API (Node/Hono/Fastify or a BaaS like Supabase/PocketBase to start). Store ProjectFile as the canonical schema — which is why #71/#69and F11 come first. Acceptance sketch: sign in on two browsers, edit on A, see it on B after sync; kill network mid-edit, keep working, reconcile on reconnect. Depends on: F1, F11, #71, #69, #68.
F3 — Subscription billing (Stripe)
Problem: monetization for sync costs. Solution: Stripe Checkout + customer portal; webhooks update an entitlements table checked by the sync API. Free tier: e.g. 3 synced projects; paid: unlimited projects + version history (F6) + future collab (F7). Local-only usage stays free and unlimited forever. Acceptance sketch: upgrade unlocks limits within a minute of webhook; cancel downgrades gracefully (data readable, sync of over-quota projects paused, never deleted). Depends on: F2.
F4 — Cross-device asset sync (palettes & brushes)
Problem: custom palettes/brushes are stuck per device. Solution: sync the small payloads first — palette-persistence.ts / brush-persistence.ts already isolate the data. Ideal low-risk shakedown of the sync engine before/alongside broad project sync. Depends on: F2 (auth), can ship before full project sync is battle-tested.
F5 — Shareable read-only links
Problem: no way to show work without exporting and uploading elsewhere. Solution: publish a sprite/animation to a public URL — server renders/stores GIF/WebP/PNG plus a lightweight embedded viewer page. Growth loop; also the first "public" backend surface with minimal abuse area (read-only). Acceptance sketch: "Share" produces a URL that renders the animation without an account; unpublish works. Depends on: F2. Synergy: shared pages could offer the CRT effect (F13) as a viewer toggle, and paint-by-number boards (F14) are inherently shareable.
F6 — Delta sync + cloud version history (paid-tier feature)
Problem: whole-project blob sync re-uploads everything per change and offers no history. Solution: content-addressed per-cel blobs (hash the index buffers / PNG bytes), a project.json manifest referencing hashes, keep the last N manifests per project = version history nearly for free. The dirty-rect + patch-command work (#66) provides the change granularity. Acceptance sketch: editing one cel uploads only that cel; user can browse and restore previous cloud versions (paid tier). Depends on: F2, #66.
F7 — Real-time collaboration (later, paid tier)
Problem/opportunity: the command pattern already stamps userId on every command and supports per-user undo — it was built for this. Solution: server-authoritative command log per project session; replay commands to followers; CRDTs only if free-form concurrent editing demands it. Do not start before F6 is stable. Depends on: F2, F6, #63.
F8 — PWA / installability
Problem: the app feels like a website; offline behavior is incidental. Solution: service worker with offline caching of the shell, install prompt, file-handler registration for .aseprite/.pf so OS double-click opens the app. Reinforces the local-first story; independent of the backend. Depends on: nothing — can ship anytime.
F9 — Serialization overhaul (tracked in #71/#69/#68)
Listed for roadmap completeness: versioned schema + migration pipeline + round-trip tests is the gate between "product features" and "backend features". Treat #71 as its PRD. Coordinate with F11 — the palette model decision changes the schema; make both changes in the same format-version bump (v4) rather than two.
Same: listed for completeness; treat #67 as the actionable issue.
F11 — Palette rework: one palette, one truth (core-tool hardening)
Problem: the palette is the core tool but currently has a dual model: a main palette plus an "ephemeral/untracked" stack for colors drawn that aren't in the palette, with promotion ("track this color"), demotion, index remapping, rebuild-from-drawing, and a v3.1 file field. The machinery spans 12 files (stores/palette/*, animation/palette-sync.ts, promotion events, rebuildEphemeralFromDrawing, clearEphemeralColors(skipRemap)), and the owner's own assessment is that it may solve a problem that doesn't exist. Two color lifecycles = twice the states a user (and the code) must reason about.
Direction — collapse to a single lifecycle:
The palette is the single source of truth. Drawing with a color not in the palette simply adds it to the palette, at most visually flagged "new/unsorted" (one boolean badge, not a parallel collection with its own indices).
Deleting a palette color keeps explicit, predictable semantics (offer: replace usages with nearest color / with transparency — the remap machinery that exists today is reusable here).
Loading a file always loads that file's palette (already true for v3.0+ imports via loadProject; make the auto-save path and legacy formats behave identically so there is exactly one rule).
Palette presets/switching stay as-is (they already work well per the owner).
Payoff: kills the most complex duplicated state in the app (see #71 item 6), simplifies ProjectFile (drop ephemeralPalette in v4 with a migration folding old ephemeral colors into the palette), and gives sync a simpler canonical schema. Acceptance sketch: draw with a non-palette color → it appears in the palette flagged "new"; save/load round-trips exactly one color list; old v3.1 files load with ephemeral colors folded in; no user-visible "promote/track" concept remains (or it becomes just "clear new flag"). Depends on:#68 (tests first), coordinates with #71/#69 (same format bump). Do before F2 freezes the schema.
F12 — Reference image layers (background guides)
Problem: artists work from references; today the only way is a second window or importing the image as pixels into a drawing layer (destructive, wrong resolution). Solution: a new reference layer type — the layer system already discriminates types (image | text), so it extends cleanly:
Stores the original bitmap untouched (PNG/JPEG bytes in the project file), rendered with its own transform: position, scale (non-integer allowed), opacity; move/resize via the existing pf-transform-handles interaction.
Never contributes to export, palette, index buffers, or flattening; excluded from magic wand/fill sampling; lockable and hideable like any layer.
Nice-to-haves: opacity slider default ~50%, desaturate toggle, "above/below artwork" position. Acceptance sketch: import an image as reference, move/scale it freely at any zoom, draw over it, export contains zero reference pixels, project save/load restores the reference with its transform. Depends on: schema addition — coordinate with the v4 format bump (F11/Refactor: complexity hotspots — suggested decompositions #71) to avoid a v4.1 right behind it.
F13 — CRT display effect (non-destructive)
Problem/opportunity: pixel art was born on CRTs — scanlines, phosphor masks, and color bleed are part of the aesthetic. A live CRT view makes work look great and is pure delight. Solution: a display-only effect pipeline (never touches pixel data, cels, or the project file — same principle as the preview background modes in #52):
WebGL/WebGPU shader pass over the composited output: scanlines, phosphor/aperture-grille mask, barrel curvature, bloom/bleed, vignette — each with intensity sliders + a few presets.
Apply in the live preview overlay first (cheapest surface), then optionally the main viewport ("view mode: CRT") and export (baked into GIF/WebP/PNG copies at export time only, clearly labeled).
Architecture note: build it as a generic post-process view-effect slot, not a hardcoded CRT toggle — future effects (LCD grid, Game Boy palette preview) reuse the slot. Acceptance sketch: toggle CRT on the preview, tune intensity, project data unchanged (verify via save/load hash); export "with effect" produces a separate styled copy. Depends on: nothing hard; keep it out of ProjectFile (settings-level preference).
Problem/opportunity: onboarding + fun. Turn any image into a guided pixel-art exercise: generate a pixel-art rendition and palette from a photo, but show a numbered grid instead of the finished art, so the user paints it cell by cell. Solution sketch (pipeline):
Image upload → downscale to target sprite size → color quantization to N colors. Building blocks already exist: stores/palette/extraction.ts (palette extraction) and the indexed-color model is literally numbers-per-pixel (the cel index buffer is the paint-by-number key).
Generate a "guided project": palette = quantized colors (numbered swatches), each cell renders its target number until painted; painting with the correct color fills the cell, wrong color gives soft feedback.
Completion state (per-color progress, % done), then reveal the finished piece — optionally with the CRT effect (F13) for the wow moment. Product notes: works fully client-side (no backend needed for MVP); user-uploaded images stay local. Later synergies: share a puzzle link (F5), daily challenge (backend), premium puzzle packs (F3). Acceptance sketch: upload a photo, pick sprite size + color count, get a playable numbered board; finishing reveals the artwork; the result is a normal project afterwards (fully editable). Depends on: palette rework (F11) strongly recommended first — guided mode leans entirely on a trustworthy single-palette model. Sizing: large — own PRD when picked up.
Out of scope (for this roadmap)
Mobile-native apps.
Marketplace/community features beyond share links.
Implementation slices are intentionally not created here. Lift one feature section (F1–F14) at a time into a full PRD, or use $to-issues on a feature section to create child issues and link them as sub-issues. Recommended first picks: F11 (palette rework) together with #71's serialization work, then F1 (project library) after #65/#67 land.
PRD — Feature Roadmap
Date: 2026-07-06 (rev. 2 — added owner feedback: multi-open directions, palette rework, reference layers, CRT effect, paint-by-number)
Status: Draft
Purpose
This is a meta-PRD: a prioritized roadmap where each feature section is written to be lifted into its own full PRD (or broken directly into issues with
$to-issues). Long-term product goal: a backend that stores and syncs pixel-art drawings, with accounts and a low monthly subscription for more sync.Guiding constraints
Suggested order of operations
F1 — Local project library (multi-project)
Problem: the app persists exactly one project (
'current-project'IndexedDB slot). Users cannot keep multiple works in progress.Solution: new/open/duplicate/rename/delete with thumbnails, stored in the existing IndexedDB
spritesstore keyed by project UUID. Entry point: File menu + a start screen / project browser.Why first: forces the project-identity model and repository interface (#65) that cloud sync requires; big UX win on its own.
Scope notes: thumbnail = downscaled composite of frame 1; store
lastOpenedProjectIdin settings; migrate the legacy single slot into a normal project record on first run.Acceptance sketch: create ≥2 projects, switch between them with full undo isolation, reload restores the last-opened one, deleting prompts for confirmation.
Depends on: #65.
F1b — Multiple projects open at once
Problem: switching projects (F1) still means one project at a time; artists want to work across pieces (compare, copy pixels/palettes).
Candidate directions:
The real work is not the UI — it is state. Every store is currently a module singleton; "which project is selected" must become a first-class concept:
ProjectContext(layers, animation, palette, selection, history, viewport, dirty-rect per project) — this is Architecture: break the singleton store web (history ↔ project circular import) #63's composition root taken to its conclusion.activeProjectContextsignal; all panels (palette panel notably, timeline, layers, context bar) read through it and rebind on switch.Acceptance sketch (tabs): two projects open in tabs; switching tabs swaps canvas, palette, timeline, layers, and undo history correctly; copying a selection from tab A pastes into tab B with sensible color handling.
Depends on: F1, #63. Sizing: large — plan as its own PRD with the ProjectContext refactor as slice 1.
F2 — Accounts + cloud sync MVP (the backend)
Problem: work lives in one browser's IndexedDB; clearing site data destroys it; no cross-device access.
Solution: email magic-link or OAuth login; sync the project library as whole-project blobs, last-write-wins per project; conflict = "keep both copies". Client stays local-first — sync is a background reconciler behind the
ProjectRepositoryinterface.Suggested stack (boring on purpose): Postgres (accounts, project metadata, entitlements) + S3-compatible object storage for project blobs (already on Scaleway) + a thin API (Node/Hono/Fastify or a BaaS like Supabase/PocketBase to start). Store
ProjectFileas the canonical schema — which is why #71/#69 and F11 come first.Acceptance sketch: sign in on two browsers, edit on A, see it on B after sync; kill network mid-edit, keep working, reconcile on reconnect.
Depends on: F1, F11, #71, #69, #68.
F3 — Subscription billing (Stripe)
Problem: monetization for sync costs.
Solution: Stripe Checkout + customer portal; webhooks update an
entitlementstable checked by the sync API. Free tier: e.g. 3 synced projects; paid: unlimited projects + version history (F6) + future collab (F7). Local-only usage stays free and unlimited forever.Acceptance sketch: upgrade unlocks limits within a minute of webhook; cancel downgrades gracefully (data readable, sync of over-quota projects paused, never deleted).
Depends on: F2.
F4 — Cross-device asset sync (palettes & brushes)
Problem: custom palettes/brushes are stuck per device.
Solution: sync the small payloads first —
palette-persistence.ts/brush-persistence.tsalready isolate the data. Ideal low-risk shakedown of the sync engine before/alongside broad project sync.Depends on: F2 (auth), can ship before full project sync is battle-tested.
F5 — Shareable read-only links
Problem: no way to show work without exporting and uploading elsewhere.
Solution: publish a sprite/animation to a public URL — server renders/stores GIF/WebP/PNG plus a lightweight embedded viewer page. Growth loop; also the first "public" backend surface with minimal abuse area (read-only).
Acceptance sketch: "Share" produces a URL that renders the animation without an account; unpublish works.
Depends on: F2. Synergy: shared pages could offer the CRT effect (F13) as a viewer toggle, and paint-by-number boards (F14) are inherently shareable.
F6 — Delta sync + cloud version history (paid-tier feature)
Problem: whole-project blob sync re-uploads everything per change and offers no history.
Solution: content-addressed per-cel blobs (hash the index buffers / PNG bytes), a
project.jsonmanifest referencing hashes, keep the last N manifests per project = version history nearly for free. The dirty-rect + patch-command work (#66) provides the change granularity.Acceptance sketch: editing one cel uploads only that cel; user can browse and restore previous cloud versions (paid tier).
Depends on: F2, #66.
F7 — Real-time collaboration (later, paid tier)
Problem/opportunity: the command pattern already stamps
userIdon every command and supports per-user undo — it was built for this.Solution: server-authoritative command log per project session; replay commands to followers; CRDTs only if free-form concurrent editing demands it. Do not start before F6 is stable.
Depends on: F2, F6, #63.
F8 — PWA / installability
Problem: the app feels like a website; offline behavior is incidental.
Solution: service worker with offline caching of the shell, install prompt, file-handler registration for
.aseprite/.pfso OS double-click opens the app. Reinforces the local-first story; independent of the backend.Depends on: nothing — can ship anytime.
F9 — Serialization overhaul (tracked in #71/#69/#68)
Listed for roadmap completeness: versioned schema + migration pipeline + round-trip tests is the gate between "product features" and "backend features". Treat #71 as its PRD. Coordinate with F11 — the palette model decision changes the schema; make both changes in the same format-version bump (v4) rather than two.
F10 — CI & guardrails (tracked in #67)
Same: listed for completeness; treat #67 as the actionable issue.
F11 — Palette rework: one palette, one truth (core-tool hardening)
Problem: the palette is the core tool but currently has a dual model: a main palette plus an "ephemeral/untracked" stack for colors drawn that aren't in the palette, with promotion ("track this color"), demotion, index remapping, rebuild-from-drawing, and a
v3.1file field. The machinery spans 12 files (stores/palette/*,animation/palette-sync.ts, promotion events,rebuildEphemeralFromDrawing,clearEphemeralColors(skipRemap)), and the owner's own assessment is that it may solve a problem that doesn't exist. Two color lifecycles = twice the states a user (and the code) must reason about.Direction — collapse to a single lifecycle:
loadProject; make the auto-save path and legacy formats behave identically so there is exactly one rule).Payoff: kills the most complex duplicated state in the app (see #71 item 6), simplifies
ProjectFile(dropephemeralPalettein v4 with a migration folding old ephemeral colors into the palette), and gives sync a simpler canonical schema.Acceptance sketch: draw with a non-palette color → it appears in the palette flagged "new"; save/load round-trips exactly one color list; old v3.1 files load with ephemeral colors folded in; no user-visible "promote/track" concept remains (or it becomes just "clear new flag").
Depends on: #68 (tests first), coordinates with #71/#69 (same format bump). Do before F2 freezes the schema.
F12 — Reference image layers (background guides)
Problem: artists work from references; today the only way is a second window or importing the image as pixels into a drawing layer (destructive, wrong resolution).
Solution: a new
referencelayer type — the layer system already discriminates types (image|text), so it extends cleanly:pf-transform-handlesinteraction.Acceptance sketch: import an image as reference, move/scale it freely at any zoom, draw over it, export contains zero reference pixels, project save/load restores the reference with its transform.
Depends on: schema addition — coordinate with the v4 format bump (F11/Refactor: complexity hotspots — suggested decompositions #71) to avoid a v4.1 right behind it.
F13 — CRT display effect (non-destructive)
Problem/opportunity: pixel art was born on CRTs — scanlines, phosphor masks, and color bleed are part of the aesthetic. A live CRT view makes work look great and is pure delight.
Solution: a display-only effect pipeline (never touches pixel data, cels, or the project file — same principle as the preview background modes in #52):
Acceptance sketch: toggle CRT on the preview, tune intensity, project data unchanged (verify via save/load hash); export "with effect" produces a separate styled copy.
Depends on: nothing hard; keep it out of
ProjectFile(settings-level preference).F14 — Paint-by-number: gamified pixel-art creation
Problem/opportunity: onboarding + fun. Turn any image into a guided pixel-art exercise: generate a pixel-art rendition and palette from a photo, but show a numbered grid instead of the finished art, so the user paints it cell by cell.
Solution sketch (pipeline):
stores/palette/extraction.ts(palette extraction) and the indexed-color model is literally numbers-per-pixel (the cel index buffer is the paint-by-number key).Product notes: works fully client-side (no backend needed for MVP); user-uploaded images stay local. Later synergies: share a puzzle link (F5), daily challenge (backend), premium puzzle packs (F3).
Acceptance sketch: upload a photo, pick sprite size + color count, get a playable numbered board; finishing reveals the artwork; the result is a normal project afterwards (fully editable).
Depends on: palette rework (F11) strongly recommended first — guided mode leans entirely on a trustworthy single-palette model. Sizing: large — own PRD when picked up.
Out of scope (for this roadmap)
Issue Breakdown
Implementation slices are intentionally not created here. Lift one feature section (F1–F14) at a time into a full PRD, or use
$to-issueson a feature section to create child issues and link them as sub-issues. Recommended first picks: F11 (palette rework) together with #71's serialization work, then F1 (project library) after #65/#67 land.