Back to Experiments

Experiment 006

Stable

Building a Self-Contained Invoice System Without a Database.

Can an internal business tool with customer DB, live A4 preview, and selectable text-based PDF be built end-to-end using only local LLMs — without a traditional database, just .js files + 44 lines of PHP on shared hosting?

System Requirements

CPU

Intel Core I5 11400F

RAM

16GB DDR4 3200 MT/s

GPU

RX 6700 XT

OS

Ubuntu 26.04 LTS

Inference Engine

Llama.cpp

Coding Tools

OpenCode + Cline (VS Code)

The Model That Helped Me

ORNITH-1.0-35B-A3B-IQ4_NL-GGUF

Frontend Stack

Tailwind CDN + Vanilla JS + jsPDF 2.5.1

Status

STABLE

Screenshots

Screenshot 1 — invoice generator desktop with left form (customer, items, totals) and right live A4 preview
Form + Live A4 Preview — Desktop (lg:grid-cols-12)
Screenshot 2 — mobile view with sticky bar Preview | PDF | Print for admin on phone
Mobile Sticky Bar — Admin on Phone
Screenshot 3 — generated PDF with selectable text, BCA bank account can be highlighted and copied
Text-Based PDF — Selectable & Copyable (45KB)
Screenshot 4 — Hostinger File Manager showing customers.js file storage (100GB)
File-Based Storage — Hostinger File Manager

Visualization

Why Text-Based PDF Won — Size, Selectability, and Cost

The same invoice generated two ways. Local AI initially suggested html2canvas screenshot — human corrected to jsPDF text API. The difference is 19x size and copy-paste.

Text-Based PDF — jsPDF (Chosen)

45KB — Selectable
0 score / 100 — text-based is smaller & copyable 100

Size is KB for the first bar (45KB) and 0/100 for booleans. Rekening BCA 5771 •• 465 is selectable — masked here, full in real PDF.

Screenshot PDF — html2canvas (Rejected)

850KB — Image Only
0 size KB / boolean — screenshot is 19x heavier & not copyable 900

File-Based Flow — No Database

One customers.js file, not MySQL. Download via File Manager anytime.

Input (form) calcTotals() + updatePreview() 600ms localStorage fallback + fetch(save.php) POST {type, content} customers.js / company.js Hostinger 100GB

save.php allowlist: ['customers','company'] → file_put_contents → JSON {status:"ok"}

EXP 005’s chart was Lighthouse performance. This chart is architecture — proving file-based persistence is 19x smaller and more practical for SMB than a screenshot or a full DB.

Experiment Details

Problem

The public site from EXP 005 solved marketing — but operations were still manual. Invoices were typed in Word/Excel, customer data was scattered across WhatsApp, the 44 price items had to be remembered, and PDFs were inconsistent. I needed an internal tool that could (a) autocomplete returning customers, (b) auto-calculate from the same pricelist, (c) show a live A4 preview, and (d) generate a PDF where the bank account can be copied — without a SaaS subscription and without setting up MySQL on shared hosting.

Specifications
  1. 1.

    Single-file app: invoice.html 919 lines — lg:grid-cols-12 (form 5 + preview 7 sticky), Tailwind CDN + jsPDF 2.5.1 + html2canvas 1.4.1 (loaded but not used for final PDF), Vanilla JS only, noindex, nofollow at .../invoice/admin/data/invoice.html (internal, noindex).

  2. 2.

    Company data: company.js 19 lines — name, logo, address, whatsapp, email, rekening {bank, no, an} — editable via ⚙️ Edit Company modal (6 fields + logo upload base64 max 500KB), auto-save via save.php?file=company + localStorage fallback.

  3. 3.

    Customer DB file-based: customers.js 21 lines — array [{id, name, address, whatsapp}] with 2 dummy samples (Budi & Siti — labeled dummy), datalist autocomplete, dedup Map(name|wa) on init(), auto-grow via save.php POST JSON — storage on Hostinger 100GB file storage, downloadable via File Manager.

  4. 4.

    Backend: save.php 44 lines — header JSON + CORS, file_get_contents('php://input'), allowlist ['customers','company'], file_put_contents($filename, $content) — no DB, no ORM.

  5. 5.

    Items logic: reads indeepPricelist (22 categories → 44 items, same as EXP 005) — dependent dropdowns kategori → ukuran, price auto, qty, discount, plus dynamic extras latex +40k, lepasPasang +20k/seat, lebar75 +20k/seatcalcTotals() + renderItems().

  6. 6.

    Live preview: preview-a4 210mm, setInterval(updatePreview, 600) — header with logo, meta (No, Date, Due +1 day), 7-column table, payment box, signature — updates on every input change.

  7. 7.

    PDF generation: text-based only generateTextPDF() via jsPDF — logo from DOM canvas.toDataURL('image/png') (fixes CORS), all text via pdf.text() + pdf.rect() so BCA 5771 •• 465 is selectable (masked in portfolio, full in real PDF), filename INV-YYYYMMDD-001 - Customer.pdf.

  8. 8.

    Mobile & print: @media (max-width: 1024px) table overflow-x: auto, preview-a4 full-width, mobile-sticky-bar fixed bottom (Preview | PDF | Print), @media print hides .no-print, window.print() fallback.

