Amir Balazade

Amir Balazade

Front-end engineer

abalazade@gmail.comlinkedin.com/in/amirbalazade

Istanbul, open to remote

Index

Four roles, one codebase

An investment-pool platform. Investors, traders, accountants and admins read the same numbers and are allowed to do very different things with them — and the origin server has no inbound ports at all.

Surface
Investor, trader, accountant and admin, in one installable app
Work
Front-end architecture, deployment topology, delivery pipeline
Stack
Angular PWA, Caddy, Docker Compose, Cloudflare Tunnel, NestJS, Postgres

Domains, not layers

The obvious way to organise a four-audience app is by technical layer — all the services here, all the components there — and it works right up until a change to what a trader may see has to be traced through six folders that each know a little about everyone.

So the app is cut by domain instead. Admin, investor, trader and accountant each own their features, services, stores and routes. Domains do not import from each other; anything two of them genuinely share is promoted deliberately into shared or core, which makes promotion a decision somebody makes rather than an accident of an import statement. The question "who can see this number" has one place to look.

Nothing dials in

The part worth showing is not the front end. A finance product on a small VPS is a standing invitation: the moment ports 80 and 443 are open to the world, the origin's address is public and the box spends its life being scanned.

It is not open. A tunnel connector inside the network dials outward to the edge and holds the connection open, so traffic arrives through a channel the origin established. The firewall allows SSH and nothing else. Ports 80 and 443 are closed and the origin's address never appears in DNS — the public records point at the tunnel, not at the machine.

Deployment topology for zhambon.comA browser reaches the Cloudflare edge, which carries traffic through a tunnel that the origin opened outward. Inside the server, a tunnel connector passes requests to Caddy, which serves the static app and proxies API calls to the NestJS server, which talks to Postgres, Redis and Ollama. The firewall permits SSH only.BrowserCloudflare edgeTLS, cache rules, rate limiting, bot filteringtunnel opened from the insideno inbound ports, origin address hiddenOrigin serverfirewall: SSH only, 80 and 443 closedprivate container networktunnel connectorCaddyAPI serverstatic appPostgres, Redis,Ollama/apielse

Same origin, on purpose

The browser never addresses the API host. The app calls a path on its own origin, and the reverse proxy inside the network decides whether that path is the API or the app shell. There is no second origin to configure CORS for, no preflight on every call, and no API hostname sitting in a bundle for anyone to point a script at.

Deploys that leave nothing behind

A push to the main branch builds the image and pushes it to the registry tagged with the commit. The deploy step then connects to the server, hands it the job's own short-lived token to authenticate the pull, updates the one container and logs out again.

The token expires with the job. There is no long-lived registry credential living on the machine for someone who gets a shell to find later, which is the sort of thing that sits unnoticed on a box for years.

Remorch, where the dependency had to go