A Lean Canvas Macro Built on Atlassian Forge
A Confluence macro: React and MobX frontend, serverless resolvers, Confluence Storage API for persistence, drag-and-drop reordering, PDF export, and schema versioning.
A Lean Canvas Macro Built on Atlassian Forge

What was built
The macro renders an interactive Lean Canvas directly inside a Confluence page where each block supports block-name customisation and inline editing. Items within a block can be reordered with drag-and-drop. A PDF export captures the full canvas for printing. Data persists through the Confluence Storage API using app-scoped keys, so the canvas follows the page through space migrations and Confluence upgrades intact.
The stack
The frontend is a React 18 application, where state is managed with MobX 6. The choice over plain React hooks was because the canvas has eleven blocks, each holding an ordered list of items alongside derived per-block analytics (character counts, hashtag and mention extraction). Modelling the domain as MobX observable classes with makeAutoObservable() means computed values stay current without manual useEffect wiring, and components wrapped with observer() only re-render when their specific slice of state changes. TypeScript type definitions shared for both server and client sides.
Styling is split between Tailwind CSS for layout and spacing and Atlaskit (Atlassian’s design system) for interactive components: InlineEdit, TextArea, Popup, SpotlightPulse, Lozenge which is WCAG complaint.
PDF export runs through html2canvas and jsPDF. The canvas is captured as a BMP image rather than passing through jsPDF’s HTML renderer, which corrupts multi-byte characters and emoji when they appear in Confluence content. Scaling the captured canvas four times before generating the PDF keeps the output print-ready at the cost of file size.
Backend notes
Every persisted canvas carries a schema version, a creation timestamp, and a last-updated timestamp. A migration function exists in the codebase and currently runs as a no-op at v1.0.0. The reasoning for adding this from the first commit: the Confluence Storage API has no schema awareness, so the only way to know what version of a stored payload you are reading is to embed that metadata in the payload itself. Retrofitting it later means either a migration scan across all stored data or accepting an ambiguous read path indefinitely.
The Forge tunnel service produces intermittent errors during development and occasionally in production. A withRetry utility handles this with exponential backoff across three attempts, from 300ms to 3000ms, using error message pattern matching to identify transient failures. Save operations retry transparently; the UI only surfaces a failure if all three attempts are exhausted.
Challenges
The invoke() bridge is asynchronous and the round-trip is perceptible. The save flow had to account for this rather than masking it: an explicit save button rather than autosave, a loading state on initial render, and visible retry feedback if the resolver fails.
Tagging users wasn’t the most intuititve setup, and was deferred.
What this demonstrates
This is a modest size application deployed as a forge, however it shows that business process and internal tools can be readily developed with the accessible plugin-system from Atlassian.