Hypothesis

A local open-source LLM (ORNITH 35B IQ4_NL via Llama.cpp) can generate this operational tool if prompts are deterministic per component (form, preview, PDF, save.php) and the builder understands the file-persistence vs DB trade-off. File-based is sufficient for SMB (<10k customers) and more maintainable than MySQL on shared hosting.

Experiment

I built it by prompting the local model per batch — first invoice.html skeleton, then company.js modal, then customers.js autocomplete + save.php, then calcTotals/updatePreview, then generateTextPDF — each validated in a small context window via OpenCode + Cline in VS Code. Not one-shot.

What Failed
  1. 1.

    Logo CORS to PDF: fetch(logo.png) failed on file:// and on the Hostinger path ../../../assets/img/logo.png. Had to grab the already-loaded DOM <img>, draw to canvas, toDataURL('image/png'), then pdf.addImage() — initial fetch + FileReader blob approach broke.

  2. 2.

    Pricelist hallucination (continuation of EXP 005): model invented prices for Kasur 200/180 etc. Fixed by forcing verbatim read of indeepPricelist[kategori] and disabling ukuran until kategori is selected.

  3. 3.

    Mobile table overflow: 7-column table broke on phone — fixed with display:block; overflow-x:auto; and table thead/tbody display:table; width:100% at 1024px.

  4. 4.

    Dual persistence race: localStorage vs save.php went out of sync after save — fixed with Map dedup name|wa on load + Export manual button fallback when fetch fails (500 or CORS).

Iteration

Same pivot as EXP 005: stop relying 100% on AI. AI drafts syntax, human decides if save.php allowlist is safe, if pdf.addImage needs PNG conversion, if pricelist.js is the single source. Added business insight: for a small service business, file-based > MySQL — one customers.js downloadable via File Manager beats setting up phpMyAdmin on shared hosting.

Evidence

Figures from the live internal tool on Hostinger. You can verify via the screenshots above and by inspecting save.php (44 lines).

Total Files

5

1 html + 2 js + 1 php + 1 txt

Invoice Lines

919

invoice.html

Backend

44 lines PHP

Storage

100GB

Hostinger file-based

Preview Sync

600ms

setInterval

Customer DB

2 → ∞

auto-grow

PDF Mode

Text-based

selectable, 45KB

Revisions

~12 batched

per component

Result

The tool is stable and live internally — an admin can create an invoice in under 2 minutes, customer data auto-saves to customers.js (and localStorage), the PDF is 45KB text-selectable (vs 850KB image), and the owner can backup customers.js anytime via File Manager without DB knowledge. Not an enterprise ERP, but sufficient for a Jabodetabek SMB.

What I Learned

Determinism matters even more for internal tools: save.php must have an allowlist, logo must be converted to PNG, pricelist must be read verbatim. File-based persistence is underrated — local AI can generate CRUD without a database if the builder understands file I/O trade-offs. The model is the typer, the builder is the reviewer.

Practical Implication

For business owners: you don’t need a Rp200k/month SaaS — with local AI + Hostinger 100GB you own your invoice tool 100%, backup is one download. For recruiters: this proves the builder can ship both the public marketing site (EXP 005) and the internal ops tool (EXP 006) with the same local stack — end-to-end ownership, no cloud dependency. Start from one file (save.php), validate file writes, then batch the next component.

