# SurveyStronghold — Complete Implementation Guide

Multi-tenant SaaS survey platform (SurveyMonkey-style). Prototype: `surveystronghold.html`.  
Built with **Next.js 14 App Router**, **TypeScript**, **MySQL + Sequelize**, **NextAuth.js**, **Tailwind CSS**, **Recharts**, **Stripe**, **pdf-lib**, **papaparse**.

---

## Table of Contents

1. [Tech Stack](#1-tech-stack)
2. [Project Structure](#2-project-structure)
3. [Setup & Environment](#3-setup--environment)
4. [Database Schema](#4-database-schema)
5. [Architecture Patterns](#5-architecture-patterns)
6. [Phase 1 — Foundation](#6-phase-1--foundation)
7. [Phase 2 — Auth & Client Workspace](#7-phase-2--auth--client-workspace)
8. [Phase 3 — Survey Builder](#8-phase-3--survey-builder)
9. [Phase 4 — Distribution & Public Filler](#9-phase-4--distribution--public-filler)
10. [Phase 5 — Analytics & Export](#10-phase-5--analytics--export)
11. [Phase 6 — Admin Panel & Billing](#11-phase-6--admin-panel--billing)
12. [Phase 7 — Polish & Demo Data](#12-phase-7--polish--demo-data)
13. [API Routes](#13-api-routes)
14. [Security Model](#14-security-model)
15. [Known Limitations](#15-known-limitations)
16. [Smoke Test Checklist](#16-smoke-test-checklist)

---

## 1. Tech Stack

| Layer | Technology |
|-------|------------|
| Framework | Next.js 14.2 (App Router, Server Actions, Middleware) |
| Language | TypeScript 5 |
| Database | MySQL 8 via Sequelize 6 |
| Auth | NextAuth.js 4 (JWT strategy) |
| Styling | Tailwind CSS 3 + custom design tokens |
| Forms | react-hook-form + Zod validation |
| Charts | Recharts 3 |
| Payments | Stripe (Checkout, Portal, Webhooks) |
| Email | Nodemailer (SMTP) |
| Export | papaparse (CSV), pdf-lib (PDF) |
| QR Codes | qrcode |
| Drag & Drop | @dnd-kit |

---

## 2. Project Structure

```
src/
├── app/                          # Next.js App Router pages
│   ├── login/                    # Login page
│   ├── register/                 # Registration page
│   ├── onboarding/               # Workspace setup wizard
│   ├── forgot-password/          # Forgot password (UI only)
│   ├── app/                      # Client workspace (protected)
│   │   ├── dashboard/            # Survey list
│   │   ├── billing/              # Plans & invoices
│   │   ├── audience/             # Placeholder
│   │   └── surveys/[surveyId]/
│   │       ├── build/            # Survey builder
│   │       ├── distribute/       # Collector center
│   │       └── results/          # Analytics dashboard
│   ├── admin/                    # Super admin panel
│   │   ├── overview/
│   │   ├── clients/
│   │   ├── surveys/
│   │   ├── revenue/
│   │   └── settings/
│   ├── s/[surveySlug]/           # Public survey filler
│   └── api/                      # API routes (auth, stripe, export, qr)
├── components/
│   ├── features/                 # Feature-specific UI
│   │   ├── auth/
│   │   ├── surveys/
│   │   ├── filler/
│   │   ├── analytics/
│   │   ├── admin/
│   │   └── billing/
│   ├── shared/                   # AppShell, PageLoading, PageError, etc.
│   └── ui/                       # Button, Input, Card, Select, Toggle
├── config/                       # plans.ts, question-types.ts, theme.ts
├── hooks/                        # useDebouncedAutosave
├── lib/
│   ├── actions/                  # Server Actions (mutations)
│   ├── queries/                  # Read-only DB queries
│   ├── auth/                     # NextAuth, require-auth, guards
│   ├── db/                       # Sequelize models, migrations, seeders
│   ├── email/                    # Nodemailer mailer
│   ├── stripe/                   # Stripe client
│   ├── utils/                    # Helpers (logic, validation, slugify)
│   └── validation/               # Zod schemas
├── types/                        # TypeScript interfaces
└── middleware.ts                 # Route protection
```

---

## 3. Setup & Environment

### Prerequisites

- Node.js 20+
- MySQL 8 (local or Docker)

### Quick Start

```bash
cp .env.example .env
# Edit .env — set DATABASE_URL, NEXTAUTH_SECRET, IP_HASH_SALT

# Option A: Docker MySQL
docker compose up -d

# Option B: Local MySQL — create database `surveystronghold`

npm install
npm run db:migrate
npm run db:seed          # Optional demo data
npm run dev              # http://localhost:3000
```

### Environment Variables

| Variable | Required | Purpose |
|----------|----------|---------|
| `DATABASE_URL` | Yes | MySQL connection string |
| `NEXTAUTH_SECRET` | Yes | JWT signing secret |
| `NEXTAUTH_URL` | Yes | App base URL for auth callbacks |
| `NEXT_PUBLIC_APP_URL` | Yes | Public app URL (links, QR, email) |
| `IP_HASH_SALT` | Recommended | Salt for respondent IP hashing |
| `GOOGLE_CLIENT_ID/SECRET` | Optional | Google OAuth |
| `STRIPE_*` | Optional | Billing (Checkout, webhooks) |
| `SMTP_*` | Optional | Email campaigns (overridden by admin settings) |

### NPM Scripts

| Script | Description |
|--------|-------------|
| `npm run dev` | Development server |
| `npm run build` | Production build |
| `npm run lint` | ESLint |
| `npm run db:migrate` | Run Sequelize migrations |
| `npm run db:seed` | Seed demo data |
| `npm run db:migrate:undo` | Rollback last migration |

---

## 4. Database Schema

11 migrations + 1 unique index migration. All tables use UUID primary keys.

### Entity Relationship

```
Workspace (tenant)
  ├── Users (many)
  ├── Surveys (many)
  │     ├── Questions (many)
  │     │     └── LogicRules (many) — skip/end branching
  │     ├── Collectors (many) — distribution channels
  │     └── Respondents (many)
  │           └── Answers (many)
  └── Invoices (many)

SystemSettings (key-value platform config)
AuditLogs (actor, workspace, action, metadata)
```

### Tables Summary

| Table | Key Fields |
|-------|------------|
| **Workspaces** | `name`, `planTier`, `subscriptionStatus`, `stripeCustomerId`, `stripeSubscriptionId`, `responseQuotaUsed/Limit`, `isSuspended`, `billingCycle`, branding fields |
| **Users** | `email`, `passwordHash`, `role` (super_admin/client), `workspaceId`, `isActive`, `avatarUrl` |
| **Surveys** | `workspaceId`, `title`, `status` (draft/active/closed), `welcomeScreenConfig`, `thankYouScreenConfig`, `themeConfig`, `redirectUrl`, `publishedAt`, `closedAt` |
| **Questions** | `surveyId`, `type`, `title`, `position`, `isRequired`, `optionsConfig`, `validationConfig`, `randomizeOptions` |
| **LogicRules** | `questionId`, `conditionType`, `conditionValue`, `action`, `targetQuestionId` |
| **Collectors** | `surveyId`, `type`, `slug`, `isActive`, `config` |
| **Respondents** | `surveyId`, `collectorId`, `status`, `startedAt`, `completedAt`, `timeToCompleteSeconds`, `ipHash`, `userAgent` |
| **Answers** | `respondentId`, `questionId`, `value` (JSON) |
| **Invoices** | `workspaceId`, `amount`, `currency`, `status`, `stripeInvoiceId` (unique) |
| **SystemSettings** | `key`, `value` (JSON) |
| **AuditLogs** | `actorUserId`, `workspaceId`, `action`, `metadata` |

### Migrations

```
20240812000001-create-workspaces.js
20240812000002-create-users.js
20240812000003-create-surveys.js
20240812000004-create-questions.js
20240812000005-create-logic-rules.js
20240812000006-create-collectors.js
20240812000007-create-respondents.js
20240812000008-create-answers.js
20240812000009-create-invoices.js
20240812000010-create-system-settings.js
20240812000011-create-audit-logs.js
20240812000012-unique-stripe-invoice-id.js
```

---

## 5. Architecture Patterns

### Multi-Tenant Scoping

Every client data access is scoped via `workspaceId` from the authenticated session. Surveys are fetched with `getScopedSurvey(surveyId, workspaceId)` which enforces `WHERE id = ? AND workspaceId = ?`.

### Server Actions

All mutations go through `"use server"` actions in `src/lib/actions/` with Zod validation. Return type:

```typescript
type ActionResult<T = void> =
  | { success: true; data?: T }
  | { success: false; error: string };
```

### Lazy DB Initialization

Sequelize connects lazily via `initDb()` — no DB connection during Next.js static build. Global singleton prevents duplicate initialization.

### Auth Guards

| Guard | Use Case |
|-------|----------|
| `requireAuth()` | Any authenticated user; re-checks `isActive` from DB; enforces maintenance mode |
| `requireClientWorkspace()` | Client app features; requires workspace, checks suspension |
| `requireSuperAdmin()` | Admin panel; always re-validates role from DB (not stale JWT) |
| `requireSurveyExport()` | CSV/PDF export; scoped survey + suspension check |

### JWT Session Sync

On `trigger === "update"`, JWT callback re-reads user from DB — never trusts client-supplied `workspaceId`. Google OAuth users are looked up/created by email on sign-in.

### Post-Auth Redirect

```typescript
getPostAuthRedirectPath(user):
  super_admin + no workspace → /admin/overview
  has workspace            → /app/dashboard
  no workspace (client)    → /onboarding
```

---

## 6. Phase 1 — Foundation

### Design System

**File:** `src/config/theme.ts` → wired into `tailwind.config.ts`

| Token | Usage |
|-------|-------|
| `primary`, `accent`, `navy` | Brand colors |
| `bg`, `card`, `text`, `muted`, `border` | Layout |
| `success`, `warning`, `danger` | Status colors |
| Custom radius, shadow, fonts (sans + mono) | UI consistency |

### Database Layer

- **Models:** `src/lib/db/models/` — one file per entity
- **Associations:** defined in `src/lib/db/index.ts`
- **Migrations:** Sequelize CLI, `src/lib/db/migrations/`
- **Config:** `.sequelizerc` points to migrations folder

### Next.js Config

**File:** `next.config.mjs`

```javascript
serverComponentsExternalPackages: ["mysql2", "sequelize"]
```

Prevents bundling issues with native MySQL driver.

### Middleware

**File:** `src/middleware.ts`

Protects `/admin/*`, `/app/*`, `/onboarding/*`:

| Route | Rule |
|-------|------|
| `/admin/*` | `role === super_admin` |
| `/app/*` | `role === client OR super_admin`; client needs `workspaceId`; super_admin without workspace → `/admin/overview` |
| `/onboarding/*` | `role === client` only |

Public routes (`/`, `/login`, `/register`, `/s/[slug]`) are unprotected.

---

## 7. Phase 2 — Auth & Client Workspace

### 7.1 Registration

| Item | Detail |
|------|--------|
| **Route** | `/register` |
| **UI** | `RegisterForm.tsx` — name, email, password |
| **Action** | `registerUser()` in `src/lib/actions/auth.ts` |
| **Validation** | `registerSchema` — email, password min 8 chars |
| **Behavior** | Creates `User` with `role: client`, `workspaceId: null` |
| **Guard** | Blocks if `platform.signupsEnabled === false` |

After registration, user signs in manually (no auto-login).

### 7.2 Login

| Item | Detail |
|------|--------|
| **Route** | `/login` |
| **Providers** | Credentials (email/password) + Google OAuth (if env configured) |
| **UI** | `LoginForm.tsx` — remember me checkbox, forgot password link |
| **Open Redirect Protection** | `safeRedirectPath()` on `callbackUrl` query param |
| **Post-login** | Redirect via `getPostAuthRedirectPath()` |

**Credentials flow:** `findUserByEmail` → `bcryptjs` verify → update `lastLoginAt` → JWT.

**Google flow:** Auto-create user on first sign-in if email not found; role defaults to `client`.

### 7.3 Onboarding

| Item | Detail |
|------|--------|
| **Route** | `/onboarding` |
| **UI** | `OnboardingWizard.tsx` — 3-step wizard |
| **Fields** | Company name, team size, survey goals |
| **Action** | `completeOnboarding()` |
| **Transaction** | Row-lock user → create `Workspace` (free plan, 100 quota) → link `user.workspaceId` |
| **After** | Client calls `session.update()` → JWT re-syncs workspaceId from DB |

### 7.4 Forgot Password

| Item | Detail |
|------|--------|
| **Route** | `/forgot-password` |
| **Status** | **UI only** — no backend email/reset token flow implemented |
| **Behavior** | Shows honest message that reset is not yet available |

### 7.5 Dashboard

| Item | Detail |
|------|--------|
| **Route** | `/app/dashboard` |
| **Layout** | `AppShell` + `AppSidebar` |
| **Data** | `getWorkspaceSurveys(workspaceId, filters)` |
| **Features** | Search by title, filter by status (all/draft/active/closed), response counts |
| **Actions** | Create survey, duplicate, delete, status change via `SurveyTable.tsx` |

**Create survey:** `createSurvey()` → draft with default welcome screen config → redirect to builder.

---

## 8. Phase 3 — Survey Builder

### 8.1 Builder Page

| Item | Detail |
|------|--------|
| **Route** | `/app/surveys/[surveyId]/build` |
| **Data** | `getBuilderSurvey(surveyId, workspaceId)` — questions + logic rules |
| **UI** | `SurveyBuilder.tsx` — canvas + question library + settings panel |

### 8.2 Question Types (8)

Defined in `src/config/question-types.ts`:

| Type | Label | Default Config |
|------|-------|----------------|
| `multiple_choice` | Multiple Choice | choices array |
| `rating` | Rating Scale | minRating 1, maxRating 5 |
| `nps` | NPS Score | 0–10 scale |
| `open_text` | Open Text | placeholder |
| `matrix` | Matrix / Likert | rows + columns |
| `file_upload` | File Upload | (filename only in filler) |
| `dropdown` | Dropdown | choices array |
| `date_time` | Date / Time | datetime-local input |

### 8.3 Builder Actions

**File:** `src/lib/actions/questions.ts`

| Action | Description |
|--------|-------------|
| `addQuestion(surveyId, type)` | Insert at end with defaults from config |
| `updateQuestion(questionId, input)` | Title, required, options, validation |
| `reorderQuestions(surveyId, orderedIds)` | DnD position update |
| `deleteQuestion(questionId)` | Remove question + cascade logic rules |
| `upsertLogicRule(input)` | Replace all rules for a question (destroy + create) |
| `updateSurveyWelcome(input)` | Welcome screen title/description/button |

### 8.4 Drag & Drop

- **Library:** `@dnd-kit/core` + `@dnd-kit/sortable`
- **Component:** `QuestionCard.tsx` in sortable context
- **Autosave:** `useDebouncedAutosave` hook debounces title/option edits

### 8.5 Skip Logic / Branching

**Model:** `LogicRules` table linked to `questionId`.

| Condition Type | Meaning |
|----------------|---------|
| `equals` | Answer equals `conditionValue` |
| `not_equals` | Answer does not equal value |
| `any_answer` | Any non-empty answer |

| Action | Meaning |
|--------|---------|
| `skip_to_question` | Jump to `targetQuestionId` |
| `end_survey` | End survey early |

**UI:** `QuestionSettingsPanel.tsx` — add/edit/remove rules per question.

**Engine:** `src/lib/utils/survey-logic.ts`

- `resolveNextQuestionId()` — evaluates rules, falls back to next in order
- `getAnsweredQuestionPath()` — reconstructs actual path taken (used for validation)

### 8.6 Publish

| Item | Detail |
|------|--------|
| **Action** | `publishSurvey(surveyId)` |
| **Behavior** | Sets `status: active`, `publishedAt`; creates default `web_link` collector with slug `{title-slug}-{random}` |
| **UI** | `SurveyStatusSelect.tsx` or publish button in builder |

### 8.7 Duplicate Survey

| Item | Detail |
|------|--------|
| **Action** | `duplicateSurvey(surveyId)` |
| **Copies** | Survey metadata, all questions, all logic rules (with remapped question IDs) |
| **Status** | New copy is always `draft` |

### 8.8 Survey Status Lifecycle

```
draft → active → closed
         ↑__________|  (re-open)
```

`transitionSurveyStatus()` handles `publishedAt` / `closedAt` timestamps.

---

## 9. Phase 4 — Distribution & Public Filler

### 9.1 Collector Center

| Item | Detail |
|------|--------|
| **Route** | `/app/surveys/[surveyId]/distribute` |
| **UI** | `CollectorCenter.tsx` — tabbed interface |

| Tab | Feature |
|-----|---------|
| **Web Link** | Copy public URL `/s/{slug}` |
| **Embed** | `<iframe>` snippet with `?embed=1` |
| **QR Code** | `/api/qr?url=...` — same-origin URL validation |
| **Email** | Send invites via SMTP (max 50 recipients per campaign) |

**Email action:** `sendEmailCampaign()` — parses comma/newline emails, HTML-escapes body, creates `email_campaign` collector if needed.

### 9.2 Public Filler

| Item | Detail |
|------|--------|
| **Route** | `/s/[surveySlug]` |
| **Data** | `getPublicSurveyBySlug(slug)` — active collector + active survey + questions + logic |
| **UI** | `SurveyFiller.tsx` |
| **Submit** | `submitSurveyResponse()` server action |

### 9.3 Filler UX Flow

```
Welcome Screen (optional)
    ↓
Questions (one at a time, progress bar)
    ↓  Back / Next / Enter keyboard shortcuts
    ↓  Number keys for MC/NPS quick select
Thank You Screen
    ↓  Optional redirect after 3s
```

**Phases:** `welcome` → `questions` → `thanks`

**Embed mode:** `?embed=1` removes outer padding/background.

### 9.4 Submit Validation

**Server:** `src/lib/actions/filler.ts`

1. Validate slug ↔ surveyId ↔ collectorId match
2. Build answer map from payload
3. Compute `visiblePath` via `getAnsweredQuestionPath()` — only questions on actual branch path
4. Validate each visible question (required, format)
5. Reject answers for questions NOT on visible path
6. Transaction with row-lock on `Workspace`:
   - Check `isSuspended`
   - Check `responseQuotaUsed < responseQuotaLimit`
   - Create `Respondent` + `Answer` rows
   - Increment quota

**Client:** `SurveyFiller` submits only path-visible answers (prevents stale answers after back-navigation).

### 9.5 Privacy

- Respondent IP stored as SHA-256 hash via `hashIp()` + `IP_HASH_SALT`
- User-agent stored as plain text for analytics

### 9.6 Post-Survey Redirect

Survey `redirectUrl` sanitized by `safeSurveyRedirectUrl()`:
- Relative paths (`/thank-you`) allowed
- Absolute `http://` and `https://` allowed
- `javascript:`, `data:`, etc. blocked

---

## 10. Phase 5 — Analytics & Export

### 10.1 Analytics Dashboard

| Item | Detail |
|------|--------|
| **Route** | `/app/surveys/[surveyId]/results` |
| **Query** | `getSurveyAnalytics(surveyId, workspaceId, filters)` |
| **UI** | `AnalyticsDashboard.tsx` |

### 10.2 Summary Metrics

| Metric | Source |
|--------|--------|
| Total responses | Completed respondents count |
| Completion rate | completed / (completed + abandoned) |
| Avg time to complete | Mean `timeToCompleteSeconds` |
| NPS score | Promoters (9–10) minus Detractors (0–6) |

### 10.3 Charts (Recharts)

| Chart | Component | Data |
|-------|-----------|------|
| Responses over time | `ResponsesOverTimeChart` | Daily completion counts |
| Traffic sources | `TrafficSourceChart` | By collector type |
| Choice bars | `ChoiceBarsChart` | MC/dropdown distribution |
| NPS breakdown | `NpsChart` | Promoters/passives/detractors |
| Rating distribution | `RatingChart` | Star rating histogram |
| Word cloud | `WordCloud` | Open text token frequency |

### 10.4 Filters

**URL params:** `?range=7d|30d|90d|all&status=all|completed|in_progress|abandoned`

Parsed by `parseAnalyticsFilters()` in `analytics-queries.ts`.

### 10.5 Per-Question Analytics

Each question gets a `QuestionResultCard` with type-specific visualization:
- **multiple_choice / dropdown:** horizontal bar chart
- **nps:** NPS gauge + breakdown
- **rating:** star distribution
- **open_text:** word cloud + sample responses
- **matrix:** row × column heatmap-style counts
- **date_time / file_upload:** raw value list

### 10.6 Individual Respondent View

| Item | Detail |
|------|--------|
| **Route** | `/app/surveys/[surveyId]/results/respondent/[respondentId]` |
| **Query** | `getRespondentDetail()` |
| **UI** | Question titles + formatted answer values |

### 10.7 Export

| Format | Route | Library |
|--------|-------|---------|
| **CSV** | `GET /api/surveys/[surveyId]/export/csv` | papaparse |
| **PDF** | `GET /api/surveys/[surveyId]/export/pdf` | pdf-lib |

**Auth:** `requireSurveyExport()` — workspace scope + suspension check + DB user re-validation.

**PDF safety:** `pdfSafeText()` strips/replaces Unicode characters that crash pdf-lib (em dashes, etc.).

**CSV columns:** Respondent ID, status, timestamps, time-to-complete, then one column per question.

---

## 11. Phase 6 — Admin Panel & Billing

### 11.1 Admin Panel

| Route | Page | Features |
|-------|------|----------|
| `/admin/overview` | Platform KPIs | Total clients, surveys, responses, MRR; revenue chart; live audit log stream |
| `/admin/clients` | Client management | List workspaces, suspend/unsuspend, change plan |
| `/admin/surveys` | All surveys | Cross-tenant survey list with status |
| `/admin/revenue` | Revenue analytics | MRR trend, plan breakdown |
| `/admin/settings` | System settings | Signups toggle, maintenance mode, SMTP config |

**Layout:** `AdminShell` + `AdminSidebar` (separate from client `AppShell`).

**Guard:** Every admin page calls `requireSuperAdmin()` which re-checks `role === super_admin` AND `isActive` from DB.

### 11.2 Admin Actions

**File:** `src/lib/actions/admin.ts`

| Action | Description |
|--------|-------------|
| `toggleWorkspaceSuspension(workspaceId)` | Flip `isSuspended` — blocks filler + client app |
| `adminSetWorkspacePlan({ workspaceId, planTier })` | Manual plan override (enterprise) |
| `updatePlatformSettings(input)` | Save signups/maintenance/SMTP to `SystemSettings` |

### 11.3 Platform Settings

**File:** `src/lib/queries/system-settings-queries.ts`

| Key | Default | Effect |
|-----|---------|--------|
| `platform.signups_enabled` | `true` | Blocks registration when false |
| `platform.maintenance_mode` | `false` | Non-admin users blocked in `requireAuth()` |
| `smtp` | env vars | Nodemailer config for email campaigns |

### 11.4 Billing (Client)

| Item | Detail |
|------|--------|
| **Route** | `/app/billing` |
| **UI** | `BillingPlans.tsx` + `InvoiceTable.tsx` |

### 11.5 Plan Tiers

**File:** `src/config/plans.ts`

| Plan | Price | Response Quota | Notes |
|------|-------|----------------|-------|
| **Free** | $0 | 100/mo | 3 active surveys, basic types |
| **Pro** | $39/mo or $372/yr | 10,000/mo | Logic jump, branding, unlimited surveys |
| **Enterprise** | Contact sales | Unlimited | SSO, white-label (manual admin assignment) |

### 11.6 Stripe Integration

| Action | Description |
|--------|-------------|
| `createCheckoutSession({ planTier, billingCycle })` | Stripe Checkout for Pro upgrade |
| `createBillingPortalSession()` | Manage subscription / payment method |
| `downgradeToFree()` | Cancel Stripe subscription + apply free plan |

**Webhook:** `POST /api/stripe/webhook`

| Event | Handler |
|-------|---------|
| `checkout.session.completed` | Save customer/subscription IDs; upgrade plan only if `payment_status === paid` |
| `checkout.session.async_payment_succeeded` | Upgrade plan for delayed payments |
| `customer.subscription.updated` | Sync subscription status |
| `customer.subscription.deleted` | Downgrade to free |
| `invoice.paid` | Record invoice (deduped by `stripeInvoiceId` unique index) |
| `invoice.payment_failed` | Set workspace `past_due` |

---

## 12. Phase 7 — Polish & Demo Data

### 12.1 Loading / Error / Empty States

| Component | Usage |
|-----------|-------|
| `PageLoading.tsx` | Skeleton spinner for route transitions |
| `PageError.tsx` | Error boundary UI with retry |
| `EmptyState.tsx` | No-data placeholders |

**Route-level files:**

```
src/app/app/loading.tsx
src/app/app/error.tsx
src/app/app/not-found.tsx
src/app/admin/loading.tsx
src/app/admin/error.tsx
src/app/app/dashboard/loading.tsx
src/app/app/billing/loading.tsx
src/app/app/surveys/[surveyId]/build/loading.tsx
src/app/app/surveys/[surveyId]/results/loading.tsx
src/app/s/[surveySlug]/error.tsx
src/app/s/[surveySlug]/not-found.tsx
```

### 12.2 Audience Page

**Route:** `/app/audience` — placeholder with empty state ("Coming soon").

### 12.3 Demo Seeder

**File:** `src/lib/db/seeders/20240812000001-demo-data.js`

| Item | Value |
|------|-------|
| **Login** | `demo@surveystronghold.com` / `Demo1234!` |
| **Workspace** | Demo company on Pro plan |
| **Survey** | "Customer Satisfaction — Q3 2026" (active) |
| **Questions** | NPS, multiple choice, rating, open text |
| **Respondents** | ~200 with realistic weighted answers |
| **Collectors** | web_link, embed, qr_code |
| **Idempotent** | Skips if demo email already exists |

**Run:** `npm run db:seed`

### 12.4 Demo Admin Seeder

**File:** `src/lib/db/seeders/20240812000002-demo-admin.js`

| Item | Value |
|------|-------|
| **Login** | `admin@surveystronghold.com` / `Admin1234!` |
| **Role** | `super_admin`, no workspace |
| **Landing page** | `/admin/overview` (redirected automatically after login) |
| **Idempotent** | Skips if admin email already exists |

Uses the same `/login` form as clients — role is looked up from the DB after
credentials are verified, so there is no separate admin login URL. The login
form now redirects super admins straight to `/admin/overview` and clients to
`/app/dashboard` (or `/onboarding` if they haven't finished setup).

**Run:** `npm run db:seed` (runs alongside the client demo seeder)

### 12.5 Docker Compose

**File:** `docker-compose.yml` — MySQL 8.0 on port 3306 with healthcheck.

---

## 13. API Routes

| Method | Route | Purpose | Auth |
|--------|-------|---------|------|
| GET/POST | `/api/auth/[...nextauth]` | NextAuth handlers | Public |
| GET | `/api/qr?url=` | Generate QR code PNG | Public (same-origin URL only) |
| POST | `/api/stripe/webhook` | Stripe event handler | Stripe signature |
| GET | `/api/surveys/[surveyId]/export/csv` | Download CSV | Session + workspace scope |
| GET | `/api/surveys/[surveyId]/export/pdf` | Download PDF | Session + workspace scope |

All other mutations use **Server Actions** (no REST API).

---

## 14. Security Model

### Authentication

- JWT sessions (not database sessions)
- Passwords hashed with bcryptjs
- Google OAuth optional; auto-provisions client users
- Deactivated users (`isActive: false`) blocked on every request via DB re-check

### Authorization

- **Tenant isolation:** All queries filter by `workspaceId`
- **Survey scope:** `getScopedSurvey()` prevents cross-tenant access
- **Admin:** DB role re-validation (JWT role alone is not trusted for sensitive ops)
- **Export:** Suspended workspaces blocked
- **Maintenance mode:** Non-admins redirected to login

### Input Validation

- All server actions use Zod schemas in `src/lib/validation/`
- Email campaign body HTML-escaped before sending
- Filler answers validated per question type
- Open redirect blocked on login via `safeRedirectPath()`
- Post-survey redirect sanitized via `safeSurveyRedirectUrl()`
- QR API only accepts same-origin URLs

### Data Privacy

- IP addresses hashed before storage
- No PII required from survey respondents
- Audit logs track admin actions with metadata

### Quota & Abuse

- Response quota enforced in locked transaction (fail-closed)
- Email campaigns capped at 50 recipients
- No public filler rate limiting yet (see limitations)

---

## 15. Known Limitations

| Item | Status |
|------|--------|
| Forgot password backend | Not implemented (UI only) |
| Audience management | Placeholder page |
| Integrations | Not implemented |
| Public filler rate limiting | No IP throttle / CAPTCHA |
| File upload in filler | Records filename only, no actual upload |
| Enterprise self-serve checkout | Contact sales / admin manual assignment |
| Middleware admin check | Uses JWT role; pages re-verify from DB |
| Custom domain / white-label | Schema fields exist, UI not built |
| SSO | Not implemented |

---

## 16. Smoke Test Checklist

Run with MySQL up (`docker compose up -d` + migrate + seed):

### Auth Flow
- [ ] Register new account → redirects to onboarding
- [ ] Complete onboarding → dashboard loads
- [ ] Log out → log in → lands on dashboard
- [ ] Demo login works: `demo@surveystronghold.com` / `Demo1234!`

### Survey Lifecycle
- [ ] Create survey from dashboard
- [ ] Add all 8 question types in builder
- [ ] Add skip logic (equals → skip to Q3)
- [ ] Reorder questions via drag & drop
- [ ] Publish survey → status becomes active

### Distribution & Filler
- [ ] Copy web link from distribute page
- [ ] Open in incognito → welcome screen → answer all questions → thank you
- [ ] Test branching: answer triggers skip → submit succeeds
- [ ] Test back navigation + different branch → submit succeeds
- [ ] QR code loads image
- [ ] Email tab sends (requires SMTP config)

### Analytics
- [ ] Results page shows charts and metrics
- [ ] Filter by date range and status
- [ ] Click respondent → detail view
- [ ] Export CSV downloads
- [ ] Export PDF downloads

### Duplicate & Delete
- [ ] Duplicate survey → copy has questions + logic rules
- [ ] Delete survey → removed from dashboard

### Billing
- [ ] Billing page shows current plan and quota
- [ ] Stripe checkout opens (requires Stripe keys)

### Admin (requires super_admin role)
Log in at `/login` with the seeded admin account: `admin@surveystronghold.com`
/ `Admin1234!` (created by `npm run db:seed`). To promote any other existing
user instead, run:
```sql
UPDATE Users SET role = 'super_admin' WHERE email = 'your@email.com';
```
- [ ] `/admin/overview` loads KPIs
- [ ] Suspend client → their filler returns unavailable
- [ ] Toggle maintenance mode → non-admin blocked
- [ ] Update SMTP settings

### Super Admin Edge Case
- [ ] Super admin without workspace → login redirects to `/admin/overview` (no loop)

---

## Appendix: Key File Reference

| Feature | Primary Files |
|---------|---------------|
| Auth | `auth-options.ts`, `require-auth.ts`, `verify-session-user.ts`, `middleware.ts` |
| Surveys CRUD | `actions/surveys.ts`, `queries/survey-queries.ts` |
| Builder | `SurveyBuilder.tsx`, `actions/questions.ts`, `queries/builder-queries.ts` |
| Logic engine | `utils/survey-logic.ts` |
| Filler | `SurveyFiller.tsx`, `actions/filler.ts`, `queries/filler-queries.ts` |
| Collectors | `CollectorCenter.tsx`, `actions/collectors.ts` |
| Analytics | `AnalyticsDashboard.tsx`, `queries/analytics-queries.ts` |
| Export | `api/surveys/.../export/csv`, `.../pdf`, `require-survey-export.ts` |
| Billing | `actions/billing.ts`, `queries/billing-queries.ts`, `config/plans.ts` |
| Admin | `actions/admin.ts`, `queries/admin-queries.ts`, `admin/*` |
| Settings | `queries/system-settings-queries.ts`, `SystemSettingsForm.tsx` |
| Demo seed | `seeders/20240812000001-demo-data.js` |

---

*Last updated: August 2026 — reflects all 7 implementation phases.*
