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?

DecisionReason
src/types/grc.ts is the only types fileOne source of truth. No scattered inline types. TypeScript compiler enforces contracts everywhere.
src/lib/constants.ts holds all dataComponents contain zero hard-coded data. Swap static data for real API calls in one file later.
src/lib/prompts.ts holds all AI promptsPrompts can be reviewed and updated by non-engineers without reading React code.
src/app/api/ai/route.ts is the only AI callerAPI 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 gateAuth checked in one place before any page renders. Impossible to accidentally ship an unprotected page.
(auth) route groupLogin lives in its own layout with no GRC UI chrome. Prevents the authenticated layout from wrapping the login form.

Authentication & Access Control

No self-registration. All accounts are created by administrators and distributed to authorised staff through the organisation's IAM processes.

How Login Works — Two Steps

STEP 1 — CREDENTIALS

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).

STEP 2 — MFA VERIFICATION

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.

EVERY SUBSEQUENT REQUEST

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

PropertyValueReason
StrategyJWT (httpOnly cookie)No database required; cookie inaccessible to JavaScript
Session expiry8 hoursAppropriate for business-hours tools; forces daily re-auth
MFA stateStored in JWT tokenmfaVerified: false until step 2 completes
Cookie flagshttpOnly, secure, sameSite=laxPrevents XSS token theft and CSRF
MFA brute-force5 attempts / 15 min6-digit codes have only 1M combinations
Email OTP send3 sends / 15 minPrevents email flooding attacks
Error messagesDeliberately vague at step 1Never reveals whether the email exists
TOTP window±1 step (30s tolerance)Accommodates clock skew without excessive attack surface

User Roles

RolePermissions
adminFull access — view all tabs, approve risk variances, manage incidents, trigger evidence collection, manage users
analystStandard access — view all tabs, run AI assessments, trigger evidence collection; cannot approve risk variances
viewerRead-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.

ADMINISTRATOR: SET UP TOTP FOR A NEW USER
1
Generate a TOTP secret:
node -e "console.log(require('otplib').authenticator.generateSecret())"
2
Generate the QR code URI for the user to scan:
node -e "
  const { authenticator } = require('otplib');
  const uri = authenticator.keyuri(
    'user@company.com',
    'GRC Command Center',
    'YOUR_SECRET_HERE'
  );
  console.log(uri);"
3
Paste the otpauth:// URI into a QR code generator (offline tool). Send the QR image to the user through your secure credential distribution process.
4
Add totpSecret to the user's record in AUTHORISED_USERS and redeploy.
USER: FIRST-TIME SETUP
  1. Open Google Authenticator → tap +Scan a QR code
  2. Scan the QR image your administrator sent you. Alternatively tap Enter a setup key and type the Base32 secret.
  3. An entry called "GRC Command Center" appears with a refreshing 6-digit code.
  4. Log in with email + password, then enter the 6-digit code when prompted.
Lost device?

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.

Codes expire after 10 minutes
Single-use — deleted after successful verification
Rate-limited to 3 sends per 15 minutes per IP
SMTP CONFIGURATION

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
Multi-instance note:

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

Prerequisites: Node.js 18.17+, npm, an AI provider API key (Claude or OpenAI), and an SMTP account for email OTP.
1
Clone or unzip the project
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
2
Install dependencies
npm install
3
Copy and fill in environment variables
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=claude
4
Start the development server
npm run dev

Open http://localhost:3000 — you will be redirected to /login immediately.

Available Scripts

ScriptDescription
npm run devStart development server with Turbopack (fast hot reload)
npm run buildBuild for production
npm run startStart production server (run after build)
npm run lintRun ESLint
npm run type-checkRun 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.

1
Generate a bcrypt password hash
node -e "require('bcryptjs').hash('the_initial_password', 12).then(console.log)"

This outputs: $2a$12$abcdefghijklmnopqrstuuVWXYZ0123456789...

2
Generate a TOTP secret (if using authenticator app)
node -e "console.log(require('otplib').authenticator.generateSecret())"
  # Output: JBSWY3DPEHPK3PXP (Base32 string)
3
Add the user to AUTHORISED_USERS in your .env.local
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"
    }
  ]'
4
Redeploy the application
# 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
5
Distribute credentials to the user

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

FieldTypeDescription
idstringUnique identifier — any string, must be unique across users
emailstringThe user's email address — used as their login username
namestringFull name shown in the session
roleadmin | analyst | viewerControls what the user can view and approve
hashedPasswordstringbcrypt hash of their password. Never store plaintext.
totpSecretstring | nullBase32 TOTP secret for authenticator app. null until set up.
mfaMethodtotp | emailWhich MFA method this user is configured to use
To revoke access:

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).

Replacing with a database:

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:

❌ BEFORE — MANUAL FETCH
const res = await fetch(
    'https://api.anthropic.com/v1/messages',
    {
      method: 'POST',
      headers: {
        'x-api-key': key,
        'anthropic-version': '2023-06-01',
      },
      body: JSON.stringify({
        model, max_tokens, system,
        messages: [...]
      }),
    }
  )
  const data = await res.json()
  const text = data.content
    ?.map(b => b.text).join('') ?? ''
✓ AFTER — VERCEL AI SDK
const { text } = await generateText({
    model,   // from getModel() factory
    system:  systemPrompt,
    prompt:  userMessage,
    maxTokens: 1000,
    temperature: 0.2,
  })

Provider Switching

Change two environment variables in .env.local — no code changes needed anywhere in the application.

USE ANTHROPIC CLAUDE (DEFAULT)
AI_PROVIDER=claude
  AI_MODEL=claude-sonnet-4-20250514
  ANTHROPIC_API_KEY=sk-ant-your_key
