Surplus logo
Surplus Docsby Sharing Excess

Network & application security

Beyond authentication and authorization, Surplus applies a layer of network- and application-level controls to every request. They're configured centrally in apps/server/src/index.ts.

CORS

The API uses a strict, environment-scoped CORS allowlist. Only requests from approved origins are accepted, and the allowlist tightens as environments get more sensitive:

  • Production — only sharingexcess.com and its subdomains, HTTPS only.
  • Staging*.staging.sharingexcess.com (HTTPS) plus localhost for development.
  • Development — localhost and the *.surplus.dev.sharingexcess.com preview domains.

Credentials (the session cookie) are allowed only for these origins, and a request with no origin is rejected. This is what makes the SameSite=Lax session cookie safe to use for a browser SPA: the cookie isn't sent cross-site, and even if it were, an unapproved origin can't make a credentialed call.

Security headers

Hono's secureHeaders middleware applies hardening headers to every response, including HSTS (max-age=63072000; includeSubDomains; preload) so browsers pin HTTPS for two years.

Intentionally disabled. Content-Security-Policy and Cross-Origin-Embedder-Policy are off in the API header configuration (contentSecurityPolicy: false in apps/server/src/index.ts). Surplus does not enable a default CSP here: the SPA is served separately from the API, a strict policy would require hashing or nonces for inline boot scripts in apps/client/index.html, and Mapbox, Sentry, and API connect-src must be allowlisted without breaking production. Enforcing CSP prematurely is more likely to cause outages than to materially reduce XSS risk given current auth and CORS controls. A client-side, report-only CSP remains a possible future step if the asset graph is inventoried first.

Rate limiting

Rate limiting is backed by Upstash Redis (apps/server/src/helpers/rateLimit.ts) and applied at two levels:

  • Global — 100 requests per 10 seconds per IP, applied to every request. Exceeding it returns 429 with a Retry-After header.
  • Authentication — stricter limits on the OTP endpoints (5 code requests per 15 minutes; 10 validations per 15 minutes), described in Authentication.

Input validation

Every endpoint validates its input and output against Zod schemas declared in the shared contract. Malformed input is rejected at the boundary with a 400 before any handler logic runs, and responses are validated on the way out too. Because the same schemas are shared with the client, validation can't drift between the two sides.

This schema-first approach also closes off classes of injection: queries are built through the Drizzle ORM with parameterized values, never string-concatenated SQL.

Request correlation

Every request carries an x-request-id (generated by the client or the server) that flows through logs and traces and is returned to the client on errors. It contains no sensitive data and exists purely for correlation and support. See Monitoring.

Error handling

Errors are normalized by a central interceptor (apps/server/src/helpers/middleware.ts). Clients receive a clean status, a safe message, and a requestId — never internal stack traces. Full details (stack, context) are logged server-side for operators and surfaced in monitoring, but never returned to the browser.