The map: every technology and where it sits
Every technology in {{brand}} on one map: the six layers a request passes through, what each technology does and why it was chosen, one request traced end to end, the seam rule that keeps the parts apart, and how to read the rest of the course.
In short
{{brand}} is built from six layers. People work in clients: a web app, an installable app, a browser extension and a command line. Every request crosses the edge and reaches one .NET 10 API made of separate modules. The API keeps records in PostgreSQL and files in storage, and reaches outside services such as AI models, email and payments only through small adapters. Docker, Azure and Azure DevOps build, check and run it all.
After this lesson you can Name every technology in the stack and say what it is for. Trace one request from a click to the database and back. Defend each big choice against its alternatives, and explain the seam rule that keeps the parts apart.
Words you need
- Layer A group of parts with one job, such as drawing screens or keeping data. In the request path, a layer calls only the layer below it.
- Edge Everything between the internet and the application: DNS, the HTTPS certificate, bot checks and the reverse proxy.
- Reverse proxy A web server that receives every request first and forwards it to the right program behind it. {{brand}} uses nginx.
- Modular monolith One deployable application cut into modules with hard borders, instead of many small services that talk over the network.
- Module One product area - Boards, Jobs, API Tester - with its own code, its own database schema and its own on and off switch.
- Seam A contract (an interface) in a shared kernel that one module implements and another uses, without either knowing the other.
- Adapter A small class that talks to one vendor, such as an email service, behind a contract. Changing vendor changes only the adapter.
- Tenant One customer's workspace - an organization. Data of one tenant must never reach another.
- Middleware One ordered step that every request passes before the business code runs. It can reject or change the request.
- Outbox A table where a fact is saved in the same transaction as the data, then delivered later and retried until it works.
- Optimistic update The screen shows the result at once and undoes it if the server refuses.
- CORS Browser rules about which other websites may call an API.
The six layers at a glance
Read the map from top to bottom. A request starts in a client, crosses the edge and is handled by the API. The API reads and writes data and calls outside services. The sixth layer, run and ship, holds everything that builds, checks and hosts the other five.
The direction rule For a request, calls go only down: the web app never touches the database, and a module never talks to an outside service directly; it asks a contract, and an adapter answers. Answers flow back up. The sixth layer is different: it does not handle requests, it builds, checks and hosts the other five. Modules also never call each other directly; they meet only through seams, described below.
Layer 1: Clients - where people work
Four clients reach the same API, and all of them speak HTTPS and JSON, so the server has one way in. The web app is a single-page application: the browser downloads the code once, and after that only data travels. Screens load in chunks, so a page downloads only the code it needs.
- React 19 The UI library. Screens are components - functions that turn data into what you see. The largest ecosystem and hiring pool.
- TypeScript JavaScript with types. The compiler catches a wrong field name or a missing case before the code ever runs.
- Vite The build tool: a fast development server, and a production build with the Rolldown bundler that splits code into cacheable chunks.
- Tailwind CSS Styling with small utility classes next to the markup, on top of shared design tokens for colours and spacing.
- TanStack Query Owns server state: fetching, caching, refreshing in the background and optimistic updates, all keyed by query keys.
- Redux Toolkit A small global store, kept for a few cross-cutting events. Most state is server state and lives in TanStack Query.
- React Router Maps addresses to screens, so every screen has a link you can bookmark and share.
- i18next Interface text in English, Russian and Azerbaijani, loaded in parts from locale files as screens need them.
- SignalR client Keeps a WebSocket open, so the server can say that something changed while you look at it.
- Rich content Tiptap for rich text, React Flow with the ELK layout engine for diagrams and Recharts for charts. Long lists are virtualised, so only the rows on screen are drawn. MiniSearch searches Help in the browser, and DOMPurify cleans HTML.
- Installable app (PWA) The same web app with a web app manifest and a service worker: install it on a phone or desktop, get push notifications and an offline page.
- Browser extension A Manifest V3 extension: a background service worker and content scripts that capture browser traffic and record clicks for the testing tools.
- Command line (CLI) A .NET tool called kamocore, also built as single-file programs. It runs API suites in pipelines with kamocore run, and offers tools to coding agents over MCP with kamocore mcp.
Why server state needs its own tool Data from the server is a copy that can go stale. A query library knows when each copy was fetched, refreshes it when needed and shares it between screens. Writing that by hand in a global store is where most frontend bugs come from.
Layer 2: The edge - the way in
The edge decides what may reach the application at all. Two parts sit outside the code - Cloudflare and nginx - and one sits inside it: the middleware pipeline of ASP.NET Core. Each one stops a different kind of bad traffic before any business code runs.
- DNS and HTTPS Cloudflare answers for the domain name, holds the certificate and ends the encrypted connection close to the visitor.
- Bot check On public forms such as sign-up and sign-in, Turnstile - Cloudflare's check without puzzles - tells people from scripts; the API verifies its answer.
- Hidden origin A common design keeps the application off the public internet, so it can be reached only through the proxy in front of it.
- Reverse proxy nginx serves the web app's files and forwards API paths to the API on the same origin.
- Middleware pipeline Inside the API, ordered steps run for every request: security headers, cross-origin rules, sign-in, rate limits and access checks.
Why the same origin matters When the app and its API share one address, the browser sends the session cookie without cross-site rules. CORS can stay strict, and the page's Content Security Policy names one origin. One public entry point is also easier to protect. Real-time connections and streamed answers need the extra lines above: a proxy that buffers or drops the Upgrade header breaks them.
Layer 3: The API - where the rules live
The API is one ASP.NET Core application on .NET 10. It holds many product modules - Boards, Jobs, API Tester and the rest - and the platform parts every module needs: accounts, access and settings. Every request follows the same short path: endpoint, mediator, handler.
- .NET 10 and C# A fast, typed runtime with nullable checks, and warnings treated as errors. One language for the server, the command line and the tests.
- Minimal APIs Routes declared as small functions. A source generator writes each route's request code at compile time, so the first request after a deploy is not slow.
- Mediator (CQRS) Each use case is a command (it changes data) or a query (it only reads), sent to exactly one handler. This split is called CQRS. A mediator is the small piece that finds the handler for a message.
- Dependency injection The built-in container creates handlers per request and hands them what they need - the database context, the caller and the adapters. Code asks for a contract; configuration decides which class answers.
- EF Core 10 The ORM (object-relational mapper): LINQ becomes SQL, changes are tracked and saved in one transaction, and migrations evolve the schema.
- ASP.NET Core Identity Accounts, password hashing and passkeys come from ASP.NET Core Identity. Sessions are signed tokens carried in a cookie, and authenticator-app codes follow the TOTP standard. Sign-in with Google, Microsoft or a company's own provider uses OpenID Connect.
- SignalR Real-time messages over WebSockets, sent to groups of connections - for example everyone who has one board open.
- Background services Hosted services run on timers inside the API: reminders, scheduled checks, deliveries of events that need a retry.
- Rate limiter and OpenAPI The built-in rate limiter protects expensive routes. An OpenAPI document describes the routes for the API reference.
- Streaming AI answers reach the browser word by word as server-sent events, a plain HTTP response that stays open.
- Document libraries ClosedXML and Open XML write Excel and Word files, QuestPDF writes PDFs, ScottPlot draws charts, PdfPig reads uploaded PDFs. They are shared helpers that code calls directly - they need no outside account.
- Libraries for what people test HttpClient calls APIs; Npgsql, SqlClient, SQLite and StackExchange.Redis reach databases and caches; Playwright drives real browsers.
- Endpoint Maps one route, binds the request body and sends one message. No business logic and no try/catch.
- Mediator Finds the one handler registered for that message type and runs it.
- Handler Checks the caller's right, applies the domain rules, saves through EF Core and returns a response record - never a database entity.
- Error handler A typed exception becomes the right status code, such as 400, 403, 404 or 409, and anything unexpected becomes a plain 500 with no details.
Layer 4: Data - where things are kept
Data has several homes, each chosen for what it holds. Records go to PostgreSQL, files to object storage, short-lived cached answers to memory or Redis, and the encryption keys behind saved secrets to a key ring.
- One schema per module A common layout gives each module its own PostgreSQL schema and its own EF Core context, so no module reads another module's tables. A large customer can even be routed to a database of its own, with the same schemas.
- Soft deletes A delete marks the row with a deleted time instead of removing it, and unique indexes cover only live rows.
- Change log An audit table can record every change in the same save as the data, so history cannot drift from the truth. Decide up front which values must never be copied into it.
- Outbox A fact other modules must hear is written to an outbox table in the same transaction, then delivered right after the commit, retried with growing delays, and set aside for an operator if it keeps failing - so nothing is lost silently.
- Tenant filters A common way to isolate tenants is a global query filter: it adds the caller's workspace to every query, so a forgotten where-clause cannot show another tenant's rows.
Layer 5: Outside services - bought, not built
Some jobs are better bought than built: sending email, taking payments, running a large language model. Each one is reached through an adapter behind a contract, picked by configuration when the server starts. A module never knows which vendor answers.
Why plain HTTPS can be enough Many model servers accept the same chat completions format over HTTPS, so one client can talk to several of them; Anthropic uses its own messages format. Before you choose a vendor SDK over plain HTTP, read the vendor's documentation and weigh the extra dependency.
Layer 6: Run and ship - from a commit to a running service
The last layer turns source code into a running, watched service. Everything ships as Docker images - the same images on a laptop, in the hosted cloud and on a customer's own servers. One version number names both images of a release.
- Pull request Code lives in Azure DevOps; changes reach the main branch only through a pull request.
- Build and test Azure Pipelines builds the .NET solution and runs the xUnit suites, one of them on a real PostgreSQL. The web app runs its checks, tests, lint and production build.
- Scan NuGet and npm audits look for known vulnerabilities, Trivy scans both images, and a CycloneDX bill of materials lists every component.
- Package Multi-stage Dockerfiles build the API with the .NET SDK and run it on a slim runtime image. The web app is built on Node and served by nginx.
- Release The pipeline checks the code; it does not deploy it. A release is a separate, deliberate step done by a person: the database is migrated first from the new image, then the containers switch, then a health check confirms the release - or the previous version is started again.
- Watch Logs, traces and errors go to Application Insights in the hosted cloud, or to any OpenTelemetry collector on a private installation.
One request, end to end
Follow one real action: a person drags a card into the Done column of a board. On its way in it crosses the client, the edge, the API and the data layer, and on the way back it reaches other people's screens too.
- Optimistic update The web app moves the card on screen at once and sends a POST with the target column. TanStack Query keeps the old state in case the server says no.
- Edge Cloudflare ends HTTPS and passes the request to nginx, which forwards the API path to the API.
- Middleware The pipeline reads the session cookie, works out who is asking and applies rate limits.
- Access The API confirms the caller belongs to the workspace the request is for, and that the Boards module is on for it.
- Endpoint and mediator The move endpoint binds the body and sends one command; the mediator finds its handler.
- Handler The handler checks the person may edit this board, then moves the card inside a transaction, so two drags at once cannot interleave.
- Database EF Core saves the card's new place together with the outbox fact other parts must hear, in one commit to PostgreSQL.
- Response The API answers 200 with a small JSON body. The web app keeps its optimistic picture and fetches only that card again.
- Live update SignalR tells everyone with the board open which cards changed; their screens fetch just those cards. When very many cards changed at once, it tells them to reload the board instead.
- Other modules The outbox delivers the fact that a card moved, and the activity stream records it - without Boards knowing who listens.
- If it fails A refusal comes back as a short JSON error with a status code such as 400, 403 or 409. The web app puts the card back where it was.
The options in the market
Four decisions shape everything else: how many deployable pieces the system has, which platform the server is written on, which database keeps the records, and how screens learn that something changed. Here are the honest options for each.
- Classic monolith One application and one big codebase, with no hard borders inside.; Simplest to start; One deploy, one database; Parts reach into each other over time; One change ripples through areas it should not touch
- Modular monolith One deployable, cut into modules that meet only through contracts.; Real borders, checked by tests; In-process calls: no network failures between parts; One transaction can cover a whole use case; A module can still move out later; Everything scales together; Needs discipline and automated checks to keep the borders
- Microservices Each area is its own service with its own database, talking over the network.; Deploy and scale each part on its own; Teams own services end to end; Network calls fail and add delay; Distributed transactions, many pipelines and much more to run; Too heavy for a small team
- Serverless functions Small functions the cloud runs on demand.; Pay per call; No servers to manage; Cold starts; Long jobs and WebSockets are awkward; Ties the code to one cloud
- .NET 10 with ASP.NET Core C#, compiled, typed.; Very fast in public benchmarks; Strong types and nullable checks; An ORM (EF Core) and real time (SignalR) from the same vendor; One language for server, CLI and tests; Smaller start-up ecosystem than JavaScript; Larger container images than Go
- Node.js (Express or NestJS) JavaScript or TypeScript on the server.; Same language as the web app; Huge package ecosystem; Types vanish at runtime; CPU-heavy work blocks the event loop
- Java with Spring Boot The enterprise standard.; Mature and proven; Large ecosystem; More ceremony and configuration; Slower start-up without extra tooling
- Go Small, compiled services.; Small, fast binaries; Simple concurrency; No ORM or real-time framework of the same depth; More code for the same feature
- PostgreSQL An open-source relational database.; Transactions and strong indexes; JSON columns, full-text search and advisory locks in one engine; Free, and managed by every major cloud; Some features are PostgreSQL-only, so changing engine takes care
- MySQL or MariaDB Widespread relational databases.; Easy to find, simple to run; Hosting and tooling everywhere; Fewer JSON and indexing options; Fewer advanced features out of the box
- SQL Server Microsoft's relational database.; Strong tooling; Deep integration with .NET; Licence cost for most uses; Heavier to host
- MongoDB A document database.; Flexible record shape; Easy to spread across many servers; Joins and transactions across many records are weaker; Deeply related data is duplicated or fetched in several steps
- SignalR over WebSockets A long-lived, two-way connection.; Changes arrive at once; Groups, reconnects and fallbacks are built in; Runs inside the same server; Several copies of the server need a backplane such as Redis; Open connections use server memory
- Polling The screen asks again every few seconds.; The simplest to build; Works through any proxy; Changes arrive late; Most requests find nothing new
- Server-sent events A one-way stream over plain HTTP.; Simple, and works with ordinary HTTP tools; Reconnects by itself; Only server to browser; No groups or presence built in
What {{brand}} chose, and why
{{brand}} is built by a small team and offers many connected products in one place. A card follows a test, a failed test becomes a bug, tracked time lands where the work lives. That needs one transaction and fast in-process calls between parts, plus hard borders so the parts stay apart. A modular monolith gives both.
The rule behind every choice Pick the simplest thing that will still work when the product grows, and put a seam where it might have to change. One deployable, but modules that could move out. One database, but a schema per module. One cloud, but an adapter per vendor.
- Microservices pay off when many teams must deploy on their own schedules, or one part needs very different scale.
- Serverless fits short, spiky jobs with long idle time - not long sessions and WebSockets.
- Kubernetes earns its complexity when you run many services or many copies; the Private edition ships a Helm chart for customers who already run it.
- A document database fits data with few relations and a free shape; most of {{brand}}'s data is deeply related.
Seams: the one rule that holds it together
Modules never reference each other. Sometimes one part needs something from all the others: Home needs every module's urgent items, and the assistant needs facts from everywhere. Then it declares a contract in the shared kernel. Each module implements it, and dependency injection hands the consumer every implementation.
- Declare Put an interface in the shared kernel. The kernel references no module, so it can never pull one in.
- Implement Each module that has something to offer writes a class for it, inside its own project.
- Register The module registers that class in dependency injection when the server starts.
- Consume The consumer asks for all implementations and combines the answers. It never learns the modules' names.
- Guard A test reads each module's references and fails the build if one module ever points at another.
Events are seams too A module publishes a fact and any module may handle it. The publisher never knows who listens, so adding a listener never touches the publisher. Facts that must not be lost travel through the outbox: delivered right after the commit, retried with growing delays, and set aside for an operator if they keep failing.
Adapters: every vendor behind a contract
The same idea keeps vendors out of the modules. Each outside need is a contract in the shared kernel, each vendor is an adapter, and one setting picks the adapter when the server starts. A wrong or half-filled setting stops the server at startup, not a user's upload an hour later.
A stand-in for every contract Each contract has a local stand-in: email written to a folder, files on disk, a fake payment provider. A developer's machine and every test run then work without a single vendor account.
Health probes: the other half of failing fast A server should expose two probes. Live says the process is running; ready says it can serve people right now. A platform uses them to decide when to send traffic or restart a copy, and start-up checks that stop the boot on a blocking problem complete the picture.
Growing from one copy to many
Any modular monolith can scale horizontally: run several copies behind a proxy. First, state that lives inside one process has to move to a shared place, so every copy sees the same world.
The key you cannot lose Any key that encrypts stored data must live in a shared, backed-up place before you run a second copy, or before any redeploy that replaces a container's disk. Lose it, and what it encrypted stays unreadable for good. If the key is itself protected by a certificate, that certificate needs the same care.
How to read this course
Every later lesson zooms into one box of this map. Read them in sequence to follow how the product was built, or jump straight to the layer you need today. Each lesson sits under the layer it mostly belongs to.
- /help/technology/frontend React, TypeScript, Vite, TanStack Query and a layered web app
- /help/technology/translations Interface languages and the translation pipeline
- /help/technology/collaboration-techniques Techniques for many people working together live
- /help/technology/diagrams-code-analysis Diagrams, graph layout and reading code structure
- /help/technology/ai-agents-mcp AI agents and the Model Context Protocol
- /help/technology/request-path The middleware pipeline and the life of a request
- /help/technology/architecture The modular monolith, seams and dependency rules
- /help/technology/sign-in Passwords, passkeys, sessions and two-step sign-in
- /help/technology/sso OpenID Connect and company single sign-on
- /help/technology/access Roles, permissions, plans and tenant isolation
- /help/technology/realtime-events SignalR, WebSockets and the outbox pattern
- /help/technology/search-reports Search, reports and exports
- /help/technology/ai-chunking Retrieval-augmented generation: finding what the assistant reads
- /help/technology/ai-safety Keeping AI answers grounded and safe
- /help/technology/http-api-testing How HTTP API tests are run and checked
- /help/technology/secure-links-privacy Secure links, personal data and privacy
- /help/technology/data PostgreSQL, EF Core, migrations, soft deletes and races
- /help/technology/files-notifications File storage, scanning, email and push notifications
- /help/technology/ai-models Language models, providers, tokens and cost
- /help/technology/browser-automation Driving real browsers with Playwright
- /help/technology/database-drivers-redis Database drivers and Redis
- /help/technology/payments Paddle, plans and signed webhooks
- /help/technology/testing Backend and frontend tests, test hosts and real-database tests
- /help/technology/private-edition Running on a customer's own servers
- /help/technology/azure-devops Azure, Docker, pipelines, scans and releases
Try it yourself
The goal: watch one request cross the edge, the API and the data layer on your own laptop. You need free tools only - Docker Desktop, the .NET SDK and a browser - and about thirty minutes.
- Create the project Run dotnet new web -o api, then cd api and dotnet add package Npgsql.DependencyInjection. Replace Program.cs with the code below.
- Add the files Create api/Dockerfile, nginx.conf and site/index.html from the blocks below, next to docker-compose.yml.
- Start everything Run docker compose up --build. A postgres container with a named volume keeps its data when the container is replaced.
- Trace the request Open http://localhost:8080, then the browser's network tab. Call /api/time and match each hop to the logs from docker compose logs.
- Check the single door Run curl http://localhost:8080/api/time: it prints the database time. Only nginx's port is published; the API has none.
- Draw your map Write the six layers on paper and place each tool you used. Then add what a second API copy would need.
Common mistakes
- Borders nobody enforces: a layering rule without a test erodes within months. Check project references in the build.
- Vendor libraries in business code: the day the vendor changes, every module changes. Put each outside service behind a contract and one adapter.
- Microservices too early: network calls, retries and distributed transactions cost a small team more than they save.
- State on one machine: sessions, caches and keys kept in one process or on one disk break the moment a second copy starts.
- The wrong address baked into a web build: build-time variables are fixed inside the bundle. Check them before you ship.
- Trusting the client for the tenant: the workspace a request acts in must be checked on the server, never taken from the body.
Interview questions
- Describe your stack in one minute. Six layers. React and TypeScript clients, and Cloudflare and nginx at the edge. One .NET 10 API made of modules, with PostgreSQL and object storage for data. AI, email and payments sit behind adapters, and Docker, Azure and Azure DevOps run and ship it.
- What is a modular monolith, and why choose it? One deployable cut into modules that meet only through contracts. You get real borders without network calls or distributed transactions, and a module can still move out later because nothing else references it.
- How do your modules talk to each other? Through interfaces in a shared kernel that modules implement and consumers discover through dependency injection, and through published events. A test fails the build if a module references another module.
- Walk me through one request. An optimistic update in the browser, then Cloudflare, nginx and the middleware pipeline. Then an access check, one endpoint, one command, one handler, and EF Core and PostgreSQL in one transaction. Finally SignalR tells other viewers which records changed.
- How would you scale it? Run several stateless copies behind the proxy. Move the cache, the real-time backplane and the key ring to shared services, because each copy must see the same state. The database becomes the next limit, so I would add read replicas and indexes before splitting services.
- Why PostgreSQL and not a document database? The data is deeply related and needs transactions. PostgreSQL also gives JSON columns, full-text search and advisory locks, so one engine covers jobs that would otherwise need extra services.
- Why write your own mediator? A mediator is a simple lookup from message type to handler, so a package adds a dependency for little. The trade-off is that you must put cross-cutting work such as logging and errors in middleware instead of in pipeline behaviours.
- How do you swap a vendor? Write a new adapter for the existing contract and change one setting. Selection happens at startup and a half-configured adapter stops the boot, so the mistake never reaches a user.
- Where does server data live in the web app? In the query cache, keyed by query keys. It is a copy that can go stale, so I refresh it on a timer, after my own writes and when a real-time message names a changed key.
- What is the edge, and why serve the app and API from one origin? Everything in front of the code: DNS, HTTPS, bot checks and the reverse proxy. One origin keeps cookies first-party, CORS strict and the security policy short.
Related
- /help/technology/architecture
- /help/technology/request-path
- /help/technology/data