Open-source SOC2 compliance dashboard. Evidence upload, AI-powered control mapping, human review gate, full audit log. The $100K/yr Vanta alternative — for free.
Live demo: The homepage shows your live compliance numbers pulled from your Supabase database.
SOC2 compliance is 3 tables in a trench coat:
- controls — Seeded SOC2 Trust Services Criteria (71 controls across 5 categories)
- evidence — Files uploaded as proof, linked to controls, with AI proposals and human review
- control_status — A Postgres VIEW that computes which controls are passing/in-review/not-started
The workflow:
- Upload evidence (PDF, screenshot, doc) against a control
- Claude reads the file and proposes which controls it satisfies (with confidence levels)
- An admin reviews the AI proposal, views the file, and clicks Accept or Reject
- The control turns green on the dashboard
- Every action is logged in the audit trail
The human review gate is non-negotiable. AI proposes, a human confirms. Always. No auto-approval at any confidence level. This is what makes your SOC2 audit defensible.
- Next.js 16 with React 19 (App Router)
- Supabase (auth, database, storage)
- Tailwind CSS v4 + custom shadcn-style UI components
- Claude API for AI evidence analysis
- TypeScript throughout
- Deploy on Vercel
git clone https://github.com/Stevekaplanai/soc2-dashboard.git
cd soc2-dashboard
npm install- Go to supabase.com and create a new project (free tier is fine)
- Go to SQL Editor
- Paste the migration file from
/supabase/migrations/0001_soc2_dashboard.sql— run it - Paste the seed file from
/supabase/seed.sql— run it (loads 71 SOC2 controls) - The migration creates the private
evidence-filesbucket and its upload rules - Copy your Project URL and anon key from Settings > API
Create a .env.local file:
NEXT_PUBLIC_SUPABASE_URL=https://yourproject.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
ANTHROPIC_API_KEY=your-anthropic-api-keynpm run dev- Go to
/auth/signupand create an account - In Supabase SQL editor, run:
UPDATE auth.users
SET raw_app_meta_data = COALESCE(raw_app_meta_data, '{}'::jsonb) || '{"role": "admin"}'::jsonb
WHERE email = 'steve@stevekaplan.ai';- Log out and log back in — you now have access to
/dashboard/review
- Push your repo to GitHub
- Go to vercel.com, click "Add New Project", import the repo
- Set these environment variables in the Vercel dashboard:
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEYANTHROPIC_API_KEY
- Click Deploy (~90 seconds)
soc2-dashboard/
├── src/
│ ├── app/
│ │ ├── layout.tsx # Root layout
│ │ ├── page.tsx # Landing page (live compliance numbers)
│ │ ├── globals.css # Tailwind + theme
│ │ ├── dashboard/
│ │ │ ├── page.tsx # Controls dashboard (grouped by category)
│ │ │ └── review/page.tsx # Admin review queue
│ │ ├── auth/
│ │ │ ├── login/page.tsx # Supabase auth login
│ │ │ ├── signup/page.tsx # Supabase auth signup
│ │ │ └── signout/route.ts # Sign out route handler
│ │ └── api/
│ │ └── signed-url/route.ts # Generate signed URLs for evidence files
│ ├── components/
│ │ ├── ui/ # Badge, Button, Card, Sheet components
│ │ ├── dashboard-client.tsx # Dashboard with collapsible categories + upload sheet
│ │ ├── upload-evidence.tsx # Evidence upload form
│ │ └── review-queue-client.tsx# Admin review queue with Accept/Reject/Ask
│ ├── lib/
│ │ ├── supabase/ # Client, server, admin Supabase clients
│ │ ├── actions.ts # Server actions (upload, review)
│ │ ├── claude.ts # Claude AI evidence analysis
│ │ ├── types.ts # TypeScript types
│ │ └── utils.ts # cn() helper
│ └── proxy.ts # Session refresh + dashboard access gate
├── supabase/
│ ├── migrations/
│ │ └── 0001_soc2_dashboard.sql # Tables, view, RLS, storage bucket
│ └── seed.sql # 71 SOC2 Trust Services Criteria controls
├── package.json
├── next.config.ts
├── tsconfig.json
└── README.md
| Column | Type | Description |
|---|---|---|
| id | UUID PK | Auto-generated |
| code | TEXT UNIQUE | e.g. "CC6.1" |
| title | TEXT | e.g. "Logical Access Controls" |
| category | TEXT | CC, A, PI, C, P |
| description | TEXT | What the control requires |
| status | TEXT | not_started / in_review / passing (computed) |
| Column | Type | Description |
|---|---|---|
| id | UUID PK | Auto-generated |
| control_id | UUID FK | → controls |
| file_url | TEXT | Supabase Storage path |
| file_name | TEXT | Original filename |
| uploaded_by | UUID FK | → auth.users |
| uploaded_at | TIMESTAMPTZ | When uploaded |
| ai_proposed_controls | JSONB | Claude's analysis result |
| ai_confidence | TEXT | high / medium / low |
| review_status | TEXT | pending / accepted / rejected |
| reviewed_by | UUID FK | → auth.users (admin) |
| reviewed_at | TIMESTAMPTZ | When reviewed |
| notes | TEXT | Reviewer notes |
Check constraint: review_status = 'accepted' requires both reviewed_by and reviewed_at. The database enforces the human review gate even if application code misbehaves.
Computed view — not a table. Returns control_id, code, title, category, status, evidence_count, last_evidence_at. The dashboard reads this view. Always current.
| Column | Type | Description |
|---|---|---|
| id | UUID PK | Auto-generated |
| action | TEXT | uploaded / accepted / rejected / requested_more_info |
| evidence_id | UUID FK | → evidence |
| control_id | UUID FK | → controls |
| performed_by | UUID FK | → auth.users |
| performed_at | TIMESTAMPTZ | When action occurred |
| note | TEXT | Optional note |
- controls: Readable by any authenticated user; not user-editable
- control_status (view): Readable by authenticated users and obeys the underlying RLS rules
- evidence: Users see only their own uploads; admins see all; review changes go through the admin-only database function
- audit_log: Readable by admins and written by trusted server/review paths only
- Storage (evidence-files): Users upload to their own folder; admins see all
When evidence is uploaded, Claude analyzes it:
- File uploaded to Supabase Storage (private bucket, signed URLs)
- Server action generates a short-lived signed URL
- Claude receives the file + the full SOC2 controls list as context
- Returns JSON:
[{control_code, control_title, confidence, reasoning}] - Results stored in
evidence.ai_proposed_controlsandevidence.ai_confidence - Admin sees the proposal in the review queue with color-coded confidence
The app uses claude-sonnet-4-6. DOCX files are converted to text with Mammoth before analysis; PDF and image files are sent as native document/image inputs.
Supported file types: PDF (native), PNG/JPG (native), DOCX (text extraction)
The AI analysis function only writes to ai_proposed_controls and ai_confidence. It never touches review_status. The review_status column only moves via explicit admin Accept/Reject actions. There's a database check constraint enforcing this.
Claude proposes. A human confirms. Always.
- Vercel hobby tier: $0/month
- Supabase free tier: $0/month
- Claude API: ~$0.01-0.03 per evidence analysis (cents)
- Total: ~$0/month for a full SOC2 compliance tool
Vanta charges $100K/year for the same workflow. You own this one.
MIT — do whatever you want with it.