Skip to content

Architecture

Why Mavri serves the web app and API from one container

Mavri runs its React interface, REST API and signed webhooks through one Go server and one SQLite database. The trade-offs behind that shape, from deployment to origin security.

By · Published · 5 min read

Mavri has one Go server, one React interface, one SQLite database and one container. The browser app and versioned REST API share the same origin; signed webhooks enter through that server too. I chose that shape for the person installing an invoicing app on a small server, not because one container is an architectural virtue by itself.

The deployment problem I was solving

A self-hosted app can keep records under its owner's control and still be too demanding to run. If the install needs a web server, a separate API service, a database server, a queue and a dozen environment variables before the first invoice, the operator inherits a small platform project along with the billing tool.

For Mavri's current job—quotes, invoices, payment records, reports and a modest number of integrations—SQLite avoids asking the operator to provision and tune a separate database service. The database lives in a persistent data volume. Rebuilding the container does not replace that volume, and the app provides a one-file backup and restore path.

Why one origin matters

Serving the web app and API from one origin keeps browser sessions first-party. There is no separate API hostname to configure for CORS, no second application port to expose, and no cross-origin cookie problem to debug when an owner opens Mavri on another device on the local network.

The same server handles REST requests and signed store webhooks. A store connection verifies the HMAC signature before accepting an order; idempotency is checked against the store connection and external order ID, so a repeated delivery does not create a second invoice. That is a property of the receiving application, not something Docker or the webhook sender provides for free.

What one container does not solve

A simple install is not an operations plan. The owner still chooses where Mavri runs, who can reach it, whether remote access uses HTTPS, where backups are stored and when updates are applied. The application can make these steps visible, but it cannot make a home router, VPS or NAS secure by existing inside a container.

SQLite also sets a boundary. It is a sensible fit for a single Mavri installation serving a small business workflow; it is not a promise of unlimited concurrent writes, multi-region replication or a managed high-availability database. If those are requirements, the product is the wrong fit today.

The decision I would revisit first

I would split the deployment only when there was a concrete operational reason: measured contention, an independently scaled worker, or a maintenance need that could not be handled inside the current process. Until then, a single origin and a single persistent data location make the important path easier to understand: install, create a quote, convert it, record payment and restore the data if a server fails.

That is the design goal for Mavri: reduce the number of moving parts an owner must operate without pretending that self-hosting removes operational responsibility. The Mavri install guide states the current requirements and limits; the source repository is where the implementation lives.

Next, see how a document keeps a snapshot of its details and how webhook retries are made safe. The full write-up of the product is in the Mavri case study; the Mavri website has the install guide and docs.

References

Author

Raktim Ranjit is a software engineer and the founder of NodeDR Infotech. He builds and maintains the software described here.

Have something in mind?

Let’s build something useful.

Tell me about the idea, product, or workflow you’re working through.

Tap to say hello