A missing Sentry DSN meant "no error tracking configured yet," not
"the API should refuse to start" — but env validation treated it as
the latter, hard-failing startup in production if it wasn't set.
Sentry initialization was already conditional on SENTRY_DSN being
present (src/main.ts), so this only affects the boot-time validation
gate, not whether Sentry actually turns on when configured.
PushSubscription.toJSON() includes an expirationTime field (often
null, sometimes a number for renewable subscriptions), but
SavePushSubscriptionDto didn't declare it, so saving a push
subscription 400'd under the whitelist validation pipe whenever the
browser included it.
Session IP and user-agent were enforced as an exact-match gate on
every authenticated request and on refresh: any mismatch threw
Invalid session binding / Session binding mismatch and forced a
logout. In production this made the app nearly unusable, because
real users' IPs change constantly and normally in ways that have
nothing to do with account theft — mobile network handovers,
corporate NAT pools, VPNs, CDN edge changes, Wi-Fi/cellular
switching.
The still-enforced single-use rotating session nonce plus
refresh-token rotation and revocation already prevent a stolen
token from being replayed on another device, so the hard IP/UA gate
was mostly adding false-positive lockouts rather than real security.
Now IP and user-agent changes are recorded as a SESSION_BINDING_DRIFT
abuse event (feeding the existing risk-scoring system) instead of
rejecting the request, so the security signal isn't lost, it's just
no longer a blanket block on legitimate use.
Without Express's trust-proxy setting, req.ip resolves to the load
balancer's own connecting IP for every request in production, not
the real client IP. That silently broke per-IP rate limiting: every
user behind the same proxy shared one 100-requests-per-60-seconds
throttler bucket instead of getting their own, so any real traffic
volume (the dashboard alone fires ~6 requests per load) could 429
everyone, including on login/registration.
The register form collects a full name, but RegisterDto rejected it as
an unknown field under the global whitelist validation pipe, so
registration always failed 400 whenever a name was entered.