HISABLY
Offline-first billing & invoicing SaaS for SMBs.
My own SaaS — an offline-first billing and invoicing app aimed at small and mid-sized businesses, launching in the UAE with VAT-compliant tax invoices, quotations, credit notes, and proforma. Built as a Turborepo monorepo: a Next.js 14 PWA that works without a connection (IndexedDB via Dexie), a FastAPI + async SQLAlchemy backend on PostgreSQL, and a shared TypeScript package that holds the invoice math + a pluggable tax-regime engine.

Numbers about the client's business — useful context, not my engineering output.
Numbers I produced — measurable, attributable to the work I did.
My specific scope on this project — separate from anything the client team supplied.
- Turborepo workspaces — apps/web (Next.js 14 PWA) + apps/api (FastAPI async) + packages/shared
- Offline-first IndexedDB (Dexie) layer that keeps invoicing usable without a connection
- Pluggable multi-country tax engine — UAE VAT live; Saudi Arabia & Pakistan on the roadmap
- Single source of truth for invoice math in TypeScript, with a Python port cross-validated against shared fixtures (totals agree to the fils)
- Full auth (email + OTP), tenant-scoped data model, customers / suppliers / items / accounts, all 6 invoice types, payments + allocations, expenses, reports
- PDF invoice rendering, recurring invoice scheduler endpoint, low-stock alerts, dashboard KPIs
From brief to production system.
Most SMB billing tools in this region either assume you're always online or stop at one country's tax rules. A shopkeeper or contractor in the UAE needs UAE VAT today, but they also operate cross-border into KSA and Pakistan — and they cannot lose the ability to bill a customer when the internet flakes. The hard part is keeping invoice math (discounts, VAT rounding, totals) provably identical between the offline web client and the server, and pluggable enough that adding a new country isn't a rewrite.
Three-package Turborepo: `apps/web` (Next.js 14 App Router PWA with Dexie for offline persistence), `apps/api` (FastAPI + async SQLAlchemy + PostgreSQL + Alembic migrations), and `packages/shared` (TypeScript invoice math + pluggable tax-regime engine, mirrored in Python on the backend). Core invoicing loop — auth, tenants, customers/suppliers/items/accounts, all six invoice types, payments + allocations, expenses, reports, settings — is real and tenant-scoped, not mocked.
Private beta running on hisably-web.vercel.app. Core invoicing loop ships VAT-compliant tax invoices, quotations, credit notes, proforma, and purchase bills; the same math runs offline in the browser and on the API. A full read-only audit catalogued every screen against the backend so the roadmap is data-driven, not guesswork.
The boundaries that shaped the build.
- 01Must work fully offline — billing cannot stop when the connection does
- 02VAT/discount math must be identical between the offline web client and the server, to the fils
- 03Tax engine must be pluggable, not hard-coded to UAE — KSA and Pakistan are on the roadmap
- 04Tenant-scoped data model from day one — multi-tenant SaaS, not a single-business app
What I chose, and what I gave up.
Invoice math lives in a shared TypeScript package, with a Python port cross-validated against the same fixtures
Single-language simplicity. Won correctness — both the offline client and the server compute totals identically, verified by shared fixtures.
Offline-first PWA via IndexedDB instead of a thin online-only client
Simpler state management and fewer sync edge cases. Won real reliability — the app keeps working in a warehouse, on a job site, or when 4G drops.
How it shipped, week by week.
Architecture
ERD, API route list, screen map, design tokens, and the tax-engine interface — written before scaffolding so the monorepo had a single source of truth from day one.
Core invoicing loop
Auth + tenants + customers/suppliers/items/accounts, then all 6 invoice types, payments + allocations, expenses, and reports.
Offline + PWA shell
Dexie schema for offline persistence, service worker, and sync semantics — the unsexy plumbing that makes the rest of the app actually reliable.
Tax engine + PDFs
UAE VAT regime, invoice PDF rendering, recurring template endpoint, low-stock alerts, and the dashboard KPI calls.
Audit + harden
Read-only screen-by-screen audit (UI → API → repository) to surface every Works / Partial / Dummy gap so the next milestones are data-driven.
What it does. How it's built.
Features
- VAT-compliant tax invoices, quotations, credit notes, proforma, purchase bills, delivery notes
- Offline-first PWA — keep billing without a connection, syncs when back online
- Pluggable multi-country tax engine (UAE live, KSA + PK on the roadmap)
- Customers, suppliers, items, categories, accounts — fully tenant-scoped
- Payments + allocations against open invoices
- Expenses, basic reports (sales, P&L, aging), dashboard KPIs
- Recurring invoice template endpoint with run-due trigger
- Email + OTP auth with anti-enumeration on forgot-password
- Industry-profile registry for vertical-specific item fields
Architecture
- 01Turborepo workspaces — apps/web, apps/api, packages/shared
- 02Next.js 14 App Router PWA with Dexie (IndexedDB) for offline persistence
- 03FastAPI + async SQLAlchemy 2.0 on PostgreSQL, Alembic for migrations
- 04Shared invoice math in TypeScript, ported to Python and cross-validated against shared fixtures
- 05Tenant scoping enforced in the repository layer — no cross-tenant data leaks by construction
- 06Docker Compose for the full local stack (Postgres + API + web)
- 07Vercel for the web app; API deploys planned alongside it
Annotated excerpts.
export interface TaxLine {
rate: number; // e.g. 0.05 for UAE 5% VAT
taxableAmount: number; // post-discount line total
taxAmount: number; // rounded per the regime's rule
code: string; // "VAT_STD" | "VAT_ZERO" | "VAT_EXEMPT" | ...
}
export interface TaxRegime {
country: "AE" | "SA" | "PK";
/** Per-line tax computation (handles rounding nuances per country). */
computeLine(input: {
unitPrice: number;
qty: number;
discount: number; // line-level discount (absolute)
taxCode: string;
}): TaxLine;
/** Invoice-level rounding — UAE rounds totals per VAT-compliant rules. */
finalizeTotal(subtotal: number, tax: number): {
grand: number;
rounding: number;
};
}
export const REGIMES: Record<string, TaxRegime> = {
AE: uaeVatRegime, // live
// SA: ksaVatRegime, // TODO: ZATCA e-invoicing
// PK: pkSalesTax, // TODO: provincial split + WHT
};from decimal import Decimal, ROUND_HALF_UP
UAE_VAT_STANDARD = Decimal("0.05")
def _q(d: Decimal) -> Decimal:
return d.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
def compute_line(*, unit_price: Decimal, qty: Decimal,
discount: Decimal, tax_code: str) -> dict:
taxable = _q((unit_price * qty) - discount)
rate = UAE_VAT_STANDARD if tax_code == "VAT_STD" else Decimal("0")
tax = _q(taxable * rate)
return {
"rate": float(rate),
"taxableAmount": float(taxable),
"taxAmount": float(tax),
"code": tax_code,
}
def finalize_total(subtotal: Decimal, tax: Decimal) -> dict:
grand = _q(subtotal + tax)
return {"grand": float(grand), "rounding": 0.0}Continue browsing
Have a project like this in mind? Let's talk.
Send me a brief and I'll respond within 24 hours.