← Back to Blog

How to Scan a Supabase + Vercel Health App in CI

supabasevercelhipaaci

Supabase + Vercel + Next.js is the most popular stack for healthcare startups building fast. It's also the stack where we see the most HIPAA violations — not because the tools are insecure, but because developers skip the compliance layer.

Here's how to add vlayer scanning to this exact stack in 10 minutes.

The common violations in this stack

Before we set up CI, here's what vlayer typically finds in a Supabase + Vercel app:

npx vlayer scan ./src
CRITICAL  CRED-003  NEXT_PUBLIC_SERVICE_ROLE_KEY exposed to client
  File: lib/supabase.ts:3

CRITICAL  phi-localstorage  PHI stored in localStorage
  File: components/PatientList.tsx:12

HIGH  encryption-weak-http  Unencrypted HTTP URL
  File: lib/api.ts:5

MEDIUM  audit-unlogged-delete  PHI delete without audit logging
  File: app/api/patients/route.ts:28

Compliance Score: 42/100 (F)

Let's fix the pipeline first, then the code.

Step 1: Add vlayer to your Vercel build

Create .github/workflows/hipaa-scan.yml:

name: HIPAA Compliance Scan

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  hipaa-scan:
    name: vlayer HIPAA Scanner
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Run HIPAA scan
        run: npx verification-layer scan ./src -f json -o vlayer-report.json

      - name: Upload report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: vlayer-hipaa-report
          path: vlayer-report.json

Step 2: Fix the Supabase client pattern

The #1 violation in Supabase apps: using the service role key in client-side code.

// WRONG: service role key in NEXT_PUBLIC_ variable
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!
);

vlayer catches this:

CRITICAL  CRED-003
  Sensitive value exposed via NEXT_PUBLIC_ environment variable
  Pattern: NEXT_PUBLIC_.*SERVICE_ROLE
  File: lib/supabase.ts:3

Fix: Two clients — anon for the browser, service role for the server:

// lib/supabase-browser.ts (client components)
import { createBrowserClient } from '@supabase/ssr';

export const supabase = createBrowserClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

// lib/supabase-server.ts (server components, API routes)
import { createServerClient } from '@supabase/ssr';

export function createServer(cookieStore) {
  return createServerClient(
    process.env.SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY!, // no NEXT_PUBLIC_ prefix
    { cookies: { getAll: () => cookieStore.getAll() } }
  );
}

Step 3: Enable RLS and stop using SELECT *

Supabase Row Level Security is your access control layer. Without it, any authenticated user can read any row.

// WRONG: no RLS, SELECT * returns all fields
const { data } = await supabase.from('patients').select('*');

vlayer output:

MEDIUM  select-star
  SELECT * on sensitive table retrieves more data than necessary
  File: app/api/patients/route.ts:5

Fix: Select only the fields you need:

const { data } = await supabase
  .from('patients')
  .select('id, name, appointment_date');

And enable RLS in your Supabase migration:

ALTER TABLE patients ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can view own patients"
  ON patients FOR SELECT
  USING (auth.uid() = provider_id);

Step 4: Add audit logging

Supabase doesn't log PHI access by default. You need an audit table.

CREATE TABLE audit_log (
  id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id uuid REFERENCES auth.users(id),
  action text NOT NULL,
  resource text NOT NULL,
  resource_id text,
  created_at timestamptz DEFAULT now()
);
async function auditLog(userId: string, action: string, resource: string, resourceId: string) {
  await supabase.from('audit_log').insert({ user_id: userId, action, resource, resource_id: resourceId });
}

Step 5: Verify the fix

Run the scan again:

npx vlayer scan ./src
Compliance Score: 94/100 (A)
0 critical, 0 high, 2 medium findings

The CI pipeline will pass. Every future PR gets scanned automatically. Ship it.

npx vlayer scan ./src