JK
>LADDAR // LADDAR
INITIERAR VY
Multi-tenant SaaS-plattform byggd med Go, PostgreSQL, Redis och Docker för automatiserad pedagogisk diagnostik.
Utbildningsinstitut saknar verktyg för tidig upptäckt av kunskapsluckor kombinerat med säker tentamensövervakning.
| Teknik | Version | Syfte | Varför vald |
|---|---|---|---|
| Go | 1.25.1 | Backend language | Strong concurrency, fast compilation |
| Gin | 1.12.0 | HTTP framework | High performance, mature middleware ecosystem |
| PostgreSQL | 12+ | Primary database | JSONB for flexible analysis storage, pg_trgm for fuzzy search |
| React | 19.2.4 | Frontend SPA | Modern hooks-based architecture |
| TypeScript | ~6.0.2 | Type-safe JS | Compile-time safety |
| Vite | 8.0.4 | Build tool/dev server | Fast HMR, ESM-native |
| Tailwind CSS | 4.2.2 | Styling | Utility-first, no CSS-in-JS runtime |
| Redis | 9.7.0 (go-redis) | Cache, queues, rate limiting | Optional; degrades gracefully without it |
| JWT (golang-jwt) | 5.3.1 | Auth tokens | Stateless auth |
| lib/pq | 1.12.3 | PostgreSQL driver | Mature, standard library |
| Metod | Rutt | Syfte |
|---|---|---|
| POST | /auth/login | User login (admin/coach) |
| POST | /student/login | Student login (student_code) |
| POST | /student/assignments/:id/start | Start exam attempt |
| POST | /student/assignments/:id/submit | Submit exam answers |
| POST | /student/assignments/:id/autosave | Autosave answers |
| POST | /student/assignments/:id/video-chunk | Upload video chunk |
| GET | /admin/tests | List tests |
| POST | /admin/tests | Create test |
| POST | /admin/assignments | Create assignment |
| POST | /admin/sqi/compute | Compute SQI for attempt |
| POST | /admin/sqi/compute-batch | Batch SQI computation |
| GET | /admin/students/:id/sqi | Get student SQI metrics |
| GET | /super-admin/tenants | List tenants |
| POST | /super-admin/plans | Create subscription plan |
| GET | /health | Health check (PostgreSQL ping) |
| Kolumn | Typ | Anteckningar |
|---|---|---|
| idPK | SERIAL | Auto-generated primary key |
| name | VARCHAR(100) | Organization/institute name |
| created_at | TIMESTAMP | Default NOW() |
| suspended_at | TIMESTAMP | Null if active |
| plan_id | INT | FK to subscription_plans (reference only) |
Multi-tenant root entity. Every other table has tenant_id FK with CASCADE delete.
| Kolumn | Typ | Anteckningar |
|---|---|---|
| idPK | SERIAL | Auto-generated primary key |
| tenant_id | INT | FK → tenants CASCADE |
| VARCHAR(100) | UNIQUE NOT NULL | |
| password | TEXT | bcrypt hashed |
| role | VARCHAR(20) | CHECK super_admin/admin/coach |
Students do NOT have user accounts; they authenticate via student_code.
| Kolumn | Typ | Anteckningar |
|---|---|---|
| idPK | SERIAL | Auto-generated primary key |
| tenant_id | INT | FK → tenants CASCADE |
| coach_id | INT | FK → coaches CASCADE |
| student_code | VARCHAR(50) | Auth credential (no password) |
| name | VARCHAR(100) | Student display name |
| batch_id | INT | FK → batches (optional) |
Partial unique index on (tenant_id, student_code) WHERE deleted_at IS NULL.
| Kolumn | Typ | Anteckningar |
|---|---|---|
| idPK | SERIAL | Auto-generated primary key |
| tenant_id | INT | FK → tenants CASCADE |
| title | TEXT | Test title |
| subject_id | INT | FK → subjects |
| coach_id | INT | FK → coaches CASCADE |
| duration | INT | Duration in seconds |
| subject_name | VARCHAR(100) | Denormalized for SQI display |
subject_name denormalized in migration 6 to avoid JOIN for SQI.
| Kolumn | Typ | Anteckningar |
|---|---|---|
| idPK | SERIAL | Auto-generated primary key |
| test_id | INT | FK → tests CASCADE |
| question_text | TEXT | NOT NULL |
| correct_answer | CHAR(1) | CHECK A/B/C/D |
| marks | FLOAT | Positive marks |
| neg_marks | FLOAT | Negative marks |
| importance | VARCHAR(10) | high/medium/low |
| difficulty | CHAR(1) | E(asy)/M(edium)/H(ard) |
| expected_time | FLOAT | Expected time in seconds |
| concept_tag | VARCHAR(100) | Concept grouping |
MCQ questions with metadata for SQI weight computation.
| Kolumn | Typ | Anteckningar |
|---|---|---|
| idPK | SERIAL | Auto-generated primary key |
| attempt_id | INT | UNIQUE FK → attempts CASCADE |
| sqi_score | FLOAT | Overall SQI score |
| raw_score | FLOAT | Raw marks scored |
| analysis_json | JSONB | Full DiagnosticPayloadV2 |
| version | VARCHAR(10) | Default 'v2' |
SQI analysis stored as JSONB — schema evolves without migrations.
tenants → users/coaches/students/subjects/tests/batches (1:N, tenant_id FK CASCADE). coaches → students (1:N). tests → questions (1:N CASCADE). students → assignments → attempts → answer_logs → attempt_results. 22+ tables total across 18 migrations.
Beslut: Every table has tenant_id FK with CASCADE delete instead of schema-per-tenant
Varför: Simpler than schema-per-tenant; adequate for expected scale
Fördelar: Single database, simple queries, easy cross-tenant analytics
Avvägningar: Must include tenant_id in every query; no physical data isolation
Beslut: Students login with student_code only, no password or user account
Varför: Students are numerous, short-lived, and don't need full user accounts
Fördelar: Simplified onboarding, no password management
Avvägningar: Weaker auth (code-only), no email/password recovery
Beslut: SQI v2 engine has no DB dependencies; takes inputs, returns payload
Varför: Separation of analytics from data access; testable in isolation
Fördelar: Unit testable, deterministic, reusable
Avvägningar: Requires data to be fetched and assembled before calling
Beslut: Store full DiagnosticPayloadV2 as JSONB in attempt_results
Varför: Schema evolves faster than migrations; analysis payload is read-heavy, write-once
Fördelar: No migration needed for analysis schema changes; flexible querying
Avvägningar: No referential integrity on nested data; harder to query specific fields
Beslut: Redis is optional — in-process queue and in-memory rate limiter as fallbacks
Varför: Development simplicity; single-binary deployment possible
Fördelar: Works without Redis in development; scales with Redis in production
Avvägningar: In-process queue loses jobs on restart; in-memory rate limiter isn't shared
Beslut: ffmpeg concat on first stream request, not on upload
Varför: Most videos are never watched; avoid wasted compute
Fördelar: Saves storage and compute for unwatched recordings
Avvägningar: First viewer experiences delay; requires ffmpeg on server