Case 01Tax and ERP

Steuer

Invoices that arrive on their own and land reconciled in Linx.

A multi-company tax platform that captures every NF-e, CT-e and NFS-e issued against the client’s company IDs, keeps XML and PDF ready to look up and posts the reconciled entry straight into the Linx ERP database, inside the store’s own network. The tax team stops re-keying invoices and only handles the exceptions the system flags.

Client
Fashion retail chains on Linx ERP · head office and branches
Period
2024 — 2026
Team
5-person squad
Role
Full stack engineer · from tax capture to the Linx entry
Stack
Bun · Fastify · TypeScript · Zod · Prisma · MySQL
Confidentiality
Client data kept private
02

Results in numbers

  • 12kinvoices processed per monthNF-e, CT-e and NFS-e across clients
  • 90%of Linx entries with no manual keyingthe tax team only handles exceptions
  • 42Linx tables integrated by the gatewayentries, items, taxes, stock, orders and master data
  • 60 sbetween health checks on every tunnel3 consecutive failures trigger automatic reconnection
03

The business context

Client
Fashion retail chains on Linx ERP · head office and branches
Period
2024 — 2026

A fashion chain receives invoices all day long: merchandise from suppliers, transfer slips from the distribution center, freight from carriers and services hired by each branch. Every document has to be found, assigned to the right company ID, checked against the purchase order and posted to Linx, which runs on SQL Server in each client’s local server. When that entry depends on someone downloading XML and re-keying it line by line, stock lags behind, the tax close gets tight and every wrong link turns into accounting rework.

04

The challenge

The challenge had three fronts. Capture relied on a cursor-paginated API, and any interruption mid-period could leave invoices behind. The group has a head office and branches with different access rights, so every document and every screen had to respect the right company. And Linx is not in the cloud: it sits behind each chain’s firewall, with no public address, and only accepts an entry when order, supplier, branch and taxes all match.

  1. Capture that never drops an invoice

    The provider paginates by cursor inside a date window. If a run dies midway and the window moves on, documents vanish unnoticed — and a client’s first sync covers more than a year of invoices.

  2. Head office, branches, distinct access

    A single chain has dozens of company IDs, users who only see their own branch and profiles with different permissions per screen. Data crossing from one company to another would be unacceptable.

  3. An ERP inside the client’s network

    The Linx database runs on-premise. Reaching it without opening a port to the internet, and writing the entry without duplicating an invoice or leaving half a record behind, was the most delicate part.

05

What I delivered

I split the product into four services with clear boundaries. The Bun and Fastify core backend captures invoices per company, records each header as pending and publishes the batch to RabbitMQ, with n8n orchestrating schedule and consumption. The Laravel panel holds login, permissions and tax operations. The Linx gateway validates and writes the entry into the ERP database, reached through a .NET 8 Windows agent that keeps a WireGuard tunnel open from the inside out.

  1. Resumable capture

    NF-e, CT-e and NFS-e per company ID, with history back to January of the previous year and exact cursor resume after any failure.

  2. Automatic Linx entry

    Validated preview, line-by-line invoice × order matching and an atomic write through Linx’s own procedures.

  3. Steuer Connector

    A Windows agent with an MSI installer, WireGuard tunnel, heartbeat and automatic reconnection — the ERP keeps every port closed.

  4. XML and PDF on demand

    XML archived per company in S3, download through signed URLs and DANFE rendered on the fly, scoped to the tenant.

06

How it works

Follow the work through the system step by step, or click any component on the board to see what it does.

12 components · 13 connections · 5 layers

Click a component to see details←→to navigateWalk through the flow below

EXTERNALSEFAZ and cityhallsDF-eEXTERNALTax providersPlugStorage /PlugNotasSERVICEOrchestratorn8nSERVICECore backendBun / FastifyDATABASEXML archiveAWS S3QUEUEImport queueRabbitMQDATABASETax databaseMySQL / PrismaOUTPUTSteuer panelLaravelSERVICELinx gatewayFastify / PrismaSERVICEInstallerWiX MSISECURITYSteuer Connector.NET 8 / WireGuardEXTERNALLinx ERPSQL Server

