← Back to Blog

7 HIPAA Issues We Keep Finding in Next.js Health Apps

hipaanextjshealthcaresecurity

We scan hundreds of healthcare codebases every month. Next.js is the most popular framework in our user base, and the same 7 violations show up again and again.

Here they are, with real code, real vlayer output, and real fixes.

1. PHI in console.log

The most common violation. Developers log patient data during debugging and forget to remove it.

// app/api/patients/route.ts
export async function GET(req: Request) {
  const patient = await db.patients.findUnique({ where: { id } });
  console.log(`Fetched patient: ${patient.name}, SSN: ${patient.ssn}`);
  return Response.json(patient);
}

vlayer output:

CRITICAL  phi-console-log
  PHI detected in console.log statement
  File: app/api/patients/route.ts:4
  HIPAA: §164.312(a)(1) — Access Control

Fix: Remove the log, or use a structured logger with PHI redaction:

import { logger } from '@/lib/logger';

export async function GET(req: Request) {
  const patient = await db.patients.findUnique({ where: { id } });
  logger.info('Patient fetched', { patientId: patient.id });
  return Response.json(patient);
}

2. PHI in localStorage

Client components caching patient data in the browser. localStorage has no encryption, no expiry, and persists across sessions.

// components/PatientSearch.tsx
'use client';

useEffect(() => {
  const data = await fetch('/api/patients');
  const patients = await data.json();
  localStorage.setItem('patient-medical-records', JSON.stringify(patients));
}, []);

vlayer output:

CRITICAL  phi-localstorage
  PHI stored in localStorage without encryption
  File: components/PatientSearch.tsx:6
  HIPAA: §164.312(a)(2)(iv) — Encryption

Fix: Use server-side session storage or encrypted in-memory state. Never persist PHI client-side.

const [patients, setPatients] = useState([]);

useEffect(() => {
  fetch('/api/patients').then(r => r.json()).then(setPatients);
}, []);

3. NEXT_PUBLIC_ secrets

Next.js inlines NEXT_PUBLIC_* variables into the client bundle. Developers put sensitive keys there without realizing they're public.

// lib/supabase.ts
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SERVICE_ROLE_KEY! // exposed to every browser
);

vlayer output:

CRITICAL  CRED-003
  Sensitive value exposed via NEXT_PUBLIC_ environment variable
  File: lib/supabase.ts:3
  HIPAA: §164.312(a)(2)(i) — Access Control

Fix: Use the anon key client-side. Service role key goes in server-only code:

// lib/supabase-client.ts (client)
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

// lib/supabase-server.ts (server only)
const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

4. No auth on API routes

Next.js API routes are public by default. No middleware, no token check, nothing.

// app/api/patients/route.ts
export async function GET() {
  const patients = await db.patients.findMany();
  return Response.json(patients); // anyone can call this
}

vlayer output:

HIGH  no-auth-middleware
  PHI endpoint has no authentication check
  File: app/api/patients/route.ts:2
  HIPAA: §164.312(d) — Person or Entity Authentication

Fix: Check auth in every route handler that touches PHI:

import { auth } from '@/lib/auth';

export async function GET(req: Request) {
  const session = await auth();
  if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 });

  const patients = await db.patients.findMany();
  return Response.json(patients);
}

5. MD5 password hashing

crypto.createHash('md5') in a signup or login route. MD5 is broken. HIPAA requires strong cryptographic controls.

import crypto from 'crypto';

const hashedPassword = crypto.createHash('md5').update(password).digest('hex');

vlayer output:

HIGH  encryption-weak-md5
  MD5 hash function is not suitable for protecting PHI
  File: app/api/auth/signup/route.ts:5
  HIPAA: §164.312(a)(2)(iv) — Encryption and Decryption

Fix: Use bcrypt or argon2:

import bcrypt from 'bcrypt';

const hashedPassword = await bcrypt.hash(password, 12);

6. PHI in URL query parameters

Passing SSN, MRN, or DOB as query params. These get logged in browser history, server access logs, and CDN caches.

// Client component
const res = await fetch(`/api/patients?ssn=${patient.ssn}&dob=${patient.dob}`);

vlayer output:

CRITICAL  phi-query-param
  PHI in URL query parameter
  File: components/PatientLookup.tsx:8
  HIPAA: §164.312(e)(2)(ii) — Transmission Security

Fix: Use POST with a request body, or lookup by non-PHI identifier:

const res = await fetch('/api/patients', {
  method: 'POST',
  body: JSON.stringify({ patientId: patient.id }),
});

7. Unlogged PHI deletion

DELETE endpoints with no audit trail. HIPAA requires logging of who accessed, modified, or deleted PHI and when.

export async function DELETE(req: Request) {
  const { id } = await req.json();
  await db.patients.delete({ where: { id } });
  return Response.json({ deleted: true });
}

vlayer output:

MEDIUM  audit-unlogged-delete
  PHI delete operation may lack audit logging
  File: app/api/patients/route.ts:3
  HIPAA: §164.312(b) — Audit Controls

Fix: Log every PHI operation with who, what, and when:

export async function DELETE(req: Request) {
  const session = await auth();
  const { id } = await req.json();

  await db.patients.delete({ where: { id } });
  await auditLog.create({
    action: 'DELETE',
    resource: 'patient',
    resourceId: id,
    userId: session.user.id,
    timestamp: new Date(),
  });

  return Response.json({ deleted: true });
}

Scan your project now

Every one of these issues takes under 5 minutes to fix. Finding them is the hard part — and that's what vlayer does.

npx vlayer scan ./src

140+ rules. 5 HIPAA categories. Zero configuration.