Everything you need to deploy GRC Crest.
Project structure, authentication system, MFA setup, quick-start guide, adding users, all four hosting options, every environment variable, and the contributing guide. No part of the README omitted.
Project Structure
This is a Next.js 15 application with React 19, TypeScript 5.8, Tailwind CSS 4, Vercel AI SDK 4, and NextAuth v5. The codebase is structured for clarity, separation of concerns, and long-term maintainability.
grc-command-center/ │ ├── src/ │ │ │ ├── types/ │ │ ├── grc.ts ← ALL domain TypeScript interfaces │ │ │ Single source of truth for every data shape. │ │ └── next-auth.d.ts ← NextAuth v5 module augmentation │ │ Adds mfaVerified and role to Session + JWT types │ │ │ ├── lib/ │ │ ├── constants.ts ← All static data (layers, events, incidents) │ │ ├── aiClient.ts ← Browser-side AI client — NO API keys here │ │ │ Calls /api/ai proxy only │ │ ├── prompts.ts ← All AI system prompts as versioned constants │ │ ├── auth.ts ← NextAuth v5 config, JWT callbacks, authorized guard │ │ ├── users.ts ← User store (AUTHORISED_USERS env var) │ │ │ Replace with Prisma/DB for production scale │ │ └── mfa.ts ← TOTP (otplib) + Email OTP (Nodemailer) │ │ │ ├── styles/ │ │ └── globals.css ← Tailwind v4 import + CSS custom properties │ │ │ ├── components/ │ │ ├── ui/ ← Reusable primitive components │ │ │ ├── StatusBadge.tsx │ │ │ ├── GlowButton.tsx │ │ │ └── CodeBlock.tsx │ │ ├── tabs/ ← One file per tab — independently testable │ │ │ ├── OverviewTab.tsx │ │ │ ├── PipelineTab.tsx │ │ │ ├── CodeReferenceTab.tsx │ │ │ ├── VendorRiskTab.tsx │ │ │ ├── OPAGeneratorTab.tsx │ │ │ ├── EvidenceTrackerTab.tsx │ │ │ └── IncidentPlaybookTab.tsx │ │ ├── GRCDashboard.tsx ← Root client: header + tab router + layout │ │ └── LiveSidebar.tsx ← Real-time automation event feed │ │ │ ├── middleware.ts ← SINGLE auth enforcement point for ALL routes │ │ Every request checked here before rendering │ └── app/ │ ├── layout.tsx ← Root HTML layout + metadata │ ├── page.tsx ← Dashboard entry (server component) │ ├── (auth)/ ← Route group — auth layout, no GRC chrome │ │ ├── layout.tsx │ │ └── login/page.tsx ← Two-step login: credentials → MFA │ └── api/ │ ├── ai/route.ts ← Vercel AI SDK proxy (API keys server-only) │ └── auth/ │ ├── [...nextauth]/route.ts ← NextAuth v5 handler │ ├── mfa-verify/route.ts ← Verifies TOTP or email OTP │ └── send-email-otp/route.ts ← Generates + emails 6-digit OTP │ ├── .env.example ← All env vars documented with examples ├── .gitignore ├── next.config.ts ├── postcss.config.mjs ├── tsconfig.json ← Strict TypeScript └── package.json
Why this structure?
| Decision | Reason |
|---|---|
| src/types/grc.ts is the only types file | One source of truth. No scattered inline types. TypeScript compiler enforces contracts everywhere. |
| src/lib/constants.ts holds all data | Components contain zero hard-coded data. Swap static data for real API calls in one file later. |
| src/lib/prompts.ts holds all AI prompts | Prompts can be reviewed and updated by non-engineers without reading React code. |
| src/app/api/ai/route.ts is the only AI caller | API keys never reach the browser. Single, auditable point of external AI contact. |
| One file per tab in src/components/tabs/ | Each tab is independently testable. A broken tab never affects other tabs. |
| src/middleware.ts is the single auth gate | Auth checked in one place before any page renders. Impossible to accidentally ship an unprotected page. |
| (auth) route group | Login lives in its own layout with no GRC UI chrome. Prevents the authenticated layout from wrapping the login form. |
Authentication & Access Control
How Login Works — Two Steps
User enters email + password. NextAuth Credentials provider verifies against AUTHORISED_USERS via bcrypt comparison. On success: partial session created with mfaVerified: false. On failure: "Invalid credentials" (deliberately vague — no user enumeration).
User submits a 6-digit code via authenticator app (TOTP) or email. Server verifies against stored secret or OTP. On success: updateSession({ mfaVerified: true }) upgrades the token → redirect to dashboard. Rate-limited to 5 attempts / 15 minutes.
Middleware reads JWT from httpOnly cookie. Checks: session exists AND mfaVerified === true. If either is false → redirect to /login. A user who completed step 1 but not step 2 cannot access any page.
Session Security Properties
| Property | Value | Reason |
|---|---|---|
| Strategy | JWT (httpOnly cookie) | No database required; cookie inaccessible to JavaScript |
| Session expiry | 8 hours | Appropriate for business-hours tools; forces daily re-auth |
| MFA state | Stored in JWT token | mfaVerified: false until step 2 completes |
| Cookie flags | httpOnly, secure, sameSite=lax | Prevents XSS token theft and CSRF |
| MFA brute-force | 5 attempts / 15 min | 6-digit codes have only 1M combinations |
| Email OTP send | 3 sends / 15 min | Prevents email flooding attacks |
| Error messages | Deliberately vague at step 1 | Never reveals whether the email exists |
| TOTP window | ±1 step (30s tolerance) | Accommodates clock skew without excessive attack surface |
User Roles
| Role | Permissions |
|---|---|
| admin | Full access — view all tabs, approve risk variances, manage incidents, trigger evidence collection, manage users |
| analyst | Standard access — view all tabs, run AI assessments, trigger evidence collection; cannot approve risk variances |
| viewer | Read-only — view dashboards and evidence; cannot trigger AI features or approve anything |
MFA Setup
TOTP — Google Authenticator
TOTP (Time-based One-Time Password, RFC 6238) generates a new 6-digit code every 30 seconds. Compatible with Google Authenticator, Microsoft Authenticator, Authy, 1Password, and any RFC 6238-compliant app.
node -e "console.log(require('otplib').authenticator.generateSecret())"node -e "
const { authenticator } = require('otplib');
const uri = authenticator.keyuri(
'user@company.com',
'GRC Command Center',
'YOUR_SECRET_HERE'
);
console.log(uri);"- Open Google Authenticator → tap + → Scan a QR code
- Scan the QR image your administrator sent you. Alternatively tap Enter a setup key and type the Base32 secret.
- An entry called "GRC Command Center" appears with a refreshing 6-digit code.
- Log in with email + password, then enter the 6-digit code when prompted.
Admin sets totpSecret: null and changes mfaMethod: "email" in AUTHORISED_USERS, then redeploys. User can log in via email OTP while a new TOTP secret is generated and distributed.
Email OTP
A 6-digit code is generated server-side, stored temporarily in memory (replace with Redis for multi-instance), and sent to the user's registered email address via Nodemailer/SMTP.
Gmail (use an App Password — not your main password):
SMTP_HOST=smtp.gmail.com SMTP_PORT=587 SMTP_SECURE=false SMTP_USER=noreply@yourcompany.com SMTP_PASS=your_16_char_app_password
SendGrid (recommended for production):
SMTP_HOST=smtp.sendgrid.net SMTP_PORT=587 SMTP_SECURE=false SMTP_USER=apikey SMTP_PASS=your_sendgrid_api_key
The email OTP store is in-memory. For Vercel serverless or multiple Docker containers, replace with Redis in src/lib/mfa.ts:
await redis.setex(`otp:${email}`, 600, code)
Quick Start — Local Development
git clone https://github.com/igefadele/grc-crest.git cd grc-crest # OR from the zip: unzip grc-command-center-v4-final.zip cd grc-command-center
npm install
cp .env.example .env.local
Open .env.local and fill in the minimum required values:
# Generate AUTH_SECRET with: openssl rand -base64 32
AUTH_SECRET=replace_with_32_char_random_string
NEXTAUTH_URL=http://localhost:3000
# At least one user (see "Adding Users" section for bcrypt hash generation)
AUTHORISED_USERS='[{"id":"usr_001","email":"you@company.com","name":"Your Name","role":"admin","hashedPassword":"$2a$12$...","totpSecret":null,"mfaMethod":"totp"}]'
# AI provider
AI_PROVIDER=claude
AI_MODEL=claude-sonnet-4-20250514
ANTHROPIC_API_KEY=sk-ant-your_key_here
# SMTP for email OTP
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=you@gmail.com
SMTP_PASS=your_app_password
TOTP_ISSUER=GRC Command Center
# Shown in dashboard header
NEXT_PUBLIC_AI_PROVIDER=claudenpm run dev
Open http://localhost:3000 — you will be redirected to /login immediately.
Available Scripts
| Script | Description |
|---|---|
| npm run dev | Start development server with Turbopack (fast hot reload) |
| npm run build | Build for production |
| npm run start | Start production server (run after build) |
| npm run lint | Run ESLint |
| npm run type-check | Run TypeScript compiler check (tsc --noEmit) — must pass before committing |
Adding Users (Administrator Guide)
There is no registration form. All accounts are provisioned by administrators. This mirrors enterprise IAM practice — access is granted, not claimed.
node -e "require('bcryptjs').hash('the_initial_password', 12).then(console.log)"This outputs: $2a$12$abcdefghijklmnopqrstuuVWXYZ0123456789...
node -e "console.log(require('otplib').authenticator.generateSecret())"
# Output: JBSWY3DPEHPK3PXP (Base32 string)AUTHORISED_USERS='[
{
"id": "usr_001",
"email": "analyst@yourcompany.com",
"name": "Jane Smith",
"role": "analyst",
"hashedPassword": "$2a$12$your_generated_hash",
"totpSecret": "JBSWY3DPEHPK3PXP",
"mfaMethod": "totp"
},
{
"id": "usr_002",
"email": "viewer@yourcompany.com",
"name": "Bob Jones",
"role": "viewer",
"hashedPassword": "$2a$12$another_generated_hash",
"totpSecret": null,
"mfaMethod": "email"
}
]'# Vercel: update env var in dashboard → trigger redeploy # Docker: restart container with new env docker compose up -d --force-recreate # Local dev: save .env.local — server hot-reloads env vars
Send the email address, initial password, and (if TOTP) the TOTP secret through your organisation's secure credential distribution process. The user cannot retrieve or reset these themselves.
AUTHORISED_USERS Field Reference
| Field | Type | Description |
|---|---|---|
| id | string | Unique identifier — any string, must be unique across users |
| string | The user's email address — used as their login username | |
| name | string | Full name shown in the session |
| role | admin | analyst | viewer | Controls what the user can view and approve |
| hashedPassword | string | bcrypt hash of their password. Never store plaintext. |
| totpSecret | string | null | Base32 TOTP secret for authenticator app. null until set up. |
| mfaMethod | totp | email | Which MFA method this user is configured to use |
Remove the user from AUTHORISED_USERS and redeploy. Their existing JWT session will fail validation on the next request. For immediate revocation, also rotate AUTH_SECRET (this invalidates ALL active sessions).
The AUTHORISED_USERS env var is a zero-dependency approach for small teams. For production scale, replace getUserByEmail() and verifyPassword() in src/lib/users.ts with your database queries (Prisma, Drizzle, etc.). The rest of the auth flow is unchanged.
AI Integration — Vercel AI SDK
Why Vercel AI SDK Instead of Raw Fetch?
The previous approach used manual fetch() calls to each provider's API, requiring separate request parsers, response parsers, error handling, and an if/else chain per provider. The Vercel AI SDK replaces all of this with a single function:
Provider Switching
Change two environment variables in .env.local — no code changes needed anywhere in the application.
AI_PROVIDER=claude AI_MODEL=claude-sonnet-4-20250514 ANTHROPIC_API_KEY=sk-ant-your_key
AI_PROVIDER=openai AI_MODEL=gpt-4o OPENAI_API_KEY=sk-your_key
AI_PROVIDER=openai AI_MODEL=gpt-4o-mini OPENAI_API_KEY=sk-your_key
AI_PROVIDER=custom AI_MODEL=your-model-name OPENAI_API_KEY=not-needed CUSTOM_LLM_ENDPOINT=https://your-endpoint/v1/chat/completions
- Install: npm install @ai-sdk/your-provider
- Add a case in getModel() in src/app/api/ai/route.ts
- Add env vars to .env.example
- Update AIProvider type in src/types/grc.ts
Hosting Guide
Option A: Vercel (Recommended — Free)
Zero configuration — Vercel auto-detects Next.js and handles all server-side routes including /api/auth/* and /api/ai.
git init && git add . && git commit -m "Initial commit" git remote add origin https://github.com/your-username/grc-command-center.git git push -u origin main
npm install -g vercel vercel
Or go to vercel.com → New Project → import your GitHub repo.
Project → Settings → Environment Variables. Critical ones:
AUTH_SECRET ← openssl rand -base64 32 NEXTAUTH_URL ← https://your-project.vercel.app AUTHORISED_USERS ← full JSON array (paste as single line) AI_PROVIDER ← claude AI_MODEL ← claude-sonnet-4-20250514 ANTHROPIC_API_KEY ← sk-ant-... TOTP_ISSUER ← GRC Command Center SMTP_HOST / SMTP_USER / SMTP_PASS NEXT_PUBLIC_AI_PROVIDER ← claude
vercel --prod
Option B: Netlify
# Install Next.js plugin first npm install @netlify/plugin-nextjs
Create netlify.toml:
[[plugins]]
package = "@netlify/plugin-nextjs"npm install -g netlify-cli netlify deploy --prod
Add env vars: Site settings → Environment variables → add all from .env.local.
Option C: Docker / Self-Hosted
First add output: 'standalone' to next.config.ts.
Dockerfile:
FROM node:20-alpine AS deps WORKDIR /app COPY package*.json ./ RUN npm ci --only=production FROM node:20-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/public ./public EXPOSE 3000 CMD ["node", "server.js"]
docker-compose.yml:
version: '3.8'
services:
grc:
build: .
ports:
- "3000:3000"
environment:
- AUTH_SECRET=${AUTH_SECRET}
- NEXTAUTH_URL=https://your-domain.com
- AUTHORISED_USERS=${AUTHORISED_USERS}
- AI_PROVIDER=claude
- AI_MODEL=claude-sonnet-4-20250514
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- TOTP_ISSUER=GRC Command Center
- SMTP_HOST=${SMTP_HOST}
- SMTP_USER=${SMTP_USER}
- SMTP_PASS=${SMTP_PASS}
- NEXT_PUBLIC_AI_PROVIDER=claude
restart: unless-stoppeddocker compose up -d
Option D: AWS, GCP, Azure
| Platform | Service | Notes |
|---|---|---|
| AWS | AWS Amplify | Connect GitHub repo. Auto-detects Next.js. Add env vars in Amplify console. |
| AWS | ECS / Fargate | Use Docker image. Inject secrets via AWS Secrets Manager references in task definition. |
| AWS | App Runner | Simplest managed option. Connect container registry. Set env vars in service config. |
| GCP | Cloud Run | gcloud run deploy grc --image gcr.io/project/grc --set-secrets AUTH_SECRET=grc-secret:latest |
| GCP | Firebase Hosting | Supports Next.js SSR via Cloud Functions. |
| Azure | Container Apps | Deploy Docker image. Set env vars as secrets in container environment config. |
| Azure | App Service | Node.js deployment. Add env vars under Configuration → Application settings. |
Environment Variables Reference
All variables live in .env.local for local development. Set them in your hosting platform's secret manager for production.
Authentication
| Variable | Required | Example | Description |
|---|---|---|---|
| AUTH_SECRET | YES | 32-char random string | Signs JWT tokens. Generate: openssl rand -base64 32. Keep absolutely secret. |
| NEXTAUTH_URL | YES | http://localhost:3000 | Full URL of your deployment. Must match exactly — mismatches break callbacks. |
| AUTHORISED_USERS | YES | JSON array | See "Adding Users" section for the full schema. Paste as a single line on Vercel. |
| TOTP_ISSUER | No | GRC Command Center | Name shown in the authenticator app. Defaults to "GRC Command Center". |
Email / SMTP (for Email OTP MFA)
| Variable | Required | Example | Description |
|---|---|---|---|
| SMTP_HOST | YES* | smtp.gmail.com | SMTP server hostname. *Required when using email OTP. |
| SMTP_PORT | YES* | 587 | 587 for STARTTLS; 465 for SSL. |
| SMTP_SECURE | No | false | true for SSL/port 465; false for STARTTLS/port 587. |
| SMTP_USER | YES* | you@company.com | SMTP username / email address. |
| SMTP_PASS | YES* | app_password_here | SMTP password or App Password. Never your main account password. |
| SMTP_FROM_NAME | No | GRC Command Center | Display name in the From field of OTP emails. |
AI Provider
| Variable | Required | Example | Description |
|---|---|---|---|
| AI_PROVIDER | YES | claude | Active provider: claude, openai, or custom. |
| AI_MODEL | YES | claude-sonnet-4-20250514 | Model identifier for the active provider. |
| AI_MAX_TOKENS | No | 1000 | Max completion tokens. Defaults to 1000. |
| ANTHROPIC_API_KEY | If claude | sk-ant-... | From console.anthropic.com. |
| OPENAI_API_KEY | If openai | sk-... | From platform.openai.com. |
| CUSTOM_LLM_ENDPOINT | If custom | https://... | OpenAI-compatible endpoint for self-hosted LLMs. |
Public (Browser-Visible — NO Secrets)
| Variable | Required | Example | Description |
|---|---|---|---|
| NEXT_PUBLIC_AI_PROVIDER | No | claude | Shows active AI provider label in the dashboard header badge. Safe for browser — contains only the provider name, not any key. |
Contributing
Contributions are welcome. Please follow these steps and standards.
git checkout -b feature/your-feature-name
- Add new domain types to src/types/grc.ts — never inline
- Add new auth types to src/types/next-auth.d.ts
- Add new static data to src/lib/constants.ts
- Add new AI prompts to src/lib/prompts.ts as named const exports
- Add new auth logic to src/lib/auth.ts or src/lib/mfa.ts
- Add new tab components to src/components/tabs/ — register in GRCDashboard.tsx and constants.ts
- All new API routes must verify mfaVerified: true — never rely solely on middleware
npm run type-check # tsc --noEmit — must be zero errors npm run lint # ESLint — must be zero errors
Provider-agnostic behaviour must be verified. Switch AI_PROVIDER and confirm the feature works identically with both providers.
Credentials → TOTP → dashboard → session expiry. Ensure mfaVerified: false sessions are correctly blocked from all routes.
Update README.md and the relevant web page for any new feature, env var, or structural change.
With a clear description of what was changed and why. Reference the issue number if applicable.
Coding Standards
| Standard | Rule |
|---|---|
| Types | All data models defined in src/types/grc.ts as TypeScript interfaces — never inline in components |
| AI prompts | All system prompts in src/lib/prompts.ts as named const exports — not inside component functions |
| Colours | All colours referenced via CSS custom properties — no hardcoded hex strings in components |
| 'use client' | Apply only to components that use React hooks or browser APIs — not sprayed across the codebase |
| API routes | All API routes must independently verify mfaVerified: true — never rely solely on middleware |
| TypeScript | Strict mode always. tsc --noEmit must pass with zero errors before any PR is merged |
| Data | No hard-coded data in components — all static data in src/lib/constants.ts |