HealthStory AI← Back to the product

Stage 11 · Security engineering

Security & data integrity, documented

Health stories are personal. This page explains exactly how this system authenticates, authorizes, validates, limits, stores and audits — with a live status readout from this very deployment.

Live security statusLoading…

Querying GET /api/security/status …

Scope: HealthStory AI is a communication and journaling tool. It performs no diagnosis, gives no treatment advice, computes no health scores, and stores no real patient records — all demonstration data is fictional (see README.md).

Access control

Who may do what

Three independent layers — credentials, sessions and per-object checks — each verified on every request.

Authentication

Passwords are hashed with bcrypt cost 12. Login is timing-equalized against a dummy hash so unknown emails are not distinguishable by response time. Failed logins are audited with the attempted address — never the password.

Revocable sessions

The session cookie is HttpOnly, SameSite=Lax and Secure in production, carrying an HS256 JWT. A signature is not enough: every request also resolves a row in user_sessions, so logout invalidates the token server-side. Other devices keep their own independent sessions.

Object-level authorization

Every story, document, follow-up and communication route re-checks ownership against the session user — reads and writes both. Probes of foreign ids return 403 and write an ACCESS_DENIED audit entry recording who tried what.

Untrusted input

Everything a client sends is treated as hostile

Strict validation, no mass assignment

Every JSON body passes a content-type check (415), a size cap (413), a JSON parse (400) and then a strict zod schema. Unknown keys are rejected with a field-level 400, so a payload carrying userId, currentVersion, status or storageKey never reaches the database — those values are derived server-side from the session and the request context.

  • Resource caps: events, questions, follow-ups, documents, text length
  • Validation errors name the offending path, nothing else

Rate limiting

Fixed windows per user+IP, tunable through environment variables. Responses carry Retry-After and each 429 writes a RATE_LIMIT_TRIGGERED audit entry.

  • login 10 / 5 min — brute force
  • register 10 / min — account flooding
  • ai 20 / min — organize, preview, analyze, generate
  • upload 20 / 10 min — file flooding
  • write 120 / min — general mutations
CORS & CSRF: no Access-Control-Allow-* header is ever emitted, so foreign origins cannot read API responses; mutations require a JSON body (cross-site forms cannot send one without a CORS preflight that is never granted) and the session cookie is SameSite=Lax.

Data at rest

Files, database and audit trail

Private file storage

Uploads must pass an extension allow-list, extension↔MIME consistency, magic-byte checks, a size cap and strict base64 decoding. Bytes are written under a git-ignored storage/ directory outside the web root; the key is generated by the server from user/story/document ids and re-checked against traversal on read.

  • sha-256 checksum recorded per file
  • Download requires auth + ownership and is audited (DOCUMENT_ACCESS)
  • Delete = transactional soft-delete, then bytes removed

Migrations & transactions

Schema changes ship as reviewed Prisma migrations (baseline + Stage 11 migrations, see DATABASE.md). Every multi-row write — story + first version, events + audit + version, follow-up + items — runs in a single transaction, so a failure rolls back the whole unit. There are no half-written stories.

  • Version chain: currentVersion bumps in the same transaction as its story_versions row
  • Seed: localhost-guarded, idempotent, integrity-clean

Audit trail

Successes and failures are both recorded: logins (success and failure), logout, denied object access, rate-limit trips, story, event, question, follow-up and document writes, AI operations and communication generation — every action in the AuditAction enum (live list in the status readout above).

Story Integrity

An independent checker, not a promise

Foreign keys guarantee references exist; they cannot prove that a file on disk matches its row, that a version chain is unbroken, or that confirmation flags agree with status. GET /api/stories/:id/integrity proves exactly those things — read-only, per story, eight checks:

version-chainVersion rows 1..currentVersion exist exactly once
document-ownershipDocuments belong to the story owner
document-storageStored bytes exist, checksum matches, deletes leave nothing behind
question-ownershipQuestions belong to the story owner
followup-ownershipFollow-ups belong to the story owner
confirmation-consistencyconfirmedByUser agrees with status and needsConfirmation
provenance-consistencycreatedBy agrees with event status
communication-selectionsSelections reference rows that still exist
Report, never repair: the checker returns pass / warn / fail per check plus an overall status. It cannot modify data — diagnosing is the operator's job.

Verification

Reproduce the claims

The security suite is part of the repository — run it against a fresh dev server.

# terminal 1
npm run dev
# terminal 2 — ~150 assertions, exit 0 = all pass
npm run test:security
# database story: migrations, seed, integrity
npm run db:reset && npm run db:seed

Response headers (next.config.ts)

  • Content-Security-Policy — default-src 'self', object-src 'none', frame-ancestors 'none'; eval and HMR sockets only outside production
  • X-Content-Type-Options nosniff · X-Frame-Options DENY
  • Referrer-Policy strict-origin-when-cross-origin
  • Permissions-Policy — camera, geolocation, payment, USB off; microphone stays on for the journal's voice input
  • COOP / CORP same-origin · HSTS production only

Honest limitations

  • Rate-limit state is in-memory — per instance, lost on restart; the documented path is Redis
  • AUTH_SECRET needs an explicit value in production (dev fallback exists and is documented)
  • No password-reset flow (no mail provider in scope) and no 2FA yet
  • CSP keeps 'unsafe-inline' scripts for Next.js's bootstrap; nonces are the upgrade path

Full detail and the hardening roadmap: SECURITY.md · schema and migrations: DATABASE.md