drag to see the flow

Legend

  • Synchronous
  • Asynchronous
  • Secure channel
  • External integration
Text description of the flow
  1. From SEFAZ and city halls to Tax providers — DF-e distribution (external integration).
  2. From Tax providers to Core backend — Headers by cursor (external integration).
  3. From Orchestrator to Core backend — Triggers capture (synchronous).
  4. From Core backend to Import queue — Publishes batch (asynchronous).
  5. From Core backend to Tax database — Records pending (synchronous).
  6. From Import queue to Tax database — Stores invoice and items (asynchronous).
  7. From Import queue to XML archive — Archives XML (asynchronous).
  8. From Tax database to Steuer panel — Per-company lookup (synchronous).
  9. From XML archive to Steuer panel — Signed URL (synchronous).
  10. From Steuer panel to Linx gateway — Preview and confirm (synchronous).
  11. From Linx gateway to Steuer Connector — WireGuard tunnel (secure channel).
  12. From Installer to Steuer Connector — Provisions (synchronous).
  13. From Steuer Connector to Linx ERP — Restricted proxy (secure channel).

Walk through the flow

←→change step

6 steps

From start to finish

Pick a step or press Play to follow the whole flow. The board highlights where the work goes as you move along.

07

System components

  1. Incremental capture per company

    What it solves
    Bring in every invoice issued against each company ID, history included, without ever skipping a page.
    How it works
    For each active company, the backend reads the last run: if it stopped midway, it resumes with the same cursor and window; if it finished, it builds the next monthly window. On first access it syncs from January of the previous year, month by month, until it reaches today. Every page stores cursor, period and totals, and the loop only ends when the window reaches today with an empty page.

    Bun · Fastify · Prisma · MySQL

  2. Import queue and tax archive

    What it solves
    Separate invoice discovery from the heavy work of downloading, archiving and parsing each XML.
    How it works
    Headers enter as pending, with the access key unique in the database, and the batch goes to a durable RabbitMQ queue with persistent messages. n8n consumes the queue, the backend downloads the XML, archives it in S3 per company and stores header and line items by status — authorized, cancelled, denied or in contingency. A failure flags the invoice as errored and it comes back on the next run.

    RabbitMQ · n8n · AWS S3 · TypeScript

  3. Multi-company tax panel

    What it solves
    Give each person in the group the exact view of their own company, with the access level their role requires.
    How it works
    A Laravel global scope filters every query by the logged-in user’s company, so branch users only see their branch while the head office sees the group. Per-menu permissions define whether a person has no access, read access or full operation on each screen. It holds XML and PDF lookup and download, DANFE, tax calculation, CFOP and inbound × outbound reports, and NFS-e management.

    Laravel · PHP 8.2 · MySQL

  4. Signed entry preview

    What it solves
    Show the operator exactly what will enter Linx and prevent it from changing before confirmation.
    How it works
    The gateway resolves supplier, branch, operation type, series and purchase order, finds the source transfer slip at the store or the DC and compares quantity and value line by line, within a one-cent tolerance. The preview returns an HMAC-SHA256-signed snapshot; on confirmation the hash is recomputed and any change is rejected. For NFS-e, 26 diagnostic codes point to what is missing in the Linx master data.

    Fastify · Zod · Prisma · SQL Server

  5. Atomic, idempotent write

    What it solves
    Make sure each invoice enters the ERP exactly once, whole or not at all.
    How it works
    Confirmation acquires a two-layer idempotency key — 5 minutes in memory and 30 minutes in the database — and checks whether the invoice already exists in Linx. In a single transaction it inserts into ENTRADAS and runs LX_ORQUESTRA_ENTRADA, which generates the transfer and tax records. The transaction code comes from the tax type and origin: new order, DC or store. If the procedure fails, the rollback leaves no orphan record.

    TypeScript · Prisma · SQL Server · Linx

  6. Steuer Connector

    What it solves
    Connect the Linx database to the platform without exposing anything on the client network.
    How it works
    A .NET 8 Windows service with five explicit states. It activates with a one-time token, stores its configuration encrypted with DPAPI, brings up the WireGuard tunnel and a TCP proxy that only forwards to the authorized database host and port. Heartbeat every 60 s, reconnection after 3 failures, a 2-minute watchdog and exponential backoff with jitter. A local panel talks to the service over a Named Pipe, and the MSI checks the environment before activation.

    C# · .NET 8 · WireGuard · WiX

