Security architecture, threat mitigations, and best practices for the Crypto Vision platform.
- Security Architecture
- Transport Security
- Authentication
- Rate Limiting
- Input Validation
- Output Security
- HTTP Security Headers
- CORS Policy
- Secret Management
- Error Redaction
- Dependency Security
- Deployment Security
Client Request
│
▼
┌────────────────────────────────────────────────────┐
│ Layer 1: Transport (TLS) │
│ • HTTPS enforced in production │
│ • HSTS with 1-year max-age │
└────────────────────┬───────────────────────────────┘
│
┌────────────────────▼───────────────────────────────┐
│ Layer 2: HTTP Security Headers │
│ • CSP, X-Frame-Options, X-Content-Type-Options │
│ • Referrer-Policy, Permissions-Policy │
└────────────────────┬───────────────────────────────┘
│
┌────────────────────▼───────────────────────────────┐
│ Layer 3: Body Size Limiting (256KB) │
│ • Prevents memory exhaustion attacks │
└────────────────────┬───────────────────────────────┘
│
┌────────────────────▼───────────────────────────────┐
│ Layer 4: CORS Validation │
│ • Whitelist in production, open in development │
└────────────────────┬───────────────────────────────┘
│
┌────────────────────▼───────────────────────────────┐
│ Layer 5: Rate Limiting (sliding window) │
│ • Per-IP for anonymous, per-key for authenticated │
│ • Redis backend with Lua atomic operations │
└────────────────────┬───────────────────────────────┘
│
┌────────────────────▼───────────────────────────────┐
│ Layer 6: Authentication (API Key) │
│ • Timing-safe comparison to prevent timing attacks│
│ • Tier assignment for quota enforcement │
└────────────────────┬───────────────────────────────┘
│
┌────────────────────▼───────────────────────────────┐
│ Layer 7: Input Validation (Zod schemas) │
│ • Type coercion, range checks, pattern matching │
│ • Sanitization of string inputs │
└────────────────────┬───────────────────────────────┘
│
┌────────────────────▼───────────────────────────────┐
│ Layer 8: Output Security │
│ • Source obfuscation (upstream names hidden) │
│ • Error detail redaction in production │
│ • Response envelope normalization │
└────────────────────────────────────────────────────┘
In production, all traffic must use HTTPS. This is enforced at the infrastructure level:
- Cloud Run / GKE — TLS termination at the load balancer
- Self-hosted — TLS via reverse proxy (Nginx, Caddy, Traefik)
The secureHeaders() middleware sets:
Strict-Transport-Security: max-age=31536000; includeSubDomains
This instructs browsers to always use HTTPS for the domain for 1 year, preventing protocol downgrade attacks.
See API Authentication for the full guide.
Key security properties:
- Timing-safe comparison — API keys are compared using constant-time comparison (
crypto.timingSafeEqual) to prevent timing side-channel attacks - Key hashing — Keys stored in Redis can be configured to store only hashes, preventing exposure if Redis is compromised
- Tier isolation — Each API key is assigned a tier that determines rate limits and feature access
- Admin separation — Admin keys are stored in a separate environment variable (
ADMIN_API_KEYS) and are never mixed with regular keys
Sliding-window rate limiting with dual backend:
- Redis (distributed) — Uses an atomic Lua script for
INCR→PEXPIREin a single round trip, preventing race conditions - In-memory (fallback) —
Map<key, {count, windowStart}>with periodic cleanup when Redis is unavailable
| Tier | Limit | Window | Key Required |
|---|---|---|---|
public |
30 | 60s | No |
basic |
200 | 60s | Yes |
pro |
2,000 | 60s | Yes |
enterprise |
10,000 | 60s | Yes |
Rate limit status is communicated via standard HTTP headers:
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 25
X-RateLimit-Reset: 1709568060
On limit exceeded (429):
Retry-After: 45
Rate limiting alone doesn't prevent DDoS attacks. In production, use additional layers:
- Cloud Armor / Cloudflare — Edge-level DDoS protection
- IP-based rate limiting — Applied before API key check
- Body size limit — 256KB prevents memory exhaustion from large payloads
All request parameters (query strings, path parameters, request bodies) are validated using Zod schemas before processing:
const QuerySchema = z.object({
coinId: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),
vs_currency: z.enum(['usd', 'eur', 'gbp', 'btc', 'eth']).default('usd'),
days: z.coerce.number().int().min(1).max(365).default(30),
limit: z.coerce.number().int().min(1).max(250).default(100),
});Validation patterns used throughout:
| Pattern | Purpose |
|---|---|
z.string().min(1).max(N) |
Bounded string length |
z.string().regex(/^[a-z0-9-]+$/) |
Alphanumeric + hyphen only (coin IDs) |
z.coerce.number().int().min(1).max(N) |
Bounded integers |
z.enum([...]) |
Closed set of allowed values |
z.string().trim() |
Whitespace stripping |
z.string().toLowerCase() |
Case normalization |
Any user-supplied path segments are sanitized to prevent directory traversal:
// Blocked patterns: ../../, %2e%2e%2f, ..%5c
const sanitized = input.replace(/\.\.[/\\]/g, '');All database queries use Drizzle ORM with parameterized queries. No raw SQL strings are constructed from user input.
Upstream provider names (CoinGecko, DeFiLlama, Binance, etc.) are never exposed in error responses to clients. This prevents attackers from discovering the exact upstream services and targeting them directly.
The response-envelope.ts module maps path segments to generic source names for the meta.source field, but error details about specific upstream failures are sanitized before reaching the client.
Error responses follow a consistent format that never includes:
- Internal stack traces
- Database query details
- Upstream API URLs or headers
- Configuration values or secrets
- File paths or line numbers
Production error response:
{
"success": false,
"error": {
"code": "UPSTREAM_ERROR",
"message": "Data source temporarily unavailable. Please try again."
}
}Development error response (only when NODE_ENV=development):
{
"success": false,
"error": {
"code": "UPSTREAM_ERROR",
"message": "Data source temporarily unavailable. Please try again.",
"details": {
"source": "coingecko",
"status": 429,
"retryAfter": 60
}
}
}The secureHeaders() middleware sets the following headers on all responses:
| Header | Value | Purpose |
|---|---|---|
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
HTTPS enforcement |
X-Frame-Options |
DENY |
Prevents clickjacking |
X-Content-Type-Options |
nosniff |
Prevents MIME type sniffing |
X-XSS-Protection |
1; mode=block |
Legacy XSS filter |
Referrer-Policy |
strict-origin-when-cross-origin |
Limits referrer information |
Content-Security-Policy |
default-src 'self' |
Restricts resource loading |
Permissions-Policy |
geolocation=(), camera=(), microphone=() |
Disables unused browser APIs |
| Environment | CORS Configuration |
|---|---|
| Development | origin: * (all origins allowed) |
| Production | Whitelist via CORS_ORIGINS env var |
# Production CORS configuration
CORS_ORIGINS=https://cryptocurrency.cv,https://dashboard.cryptocurrency.cv,https://www.cryptocurrency.cvThe CORS middleware validates:
Originheader against the whitelistAccess-Control-Request-Methodfor preflight requestsAccess-Control-Request-Headersfor custom headers (e.g.,X-API-Key)
All secrets are loaded from environment variables — never hardcoded:
| Secret | Env Var | Required |
|---|---|---|
| API keys (static) | API_KEYS |
No |
| Admin API keys | ADMIN_API_KEYS |
No |
| Redis password | REDIS_URL (embedded) |
No |
| PostgreSQL password | DATABASE_URL (embedded) |
No |
| AI provider keys | GROQ_API_KEY, OPENAI_API_KEY, etc. |
No (per provider) |
| BigQuery credentials | GOOGLE_APPLICATION_CREDENTIALS |
For BigQuery features |
- Never log secrets — All logging sanitizes API keys, passwords, and tokens before output
- Minimal scope — Each API key has only the permissions it needs (tier-based)
- Rotation — Dynamic keys can be rotated at runtime via the admin API without restarts
- No .env in production — Use actual environment variables (K8s secrets, Cloud Run secrets)
- Zod validation —
env.tsvalidates all required environment variables at startup, failing fast with clear error messages if any are missing
- npm audit — Run regularly to check for known vulnerabilities
- Dependabot — Configured for automatic dependency update PRs
- Lock file —
package-lock.jsonpinned to prevent supply chain attacks
The project uses minimal runtime dependencies, preferring built-in Node.js APIs where possible:
crypto.timingSafeEqualinstead of third-party comparison librariescrypto.randomUUIDinstead of uuid packages- Native
fetchinstead of Axios/node-fetch
- Non-root user — Dockerfile runs the application as a non-root user
- Minimal base image — Uses
node:22-slimto reduce attack surface - No secrets in images — All secrets are injected at runtime via environment variables
- Read-only filesystem — Container filesystem is read-only in production (K8s
readOnlyRootFilesystem: true)
- Internal-only ports — Only port 8080 is exposed to the load balancer
- No direct database access — All database connections go through internal cluster networking
- Egress restrictions — Outbound traffic limited to known upstream APIs
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]See Deployment and Infrastructure for full deployment security configuration.