# Kaboot: Account-Based Quiz Persistence with Authentik + SQLite
## Overview
Add user accounts via Authentik (OIDC) and persist quizzes to SQLite database. Users can save custom and AI-generated quizzes to their account and load them later.
## Architecture
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Docker Compose Stack │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ Authentik │ │ PostgreSQL │ │ Redis │ │ Kaboot │ │
│ │ (IdP) │◀───│ (Authentik │ │ (Authentik │ │ Backend │ │
│ │ :9000 │ │ DB) │ │ Cache) │ │ :3001 │ │
│ └──────┬───────┘ └──────────────┘ └──────────────┘ └─────┬─────┘ │
│ │ │ │
│ │ OIDC/OAuth2 SQLite │ │
│ │ │ │ │
│ ┌──────▼───────────────────────────────────────────────▼─────────▼─────┐ │
│ │ Kaboot Frontend (Vite) │ │
│ │ :5173 │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## Phase 1: Infrastructure Setup
### 1.1 Docker Compose Configuration
- [x] Create `docker-compose.yml` with all services:
- [x] PostgreSQL (Authentik database)
- [x] Redis (Authentik cache)
- [x] Authentik server
- [x] Authentik worker
- [x] Kaboot backend service
- [x] Create `.env.example` with required variables:
- [x] `PG_PASS` - PostgreSQL password
- [x] `AUTHENTIK_SECRET_KEY` - Authentik secret
- [x] `PG_USER`, `PG_DB` - Optional customization
- [x] Create setup script to generate secrets (`scripts/setup.sh`)
- [x] Add `authentik/` directory structure for volumes:
- [x] `authentik/media/`
- [x] `authentik/certs/`
- [x] `authentik/custom-templates/`
- [x] Update `.gitignore` for new files:
- [x] `.env`
- [x] `authentik/media/*`
- [x] `authentik/certs/*`
- [x] `server/data/`
### 1.2 Authentik Configuration Documentation
- [x] Document initial setup steps in `docs/AUTHENTIK_SETUP.md`:
- [x] Navigate to `http://localhost:9000/if/flow/initial-setup/`
- [x] Create admin account
- [x] Create OAuth2/OIDC Application + Provider:
- [x] Application name: `Kaboot`
- [x] Application slug: `kaboot`
- [x] Provider type: `OAuth2/OIDC`
- [x] Client type: `Public` (SPA with PKCE)
- [x] Client ID: `kaboot-spa`
- [x] Redirect URIs: `http://localhost:5173/callback`, `http://localhost:5173/silent-renew.html`
- [x] Scopes: `openid`, `profile`, `email`, `offline_access`
- [x] Note down OIDC endpoints (issuer, JWKS URI, etc.)
---
## Phase 2: Backend API Development
### 2.1 Project Setup
- [x] Create `server/` directory structure:
```
server/
├── Dockerfile
├── package.json
├── tsconfig.json
└── src/
├── index.ts
├── db/
├── middleware/
├── routes/
└── services/
```
- [x] Initialize `package.json` with dependencies:
- [x] `express` - Web framework
- [x] `better-sqlite3` - SQLite driver
- [x] `jsonwebtoken` - JWT verification
- [x] `jwks-rsa` - JWKS client for Authentik
- [x] `uuid` - ID generation
- [x] `cors` - CORS middleware
- [x] Dev deps: `typescript`, `@types/*`, `tsx`
- [x] Create `tsconfig.json` for Node.js
- [x] Create `Dockerfile` for backend container
### 2.2 Database Layer
- [x] Create `server/src/db/schema.sql`:
- [x] `users` table (synced from OIDC claims)
- [x] `quizzes` table (with `user_id` foreign key)
- [x] `questions` table (with `quiz_id` foreign key)
- [x] `answer_options` table (with `question_id` foreign key)
- [x] Indexes for foreign keys
- [x] Create `server/src/db/connection.ts`:
- [x] Initialize better-sqlite3 connection
- [x] Run schema on startup
- [x] Export db instance
### 2.3 Authentication Middleware
- [x] Create `server/src/middleware/auth.ts`:
- [x] JWKS client setup pointing to Authentik
- [x] `requireAuth` middleware function:
- [x] Extract Bearer token from Authorization header
- [x] Verify JWT signature against JWKS
- [x] Validate issuer matches Authentik
- [x] Attach decoded user to request
- [x] Define `AuthenticatedRequest` interface
### 2.4 API Routes
- [x] Create `server/src/routes/quizzes.ts`:
- [x] `GET /api/quizzes` - List user's quizzes (with question count)
- [x] `GET /api/quizzes/:id` - Get full quiz with questions and options
- [x] `POST /api/quizzes` - Save new quiz (upsert user from token)
- [x] `PUT /api/quizzes/:id` - Update existing quiz
- [x] `DELETE /api/quizzes/:id` - Delete quiz (verify ownership)
- [x] Create `server/src/routes/users.ts`:
- [x] `GET /api/users/me` - Get current user profile
- [x] Create `server/src/index.ts`:
- [x] Express app setup
- [x] CORS configuration (allow frontend origin)
- [x] JSON body parser
- [x] Mount routes
- [x] Error handling middleware
- [x] Start server on port 3001
### 2.5 Backend Testing
- [x] Test API with automated test suite:
- [x] Verify 401 without token
- [x] Verify endpoints work with valid Authentik token
- [x] Verify quiz CRUD operations
- [x] Verify user sync from token claims
---
## Phase 3: Frontend Authentication
### 3.1 Dependencies
- [x] Add to `package.json`:
- [x] `react-oidc-context` - React OIDC hooks
- [x] `oidc-client-ts` - Underlying OIDC client
- [ ] Run `npm install`
### 3.2 OIDC Configuration
- [x] Create `src/config/oidc.ts`:
- [x] Define `oidcConfig` object:
- [x] `authority` - Authentik issuer URL
- [x] `client_id` - `kaboot-spa`
- [x] `redirect_uri` - `${origin}/callback`
- [x] `post_logout_redirect_uri` - `${origin}`
- [x] `response_type` - `code` (PKCE)
- [x] `scope` - `openid profile email offline_access`
- [x] `automaticSilentRenew` - `true`
- [x] `userStore` - `WebStorageStateStore` with localStorage
- [x] `onSigninCallback` - Clean URL after redirect
### 3.3 Auth Provider Setup
- [x] Modify `index.tsx`:
- [x] Import `AuthProvider` from `react-oidc-context`
- [x] Import `oidcConfig`
- [x] Wrap `` with ``
### 3.4 Auth UI Components
- [x] Create `components/AuthButton.tsx`:
- [x] Use `useAuth()` hook
- [x] Show loading state while auth initializing
- [x] Show "Sign In" button when unauthenticated
- [x] Show username + "Sign Out" button when authenticated
- [x] Modify `components/Landing.tsx`:
- [x] Add `` to top-right corner
- [x] Style consistently with existing design
### 3.5 Authenticated Fetch Hook
- [x] Create `hooks/useAuthenticatedFetch.ts`:
- [x] Use `useAuth()` to get access token
- [x] Create `authFetch` wrapper that:
- [x] Adds `Authorization: Bearer ` header
- [x] Adds `Content-Type: application/json`
- [x] Handles 401 by triggering silent renew
- [x] Export `{ authFetch, isAuthenticated }`
---
## Phase 4: Quiz Library Feature
### 4.1 Quiz Library Hook
- [x] Create `hooks/useQuizLibrary.ts`:
- [x] State: `quizzes`, `loading`, `error`
- [x] `fetchQuizzes()` - GET /api/quizzes
- [x] `loadQuiz(id)` - GET /api/quizzes/:id, return Quiz
- [x] `saveQuiz(quiz, source, aiTopic?)` - POST /api/quizzes
- [x] `deleteQuiz(id)` - DELETE /api/quizzes/:id
- [x] Handle loading and error states
### 4.2 Quiz Library UI
- [x] Create `components/QuizLibrary.tsx`:
- [x] Modal overlay design (consistent with app style)
- [x] Header: "My Library" with close button
- [x] Quiz list:
- [x] Show title, question count, source badge (AI/Manual), date
- [x] Click to load quiz
- [x] Delete button with confirmation
- [x] Empty state: "No saved quizzes yet"
- [x] Loading state: Spinner
### 4.3 Landing Page Integration
- [x] Modify `components/Landing.tsx`:
- [x] Add "My Quizzes" button (only visible when authenticated)
- [x] Add state for quiz library modal visibility
- [x] Render `` modal when open
- [x] Handle quiz load: call `onLoadQuiz` prop with loaded quiz
### 4.4 Types Update
- [x] Modify `types.ts`:
- [x] Add `SavedQuiz` interface (extends `Quiz` with id, source, dates)
- [x] Add `QuizListItem` interface (for list view)
- [x] Add `QuizSource` type: `'manual' | 'ai_generated'`
### 4.5 Game Hook Integration
- [x] Modify `hooks/useGame.ts`:
- [x] Add `loadSavedQuiz(quiz)` function
- [x] Export for App.tsx to consume
---
## Phase 5: Save Integration
### 5.1 Save After AI Generation
- [x] Modify `hooks/useGame.ts`:
- [x] Add `pendingQuizToSave` state
- [x] After successful AI generation, set `pendingQuizToSave`
- [x] Add `dismissSavePrompt()` function
- [x] Export these for UI to consume
- [x] Create `components/SaveQuizPrompt.tsx`:
- [x] Modal asking "Save this quiz to your library?"
- [x] Show quiz title
- [x] "Save" and "Skip" buttons
- [x] Loading state while saving
- [x] Wire up in `App.tsx`:
- [x] Show SaveQuizPrompt in LOBBY state when authenticated and pendingQuizToSave exists
- [x] Handle save via useQuizLibrary hook
### 5.2 Save in Quiz Creator
- [x] Modify `components/QuizCreator.tsx`:
- [x] Add "Save to my library" checkbox (only shown when authenticated)
- [x] Pass `saveToLibrary` flag to `onFinalize`
- [x] Modify `hooks/useGame.ts`:
- [x] Update `finalizeManualQuiz` to accept save preference
- [x] If save requested, set `pendingQuizToSave`
### 5.3 Load Quiz Flow (Already done in Phase 4)
- [x] `loadSavedQuiz(quiz: Quiz)` function in useGame.ts
- [x] Wire up from Landing → QuizLibrary → useGame
---
## Phase 6: Polish & Error Handling
### 6.1 Loading States
- [x] Add loading indicators:
- [x] Quiz library list loading
- [x] Quiz loading when selected
- [x] Save operation in progress
- [x] Disable buttons during async operations
### 6.2 Error Handling
- [x] Display user-friendly error messages:
- [x] Failed to load quiz library
- [x] Failed to save quiz
- [x] Failed to delete quiz
- [x] Network/auth errors
- [x] Add retry mechanisms where appropriate
### 6.3 Toast Notifications (Optional)
- [x] Add `react-hot-toast` or similar
- [x] Show success toasts:
- [x] "Quiz saved successfully"
- [x] "Quiz deleted"
- [x] Show error toasts for failures
### 6.4 Edge Cases
- [x] Handle auth token expiry gracefully
- [x] Handle offline state
- [x] Handle concurrent save attempts
- [x] Validate quiz data before save
---
## Phase 7: Documentation & Deployment
### 7.1 Documentation
- [ ] Update main `README.md`:
- [ ] Add Docker Compose setup instructions
- [ ] Document environment variables
- [ ] Add Authentik configuration steps
- [ ] Create `docs/AUTHENTIK_SETUP.md` (detailed IdP setup)
- [ ] Create `docs/API.md` (backend API documentation)
### 7.2 Production Considerations
- [ ] Document production deployment:
- [ ] HTTPS setup (reverse proxy)
- [ ] Update redirect URIs for production domain
- [ ] Database backup strategy
- [ ] Authentik email configuration
- [ ] Add health check endpoints
- [ ] Add logging configuration
---
## File Structure Reference
```
kaboot/
├── docker-compose.yml # NEW
├── .env.example # NEW
├── .env # NEW (gitignored)
├── scripts/
│ └── setup.sh # NEW
├── docs/
│ ├── AUTHENTIK_SETUP.md # NEW
│ └── API.md # NEW
├── server/ # NEW
│ ├── Dockerfile
│ ├── package.json
│ ├── tsconfig.json
│ └── src/
│ ├── index.ts
│ ├── db/
│ │ ├── connection.ts
│ │ └── schema.sql
│ ├── middleware/
│ │ └── auth.ts
│ ├── routes/
│ │ ├── quizzes.ts
│ │ └── users.ts
│ └── services/
│ └── quiz.service.ts
├── authentik/ # NEW (gitignored contents)
│ ├── media/
│ ├── certs/
│ └── custom-templates/
├── src/
│ ├── main.tsx # MODIFY
│ ├── App.tsx # MODIFY
│ ├── types.ts # MODIFY
│ ├── config/
│ │ └── oidc.ts # NEW
│ ├── hooks/
│ │ ├── useGame.ts # MODIFY
│ │ ├── useAuthenticatedFetch.ts # NEW
│ │ └── useQuizLibrary.ts # NEW
│ └── components/
│ ├── Landing.tsx # MODIFY
│ ├── QuizCreator.tsx # MODIFY
│ ├── AuthButton.tsx # NEW
│ ├── QuizLibrary.tsx # NEW
│ └── SaveQuizPrompt.tsx # NEW
└── package.json # MODIFY
```
---
## Technical Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Auth Provider | Authentik | Self-hosted, full-featured OIDC, Docker-friendly |
| OIDC Library | react-oidc-context | Lightweight, hooks-based, well-maintained |
| Backend Framework | Express | Simple, widely known, sufficient for this use case |
| Database | SQLite (better-sqlite3) | Zero config, file-based, perfect for single-server |
| Token Storage | localStorage | Required for SPA silent renew; acceptable for this app |
---
## Estimated Effort
| Phase | Description | Estimate |
|-------|-------------|----------|
| 1 | Infrastructure Setup | 1 day |
| 2 | Backend API Development | 1-2 days |
| 3 | Frontend Authentication | 1 day |
| 4 | Quiz Library Feature | 1 day |
| 5 | Save Integration | 0.5 day |
| 6 | Polish & Error Handling | 0.5 day |
| 7 | Documentation & Deployment | 0.5 day |
| **Total** | | **5-6 days** |
---
## Progress Tracking
**Last Updated**: 2026-01-13
| Phase | Status | Notes |
|-------|--------|-------|
| Phase 1 | **COMPLETE** | Docker Compose, .env, setup script, Authentik docs |
| Phase 2 | **COMPLETE** | Backend API with Express, SQLite, JWT auth, Quiz CRUD |
| Phase 3 | **COMPLETE** | OIDC config, AuthProvider, AuthButton, useAuthenticatedFetch |
| Phase 4 | **COMPLETE** | useQuizLibrary hook, QuizLibrary modal, Landing integration |
| Phase 5 | **COMPLETE** | SaveQuizPrompt modal, QuizCreator save checkbox, save integration |
| Phase 6 | **COMPLETE** | Toast notifications, loading states, error handling, edge cases |
| Phase 7 | Not Started | |