
01 · Brief
Broiders is an app for managing embroidery work tied to the family business. It is meant to replace a paper receipt book and keep digitizing in house and free. It offers a simple console for managing customers, orders, and designs.
Orders is the working queue, with a customer book beside it because a lot of jobs are the same people coming back. Each order keeps its line items, due date, notes, and files, so the customer’s art and the finished stitch file live with the job. A simple accounting page counts every order toward the month it was entered and draws the goal line across the chart.
Design is one of the main reasons the app exists. Digitizing software requires sending files and money to a third party. An open source Pyembroidery library combined with Claude Code opened the door to building a personal digitizing app. With Broiders, you can type a word or drop in an SVG logo and the backend works out the real stitches, satin columns and fill rows, then writes a file the Viking will sew. The design can be saved with the customer and order for easy reuse and common patterns/settings can be saved as reusable stitch profiles.
It runs on my home server and nothing leaves the house. The whole business is one SQLite file, so a backup is a file copy.
The name is a family joke. When it is time to embroider, we ask our dog Petra, “Do you want to go do broiders?”, as if she can’t quite say embroidery. Petra knows the word now and gets excited every time it is time to “do broiders”.
02 · System
03 · Source
fill.py
def _fill(poly: Polygon, out: list[Stitch], p: StitchParams) -> None:
safe = poly.buffer(0.05) # tolerance for boundary-hugging travel checks
for i, segs in enumerate(_rows(poly, p)):
if i % 2: # serpentine: alternate rows run right-to-left
segs = [(e, s) for s, e in reversed(segs)]
for start, end in segs:
travel_to(safe, out, start, p)
_run(out, start, end, p)
def _rows(poly: Polygon, p: StitchParams) -> list[list[tuple]]:
minx, miny, maxx, maxy = poly.bounds
rows = []
y = miny + p.row_spacing_mm / 2
while y < maxy:
cut = poly.intersection(LineString([(minx - 1, y), (maxx + 1, y)]))
segs = []
for g in getattr(cut, "geoms", [cut]):
if isinstance(g, LineString) and g.length >= p.min_row_segment_mm:
x1, x2 = g.coords[0][0], g.coords[-1][0]
segs.append(((min(x1, x2), y), (max(x1, x2), y)))
if segs:
rows.append(sorted(segs))
y += p.row_spacing_mm
return rows
def travel_to(safe: Polygon, out: list[Stitch], point: tuple,
p: StitchParams) -> None:
if out:
lx, ly, _ = out[-1]
if (lx, ly) == point:
return
hop = LineString([(lx, ly), point])
if hop.length <= p.connect_max_mm and safe.covers(hop):
out.append((point[0], point[1], False)) # short in-shape travel: sew it
return
out.append((point[0], point[1], True)) # otherwise jump04 · Lessons
Lettering needs satin stitch; fill alone was not enough.
Keeping every stitch knob in one file made tuning after a test a one-file change.
05 · Revisions
| VERSION | DATE | NOTE |
|---|---|---|
| Dev | 2026-07-04 | Orders, customers, Cash Money, and fill-stitch lettering. |
| Dev | 2026-07-05 | Login gate and Docker deploy on the home server. |
| Beta | 2026-07-22 | Satin-stitch lettering. |
| Live | 2026-08-25 | Logo mode, SVG to multi-colour stitches. |