PuntoOS from the inside: how we built a POS for the e-invoicing era
What a POS had to stop being
When we started PuntoOS, the dominant point-of-sale systems in the Dominican Republic were:
- Windows apps backed by a local database.
- Sync via email or end-of-day
.csvfiles. - No ability to operate across branches.
- Inventory the owner trusted was accurate.
And since 2023, a new pressure: mandatory DGII electronic invoicing. The POS stopped being a calculator with a printer; it became a piece of real-time fiscal infrastructure.
We built PuntoOS assuming this from day one.
The architectural pillars
1. Offline-first, cloud-authoritative
The cashier must be able to sell if the internet goes down. That’s rule number one. But also: the cloud must be the source of truth. Those two constraints look contradictory until you accept the cost: eventual replication with deterministic conflict resolution.
┌──────────────────┐ ┌──────────────────┐
│ PuntoOS Cashier │ ◀──── WebSocket ───▶ │ PuntoOS Cloud │
│ │ │ │
│ Local SQLite │ │ SQL Server + │
│ Vector clock │ │ Append-only log │
│ Outbox pattern │ │ Read replicas │
└──────────────────┘ └──────────────────┘
│ │
│ If the network is down: │
│ ▶ Sale closes locally │
│ ▶ e-CF queues up │
│ ▶ Syncs when it returns │
│ │
│ If the network is up: │
│ ▶ Real-time events (WS) │
│ ▶ e-CF submitted in <2s │
Every cashier holds a local outbox: any sale, inventory adjustment, or e-CF not yet acknowledged by the cloud lives there. When connectivity returns, it drains in order with Idempotency-Key. Remote replicas can’t get ahead because the cashier signs each event with a local vector clock.
2. Real-time, multi-branch inventory
The second pillar is unified inventory. In a 50-store chain, the business question is never “how much do I have at this store?” — it’s “how much do I have total, and where is it?”
Our model: every inventory movement is an immutable event. The on-hand at any moment is a projection over that log.
-- Fact table (immutable, append-only)
CREATE TABLE inventory_event (
id UUID PRIMARY KEY,
occurred_at TIMESTAMPTZ NOT NULL,
store_id INT NOT NULL,
sku TEXT NOT NULL,
quantity NUMERIC NOT NULL, -- positive = in, negative = out
reason TEXT NOT NULL, -- 'sale', 'transfer_in', 'adjustment', ...
reference TEXT, -- id of the sale, transfer, etc.
vector_clock JSONB NOT NULL
);
-- Materialized view (projection, eventually consistent)
CREATE MATERIALIZED VIEW inventory_balance AS
SELECT store_id, sku, SUM(quantity) AS on_hand
FROM inventory_event
GROUP BY store_id, sku;
Why it matters: it makes discrepancies investigable, not mysterious. Any mismatch is reproducible by replaying the log up to the point where it broke.
3. e-CF integrated, not bolted on
PuntoOS doesn’t have an “electronic invoicing module” — it is electronic invoicing. Every sale generates the corresponding e-CF as part of the natural flow.
┌─────────────┐ Charge ┌────────────┐ Sign ┌──────────┐
│ Cashier │ ───────▶ │ Build │ ────────▶ │ DGII │
│ (UI) │ │ e-CF XML │ │ │
└─────────────┘ └────────────┘ └────┬─────┘
│ ACECF
┌────────────┐ │
│ Print with │ ◀─────────────┘
│ NCF + │
│ fiscal QR │
└────────────┘
Details that matter in production:
- Local NCF numbering: each cashier takes pre-assigned ranges from the cloud; it never runs out of sequence if the network drops.
- Automated daily close: the RFCE is built and submitted without human intervention at 23:55.
- Credit notes against offline sales: the note has to reference the original NCF, which may still be in the outbox. PuntoOS keeps them queued until the original gets its accepted ACECF.
4. Multi-country without rewriting the codebase
PuntoOS runs in five countries. Each with its own tax regime:
- 🇩🇴 DR: e-CF / DGII
- 🇨🇷 CR: Hacienda
- 🇸🇻 SV: DGI El Salvador
- 🇬🇹 GT: SAT FEL
- 🇭🇳 HN: SAR
The tax-compliance capsule is a plug-in, not a branch of the codebase. Each country has its adapter that implements the same interface:
interface TaxAdapter {
validate(sale: Sale): ValidationResult
buildDocument(sale: Sale): TaxDocument
sign(doc: TaxDocument, key: SigningKey): SignedDocument
submit(doc: SignedDocument): Promise<Acknowledgement>
receiveAcknowledgement(rawResponse: unknown): Acknowledgement
}
Adding a new country is a sprint, not a project.
Decisions we made against the grain
React Native, not pure native
For the cashier on tablet, we evaluated native Swift/Kotlin vs React Native. We chose React Native because:
- 90% of the code shared across iOS, Android, and the owner’s inventory app.
- Performance sufficient for a POS (it’s not a AAA game).
- The team can iterate the UI without waiting for two separate deploys.
What we paid: peripheral integrations (thermal printer, cash drawer, barcode scanner) required native modules. We accepted that consciously.
SQL Server, not PostgreSQL
Yes, we know. The decision came from the clients — most were already running on SQL Server with paid licenses and trained DBAs. Fighting that would have been engineering vanity, not customer service. For clients who preferred PostgreSQL, we support it too.
.NET 9 on the backend
A combination of:
- A transaction hot path that needs predictable performance (tunable GC).
- A team with deep C# experience.
- A robust library ecosystem for XAdES XML signing, certificate management, and tax authority integration.
It’s not the coolest tech. It’s the one that sleeps well in production at 3 AM.
What happens when something breaks
Three real incidents from the past year:
A branch with no internet for 14 hours
PuntoOS sold normally for all 14 hours. When the network came back, we drained 847 e-CFs in order. DGII received each with its Idempotency-Key; zero duplicates. The owner found out from our alert dashboard, not because anything stopped working.
An unexpected schema change from DGII
Mid-afternoon, DGII updated a field in the ACECF. Our strict parser started rejecting valid responses. We rolled back the parser in 12 minutes; the customer never noticed. The lesson: validate ours strictly, accept theirs permissively (Postel revisited).
A cashier typed 99,999 instead of 9.99
Impossible to prevent with code. But detectable: 3 minutes in, the “atypical ticket” alarm fired (z-score over the median of the last 200 sales). Hot reverse, credit note before the customer left the store.
What’s coming
- Inventory intelligence: SKU-level demand forecasting using real historical data.
- Automatic bank reconciliation: cross-checking sales against deposits.
- Extension marketplace: third parties building modules on top of the platform.
If you want to see PuntoOS, reach out or visit puntoos.net.
— Dawlin
Recommended for you