Technical architecture
Overview
Vaks PM is a multi-user project-management web application built to run entirely on infrastructure the organization controls. It is on-premise, that is, it sits on the organization's own servers rather than on a third-party online service. The product ships as a small set of container images, and a complete installation has no mandatory dependency on any external online service.
The whole product reduces to a handful of container images plus three durable backing stores (a relational database, a cache, and an object store). Anything else the system needs, such as load balancing, failover coordination and real-time messaging, comes from components the installation hosts itself. There are exactly two optional outbound connections, and the system runs without either:
- An email relay, SMTP (Simple Mail Transfer Protocol, the standard protocol for sending mail), used only to deliver notification emails.
- A corporate identity provider such as Microsoft Entra ID, used only if single sign-on is required.
The application is API-first: the web front end consumes the same public REST (Representational State Transfer, the conventional style for HTTP web APIs) interface that third-party integrations use. Authorization is enforced on the server for every request and every real-time event, and the front end is never trusted to police access on its own. The services that carry application logic, the API and the background worker, are stateless (they hold no local data) and so scale horizontally by simply running more copies.
Glossary
Acronyms used throughout this guide, defined once here for reference.
| Term | Meaning |
|---|---|
| On-premise | Software hosted on infrastructure the organization controls, rather than on a vendor-operated online service. |
| HA | High Availability: a topology that survives the loss of a single host without an outage. |
| DNS | Domain Name System, which maps host names to network addresses. |
| TLS | Transport Layer Security, the encryption layer behind HTTPS. |
| VIP | Virtual IP: a single network address shared by several hosts so it can fail over between them. |
| S3 | Simple Storage Service, an object-storage interface that originated at Amazon and is now an industry de-facto standard. Vaks PM stores files in an S3-compatible store it hosts itself. |
| JWT | JSON Web Token, a signed session token issued after a successful sign-in. Vaks PM stamps it with the tenant identity. |
| WS / WebSocket | A persistent, two-way browser-to-server connection used for real-time updates, as opposed to one-shot request/response calls. |
| ORM | Object-Relational Mapping, a library that maps database tables to typed code objects and manages schema changes. |
| DCS | Distributed Configuration Store: a small, highly available key-value store that holds cluster state and elects the database leader. |
| Tenant | One isolated organization served by the instance. Each tenant has its own database. |
| SMTP | Simple Mail Transfer Protocol, the protocol for sending email; used for notifications. |
| DC | Datacenter, a physical site hosting one cluster. |
Technology stack
The system is TypeScript end to end: the same typed language spans the browser front end, the back end and the shared data contracts between them, so a single type definition can guarantee that client and server agree on every payload. The table below lists each layer and the technology that fills it.
| Layer | Technology | Role |
|---|---|---|
| Front end | React + TypeScript, built with Vite; styled with Tailwind | The single-page web application served to the browser as static assets. |
| Back end | NestJS (Node.js / TypeScript) | The REST API and the real-time WebSocket channel. Stateless and horizontally scalable. |
| ORM | Prisma | Maps the database to typed code and applies schema changes at start-up. |
| Database | PostgreSQL 17 | The system of record. One database per tenant. Run in HA with an automatic-failover manager. |
| Cache / queue | Redis 7 | Cache, publish/subscribe messaging, and the background job queue. |
| Object store | S3-compatible: Garage on Swarm, MinIO on Kubernetes | Stores binary files: attachments, branding assets, document templates, audit archives. |
| Reverse proxy | Traefik on Swarm; the cluster Ingress controller on Kubernetes | Terminates TLS and routes incoming requests by host name. |
| Orchestration | Docker Swarm (reference production) or Kubernetes | Schedules the containers and supports rolling, zero-downtime updates. |
What runs
Whichever orchestrator is used, the running system is the same set of services. Application logic is kept stateless wherever possible: the api, web and worker services hold no local data and can be scaled simply by running more replicas. All durable state lives in the three backing stores (PostgreSQL, Redis and the object store).
| Service | Role | Stateful? |
|---|---|---|
web | Serves the React single-page front end (static assets). | No |
api | NestJS back end: the REST API (prefix /api/v1) plus the real-time WebSocket channel. | No |
worker | Background job runner: email sending, webhook delivery, audit forwarding, scheduled tasks. Consumes the job queue on Redis. | No |
controlplane | The shared front door in multi-tenant mode: authenticates each request and routes it to the correct tenant. The same image as api, run with a different role flag. | No |
| PostgreSQL 17 | The primary relational database. One database per tenant. | Yes |
| Redis 7 | Cache, publish/subscribe, and the background job queue. | Yes |
| Object store (S3) | File attachments, branding assets, document templates, audit archives. | Yes |
| Reverse proxy | Terminates TLS and routes by host name. Traefik on Swarm; the cluster Ingress controller on Kubernetes. | No |
Stateless tier scales out
Because api, web and worker keep nothing locally, several replicas can run side by side. They absorb load and let a rolling update proceed without downtime.
State is concentrated
All durable data sits in exactly three stores. This keeps the backup surface small and well-defined: a database, a cache and an object store.
One image, two roles
The controlplane service is the same build as api with a role flag flipped, so there is no separate codebase to maintain for the shared front door.
Control plane and data plane
The architecture separates two responsibilities into two logical planes:
- The control plane is the shared front door: the reverse proxy (which terminates TLS and routes by host name) together with a shared authentication service. When a user signs in, this service issues a
JWT(a signed session token) stamped with the tenant identity, so every later request already carries proof of which organization it belongs to. - The data plane holds the business data, isolated per tenant, each organization in its own database with its own database role.
A single container image serves either role through a configuration flag; there is no separate product for the front door. In a single-organization on-premise install this distinction is essentially invisible, and it becomes meaningful only when one instance serves several organizations. The benefit of the split is containment. A flaw in one tenant's data access (a missed WHERE clause, an SQL injection, or an attempt to reach another user's object by its identifier) is confined to that single tenant's database, because the database role for one tenant cannot read another's.
Tenancy models
A tenant is one isolated organization served by the instance. A single configuration flag sets how many tenants an instance carries and how their data is isolated. There are two modes:
| Mode | Meaning | Typical use |
|---|---|---|
dedicated | One organization per instance. The host name is not used to pick a tenant, since there is only one. This is the standard on-premise case. | A single company running its own private installation. |
pooled | Several organizations on one instance. Each is resolved from its host name and isolated in its own database, named <db>_<slug> (a base database name followed by the organization's short identifier). | An operator hosting many organizations on shared infrastructure. |
In both modes the rule is the same: one database per tenant, each with its own database role. The several databases can live inside a single highly available PostgreSQL cluster, which is shared infrastructure over separated data. The application never hard-codes which database to use; it obtains a connection through a tenant context. In dedicated mode that context always returns the single database; in pooled mode it returns <db>_<slug> for the resolved tenant.
The cells variant
The pooled model can be deployed in a stricter form called cells: instead of one shared application tier serving every tenant, each tenant gets its own application instance and its own database, sitting behind the same shared control plane. This trades some efficiency for maximum isolation, since a tenant's compute is no longer shared with anyone else's. The same image still runs everywhere; only the deployment shape differs. One useful property of this model is that a non-production tenant (for example a demo or test organization) is just another cell and is fully disposable: its database can be dropped and reseeded without touching any other tenant.
Login & routing
The reverse proxy routes incoming traffic by the Host HTTP header, the domain name the browser asked for. The crucial property is that the tenant is resolved before authentication: the proxy hands the Host to the back end, which derives which organization the request belongs to, and only then does it attempt to find the user. This ordering keeps one organization's users from being matched against another organization's accounts.
The behaviour differs between the two tenancy modes, but the infrastructure (the proxy, its routers and the TLS certificates) is identical in both. A single configuration flag flips the behaviour; in dedicated mode the host-based routing simply stays inert.
| Step | dedicated (single organization) | pooled (several organizations) |
|---|---|---|
| Reverse proxy routing | Routes by request path; the Host serves only TLS. | Routes by Host (the subdomain) and by path as a fallback. |
| Tenant resolution | The Host is ignored; there is only one organization, so resolution returns nothing. | The Host is mapped to an organization (by its short identifier or its primary domain). An unknown host is rejected. |
| User lookup | A global lookup by email address (one organization, so no ambiguity). | A lookup scoped to the resolved organization, by email and organization. |
| Result | A JWT stamped with the organization, returned with HTTP 200. | A JWT stamped with the resolved organization, returned with HTTP 200; an unknown host yields HTTP 401 (unauthorized). |
dedicated mode the proxy routes by path and the back end ignores the host; login is a global lookup by email. In pooled mode the proxy also routes by host, the back end resolves the organization first, and login is scoped to that organization, so an unknown host returns HTTP 401.Real-time channel
Beyond ordinary request/response calls, the application maintains a persistent WebSocket connection (a two-way, always-open browser-to-server channel) so that changes such as a moved task, a new comment or a status update appear live without the page being reloaded. The transport is Socket.IO, a widely used WebSocket library that adds reconnection and fallback handling.
- Authentication is required at the handshake. The connection must present a valid
JWTas it opens; an unauthenticated socket is refused before it can join anything. The connection is therefore tied to a known user and tenant from its first moment. - Events are grouped into a room per project. A client receives updates only for the projects it has joined, rather than a firehose of everything happening on the instance.
- Authorization is enforced on every event. The server applies the same role-based access checks to each real-time event that it applies to REST calls; joining a project's room and receiving its events both require the appropriate rights. The live channel is never a way around the permission model.
Network & ports
The exposure surface is deliberately small. Only ports 80 and 443 need to be reachable from clients; the reverse proxy redirects plain HTTP (80) to HTTPS (443) and terminates TLS there. Every other service, including the database, the cache, the object store and the application tier, communicates on the cluster's internal overlay network and is never published to the outside. The only connections that leave the cluster are the two optional outbound ones (the SMTP relay and webhook or notification endpoints), and both originate from the background worker.
In the HA topology shown above, two helpers sit between the application and PostgreSQL: HAProxy, a load balancer that always points to the current writable database node, and etcd, a DCS (distributed configuration store) that holds cluster state and elects the database leader. The application never connects to a PostgreSQL node directly. It goes through the load balancer, so a failover is transparent to it.
Two-datacenter model
For disaster resilience, an instance can be spread across two datacenters (DC) in an active / passive arrangement: one DC serves all traffic, the other stays ready to take over. The guiding constraints are:
- One orchestrator per DC, never stretched. A single Docker Swarm must not span both sites: its consensus quorum is fragile over a long-distance link and risks split-brain (two halves each believing they lead). Each DC therefore runs its own independent cluster, with its own local quorum.
- One writable PostgreSQL. PostgreSQL accepts writes on a single node only. The primary DC is read/write; the standby DC runs a Patroni standby cluster fed by asynchronous replication, so writes are copied over with a slight delay.
- One object store across both sites, in zones. The S3-compatible store spans both DCs as a single cluster, using a zone per site so each object is replicated geographically.
- GeoDNS in front. A geographic DNS service (or a global load balancer) sits ahead of the two control planes and steers visitors to the active DC.
Because replication is asynchronous, a failover (promoting the standby) carries a non-zero RPO (Recovery Point Objective, the maximum tolerated data loss): the few seconds of writes not yet replicated may be lost. The stateless application tier, by contrast, runs identically in both DCs and poses no such constraint. Two genuinely active sites serving writes at once are out of scope, because PostgreSQL is single-writer; that would require geo-distributing the tenants so each one is primary in a single DC.