Case study 2024 to now
duoprint.design
A print-on-demand shop with its own design editor in the browser. I designed, built and run it end to end, from the database to the last pixel of the editor.
The product
Customers upload a photo or artwork and turn it into a print: a single canvas, a picture split across two panels, or a gallery wrap that tiles one image into several adjacent strips. They place the image themselves, see a mockup of the result, pay, and the order goes to production without anyone touching it.
The shop runs on Solidus, so the catalogue, cart, checkout and admin are a proven commerce core. Everything that makes it a print shop sits on top: the editor, the print file pipeline, dispatch to production, and pricing that is correct for every country it sells to.
The editor
The editor puts one crop selection per panel over the uploaded image. Each selection must stay inside the printable frame, which is the image minus the bleed the print needs to wrap around its edges, and no two selections may overlap. Within those walls a magnet pulls edges into line with the frame and with each other, so a two-panel print lines up without pixel hunting.
Try it below. The dashed line is the bleed guide. Drag a panel toward it or toward the other panel, and watch the edge catch, hold, and let go.
Snapping that cannot wobble or drift
The first version snapped the way most hand-rolled editors do: it moved the selection to the target, and on the next pointer event it measured from that snapped position. Near a target the selection fed on its own output. It got trapped in the capture band, oscillated, and slowly slid away from the cursor. Every fix added another layer: a breakaway tracker, an oscillation detector, a fast-movement bypass, a debounce.
The replacement removes the feedback loop instead of compensating for it.
- Two rectangles. A virtual rectangle follows the pointer one to one and is only ever clamped to the walls. Snapping never writes to it. The displayed rectangle is computed fresh on every event as a pure function of the virtual one: constrain, then snap.
- No feedback, so no wobble and no drift. Because the snap decision never sees its own result, monotonic pointer motion gives monotonic display motion, and the grab point stays under the cursor.
- Hysteresis. An edge engages a target within 4 px and releases only beyond 7 px. The dead band between the two is what makes the catch feel solid. The only state is which target each axis holds.
- Screen pixels. Thresholds are measured on screen, so the magnet feels the same at every zoom level. Touch pointers get twice the distance.
- Matching edges only. Left snaps to left, top to top. The frame wins a tie against another panel, so the choice never flickers, and a snap that would push a panel out of bounds or onto its neighbour is never offered.
- An escape hatch. A toolbar toggle turns the magnet off and is remembered per device. Holding Alt suspends it while held. The walls still apply.
Coordinates stay fractional during a drag and are rounded only when the crop is submitted. Each event applies one display rectangle synchronously, which also removed the latency the old debounce added.
Fulfilment as retryable background jobs
When an order completes, a single callback on the checkout state machine hands it to production. Line items are grouped by the production partner that prints them, and the same grouping decides the shipments at checkout, so every shipment carries its own quoted rate and the dispatch can never disagree with what the customer paid for.
Each dispatch records what happened as an event: attempted, skipped, created or failed. A recorded success blocks a second submission, so running a dispatch twice is safe. Transient failures are retried out of band by background jobs, never inside the customer's request.
Some partner APIs have no dry run: the only way to validate an order is to create a real one. For those, the shop checks print files itself before the card is charged, against the partner's published rules: aspect ratio, reachability of the exact file URL, and resolution. A file that would be rejected stops checkout before any money moves.
Multi-region VAT pricing
Prices are shown VAT inclusive at the customer's own country rate, so a visitor in Germany and one in Austria see different totals for the same print, and both totals are final. Prices are entered once as German gross prices; every save derives the net amount and writes one gross price per country.
In Solidus, the order's tax address drives both the price lookup and the included VAT split. Aligning that one value with the visitor's region keeps cart prices identical to the product page, including for a guest cart that has no address yet. Every region is billed in euros, and Switzerland is handled as a VAT-free export with a clear notice about import charges before payment.
Stack
- Application
- Ruby on Rails, PostgreSQL, background jobs on Sidekiq
- Commerce
- Solidus for catalogue, cart, checkout and admin
- Payments
- Stripe, with the Payment Element at checkout
- Front end
- Hotwire (Turbo and Stimulus), server rendered, Tailwind CSS
Want something like this?
Write to hi@kridi.dev, or see the rest of my work.