08

Decisions that hold the system up

  1. A run log, not a loose cursor

    Every captured page stores cursor, date window and totals. A crash becomes an exact resume, and a new client’s historical sync advances month by month without overloading the provider or losing a document.

  2. Persist before dispatching

    Every invoice becomes a pending record, with a unique access key, before it enters the queue. If a consumer dies, the state is in the database and reprocessing duplicates nothing — the queue speeds things up but is never the only copy of the truth.

  3. Signed preview, idempotent confirm

    Splitting preview and confirmation keeps the operator in control, and the HMAC hash guarantees that what they approved is what gets written. The idempotency key guards against double clicks, timeouts and retries: the second call gets the same response, with no new entry.

  4. Inside-out tunnel, restricted proxy

    No retail chain would open a port to its ERP. The agent starts WireGuard with a 25 s keepalive, the hub only allows traffic from the peer to the database host and port, and database credentials are never sent to the agent.

09

Business results

  • Posting a purchase to Linx dropped from about 6 minutes to under 1 minute per invoice, with no line re-keyed.

  • No invoice is left behind: every captured page stores its window and cursor, and the next run resumes exactly where the last one stopped.

  • Zero duplicated or half-written ERP entries: an idempotency key, an already-posted check and a single transaction with Linx’s procedures.

  • A new client goes live the same day: the installer sets up WireGuard, the service and the firewall, activation uses a one-time code and the ERP keeps every port closed to the internet.

10

My role

Role
Full stack engineer · from tax capture to the Linx entry
Team
5-person squad

I built Steuer end to end, from the first service to the version that now processes thousands of invoices a month. I designed incremental capture and the queue model, wrote the gateway that talks straight to the Linx database — preview, matching, idempotency and the entry procedures —, carried company isolation from the panel down to the middleware and created the Steuer Connector, the agent that links each client to the platform. I also led rollout and production operations with every chain.

What I owned

  • Designed incremental capture in Bun, Fastify and Prisma: a per-company run log, monthly date windows and resume from the last cursor.
  • Modeled the durable import queue in RabbitMQ and the n8n flows that schedule capture and consume each batch.
  • Wrote the Linx gateway: 27 routes, 42 ERP tables mapped, an HMAC-signed preview and idempotent confirmation.
  • Implemented company isolation end to end: a global scope in Laravel, per-request context in the gateway and strict mode on write routes.
  • Developed the Steuer Connector in C#/.NET 8: a state machine, WireGuard tunnel, restricted TCP proxy and local control over a Named Pipe.
  • Automated the MSI installer build in GitHub Actions, versioned on every run and published to S3; the web services run in Docker.
11

Stack by layer

  • Backend and gateway

    • Bun
    • Fastify
    • TypeScript
    • Zod
    • Prisma
  • Data and queues

    • MySQL
    • SQL Server
    • RabbitMQ
    • n8n
    • AWS S3
  • Panel

    • Laravel
    • PHP 8.2
    • Blade
  • Agent and delivery

    • C#
    • .NET 8
    • WireGuard
    • WiX
    • GitHub Actions
    • Docker
12

What I take from this project

  1. When the ERP lives on the client’s network, connectivity is part of the product, not a deployment detail. Treating it as software, with states, tests and telemetry, avoided rework with every new chain.

  2. Explicit state beats inference: once the agent owned its own lifecycle, with a state machine and IPC, the false “connecting” status disappeared and support started reading the real one.

Next case 02 Kippis Workplace safety in the pocket of the people on site.

What’s the next system your business needs?

Tell me the context: a problem, an idea or a system that needs to evolve. I’ll reply with a clear technical path and the first step to get it built.