Skip to content

Network Security

Flowstate’s network architecture is designed to minimize attack surface and protect sensitive communication. Services are isolated, communication is encrypted, and the backend is never directly exposed to the public internet.

The web application uses a server-side proxy pattern for all backend API calls. Browsers never communicate directly with the backend:

Browser → Astro Server (/api/*) → Backend API (internal network)

This architecture provides several critical security benefits.

The backend API URL is never exposed to browsers. Client-side code uses relative URLs:

// Client-side code (runs in browser)
const response = await fetch('/api/rooms', {
credentials: 'include',
});

The Astro server receives the request at /api/rooms and forwards it to the backend on the internal network. The browser never sees the backend URL.

This prevents:

  • Direct attacks: Attackers can’t bypass the proxy and attack the backend directly
  • Information disclosure: The backend URL and port are not visible in network traffic
  • Reconnaissance: Attackers can’t probe the backend to discover its structure

Cross-Origin Resource Sharing (CORS) is a browser security feature that restricts which origins can make requests to an API. Configuring CORS correctly is complex and error-prone.

The proxy pattern eliminates CORS entirely. From the browser’s perspective, all requests go to the same origin (the Astro server). The browser never makes cross-origin requests, so CORS is not involved.

This simplifies the architecture and removes a common source of security misconfigurations.

The Astro proxy validates requests before forwarding them to the backend:

  • Authentication: Checks that the user is authenticated (valid session cookie)
  • Authorization: Verifies the user has permission to access the requested resource
  • Input validation: Validates query parameters and request bodies
  • Rate limiting: Enforces rate limits before the request reaches the backend

If validation fails, the proxy returns an error response without forwarding the request. This reduces load on the backend and prevents invalid requests from reaching business logic.

HTTP headers are classified as end-to-end or hop-by-hop. End-to-end headers are intended for the final recipient (the backend). Hop-by-hop headers are intended for the immediate recipient (the proxy).

The Astro proxy strips hop-by-hop headers before forwarding requests:

  • Connection
  • Keep-Alive
  • Proxy-Authenticate
  • Proxy-Authorization
  • TE
  • Trailer
  • Transfer-Encoding
  • Upgrade

This prevents header-based attacks and ensures the backend only sees headers intended for it.

In production, the backend API is not publicly accessible. It runs on an internal network (VPC, Docker network, or private subnet) and only accepts connections from the Astro server.

This architecture ensures:

  • No direct access: Attackers can’t reach the backend from the internet
  • Reduced attack surface: Only the Astro server is exposed to the public internet
  • Defense in depth: Even if the Astro server is compromised, the backend is still protected by network isolation

The Electron desktop app and future mobile clients use a separate RPC Gateway service. This public-facing service exposes only /rpc and /health endpoints.

The backend REST API is designed for internal use by the web application. It has many endpoints, complex authentication flows, and tight coupling with the Astro server.

Desktop and mobile clients need a simpler, more stable API. The RPC Gateway provides:

  • Stable interface: Connect RPC uses protocol buffers, which are versioned and backward-compatible
  • Reduced attack surface: Only /rpc and /health are exposed (no admin endpoints, no file uploads, etc.)
  • Independent scaling: The gateway can scale independently of the backend
  • Bearer token authentication: Desktop and mobile clients use Authorization headers, not cookies

The RPC Gateway is a separate service that runs the same backend codebase with a different entrypoint:

Electron/Mobile → RPC Gateway (/rpc) → Backend (internal network)

The gateway:

  1. Receives Connect RPC requests over HTTPS
  2. Validates the Bearer token in the Authorization header
  3. Forwards the request to the backend’s internal RPC handler
  4. Returns the response to the client

The gateway does not have direct database access. All data operations go through the backend’s internal services. This ensures business logic is centralized and consistent across all clients.

The RPC Gateway allows all origins (Access-Control-Allow-Origin: *). This is safe because:

  • Bearer token authentication: Requests must include a valid token in the Authorization header
  • No cookies: The gateway doesn’t use cookies, so CSRF attacks are not possible
  • Read-only for most operations: Most RPC calls are read-only (fetching data, not modifying it)

Allowing all origins simplifies client development. Desktop and mobile apps can make requests without CORS preflight checks.

All services communicate via an internal network, not public IP addresses. Only the web server and RPC gateway are exposed to external traffic. The backend API, database, and cache are accessible only within the internal network.

In production, services run in a VPC (Virtual Private Cloud) or equivalent network isolation:

  • Web server: Public subnet, accessible from the internet
  • RPC gateway: Public subnet, accessible from the internet
  • Backend: Private subnet, only accessible from web server and gateway
  • Database: Private subnet, only accessible from backend
  • Redis: Private subnet, only accessible from backend

Network ACLs (Access Control Lists) enforce these restrictions at the network layer. Even if an attacker compromises the web server, they can’t directly access the database.

Server-Sent Events (SSE) provide real-time updates from the backend to clients. SSE connections are long-lived HTTP connections that remain open for minutes or hours.

SSE connections are authenticated:

  • Web clients: Cookie authentication (same as regular API requests)
  • Desktop clients: Bearer token in Authorization header

The backend validates authentication when the SSE connection is established. If authentication fails, the connection is rejected.

SSE connections can be interrupted by network issues, proxy timeouts, or server restarts. Clients automatically reconnect when the connection is lost.

The backend sends periodic heartbeat messages to keep the connection alive:

: heartbeat

If the client doesn’t receive a heartbeat within 30 seconds, it assumes the connection is dead and reconnects.

SSE events are filtered by user. Each user only receives events relevant to them:

  • Canvas updates: Only updates to blocks the user owns
  • Room messages: Only messages in rooms the user is a member of
  • Notifications: Only notifications addressed to the user

The backend maintains a mapping of user ID to SSE connection. When an event occurs, the backend looks up the relevant users and sends the event to their connections.

All services expose health check endpoints for monitoring:

ServiceEndpointReturns
Backend/health{ status: "ok", database: "connected", redis: "connected" }
RPC Gateway/health{ status: "ok" }
Web/health{ status: "ok" }

Health checks are exempt from rate limiting to ensure monitoring systems can always verify service availability.

Backend health checks verify:

  • Database connectivity: Attempts a simple query to MongoDB
  • Redis connectivity: Attempts a PING command to Redis
  • Service status: Returns 200 OK if all checks pass, 503 Service Unavailable if any check fails

Monitoring systems (like Kubernetes liveness probes or AWS health checks) use these endpoints to detect and restart unhealthy services.

All production cookies use strict security settings:

res.cookie('session', token, {
httpOnly: true, // Prevents JavaScript access
secure: true, // HTTPS-only
sameSite: 'lax', // CSRF protection
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
});

The httpOnly flag prevents JavaScript from accessing the cookie. This stops XSS (Cross-Site Scripting) attacks from stealing session tokens:

// This returns undefined for httpOnly cookies
document.cookie;

Even if an attacker injects malicious JavaScript into the page, they can’t read the session cookie.

The secure flag ensures cookies only transmit over HTTPS connections. This prevents man-in-the-middle attacks from intercepting session tokens.

If an attacker downgrades the connection to HTTP (for example, via a rogue Wi-Fi hotspot), the browser won’t send the cookie.

The sameSite flag protects against CSRF (Cross-Site Request Forgery) attacks. With sameSite=lax, cookies are only sent for:

  • Same-site requests: Requests from the same origin
  • Top-level navigation: Clicking a link to the site

Cookies are not sent for:

  • Cross-site POST requests: Forms submitted from other sites
  • Cross-site AJAX requests: Fetch or XMLHttpRequest from other sites

This prevents attackers from tricking users into making authenticated requests from malicious sites.

All external connections use encryption:

  • HTTPS: Web and RPC traffic is encrypted with TLS 1.2 or higher
  • MongoDB: Connections use TLS in production (optional in development)
  • Redis: Connections use TLS in production (optional in development)
  • S3: File uploads and downloads use HTTPS

Production TLS configuration:

  • Minimum version: TLS 1.2 (TLS 1.0 and 1.1 are disabled)
  • Cipher suites: Only strong ciphers are allowed (no RC4, no 3DES)
  • Certificate validation: Certificates must be valid and not expired
  • HSTS: HTTP Strict Transport Security header forces HTTPS

HSTS tells browsers to always use HTTPS, even if the user types http:// in the address bar. This prevents downgrade attacks.

Redis connections use a retry strategy to handle transient failures:

const redis = new Redis({
host: REDIS_HOST,
port: REDIS_PORT,
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
return delay;
},
});

If Redis is unavailable, the client retries with exponential backoff (50ms, 100ms, 150ms, …, up to 2000ms). This handles:

  • Temporary network issues: Brief connection drops
  • Redis restarts: Service restarts during deployments
  • Failover: Redis switching to a replica during primary failure

If Redis remains unavailable, rate limiting falls back to in-memory storage (per-instance, not shared). This ensures the application remains functional even if Redis is down.

Production MongoDB uses a replica set for data integrity and high availability:

  • Primary: Handles all writes
  • Secondaries: Replicate data from the primary
  • Automatic failover: If the primary fails, a secondary is promoted

Replica sets ensure:

  • Data durability: Writes are replicated to multiple nodes before being acknowledged
  • High availability: If one node fails, the others continue serving requests
  • Read scaling: Read queries can be distributed across secondaries

The backend connects to the replica set using a connection string that lists all nodes and specifies the replica set name.

The MongoDB driver automatically discovers the primary and routes writes to it.

Flowstate’s network security measures include:

  • Server-side API proxy (backend URL never exposed to browsers)
  • CORS eliminated (all requests are same-origin)
  • Request validation (authentication, authorization, input validation)
  • Hop-by-hop headers stripped (prevents header-based attacks)
  • Production backend is internal-only (not publicly accessible)
  • RPC Gateway (separate public-facing service for desktop/mobile)
  • Network isolation (services communicate via internal network only)
  • SSE authentication (cookie or Bearer token required)
  • Health check endpoints (monitoring without authentication)
  • httpOnly cookies (prevents XSS token theft)
  • secure cookies (HTTPS-only transmission)
  • sameSite cookies (CSRF protection)
  • TLS encryption (all external connections use HTTPS)
  • Redis retry strategy (handles transient failures)
  • MongoDB replica set (data durability and high availability)

Planned improvements:

  • Web Application Firewall (WAF): Block common attack patterns (SQL injection, XSS, etc.)
  • DDoS protection: Cloudflare or AWS Shield to absorb large-scale attacks
  • IP allowlisting: Restrict admin access to specific IP ranges
  • VPN access: Require VPN for internal services in production
  • mTLS: Mutual TLS for service-to-service authentication
  • Network segmentation: Separate networks for different service tiers
  • Intrusion detection: Monitor network traffic for suspicious patterns

Network security is a continuous process. The platform evolves to address new threats and incorporate industry best practices.