Enterprise Trading Platform · architecture diagrams

Auth service

NestJS 11, TypeScript, port 3000. Four routes, one credential store, and the only place in the platform where a password exists in a readable form.

Identity is concentrated here on purpose. Every other service verifies a signature and reads claims, which is cheap and needs no shared state. Only this service hashes a password, only this service holds the hash, and only this service signs. If a team spreads that responsibility across two services, both of them now need the credential store and both of them are now a place a password can leak.

Registration returns no tokens. The client logs in afterwards, so that an unauthenticated route cannot mint a session. Refresh consumes the presented token and issues a new pair, and presenting a consumed token is treated as theft: every refresh token for that user is revoked.

Internal structure of the auth service The four routes of the auth contract on the left. Six modules in the centre: auth, users, tokens, database, common and config. On the right, the signed token pair returned to the caller and the PostgreSQL schema that holds users and refresh tokens. INBOUND OUTBOUND contracts/auth-api.yaml POST /auth/register POST /auth/login POST /auth/refresh GET /auth/me Only /auth/me requires a token. The other three are open by necessity, which is why login is the one route that is throttled. The Node auth stub answers the same four routes with the same claims in Sprints 6 and 7. Swapping it for this service is a change of base URL, nothing more. Auth service :3000 NestJS 11 · TypeScript · argon2 or bcrypt · Jest · Swagger at /docs src/ auth The four routes. One service call each. AuthController · AuthService · guards/JwtAuthGuard dto: LoginDto, RefreshDto, TokenResponseDto ThrottlerGuard on /auth/login only: five attempts a minute per address every authentication failure returns the same AUTH-401 body, whatever its cause users The only place a password is readable. UsersService · UsersRepository · PasswordService · demo-user.seeder.ts dto: RegisterDto, UserResponseDto · user.entity.ts · role.enum.ts · users.errors.ts argon2 or bcrypt at rest. No password is logged at any level, ever. tokens Signs, verifies, rotates. TokenService · RefreshTokenRepository · token-claims.ts HS256 · access token 900 seconds · refresh token 604800 seconds database Its own schema, in the shared instance. DatabaseModule · DatabaseBootstrapService · database.constants.ts migrations at boot when AUTH_RUN_MIGRATIONS is set · one pool, sized by PGPOOL_MAX common Cross-cutting. One error shape for all routes. errors/AuthExceptionFilter · error-codes.ts · error-response.dto.ts decorators/CurrentUser · logging/json.logger.ts, one JSON object per line config Validated at boot, not at first request. configuration.ts refuses to start on a missing or short JWT_SECRET JWT · HS256 an access and refresh pair, verified by every other service PostgreSQL its own schema: users and refresh tokens, nothing else This service never reads an account, an order or a position. It links a user to an existing trading account and stops there. ENVIRONMENT PORT · NODE_ENV · JWT_SECRET · JWT_ISSUER · ACCESS_TOKEN_TTL_SECONDS · REFRESH_TOKEN_TTL_SECONDS · DATABASE_URL PGHOST · PGPORT · PGUSER · PGPASSWORD · PGDATABASE · PGPOOL_MAX · AUTH_RUN_MIGRATIONS · AUTH_SEED_DEMO_USERS · LOGIN_THROTTLE_TTL_SECONDS · LOGIN_THROTTLE_LIMIT
Module names are the folders under src/. The token pair is drawn as an output because it is the thing the rest of the platform depends on, even though it leaves in an HTTP response.

The four routes

RouteGuardReturnsNotes
POST /auth/registernone201 with the userLinks a user to an existing trading account. It does not create the account, and it issues no tokens.
POST /auth/loginThrottlerGuard200 with a token pairEvery failure returns the same AUTH-401 body. Repeated failures from one address return 429.
POST /auth/refreshnone200 with a new pairThe presented token stops working immediately. Presenting a consumed token revokes every refresh token for that user.
GET /auth/meJwtAuthGuard200 with the userIdentity comes from the verified token, never from a query parameter or a client-controlled header.

Why throttling sits on one route

Five attempts a minute per address does not inconvenience someone who mistyped a password, and it does stop an unattended script working through a password list. It is a blunt control: an attacker with many addresses is unaffected, which is why it sits alongside argon2 rather than instead of it.

The guard is applied to login only. A global throttler would also cap /auth/me, which the frontend calls on every page load, and the first symptom would be a dashboard that intermittently logs the user out.

The claims contract

The Trade REST API verifies the signature with the same JWT_SECRET and reads the claims. It does not call this service to check a token, because a network call per request would make identity an availability dependency for trading. Configure the issuer check to accept any issuer, so that swapping the Node auth stub for this service in Sprint 8 is a configuration change and not a code change.