diff --git a/README.md b/README.md
index c0df0e3..5a1d17a 100644
--- a/README.md
+++ b/README.md
@@ -17,18 +17,19 @@ Monorepo Nx listo para producciΓ³n que incluye autenticaciΓ³n JWT con rotaciΓ³n
### π― **Stack TecnolΓ³gico**
- **Frontend**: Angular 21 con standalone components, Signals API y control flow nativo (`@if` / `@for` / `@switch`)
-- **Gateway**: Express 5 + `http-proxy-middleware` β ΓΊnico servicio expuesto a Internet, gestiona tokens y CORS
-- **API**: Express 5 + Sequelize, vive en red privada, expone CRUDs y endpoints `/internal/*` para el gateway
+- **Nginx (front)**: sirve la SPA y es la puerta pΓΊblica; hace reverse-proxy de `/api/*` al gateway (mismo origen)
+- **Gateway**: Express 5 + `http-proxy-middleware` β privado (`internal-network`), detrΓ‘s de nginx; gestiona tokens y CORS
+- **API**: Express 5 + Sequelize, privado (`internal-network`), expone CRUDs y endpoints `/internal/*` para el gateway
- **Base de datos**: PostgreSQL 16
- **Monorepo**: Nx 22 para gestiΓ³n eficiente
- **Build System**: esbuild (backend) + Vite (frontend)
-- **ContainerizaciΓ³n**: Docker + Docker Compose con redes `edge-network` e `internal-network`
+- **ContainerizaciΓ³n**: Docker + Docker Compose; sΓ³lo `front` (nginx) se expone, el resto vive en `internal-network` (`internal: true`)
- **UI**: Bootstrap 5 + NgBootstrap
- **i18n**: Transloco (EspaΓ±ol/Valenciano/InglΓ©s)
### π **AutenticaciΓ³n & Seguridad** β ver [`docs/SECURITY.md`](docs/SECURITY.md)
-- Arquitectura de microservicios: **gateway** pΓΊblico + **api** privado. El api nunca habla con el cliente directamente; el gateway firma un JWT interno EdDSA antes de proxiar
+- Arquitectura de microservicios: **nginx** (front) como puerta pΓΊblica β **gateway** (auth) β **api** privado. El api nunca habla con el cliente directamente; el gateway firma un JWT interno EdDSA antes de proxiar
- JWT del cliente con **dos secretos separados** (`JWT_ACCESS_SECRET`, `JWT_REFRESH_SECRET`), claims `typ` y `jti`
- **RotaciΓ³n de refresh con detecciΓ³n de reuso**: tabla `refresh_token_family` revoca la familia completa si una cookie ya rotada se vuelve a presentar
- **JWT interno Ed25519** entre gateway y api: el gateway tiene la clave privada (firma), el api sΓ³lo la pΓΊblica (verifica). Privilegio separado: un api comprometido no puede emitir tokens
@@ -47,14 +48,30 @@ Monorepo Nx listo para producciΓ³n que incluye autenticaciΓ³n JWT con rotaciΓ³n
### ποΈ **Arquitectura**
-```
-ββββββββββββ cookie+Authorization ββββββββββββ X-Internal-Auth (EdDSA) βββββββ
-β Cliente β ββββββββββββββββββββββββββΆβ Gateway β βββββββββββββββββββββββββββΆ β API β
-ββββββββββββ ββββββββββββ βββββββ
- (pΓΊblico) (privada)
- :3100 :3200
+```mermaid
+flowchart LR
+ Browser(["π Navegador"])
+
+ subgraph priv ["π internal-network Β· internal: true (sin salida a Internet)"]
+ direction LR
+ Nginx["Nginx Β· front
SPA Angular + proxy /api/*"]
+ Gateway["Gateway Β· :3100
auth JWT cliente
firma JWT interno EdDSA"]
+ API["API Β· :3200
Express 5 + Sequelize"]
+ DB[("PostgreSQL 16")]
+
+ Nginx -->|"proxy_pass /api/"| Gateway
+ Gateway -->|"X-Internal-Auth Β· EdDSA"| API
+ API --> DB
+ end
+
+ Browser ==>|"HTTPS Β· cookie + Authorization
ΓΊnica puerta pΓΊblica"| Nginx
+
+ classDef public fill:#1f6feb,stroke:#0b3d91,color:#fff;
+ class Nginx public;
```
+- **Nginx (contenedor `front`)** es la **ΓΊnica puerta pΓΊblica**: sirve la SPA y hace reverse-proxy de `/api/*` al gateway (mismo origen β las cookies viajan sin CORS). El navegador nunca habla con el gateway directamente.
+- **Gateway**, **API** y **PostgreSQL** viven todos en `internal-network` (`internal: true`), sin entrada desde Internet. El gateway autentica el JWT del cliente, firma el JWT interno EdDSA y proxia al api privado.
- PatrΓ³n Controller-Service-Repository en el api
- DTOs compartidos entre frontend y backend en `libs/rest-dto`
- Contrato interno gatewayβapi en `libs/internal-auth` (Ed25519 + scopes)
@@ -95,7 +112,7 @@ npm run dev
### Acceso a la AplicaciΓ³n
- **Frontend**: http://localhost:4200
-- **Gateway (pΓΊblico)**: http://localhost:3100/api/v1/
+- **Gateway (API del cliente)**: http://localhost:3100/api/v1/ (en dev el front la consume vΓa proxy de Vite; en docker, tras nginx)
- **API (privado)**: http://localhost:3200 (sΓ³lo accesible vΓa gateway en docker)
- **Base de datos**: localhost:5432
@@ -170,7 +187,7 @@ nx-fullstack-starter/
β β β βββ libs/auth/ # MΓ³dulo de autenticaciΓ³n (service, guards)
β β β βββ services/ # Servicios de negocio
β β βββ src/assets/i18n/ # Archivos de traducciΓ³n
-β βββ gateway/ # Servicio pΓΊblico (Express + http-proxy-middleware)
+β βββ gateway/ # Servicio de auth + proxy, privado tras nginx (Express + http-proxy-middleware)
β β βββ src/
β β βββ controllers/ # auth.controller (login/logout)
β β βββ middleware/ # hasPermission, refresh rotation
@@ -245,7 +262,7 @@ Especialista en diseΓ±o de esquemas PostgreSQL y MongoDB, migraciones sin downti
#### π§ Backend Developer
-Especialista en Express + Sequelize siguiendo arquitectura de 4 capas: Routes β Controllers β Services β Models. Trabaja sobre `apps/api` (lΓ³gica de negocio) y `apps/gateway` (auth pΓΊblico, proxy).
+Especialista en Express + Sequelize siguiendo arquitectura de 4 capas: Routes β Controllers β Services β Models. Trabaja sobre `apps/api` (lΓ³gica de negocio) y `apps/gateway` (auth de cliente, proxy).
- Patrones `AbstractCrudService` / `AbstractCrudController` para minimizar boilerplate
- Todas las respuestas HTTP a travΓ©s de `HttpResponser` (nunca `res.json()` directo)
@@ -419,7 +436,7 @@ npm run build
docker compose --env-file .env up -d
```
-> SΓ³lo `gateway` y `front` exponen puertos al exterior. `api` y `postgresdb` viven en `internal-network` con `internal: true`.
+> SΓ³lo `front` (nginx) se expone al exterior. `gateway`, `api` y `postgresdb` viven en `internal-network` con `internal: true` y no son accesibles desde Internet.
### π **InternacionalizaciΓ³n**
diff --git a/apps/gateway/AGENTS.md b/apps/gateway/AGENTS.md
index 93e2f1d..bf89628 100644
--- a/apps/gateway/AGENTS.md
+++ b/apps/gateway/AGENTS.md
@@ -3,8 +3,10 @@
> Layer-specific rules for the gateway. See the root `AGENTS.md` for global
> conventions and path aliases.
-The gateway is the **only service exposed to the public**. It owns the public-facing
-auth surface, then proxies authorized requests to the internal API.
+The gateway owns the **client-facing auth surface**, but it is **not exposed to the
+Internet**: it lives on `internal-network` (`internal: true`) and sits behind Nginx (the
+`front` container reverse-proxies `/api/*` to it). It then proxies authorized requests to
+the internal API on the same private network.
## Responsibilities
@@ -21,11 +23,11 @@ auth surface, then proxies authorized requests to the internal API.
## Request flow (must stay intact)
```
-client ββaccess tokenβββΆ gateway
- β hasPermission() β res.locals.user
- β signUserContext() β internal EdDSA JWT
- βΌ
- API /v1/* (verifies internal token, not the public one)
+browser ββ/api/*βββΆ nginx ββproxy_passβββΆ gateway
+ β hasPermission() β res.locals.user
+ β signUserContext() β internal EdDSA JWT
+ βΌ
+ API /v1/* (verifies internal token, not the public one)
```
## Hard rules
diff --git a/compose.yaml b/compose.yaml
index 768459d..ac45cb3 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -64,10 +64,7 @@ services:
- INTERNAL_JWT_PRIVATE_KEY=${INTERNAL_JWT_PRIVATE_KEY}
- CORS_ORIGIN=${CORS_ORIGIN}
- SERVICE_FQDN_GATEWAY=${SERVICE_FQDN_GATEWAY}
- ports:
- - '${GATEWAY_PORT:-3100}'
networks:
- - edge-network
- internal-network
front:
@@ -84,6 +81,7 @@ services:
- '${FRONT_PORT:-80}'
networks:
- edge-network
+ - internal-network
volumes:
starter-postgresdb:
diff --git a/docs/README_eng.md b/docs/README_eng.md
index 86ffea3..3c30dd7 100644
--- a/docs/README_eng.md
+++ b/docs/README_eng.md
@@ -17,18 +17,19 @@ Production-ready Nx monorepo including JWT authentication with refresh rotation,
### π― **Tech Stack**
- **Frontend**: Angular 21 with standalone components, Signals API and native control flow (`@if` / `@for` / `@switch`)
-- **Gateway**: Express 5 + `http-proxy-middleware` β the only service exposed to the Internet, handles tokens and CORS
-- **API**: Express 5 + Sequelize, lives in a private network, exposes CRUDs and `/internal/*` endpoints used by the gateway
+- **Nginx (front)**: serves the SPA and is the public door; reverse-proxies `/api/*` to the gateway (same origin)
+- **Gateway**: Express 5 + `http-proxy-middleware` β private (`internal-network`), behind nginx; handles tokens and CORS
+- **API**: Express 5 + Sequelize, private (`internal-network`), exposes CRUDs and `/internal/*` endpoints used by the gateway
- **Database**: PostgreSQL 16
- **Monorepo**: Nx 22 for efficient management
- **Build System**: esbuild (backend) + Vite (frontend)
-- **Containerisation**: Docker + Docker Compose with split `edge-network` and `internal-network`
+- **Containerisation**: Docker + Docker Compose; only `front` (nginx) is exposed, everything else lives on `internal-network` (`internal: true`)
- **UI**: Bootstrap 5 + NgBootstrap
- **i18n**: Transloco (Spanish / Valencian / English)
### π **Authentication & Security** β see [`SECURITY.md`](SECURITY.md)
-- Microservices architecture: public **gateway** + private **api**. The api never talks directly to clients; the gateway signs a short-lived EdDSA internal JWT before proxying.
+- Microservices architecture: **nginx** (front) as the public door β **gateway** (auth) β private **api**. The api never talks directly to clients; the gateway signs a short-lived EdDSA internal JWT before proxying.
- Client JWTs with **two separate secrets** (`JWT_ACCESS_SECRET`, `JWT_REFRESH_SECRET`), plus `typ` and `jti` claims.
- **Refresh rotation with reuse detection**: the `refresh_token_family` table revokes the whole family when an already-rotated cookie is replayed.
- **Internal Ed25519 JWT** between gateway and api: the gateway holds the private key (signs), the api only the public key (verifies). Privilege separation: a compromised api cannot mint tokens.
@@ -47,14 +48,30 @@ Production-ready Nx monorepo including JWT authentication with refresh rotation,
### ποΈ **Architecture**
-```
-ββββββββββββ cookie+Authorization ββββββββββββ X-Internal-Auth (EdDSA) βββββββ
-β Client β ββββββββββββββββββββββββββΆβ Gateway β βββββββββββββββββββββββββββΆ β API β
-ββββββββββββ ββββββββββββ βββββββ
- (public) (private)
- :3100 :3200
+```mermaid
+flowchart LR
+ Browser(["π Browser"])
+
+ subgraph priv ["π internal-network Β· internal: true (no Internet access)"]
+ direction LR
+ Nginx["Nginx Β· front
Angular SPA + proxy /api/*"]
+ Gateway["Gateway Β· :3100
client JWT auth
signs internal EdDSA JWT"]
+ API["API Β· :3200
Express 5 + Sequelize"]
+ DB[("PostgreSQL 16")]
+
+ Nginx -->|"proxy_pass /api/"| Gateway
+ Gateway -->|"X-Internal-Auth Β· EdDSA"| API
+ API --> DB
+ end
+
+ Browser ==>|"HTTPS Β· cookie + Authorization
the only public door"| Nginx
+
+ classDef public fill:#1f6feb,stroke:#0b3d91,color:#fff;
+ class Nginx public;
```
+- **Nginx (the `front` container)** is the **only public door**: it serves the SPA and reverse-proxies `/api/*` to the gateway (same origin β cookies travel without CORS). The browser never talks to the gateway directly.
+- **Gateway**, **API** and **PostgreSQL** all live on `internal-network` (`internal: true`), with no inbound from the Internet. The gateway authenticates the client JWT, signs the internal EdDSA JWT and proxies to the private api.
- Controller-Service-Repository pattern inside the api
- Shared DTOs between frontend and backend in `libs/rest-dto`
- Internal gatewayβapi contract in `libs/internal-auth` (Ed25519 + scopes)
@@ -94,7 +111,7 @@ npm run dev
### Application Access
- **Frontend**: http://localhost:4200
-- **Gateway (public)**: http://localhost:3100/api/v1/
+- **Gateway (client API)**: http://localhost:3100/api/v1/ (in dev the front consumes it via the Vite proxy; under docker, behind nginx)
- **API (private)**: http://localhost:3200 (only reachable via the gateway under docker)
- **Database**: localhost:5432
@@ -169,7 +186,7 @@ nx-fullstack-starter/
β β β βββ libs/auth/ # Authentication module (service, guards)
β β β βββ services/ # Business services
β β βββ src/assets/i18n/ # Translation files
-β βββ gateway/ # Public service (Express + http-proxy-middleware)
+β βββ gateway/ # Auth + proxy service, private behind nginx (Express + http-proxy-middleware)
β β βββ src/
β β βββ controllers/ # auth.controller (login/logout)
β β βββ middleware/ # hasPermission, refresh rotation
@@ -244,7 +261,7 @@ Expert in PostgreSQL and MongoDB schema design, zero-downtime migrations, indexi
#### π§ Backend Developer
-Expert in Express + Sequelize following a 4-layer architecture: Routes β Controllers β Services β Models. Works across `apps/api` (business logic) and `apps/gateway` (public auth, proxy).
+Expert in Express + Sequelize following a 4-layer architecture: Routes β Controllers β Services β Models. Works across `apps/api` (business logic) and `apps/gateway` (client auth, proxy).
- `AbstractCrudService` / `AbstractCrudController` patterns to minimise boilerplate
- All HTTP responses through `HttpResponser` (never bare `res.json()`)
@@ -418,7 +435,7 @@ npm run build
docker compose --env-file .env up -d
```
-> Only `gateway` and `front` expose ports to the host. `api` and `postgresdb` live on `internal-network` with `internal: true`.
+> Only `front` (nginx) is exposed. `gateway`, `api` and `postgresdb` live on `internal-network` with `internal: true` and are not reachable from the Internet.
### π **Internationalisation**
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
index cfe3412..a6f7c97 100644
--- a/docs/SECURITY.md
+++ b/docs/SECURITY.md
@@ -5,14 +5,34 @@ pasos manuales obligatorios antes de desplegar.
## Modelo de confianza
-```
-ββββββββββββ cookie+Authorization ββββββββββββ X-Internal-Auth (EdDSA) βββββββ
-β Cliente β ββββββββββββββββββββββββββββΆβ Gateway β ββββββββββββββββββββββββββββΆ β API β
-ββββββββββββ ββββββββββββ βββββββ
- (privada) (pΓΊblica)
+```mermaid
+flowchart LR
+ Client(["π Cliente"])
+
+ subgraph priv ["π internal-network Β· internal: true"]
+ direction LR
+ Nginx["Nginx Β· front
SPA + proxy /api/*"]
+ Gateway["Gateway
JWT_ACCESS_SECRET / JWT_REFRESH_SECRET (HS256)
INTERNAL_JWT_PRIVATE_KEY (Ed25519, firma)"]
+ API["API
INTERNAL_JWT_PUBLIC_KEY (Ed25519, sΓ³lo verifica)"]
+ DB[("PostgreSQL")]
+
+ Nginx -->|"proxy_pass /api/"| Gateway
+ Gateway -->|"X-Internal-Auth Β· EdDSA"| API
+ API --> DB
+ end
+
+ Client ==>|"cookie + Authorization
ΓΊnica puerta pΓΊblica"| Nginx
+
+ classDef public fill:#1f6feb,stroke:#0b3d91,color:#fff;
+ class Nginx public;
```
-- **Gateway** es el ΓΊnico servicio expuesto a Internet. Posee:
+- **Nginx** (contenedor `front`) es la puerta pΓΊblica: sirve la SPA y hace
+ reverse-proxy de `/api/*` al gateway (mismo origen, para que las cookies
+ viajen sin CORS). El cliente nunca contacta al gateway directamente.
+- **Gateway** vive en `internal-network` (privado, sin entrada desde Internet) β
+ es el servicio que firma los tokens de cara al cliente y proxia hacia el api
+ privado. Posee:
- `JWT_ACCESS_SECRET` y `JWT_REFRESH_SECRET` (HS256) para los tokens
del cliente.
- `INTERNAL_JWT_PRIVATE_KEY` (Ed25519) para firmar las llamadas que