myReadMe

Built by a security professional

Security by design

Every layer of this platform was built with security in mind — from how passwords are stored to how HTTP headers are sent. Here's exactly what's in place and why.

← Back to home

Authentication & Sessions

bcrypt password hashing

Cost factor 12 — calibrated to be slow enough to resist offline brute-force attacks while staying fast enough for legitimate logins.

Stateless JWT sessions

Sessions are signed JWTs — no server-side session table, no shared session store. Tokens are invalidated at sign-out.

Password length limits

Passwords must be 8–128 characters. The upper bound prevents bcrypt DoS attacks that exploit bcrypt's CPU cost on very long inputs.

Global email uniqueness

An email address can only be tied to one account regardless of provider (credentials, GitHub, or Google). Cross-provider hijacking is blocked at the auth callback before any upsert.

Rate Limiting & Abuse Prevention

Login throttle

10 attempts per IP per 15 minutes. Sliding-window counter, resets cleanly. Failed attempts beyond the limit return 429 with no further DB queries.

Signup throttle

5 registrations per IP per 15 minutes — makes scripted account creation costly without impacting legitimate users.

Onboarding throttle

20 onboarding requests per IP per hour — limits username enumeration via repeated attempts.

IP extraction

Reads X-Forwarded-For (Railway's edge) then X-Real-IP as fallback; the leftmost address is taken to prevent header spoofing.

Transport & HTTP Headers

HTTPS + HSTS

All traffic is HTTPS. HSTS (max-age=31536000; includeSubDomains) is set so browsers refuse plain HTTP connections for one year after the first visit.

Content-Security-Policy

CSP restricts script, style, image, font, and frame sources. Scripts are limited to 'self'; object-src and base-uri are locked to 'none' and 'self' respectively to block injection and redirect attacks.

X-Frame-Options: DENY

Prevents the site from being embedded in a third-party iframe, blocking clickjacking attacks.

X-Content-Type-Options: nosniff

Prevents browsers from MIME-sniffing a response away from the declared content type.

Permissions-Policy

Explicitly disables camera, microphone, and geolocation access — no accidental capability grants.

Referrer-Policy: strict-origin-when-cross-origin

Sends the full referrer on same-origin requests but only the origin on cross-origin ones, preventing URL leakage.

File Upload Protection

Magic byte validation

The first 12 bytes of every uploaded file are read and checked against known file signatures (JPEG: FF D8 FF, PNG: 89 50 4E 47 0D 0A 1A 0A, WebP: RIFF…WEBP, GIF: GIF8). Declared MIME type alone is not trusted.

Strict file type allowlist

Only JPEG, PNG, WebP, and GIF pass validation. All other types are rejected with 415 Unsupported Media Type before the file is written.

5 MB size cap

Uploads exceeding 5 MB are rejected at the API boundary.

User-scoped filenames

Files are written as headshot-{userId}.{ext} — predictable per user, collision-safe across users, and no path traversal possible.

Old file cleanup

When a new headshot is uploaded, the previous file under /uploads/ is deleted server-side, preventing unbounded storage accumulation.

Data Integrity & Input Validation

ORM with parameterized queries

All database access goes through Prisma, which uses parameterized queries exclusively. Raw SQL injection is structurally impossible.

Server-side validation on every route

Every API route validates shape, type, and length before touching the database. Invalid payloads return 422 Unprocessable Entity — never a 500 that might leak stack traces.

Cascading deletes

Foreign-key relationships use onDelete: Cascade so removing a user atomically removes all their content — no orphaned rows, no data leakage to future users who might reuse an ID.

Email immutability after first login

OAuth upserts never overwrite the stored email field after account creation, preventing a provider-side email change from silently retaking an account.

Access Control

Server-side auth on every admin route

requireAuth() is called at the top of every admin API handler. Unauthenticated requests receive 401 before any query runs.

User-scoped writes

Every write operation binds to the authenticated userId from the JWT — not from the request body. A user cannot modify another user's content even with a crafted payload.

Username-to-session binding

The admin layout verifies the URL username matches the session username. Visiting /{someone-else}/admin redirects to your own admin panel.

OAuth account isolation

A GitHub or Google sign-in can only access the account that was originally created with that provider identity. Email conflicts across providers are blocked at the signIn callback, not post-creation.

This platform is maintained by a professional cybersecurity administrator. Security controls are not an afterthought — they are part of the architecture. Practices are reviewed and updated as the threat landscape evolves.