Configuration Template

save.php — the entire backend (44 lines)

<?php
// save.php — 44 lines, no DB, file-based persistence
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit(); }

$input = file_get_contents('php://input');
$data = json_decode($input, true);

$allowed = ['customers', 'company'];
if (!$data || !in_array($data['type'], $allowed)) {
  http_response_code(400);
  echo json_encode(["status" => "error"]);
  exit();
}

file_put_contents($data['type'] . '.js', $data['content']);
echo json_encode(["status" => "ok", "file" => $data['type'] . '.js']);

customers.js — file-based DB (dummy samples)

/* dummy sample — not real customers */
const customersData = [
  {
    id: "CUST-001",
    name: "Budi Santoso",
    address: "Jl. Kemang Raya No. 12, Jakarta Selatan 12130",
    whatsapp: "081234567890"
  },
  {
    id: "CUST-002",
    name: "Siti Aminah",
    address: "Cluster Galaxy Blok B2 No. 8, Bekasi Selatan",
    whatsapp: "082112345678"
  }
];
// auto-updated via fetch(save.php) POST {type: "customers", content} + localStorage Map dedup
Do not copy this snippet blindly. The key lesson is the allowlist + file storage: local AI can generate the fetch + localStorage dual save, but you must ensure customers.js is never overwritten with an invalid type. That safety check is human judgment.
  1. 1.

    I kept persistence to files, not tablescustomers.js on Hostinger 100GB is downloadable via File Manager in one click, no phpMyAdmin. For <10k rows, this is faster to maintain.

  2. 2.

    I required text-based PDF (pdf.text() + pdf.rect()) instead of screenshot — local AI defaulted to html2canvas which creates a heavy image where BCA 5771 •• 465 cannot be copied.

  3. 3.

    I forced the single source indeepPricelist (22 keys, 44 items) so invoice totals always match the public calculator — any hallucination is caught by diffing against this file.

  4. 4.

    I kept localStorage as fallback — if fetch(save.php) fails (500 or CORS on Hostinger), data is still saved and an Export customers.js button appears for manual download.

FAQ

Frequently Asked Questions

Do I need a database for a small business invoice system?

No — for under ~10k customers, a single customers.js file on Hostinger file storage (100GB) is enough. It's downloadable via File Manager, no phpMyAdmin. save.php (44 lines) handles POST {type, content} with allowlist. localStorage is fallback if fetch fails.

Why is selectable text in PDF important?

Customers need to copy the bank account BCA 5771 •• 465 without retyping. Screenshot PDFs (850KB, image) don't allow copy. Text-based jsPDF (45KB) does — plus it's 19x smaller. Logo still embeds via canvas PNG conversion.

Can local AI really handle jsPDF and file save logic?

Yes, but batched per component. One-shot generation hallucinated pricelist and broke logo CORS. Batched prompting (form → preview → pdf → save.php) with human validation produced 919 lines that work on shared hosting.

For those of you unable to read this data from technical standpoint, here is the conclusion

This is not a public website — it's the internal cashier tool that comes after the website. If EXP 005 is the storefront, this is the cash register.

It lets the admin create an invoice in under 2 minutes: pick a returning customer (autocomplete), add items from the same 44-price list used on the website, see a live A4 preview, and download a PDF where the bank account can be copied.

It doesn't use a database. Customer data lives in one customers.js file on Hostinger (100GB) — you can download it anytime via File Manager, no database setup. A tiny 44-line PHP file saves it.

The PDF is text-based (45KB), not a screenshot (850KB) — so it's small and copyable, and the logo still appears. It was built with a local AI on consumer hardware, batched one component at a time.

It's not for a big enterprise with millions of rows, but for a small service business in the Jabodetabek area, it's sufficient, it's stable, it connects directly to the pricing from EXP 005, and you own it 100% — no monthly SaaS fee.

Disclaimer

A Note on Results

From a purely objective standpoint, this is not an enterprise-grade ERP. Given it was built with a 35B local model on consumer hardware and runs on shared hosting file storage, I consider it sufficient and practical for real small-business invoicing. The trade-off (file-based vs MySQL) is intentional for maintainability at this scale — with known limits if customer count grows beyond file-based practicality. That said, for the Jabodetabek service business it serves, it is stable, live, and fully owned.