Own product
Pullstok: a complete SaaS ERP with an online store
My own product: a multi-tenant SaaS ERP I design, build and run. Point of sale, stock, ARCA electronic invoicing, accounting (in development), an online store per subdomain, Mercado Pago, AI bots and a local print agent. A monorepo with 5 apps and a Node API with Prisma and PostgreSQL.
Tech stack
The technologies used and why.
Screenshots
The problem
A company that sells, retail or wholesale, with one or many branches, usually has its operation scattered: stock in a spreadsheet, invoicing in another system, QR payments in another app, accounting somewhere else and the online store on its own. Every separate tool is one more place to enter the same data twice and one more source of inconsistencies.
Pullstok is my answer to that: a multi-tenant SaaS ERP that brings a company’s whole operation into one place, from the counter to the books. It is not client work: it is my own product, which I design, build, deploy and run end to end.
One system
From quote to sale and from sale to electronic invoice, without entering anything twice. The next step, in development: every operation posting its own accounting entry.
Every business, isolated
All businesses share the platform, but each one only sees its own products, customers and sales.
Connected to the counter
Thermal printers, weight-label scales, camera barcode scanning and Mercado Pago QR payments.
What it does
Sales and counter
- Unified point of sale: sell per unit, by weight, by amount, closed bag and per blister unit, with line discounts and wholesale prices.
- Cash register: opening and closing cash sessions.
- Offline POS: the catalog is stored in IndexedDB and offline sales go to a queue that syncs later, with an idempotent identifier so no sale is ever duplicated.
- Mercado Pago QR: account linking through OAuth and one payment point per branch.
Commercial flow
- Quotes, orders and sales, plus price-less wholesale delivery notes that can be generated from the POS itself.
- Customers with running accounts: collections and account statements in PDF.
- Providers and purchases: provider accounts and balance offsets between customers and providers.
Stock and catalog
- Products with variants, presentations and multiple barcodes, stock per branch and internal Code128 barcodes.
- Camera barcode scanner on a phone or laptop.
- Categories with presets per industry.
- Price lists: parsing supplier PDF price lists with catalog matching and a review queue, cost imports, bulk increases by brand, price tiers and product upload by CSV.
- Scales: reading EAN-13 weight labels from Systel Cuora scales configured in Qendra, assigning scale codes and exporting the CSV to load them.
Tax and accounting
- ARCA (former AFIP) electronic invoicing: A, B and C invoices, credit notes, taxpayer lookup and internal vouchers that never reach the tax agency.
- Sale to invoice bridge: a sale becomes an invoice with fiscally correct amounts.
- Double-entry accounting (in development): the module I am building right now. Chart of accounts, fiscal years with closing, general journal, ledger, trial balance and automatic entries for sales, collections, purchases and provider payments.
Digital channels
- An online store per business, on its own subdomain, connected to the same catalog and the same stock.
- AI bots: a store chat with real-time handoff to a human, a sales assistant for vendors and a WhatsApp bot with function calling that drafts orders.
Platform
- Superadmin to create, manage and suspend businesses.
- Guided onboarding in three steps: business, categories and products.
- Plans (Basic, Pro and Premium) with modules enabled per plan and per business.
- Reports: daily dashboard, statistics with charts, sales export to PDF and Excel, and a per-vendor dashboard.
- Per-business branding, business hours that lock access after hours, password reset by email and daily backups per business.
Architecture
Pullstok is a pnpm workspaces monorepo with five apps, each with a clear responsibility:
- ERP app: a React 18 SPA with Vite, Tailwind v4 and shadcn/Radix. Every view is lazy-loaded, with separate layouts for auth, the panel, onboarding and superadmin. TanStack Query handles server state and Socket.io sends lightweight change signals that invalidate queries, so the screen updates on its own.
- API: a layered Express app (routes, middlewares, controllers, services, Prisma). Tax and accounting logic lives in pure modules, with no Express or Prisma, so it can be tested in isolation. External integrations (ARCA, Mercado Pago) sit behind clients with mock versions, in a ports and adapters style.
- Store: Astro with the Node adapter, React islands and a persistent cart. Checkout goes through a server-to-server proxy, so the API is not exposed through CORS.
- Landing: static Astro with pricing, FAQ and an AI chat.
- Print agent: a dependency-free Windows executable that connects the cloud to the printer at the counter.
Multi-tenancy and security
- Each request’s business is resolved in a middleware (from the JWT, from the store subdomain or from the print agent token) and stored in an AsyncLocalStorage context.
- A Prisma extension automatically scopes about 45 models per business: it injects
organizationIdinto every query and write, fails if there is no context and blocks the operations that cannot be scoped safely. Platform operations use a separate, explicit client. - Roles: superadmin, admin, management, vendor, cashier and employee, enforced by middleware. The store chat uses a guest token that the ERP rejects.
- Zod validation on every endpoint: the validated body replaces the original one, which prevents mass assignment.
- Centralized errors: 4xx responses return generic messages and 5xx errors are reported to Sentry without leaking details to the client.
Key technical decisions
Tenant isolation in the ORM
In a multi-tenant SaaS, the worst possible bug is one business seeing another’s data. Instead of relying on remembering to filter every query, the filter lives in a Prisma extension fed by AsyncLocalStorage. Forgetting the filter is no longer possible: the query fails.
ARCA from scratch, over SOAP
No third-party SDK: the access ticket is signed as CMS/PKCS#7 with node-forge, WSAA handles authentication and WSFEv1 authorizes invoices. The ticket is cached only in memory and each business’s private key is stored encrypted with AES-256-GCM. Each business uploads its own certificate and can test in the sandbox before going to production.
A cloud to local hardware bridge
A browser cannot print RAW to a thermal printer. The print agent is a single Node executable (SEA) that listens only on localhost with an origin allowlist and sends ESC/POS bytes to the Windows spooler. To print from a phone, the agent pairs with a code and polls the API for print jobs, with heartbeat and backoff.
Money in cents, fiscally correct
The sale to invoice conversion derives the net amount from VAT-inclusive prices and turns discounts and surcharges into their own lines, so what is invoiced matches exactly what was charged. Accounting entries come from pure rules, all in integer cents.
Sales that never get lost
The POS keeps selling without internet and syncs later, idempotently. A Mercado Pago QR charge is created, polled until it is processed and only then atomically claimed by the sale, so a payment can never be attached to two sales.
Quality and testing
- API: Jest with supertest, around 230 test files (controllers, middlewares, services, validation, migrations and integrations with ARCA fixtures) plus 23 end to end suites against a real PostgreSQL in Docker.
- Front: Vitest with Testing Library, around 277 test files, including the offline queue with a simulated IndexedDB.
- Print agent: its own Vitest tests.
- Critical pieces (tax calculation, accounting entries, scale label parsing) are pure functions with dedicated tests, and scale parsing was validated against a real scale.
Infra and deploy
- Everything runs on my own VPS behind nginx: the landing at pullstok.com, the ERP at app.pullstok.com and each store on its own subdomain.
- Continuous deploy: every push to main triggers a GitHub Actions workflow that runs a 9-step script: it saves the current commit for rollback, backs up the database, updates the code, installs, generates Prisma, builds every app, applies migrations, restarts the service and checks the health of the API and the front. If the health check fails, it rolls back on its own.
- Backups: a daily database dump with retention, plus per-business backups in Cloudflare R2.
- Observability: Sentry on both the API and the front.
How I build it
Every change starts with a written spec: goal, decisions, findings from exploring the code, what is in and out of scope, and a task list. I work with a spec-driven, AI-assisted workflow where I define the architecture and the decisions, and use agents to explore, implement and review, with persistent memory across sessions and a review before every push. Commits follow conventional commits.
Numbers
1,100+ commits
With conventional commits and continuous deploy to production.
70 data models
Around 100 Prisma migrations on PostgreSQL.
~280 endpoints
Spread across about 57 API routers.
500+ test files
Across the API, the front and the print agent.
Status
Pullstok is in production and under active development. The current focus is the accounting module. Every new module (scales, the offline POS, QR payments, accounting) comes from real needs of the businesses already using it.