USE OPENAI GPT-4o
AI_PROVIDER=openai
  AI_MODEL=gpt-4o
  OPENAI_API_KEY=sk-your_key
USE OPENAI GPT-4o MINI (LOWER COST)
AI_PROVIDER=openai
  AI_MODEL=gpt-4o-mini
  OPENAI_API_KEY=sk-your_key
USE CUSTOM / SELF-HOSTED LLM
AI_PROVIDER=custom
  AI_MODEL=your-model-name
  OPENAI_API_KEY=not-needed
  CUSTOM_LLM_ENDPOINT=https://your-endpoint/v1/chat/completions
Adding a new provider:
  1. Install: npm install @ai-sdk/your-provider
  2. Add a case in getModel() in src/app/api/ai/route.ts
  3. Add env vars to .env.example
  4. Update AIProvider type in src/types/grc.ts

Hosting Guide

All hosting options require the full set of env vars — AI provider keys, AUTH_SECRET, AUTHORISED_USERS, and SMTP settings.

Option A: Vercel (Recommended — Free)

Zero configuration — Vercel auto-detects Next.js and handles all server-side routes including /api/auth/* and /api/ai.

1
Push to GitHub
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
2
Deploy with Vercel CLI
npm install -g vercel
  vercel

Or go to vercel.com → New Project → import your GitHub repo.

3
Add environment variables in Vercel dashboard

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
4
Redeploy to production
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-stopped
docker compose up -d

Option D: AWS, GCP, Azure

PlatformServiceNotes
AWSAWS AmplifyConnect GitHub repo. Auto-detects Next.js. Add env vars in Amplify console.
AWSECS / FargateUse Docker image. Inject secrets via AWS Secrets Manager references in task definition.
AWSApp RunnerSimplest managed option. Connect container registry. Set env vars in service config.
GCPCloud Rungcloud run deploy grc --image gcr.io/project/grc --set-secrets AUTH_SECRET=grc-secret:latest
GCPFirebase HostingSupports Next.js SSR via Cloud Functions.
AzureContainer AppsDeploy Docker image. Set env vars as secrets in container environment config.
AzureApp ServiceNode.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.

Critical rule: Variables prefixed NEXT_PUBLIC_ are bundled into client-side JavaScript and visible to anyone. Never put API keys, passwords, AUTH_SECRET, or AUTHORISED_USERS in a NEXT_PUBLIC_ variable.

Authentication

VariableRequiredExampleDescription
AUTH_SECRETYES32-char random stringSigns JWT tokens. Generate: openssl rand -base64 32. Keep absolutely secret.
NEXTAUTH_URLYEShttp://localhost:3000Full URL of your deployment. Must match exactly — mismatches break callbacks.
AUTHORISED_USERSYESJSON arraySee "Adding Users" section for the full schema. Paste as a single line on Vercel.
TOTP_ISSUERNoGRC Command CenterName shown in the authenticator app. Defaults to "GRC Command Center".

Email / SMTP (for Email OTP MFA)

VariableRequiredExampleDescription
SMTP_HOSTYES*smtp.gmail.comSMTP server hostname. *Required when using email OTP.
SMTP_PORTYES*587587 for STARTTLS; 465 for SSL.
SMTP_SECURENofalsetrue for SSL/port 465; false for STARTTLS/port 587.
SMTP_USERYES*you@company.comSMTP username / email address.
SMTP_PASSYES*app_password_hereSMTP password or App Password. Never your main account password.
SMTP_FROM_NAMENoGRC Command CenterDisplay name in the From field of OTP emails.

AI Provider

VariableRequiredExampleDescription
AI_PROVIDERYESclaudeActive provider: claude, openai, or custom.
AI_MODELYESclaude-sonnet-4-20250514Model identifier for the active provider.
AI_MAX_TOKENSNo1000Max completion tokens. Defaults to 1000.
ANTHROPIC_API_KEYIf claudesk-ant-...From console.anthropic.com.
OPENAI_API_KEYIf openaisk-...From platform.openai.com.
CUSTOM_LLM_ENDPOINTIf customhttps://...OpenAI-compatible endpoint for self-hosted LLMs.

Public (Browser-Visible — NO Secrets)

VariableRequiredExampleDescription
NEXT_PUBLIC_AI_PROVIDERNoclaudeShows 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.

1
Fork the repository and create a feature branch
git checkout -b feature/your-feature-name
2
Follow the existing patterns
  • 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
3
Run checks — both must pass before submitting
npm run type-check   # tsc --noEmit — must be zero errors
  npm run lint         # ESLint — must be zero errors
4
Test AI features with at least Claude AND OpenAI before submitting

Provider-agnostic behaviour must be verified. Switch AI_PROVIDER and confirm the feature works identically with both providers.

5
Test the full login flow

Credentials → TOTP → dashboard → session expiry. Ensure mfaVerified: false sessions are correctly blocked from all routes.

6
Update the documentation

Update README.md and the relevant web page for any new feature, env var, or structural change.

7
Submit a pull request

With a clear description of what was changed and why. Reference the issue number if applicable.

Coding Standards

StandardRule
TypesAll data models defined in src/types/grc.ts as TypeScript interfaces — never inline in components
AI promptsAll system prompts in src/lib/prompts.ts as named const exports — not inside component functions
ColoursAll 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 routesAll API routes must independently verify mfaVerified: true — never rely solely on middleware
TypeScriptStrict mode always. tsc --noEmit must pass with zero errors before any PR is merged
DataNo hard-coded data in components — all static data in src/lib/constants.ts