A couple of weeks ago, I finished a quick freelance job and needed to send a simple invoice for \$450.
I did not have heavy accounting software set up on my laptop, so I opened Google and searched for a free online invoice maker.
Every single result on page one was frustrating:
- "Sign in with Google or enter your work email to continue"
- Forced password creation and email verification loops
- A sluggish server-rendered page that took 8 seconds to compile
- And finally, a watermark across the exported PDF unless I paid \$15 a month
As a web developer, I had one immediate question:
Why does generating a simple one-page PDF document require a Node.js backend, a PostgreSQL database, login authentication, and monthly recurring server bills?
A standard invoice is really just an HTML form, a small calculation loop for subtotals and tax, and a clean print stylesheet.
So I spent the last few weeks building Quick Free Invoice, a 100% client-side, zero-database, zero-signup document engine.
Here is the breakdown of how it works, the architectural trade-offs, and how we solved the trickiest part: clean vector PDF generation without 800KB of third-party JavaScript bloat.
1. The Architectural Philosophy: $0 Infrastructure Forever
Most developers building a utility tool instinctively reach for the full-stack starter kit:
- Next.js or Remix
- Supabase, Prisma, or PostgreSQL
- Auth.js or Clerk
- Serverless API routes generating PDFs on AWS Lambda
That architecture introduces three real problems for a lightweight utility:
- Cold starts and latency: Waiting for a headless Chromium instance (like Puppeteer) on a serverless function to render a single PDF takes 3 to 6 seconds.
- Privacy liabilities: If you store client names, addresses, and rates in a cloud database, you now have to worry about GDPR, database encryption, and security breach risks.
- Monthly hosting bills: Once traffic scales, database connections and compute hours start draining your credit card.
The Client-Side Alternative
Instead of a backend, we shifted the entire lifecycle into the user's browser:
[User Browser]
│
├─► AstroJS Static Shell (Zero-JS baseline, sub-100ms load)
├─► Interactive Engine Island (Form inputs, reactive subtotals)
├─► window.localStorage (Client details persist offline)
└─► CSS @media print Engine (Instant 300 DPI vector PDF export)
The result:
- Zero backend compute costs: The site is hosted completely free on Cloudflare Pages global edge CDN.
- Absolute user privacy: Financial figures, hourly rates, and client contact details never leave the user's browser memory.
- Instant performance: Zero server roundtrips. When you edit a line item, the math recalculates synchronously in under 1 millisecond.
2. Solving Vector PDF Generation Without Library Bloat
The biggest challenge in client-side document generators is PDF export.
Most developers reach for libraries like jsPDF, pdfmake, or html2pdf.js. While these work, they introduce serious headaches:
- They add 300KB to 800KB of minified JavaScript to your client bundle.
- Font rendering is often blurry or misaligned because they rasterize the DOM into an HTML5 canvas before packing it into a PDF.
- Text selection and vector sharpness in the exported document are degraded.
The Solution: Native Browser @media print Normalization
Modern desktop and mobile browsers have built-in, hardware-accelerated PDF rendering engines inside their native print dialogs. They render crisp, 300 DPI vector typography and sharp SVG lines natively.
The trick is standardizing the print stylesheet so that regardless of whether the user is on Chrome, Safari, Firefox, or iOS, the output is pixel-perfect:
@media print {
/* 1. Reset page margins and force exact paper dimensions */
@page {
size: A4 portrait;
margin: 0;
}
/* 2. Hide all web navigation, toolbars, and buttons */
header,
footer,
.no-print,
.action-buttons,
.drawer-overlay {
display: none !important;
}
/* 3. Strip dark mode backgrounds to save ink and ensure contrast */
body {
background: #ffffff !important;
color: #000000 !important;
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
/* 4. Enforce strict vector print containers */
.invoice-sheet {
width: 210mm !important;
min-height: 297mm !important;
padding: 15mm 20mm !important;
margin: 0 auto !important;
box-shadow: none !important;
border: none !important;
}
/* 5. Prevent awkward page breaks in the middle of line items */
.invoice-table-row {
page-break-inside: avoid !important;
break-inside: avoid !important;
}
}
The 100ms DOM Settle Trick
When triggering the print dialog via JavaScript (window.print()), mobile browsers sometimes capture the DOM before active input focus has fully blurred.
To ensure the preview DOM is completely stable before the print engine snapshots the canvas, we run a short 100ms settle timeout:
function triggerPdfDownload() {
// Blur active input to flush state updates
if (document.activeElement) {
document.activeElement.blur();
}
// Allow DOM micro-tasks to settle
setTimeout(() => {
window.print();
}, 100);
}
This simple approach eliminated the need for heavy client-side PDF packages, cutting our client JavaScript payload by more than 70%.
3. Local Persistence with Zero User Accounts
One common complaint with no-login tools is: "If I refresh the tab, do I lose all my work?"
To solve this without user accounts or session tokens, we use a single state store backed by localStorage:
interface InvoiceState {
issuerName: string;
issuerEmail: string;
clientName: string;
clientAddress: string;
currency: string;
taxRate: number;
items: Array<{ description: string; qty: number; rate: number }>;
history: Array<SavedInvoiceSummary>;
}
// Automatically sync state on every input change with debounce
function persistDraft(state: InvoiceState) {
try {
localStorage.setItem('qfi_active_draft', JSON.stringify(state));
} catch (err) {
// Graceful degradation if user has storage quota locked
console.warn('LocalStorage unavailable', err);
}
}
When users return to the site two weeks later, the form rehydrates instantly. We also implemented a local "Recent Invoices" drawer so repeat freelancers can clone past invoices with a new sequential number in one click, completely offline.
4. Multi-Document Mode Switching
Freelancers do not just send invoices; their workflow spans multiple stages:
- Estimate: Ballpark pricing before work begins.
- Quote: A fixed, binding price agreement.
- Invoice: Demand for payment once milestones are reached.
- Receipt: Proof of payment after funds clear.
Instead of building five separate isolated web apps, we structured the core engine to accept a dynamic mode parameter:
---
// src/pages/contractor-invoice-generator.astro
import BaseLayout from '../layouts/BaseLayout.astro';
import ToolEngine from '../components/ToolEngine.astro';
---
<BaseLayout title="Contractor Invoice Generator">
<!-- Mode dynamically configures labels, itemization columns & metadata -->
<ToolEngine mode="contractor" />
</BaseLayout>
When mode="contractor" is passed, the engine automatically adds specialized columns for Materials Purchased versus Skilled Labor Hours. When mode="receipt", it swaps payment terms for payment settlement methods (Cash, Card, ACH Wire) and a clean "PAID IN FULL" vector badge.
5. What We Learned
-
Native Web APIs are capable: Between
@media print,localStorage, and CSS Grid, modern browsers have eliminated the need for heavy npm packages for most basic document utilities. - Users prefer frictionless utilities: In our first week of soft-launching on developer communities, we crossed 100+ active users and 200+ page views with an average session duration of over 2.5 minutes. When you do not gatekeep simple tools behind email signups, people stick around and actually use them.
- Static edge hosting keeps costs at zero: Zero server crashes, zero database maintenance, and \$0 operational costs.
If you ever need to generate a fast invoice, project estimate, or receipt without dealing with subscription paywalls, feel free to bookmark it:
👉 Live Tool: quickfreeinvoice.com
I would love to hear your thoughts on print media normalization, or ideas for other client-side utility features worth exploring.
Top comments (0)