> For the complete documentation index, see [llms.txt](https://docs.ipcheck.ing/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ipcheck.ing/developer/architecture/project-structure.md).

# Project Structure

MyIP is a single repository that ships **two Node processes** and contains **three code layers**. Nothing else. Once you hold that picture, every file in the tree has an obvious home.

## Two processes

| Process       | File                 | Default port              | Job                                                                                            |
| ------------- | -------------------- | ------------------------- | ---------------------------------------------------------------------------------------------- |
| Static server | `frontend-server.js` | `18966` (`FRONTEND_PORT`) | Serves the built SPA from `dist/`, proxies `/api` to the backend, handles SPA history fallback |
| API server    | `backend-server.js`  | `11966` (`BACKEND_PORT`)  | The Express 5 app — every `/api/*` route, guards, rate limiting, offline datasets              |

`pnpm start` runs both with `concurrently`. In production they are usually managed by pm2 (`ecosystem.config.cjs` defines `myip-frontend` and `myip-backend`) or by the Docker image, which runs `npm start` inside one container and exposes only `18966`.

```mermaid
flowchart LR
    B["Browser"]
    F["frontend-server.js :18966<br/>static dist/ + SPA fallback"]
    A["backend-server.js :11966<br/>Express 5 API"]
    U["Upstream providers<br/>ipinfo.io, ip-api.com, RIPEstat, OONI"]
    D["Local datasets<br/>MaxMind mmdb, CAIDA as2org / as-rel"]

    B -->|"GET / , /tools/whois , /assets/*"| F
    B -->|"GET /api/*"| F
    F -->|"http-proxy-middleware"| A
    A -->|"fetchUpstream, 8s timeout"| U
    A --> D
```

{% hint style="info" %}
Only the frontend port needs to be reachable from the internet. The backend listens on `11966` for the proxy; keep it on the same host or a private network. See [Reverse Proxy and Domains](/developer/getting-started/reverse-proxy-and-domains.md).
{% endhint %}

## Three code layers

| Directory   | Runs where           | Contents                                                                                                               |
| ----------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `frontend/` | Browser              | The Vue 3 SPA — components, router, Pinia store, locales, tool registry                                                |
| `api/`      | Node                 | One Express handler module per route, nothing else                                                                     |
| `common/`   | Node **and** browser | Code shared by both halves: validators, the fetch wrapper, guards, logger, MaxMind / CAIDA services, the report schema |

`common/` is the only layer that crosses the boundary. Modules the browser also needs (`valid-ip.js`, `fetch-with-timeout.js`, `report-schema.js`) stay free of `fs` and `process` access, and are re-exported through thin bridges in `frontend/utils/` so app code keeps importing `@/utils/...`. Node-only concerns that would break that rule live in their own file — for example the upstream User-Agent is built in `common/upstream-ua.js` (which reads `package.json` from disk) and injected into `common/fetch-with-timeout.js` at boot.

## Annotated tree

```
.
├── backend-server.js       Express app: route table, middleware order, cacheable()
├── frontend-server.js      Static server + /api proxy + SPA history fallback
├── sentry-instrument.js    Backend Sentry bootstrap, loaded via `node --import`
├── ecosystem.config.cjs    pm2 process definitions (carries the --import flag)
├── index.html              Vite entry / SPA shell
├── vite.config.js          Build config: aliases, manual chunks, dev proxy
├── Dockerfile              Two-stage build (see below)
│
├── frontend/               Vue 3 SPA  → see Frontend
│   ├── App.vue             Thin shell: global providers + <router-view>
│   ├── main.js             Bootstrap + env-gated dynamic init
│   ├── store.js            Pinia main store
│   ├── router/             Route table
│   ├── data/               Static registries: tools, sections, achievements, IP databases
│   ├── components/         Home / StandaloneTool / sections / advanced-tools / ui
│   ├── composables/        Vue-aware `useXxx` logic
│   ├── utils/              Framework-agnostic helpers (event bus, getips/, …)
│   └── locales/            en / zh / fr / ru + on-demand sub-packs
│
├── api/                    One handler per route  → see Backend
│
├── common/                 Shared code
│   ├── guards.js           Param-validating middleware
│   ├── fetch-with-timeout.js  fetchWithTimeout (5s) / fetchUpstream (8s)
│   ├── logger.js           pino singleton
│   ├── maxmind-service.js  Local GeoLite2 readers + lookup
│   ├── maxmind-updater.js  Scheduled GeoLite2 download
│   ├── caida-updater.js    Scheduled as2org / as-rel download
│   ├── as-org-db.js        CAIDA AS → organization lookup
│   ├── as-rel-db.js        CAIDA AS relationships (p2c graph)
│   ├── service-status-*.js Provider list, poller, response transform
│   ├── maxmind-db/         GeoLite2-City.mmdb · GeoLite2-ASN.mmdb
│   ├── as-org-db/          as-org2info.txt
│   └── as-rel-db/          as-rel2.txt
│
├── tests/                  Node test runner specs (`node --test`)
└── dist/                   Build output (generated, not committed)
```

## How a request flows

**Page load.** The browser asks `frontend-server.js` for a URL.

1. `/api/*` is caught first by the proxy middleware and forwarded to `http://localhost:11966/api`.
2. Otherwise `express.static` tries to serve a real file from `dist/`, applying a `Cache-Control` header per asset class — `dist/assets/**` and `dist/fonts/**` get a year plus `immutable` (Vite content-hashes them), top-level images 7 days, `index.html` and `manifest.webmanifest` zero in browsers but 24h at the edge, everything else an hour.
3. If no file matches, the SPA history fallback returns `index.html` so vue-router can resolve a client route like `/tools/whois`. The fallback is deliberately narrow: `GET` only, `Accept: text/html` only, and never for a path whose last segment contains a dot — a missing `/assets/x.js` must 404, not receive an HTML body.

**API call.** Inside the backend the request passes the middleware chain in `backend-server.js` — optional `pino-http`, rate limiter, slow-down, JSON body parser, the `no-store` default, the global referer guard, then per-route param guards and the handler. The handler makes at most one upstream call, through `fetchUpstream`. Details in [Backend](/developer/architecture/backend.md).

{% hint style="warning" %}
`backend-server.js` also mounts `express.static('./dist')`. That is a convenience for setups that expose the backend directly; the normal path is still browser → frontend server → proxy.
{% endhint %}

## Build pipeline

`pnpm build` runs Vite, which emits `dist/`:

* `@` resolves to `frontend/`.
* `manualChunks` splits heavy dependencies into their own chunks (`vendor` for vue / vue-router / vue-i18n, plus `chart`, `speedtest`, `svgmap`, `browser-detect`) and pulls the IP-source and auth helpers into `utils-getips` / `utils-auth`.
* Fonts land in `dist/fonts/`, everything else content-hashed under `dist/assets/`.
* Source maps are generated only when `SENTRY_AUTH_TOKEN` is set, as `hidden` maps, and are deleted from `dist/` after upload — see [Error Monitoring](/developer/configuration/error-monitoring.md).

Docker builds in two stages. The build stage installs with `pnpm install --frozen-lockfile` and runs `pnpm run build`; the production stage copies only `node_modules`, `package.json`, `dist/`, the two server files, `sentry-instrument.js`, `api/` and `common/`. No toolchain, no install step at runtime. See [Deploy with Docker](/developer/getting-started/deploy-with-docker.md).

## Development mode

`pnpm dev` starts Vite and the backend together. Vite serves the SPA on `FRONTEND_PORT` itself (no `dist/`, no `frontend-server.js`) and proxies `/api` to `BACKEND_PORT` — so the URL shape is identical to production. The backend runs under `nodemon` with `--import ./sentry-instrument.js`, which is a no-op without a backend DSN. Setup details are in [Dev Environment](/developer/development/dev-environment.md).

## Where to make a change

| You want to…           | Go to                                                                                           |
| ---------------------- | ----------------------------------------------------------------------------------------------- |
| Add a tool to the UI   | `frontend/data/tools.js` — see [Adding a New Tool](/developer/development/adding-a-new-tool.md) |
| Add an API route       | A new file in `api/`, wired in `backend-server.js`                                              |
| Add a shared validator | `common/`, plus a bridge in `frontend/utils/` if the browser needs it                           |
| Change copy            | `frontend/locales/` — see [i18n](/developer/development/i18n.md)                                |
| Add a test             | `tests/` — see [Testing](/developer/development/testing.md)                                     |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ipcheck.ing/developer/architecture/project-structure.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
