The goal
Provide a self-hosted, Hebrew-friendly DocuSign alternative — admins upload a PDF template, visually mark field positions, share a signer link, and receive a completed PDF where Hebrew renders correctly.
The solution
A two-part web app: a Vue 3 + Quasar frontend that uses pdfjs-dist to render PDFs for field placement and for signers, and an Express backend that stores templates, fills them with pdf-lib + fontkit (embedding NotoSansHebrew so Hebrew text renders), and rasterizes base64 signature images into the PDF.
Highlights
- Native Hebrew rendering in server-generated PDFs
- Visual field placement with automatic Y-axis correction
- Touch-friendly signature capture stored as PNG
- File-system storage keeps the system debuggable
The challenge
pdf-lib defaults to WinAnsi encoding, which silently drops Hebrew characters.
Register fontkit on every generated document and embed NotoSansHebrew-Regular.ttf explicitly so Hebrew text renders natively.
Browser Y-axis grows downward; PDF origin is bottom-left. A field placed at UI y=120 does NOT map to PDF y=120.
Single server-side transform y = page.getHeight() - f.y - 12 makes UI coordinates match the PDF without the template author thinking about it.
Signers are mostly on phones/tablets — clean signature capture is mandatory.
Frontend captures signatures as data:image/png;base64; the backend splits, Buffer.from(..., 'base64'), embedPng + drawImage at the field's coordinates.
What was delivered
- Vue 3 + Quasar frontend (UploadTemplate.vue, SignForm.vue, PdfSigner.vue)
- Express backend (index.js) with template CRUD + /api/forms/:id/fill
- Hebrew font embed pipeline (fontkit + NotoSansHebrew TTF)
- start.bat launcher for local dev
- JSON field schema (type, x, y) per template
Results
text + signature
Field types
Hebrew + Latin
Languages
Self-hosted
Architecture
What it taught me
- For any PDF product targeting Hebrew, fontkit + a real Hebrew TTF is non-negotiable
- Flipping the Y axis once server-side keeps the rest of the product simple
- File-system template storage (instead of DB) keeps the app debuggable
