> ## Documentation Index
> Fetch the complete documentation index at: https://docs.safeonlineexam.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Architecture and Security Model — Safe Online Exam

> Safe Online Exam runtime shape, code ownership, LTI and OAuth trust model, SEB lifecycle, certificate encryption boundary, and all ten persistence tables.

Safe Online Exam's architecture page describes the implementation boundaries that matter to operators, security reviewers, and contributors. It covers the system's four core responsibilities, the runtime component graph, code ownership by area, how identity and authorization are established through LTI launches and Canvas OAuth, the assessment and course model, the full SEB configuration lifecycle from instructor setup to student exit, the configuration security boundary and certificate encryption model, and the ten PostgreSQL tables with their expiration behavior. For deployment commands, see [Deployment Overview](/deployment/overview). For Canvas registration steps, see [Canvas Setup](/deployment/canvas-setup).

## System Purpose

Safe Online Exam is an LTI 1.3 tool for requiring Safe Exam Browser on Canvas Classic Quizzes and New Quizzes. It has four connected responsibilities:

1. Give instructors a course-scoped interface for discovering Canvas assessments and managing SEB policy.
2. Give verified root-account administrators a Canvas-embedded, school-wide recovery interface with controlled password reveal, active-course connection, and bulk tool rollout.
3. Generate protected SEB configurations and establish a Canvas session inside SEB without transferring a normal-browser session cookie.
4. Release the Canvas access code and approved web-tool capability only when SEB proves that it is using the current configuration.

The service is not a general Canvas proxy. A deployment is configured for one Canvas origin and one LTI deployment boundary. All identity and authorization data used for a request comes from a validated LTI launch or a server-issued, bound capability.

## Runtime Shape

The application is one Node.js process. NestJS controllers expose HTTP endpoints, services own protocol and business behavior, repositories provide PostgreSQL storage in deployed environments and in-memory storage for local or test work, and React renders page views supplied by the server app shell. The detector is a separately served browser asset that runs on Canvas quiz pages.

```text theme={null}
Canvas  ──(OIDC login and signed LTI launch)──►  NestJS service
Canvas  ──(OAuth and REST/New Quiz APIs)──────►  NestJS service
NestJS  ──(assessment, course, token, state)──►  PostgreSQL 17+
NestJS  ──(React app shell)───────────────────►  Instructor / student browser
Canvas  ──(theme loader)──────────────────────►  Canvas detector script
Detector──(Config Key proof, redemption)──────►  NestJS service
NestJS  ──(encrypted .seb configuration)──────►  Safe Exam Browser
SEB     ──(Canvas session URL, assessment)────►  Canvas
```

The production image has no runtime dependency on a Google Cloud SDK. Durable state is in PostgreSQL, application configuration is read from environment variables or mounted files, and the HTTP process listens on a configurable host and port. Multiple instances share sessions, one-time claims, admission budgets, and operation locks through PostgreSQL, so sticky sessions and a writable application filesystem are not required.

Cloud Run, Cloud SQL, Artifact Registry, Secret Manager, and Cloud Build are the recommended managed deployment, not application requirements. Another platform can run the same image when it provides PostgreSQL 17+, public HTTPS ingress, secret injection, an exact-image migration job before traffic, and a scheduled cleanup job. See [Deployment Overview](/deployment/overview) for both supported operating models.

## Code Ownership

| Area                      | Primary locations                                                                              | Responsibility                                                                                                        |
| ------------------------- | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| Bootstrap and HTTP policy | `src/server/main.ts`, `src/server/http/`                                                       | Express session setup, CORS, security headers, app-shell rendering, request integrity, bounded upstream responses.    |
| Configuration             | `src/server/config/app-config.ts`                                                              | Environment parsing, normalizing aliases, and hardened-runtime validation.                                            |
| LTI                       | `lti.controller.ts`, `lti.service.ts`, `lti-state.service.ts`                                  | OIDC initiation, signed token validation, state encryption/replay claim, role routing, and Canvas JSON configuration. |
| Canvas OAuth and APIs     | `oauth.controller.ts`, `canvas-api.service.ts`, `canvas-api-{types,helpers}.ts`                | One durable per-user grant, administrator scope upgrades, token refresh, bounded Canvas REST/New Quiz requests.       |
| Account administration    | `admin.controller.ts`, `admin-authorization.service.ts`                                        | Root-scoped course connections, recovery actions, password reveal, account-bound authorization, and tool rollout.     |
| Assessment policy         | `quiz.controller.ts`, `assessment*.ts`, `course-settings.service.ts`                           | Discovery, Canvas access-code mutations, course reset coordination, defaults, per-assessment overrides, exam tools.   |
| SEB lifecycle             | `seb.controller.ts`, `seb-content-coordinator.ts`, `seb-configuration*.ts`, `seb-*.service.ts` | Route handling, content/config coordination and policy, proof, access-code redemption, setup check, exit grants.      |
| Browser detector          | `src/server/assets/detector/`, `detector-source.ts`, `static-js.controller.ts`                 | Modular quiz-page launch UI, access-code filling, approved tools, completion detection, assembly, and delivery.       |
| Persistence               | `src/server/data/postgres/`, `postgres-repositories.ts`, `session-store.ts`                    | PostgreSQL stores and factory, atomic consumption/claims, distributed rate budgets, locks, Express sessions.          |
| Web client                | `src/client/features/`, `src/client/components/`, `src/client/lib/`, `src/client/styles/`      | View routing, role-focused workflows, shared UI and request helpers, and ordered feature styles.                      |
| Shared domain model       | `src/shared/models/`, `src/shared/models.ts`                                                   | Canvas, assessment, course, admin, external-tool, URL-rule, and role contracts behind a stable compatibility barrel.  |

The client router loads the React view module selected by the server bootstrap instead of shipping every administrator, instructor, student, and SEB workflow in the entry bundle. Route chunks and the shared React runtime use content-hashed filenames with immutable caching. The app shell reads Vite's build manifest to preload the shared runtime without adding a first-load request waterfall.

## Identity and Authorization

### LTI Launch Validation

Canvas initiates OIDC at `/lti/login`. The service verifies the issuer, requested target link URI, configured client ID, and configured deployment ID before creating encrypted state. State is valid for ten minutes and is additionally bound to a short-lived, `HttpOnly`, secure browser transaction cookie.

Canvas posts an ID token to `/lti/launch`. The service validates:

* RS256 signing against the configured Canvas JWKS.
* Issuer, audience, nonce, token age, issued/expiry timestamps, LTI version, message type, and deployment ID.
* The target link URI and initiation state tuple.
* The initiating browser transaction cookie.
* A durable, atomic PostgreSQL state claim to prevent replay.

After successful validation, the server regenerates the Express session and stores a verified LTI principal. A course principal contains the signed issuer, deployment, subject, numeric Canvas user ID, course ID, roles, and custom fields. A root-account administrator principal instead contains signed numeric account/root-account IDs and Canvas's root-admin substitution. Query parameters and request bodies cannot substitute for either principal.

<Note>
  Instructors receive the course management view. Students receive a launch-only flow and never receive management actions. The school dashboard is available only through the root-account navigation placement and requires both a standard signed LTI Administrator role and Canvas's signed root-account-admin value. A Canvas administrator must refresh the LTI registration if Canvas does not supply the configured substitutions.
</Note>

### Canvas OAuth Flow

An LTI launch authenticates a person but does not authorize Canvas API calls. The application uses a separate Canvas OAuth authorization for API access:

<Steps>
  <Step title="Initiate from a verified launch">
    An instructor opens `/api/oauth2authorize` or `/api/oauth2reauthorize` from the same-origin tool UI created by an existing verified launch. Browser requests whose Fetch Metadata identifies any other site relationship are rejected.
  </Step>

  <Step title="Record state and redirect">
    The service records encrypted, one-time state and redirects to Canvas.
  </Step>

  <Step title="Exchange the authorization code">
    `/api/oauth2callback` verifies state and exchanges the authorization code for access and refresh tokens.
  </Step>

  <Step title="Persist one grant per Canvas user">
    PostgreSQL stores one OAuth grant per Canvas user ID. Administrator authorization upgrades that same record to the complete application-plus-administrator scope set. `CanvasApiService` refreshes it when necessary.
  </Step>

  <Step title="Complete in a popup or return through Canvas">
    Instructor and administrator authorization normally runs in a popup while the signed LTI page remains open. The callback renders a non-privileged completion screen that sends a fixed completion message only to its exact same-origin opener; the opener verifies both the message origin and popup window before performing its own same-origin refresh. If a popup is unavailable, the user returns through Canvas. An OAuth callback never renders or directly redirects into an authenticated management view.
  </Step>
</Steps>

Every Canvas OAuth connection requests the same complete application scope set, including the course-list permission used for teacher-scoped tool duplication and the session-token permission required for SEB handoff. This is an explicit product decision: one durable grant must remain valid when a person is an instructor in one course and a student in another. Canvas still enforces the user's actual course permissions; the application never treats scope possession as an LTI role or course-authorization check.

<Note>
  Access and refresh tokens are encrypted before PostgreSQL persistence with an independent AES-256-GCM keyring. The record ID and Canvas user ID are authenticated as associated data, while non-secret scope and identity metadata remains queryable. Legacy plaintext rows are read-only to support a staged migration and are rewritten by the explicit maintenance command.
</Note>

Root-account administrators use `/api/admin/oauth2authorize` to upgrade their existing user grant with administrator scopes. Every admin mutation also requires a short-lived HMAC action token bound to the LTI subject, Canvas user, root account, deployment, and current Express session.

## Assessment and Course Model

### Identifiers

All persisted assessment records use a canonical public content ID:

| Assessment type | Canonical ID                        | Canvas mutation target   |
| --------------- | ----------------------------------- | ------------------------ |
| Classic Quiz    | `classicquiz_{quizId}`              | Quiz access-code API     |
| New Quiz        | `newquiz:{courseId}:{assignmentId}` | New Quiz access-code API |

The `assessments` table stores Canvas discovery data, availability verification, and SEB state. `courses` stores course-level defaults and its exam-tool catalog. Course defaults can provide URL policy, start/exit password policy, and selected exam tools; an assessment may inherit defaults, retain an explicit list of course tool IDs, and add quiz-only tool definitions.

### Canvas Discovery and Availability

Instructor discovery refreshes Classic and New Quiz data from Canvas. A learner can use an assessment only when its cached Canvas verification is current, explicitly verified, published, and within its global unlock/lock window. The verification window is 24 hours. Missing records are retained for instructor reconciliation but fail closed for learners; a failed refresh marks the cached discovery stale.

Assessment updates use short-lived PostgreSQL operation locks while Canvas and database state are changed. Administrator course resets take a course-level lease and then the same per-assessment leases, so ordinary assessment mutations cannot overlap a reset. Course refreshes, administrator connection-count writes, school-preset assignment writes, and instructor tool copies use the same course fence so they cannot recreate state after a reset. Atomic compare-and-delete/insert operations prevent overlapping workers from owning the same lease and help keep Canvas access codes aligned with persisted SEB settings. Each workflow carries a shared lease guard across nested locks and checks it after external reads and before later Canvas or PostgreSQL mutations; a failed background renewal makes the guard reject those later side effects immediately. After final ownership verification, releasing a lease is best-effort: a cleanup failure cannot replace the verified action result, and the bounded lease expires automatically.

An administrator course reset performs read-only, strict Classic Quiz and New Quiz discovery and reads every assessment's current Canvas access-code state before making the first destructive call. The discovery result is assembled in memory instead of changing cached learner-verification state that would need recovery if the preflight aborts. The reset then removes each Canvas access code with the account-administrator grant and only afterward deletes course-related transient state, assessments, the course policy, and per-course school-tool preset assignments in one PostgreSQL transaction. That transaction also stores an operation-specific reset receipt on the retained root-account course connection; the shared OAuth grant is deliberately retained. If a Canvas response is lost, a later Canvas mutation fails, or the database transaction definitively fails, the service restores the exact pre-reset Canvas state for every assessment that may have changed and restores its prior local assessment record in reverse order. If the transaction commit response is ambiguous, the service verifies the durable reset receipt before compensating. An unavailable receipt check, failed compensation, or lost lease is reported as an indeterminate result that requires refresh and verification rather than blindly restoring Canvas state.

### Exam Tools and URL Policy

Course-owned exam tools have an exact HTTPS launch URL and typed resource rules: one exact page or file, an address and related links, or an explicitly confirmed whole website. Instructors explicitly approve both tool start pages and resources, including a different HTTPS website such as a CDN asset; the instructor UI calls out cross-site access before saving. A saved instructor-owned tool can be duplicated into active Canvas courses where the same OAuth user is a teacher. The browser can only choose from a Canvas-filtered course list, and the server retrieves that list again before every target write; target IDs alone never authorize a copy. The server also snapshots each target's durable reset generation before those external reads and rejects the copy if a reset completed in the meantime or if reset-deleted course setup has not yet been recreated by a teacher launch. The copy appends a local tool without replacing the target catalog, preserves an existing equivalent definition on retry, and propagates the target course defaults so relevant configuration fingerprints are invalidated.

A dedicated YouTube video tool accepts a watch, share, Shorts, or embed link and turns it into one embedded public video with a server-owned player page and bounded media policy. The server-owned page supplies YouTube's required embedding identity while deliberately excluding YouTube browsing and Google sign-in. User-entered general rules remain restricted to safe HTTPS URLs or concrete domains; wildcards, credentials, arbitrary regular expressions, and unsafe historical patterns are rejected or quarantined.

Root administrators can create reusable school presets in `admin_tool_presets` and assign them to individual courses. An assigned definition is synchronized into the course catalog as school-managed: instructors may enable or disable it but cannot silently change its launch URL or resource access. Updating or deleting the preset synchronizes every assigned course and invalidates affected configuration fingerprints. Quiz-only definitions remain on the assessment record and never become course defaults.

<Note>
  The SEB URL filter in the generated configuration is the control that determines what can load. Changing any selected tool or URL policy changes the configuration fingerprint; students must download a new configuration.
</Note>

## SEB Lifecycle

### Instructor Configuration

When an instructor enables SEB, `AssessmentService` creates an access code, mutates the appropriate Canvas assessment, and persists SEB state only after the mutation is successful. Enabling requires an effective exit password: assessment override, course default, or configured managed default. Optional start passwords protect the inner configuration payload. Password responses are redacted by default; an instructor can make a narrowly bound, short-lived reveal request.

### Student Configuration Download

<Steps>
  <Step title="Request a configuration grant">
    A verified LTI principal calls the grant endpoint. The server mints a one-time, 120-second capability bound to the principal, course, content ID, and current settings fingerprint.
  </Step>

  <Step title="Obtain a fresh Canvas session URL">
    For the download, the service obtains a fresh Canvas session URL server-side. Browser cookies are never copied to the configuration or exposed through the API.
  </Step>

  <Step title="Build and encrypt the .seb file">
    The service builds the SEB start URL around the Canvas session URL, assembles the configuration (assessment entry route, URL policy, Config Key behavior, HMAC-bound quit URL), and encrypts it to the configured public certificate. The service holds only public encryption material.
  </Step>

  <Step title="Deliver on grant consumption">
    `GET /seb/config/:courseId/:contentId.seb` consumes the one-time capability and streams the encrypted configuration to the student's browser.
  </Step>
</Steps>

### Config Key Proof and Access-Code Release

<Steps>
  <Step title="Detector reads the Config Key">
    On the Canvas assessment page, the detector reads the SEB Config Key hash and current browser URL through the SEB JavaScript API.
  </Step>

  <Step title="Request a proof token">
    The detector posts to `POST /api/seb/access-proof/:courseId/:quizId`. The server verifies the assessment, current settings fingerprint, URL family, and Config Key hash before returning a one-time proof token valid for two minutes.
  </Step>

  <Step title="Redeem the proof for the access code">
    `POST /api/seb/access-code/:courseId/:quizId` consumes the proof token and returns the access code, approved tools, and an exit grant with sensitive response headers. A proof can be consumed exactly once.
  </Step>

  <Step title="Fill the Canvas access-code prompt">
    The detector fills only an unambiguous Canvas access-code prompt. It does not treat DOM content as authorization.
  </Step>
</Steps>

<Note>
  In an ordinary browser, the detector calls `GET /api/seb/requirement/:courseId/:quizId` before showing the SEB-required prompt. That endpoint performs one exact `assessments` primary-key lookup and returns `sebRequired: true` only when the stored course/content relationship matches and the SEB configuration is enabled, required, and usable. An absent, mismatched, disabled, malformed, or unverifiable result does not produce a launch prompt.
</Note>

### Completion and Exit

The detector waits for Canvas-authored completion evidence. Classic Quiz completion requires a successful final submission and the matching Canvas result structure; New Quiz completion requires the authoritative result UI. On confirmed completion, the detector uses a settings-bound exit grant to display a quit link. Unbound manual and automatic quit paths intentionally return `410` rather than accepting a general-purpose quit request.

### Setup Check

`/seb/check/config.seb` generates a separate configuration for testing certificate decryption, SEB runtime detection, connectivity, storage, and Config Key proof. It never releases an assessment access code and does not establish device trust. It should be part of pre-exam readiness testing, not a substitute for device management.

## Configuration Security Boundary

Generated assessment configurations include a strict Canvas and approved-resource URL filter, SEB Config Key proof setup, session-monitoring and kiosk-related policy, exit protection, and optional start-password protection. The exact plist is built by `SebConfigurationService`.

<Note>
  Use integration testing with the supported SEB clients rather than assuming a setting is honored by every client release or operating system. Validate the complete policy with a real supported client after SEB or operating-system updates.
</Note>

### Certificate Encryption Model

Certificate encryption is enabled by default, including in hardened runtimes. The public X.509 certificate or public key permits wrapping the file; the matching private identity belongs only on approved client devices. An instance that cannot distribute that identity may explicitly set `SEB_CONFIG_ENCRYPTION_ENABLED=false`; in that mode the configuration is plaintext unless an instructor sets a start password.

Assessment configurations use the SEB exam-start purpose and include the assessment start URL, an HMAC-bound quit URL, URL filter rules, a derived Browser Exam Key, and a Config Key salt. If an instructor sets a start password, the inner configuration is password-protected before certificate wrapping. The outer encrypted file uses SEB's public-key-hash (`pkhs`) format when a public certificate is configured.

The setup-check configuration is deliberately different from an assessment configuration: it starts at `/seb/check`, has no assessment access code, allows quit without an assessment exit password, and cannot redeem an access-code proof.

<Accordion title="macOS lockdown policy details">
  On macOS, the generated policy requires Automatic Assessment Configuration (AAC) through both `enableMacOSAAC` and `lockdownModePolicy`, requires installation from the system Applications location, and sets a macOS 12.1 floor through explicit version-number settings plus the coarse version field. The overlapping AAC keys cover supported SEB client generations; they do not replace device management. AAC may block third-party assistive technology, so an accommodation that requires it needs a separate approved assessment or proctoring arrangement rather than a weakened common configuration.
</Accordion>

<Accordion title="Windows lockdown policy details">
  On Windows, the configuration requests the OS-session and SEB-service controls used by the generated policy, including the kiosk desktop, process/session monitoring, and the supported SEB version floor. Client releases that do not understand a newer configuration key cannot enforce that key, so managed-device policy must also pin the approved SEB client version and integrity baseline.
</Accordion>

Assessment lockdown settings explicitly block configuration surfaces that would undermine the browser boundary: application/user switching, virtual machines, additional displays, screen capture/sharing, AirPlay, Siri and dictation, developer console, printing, downloads/uploads, open/save panels, and non-SEB clipboard transfer. They leave Canvas-required browser behavior, reload, JavaScript, and SEB-managed browser windows available so Canvas and approved web tools can function.

## Persistence and Expiration

PostgreSQL transactions and row locks implement atomic state claims, one-time token consumption, rate-budget increments, and operation-lock ownership. Claim/consume operations use conditional mutations so two app instances cannot both win the same one-time claim. Cleanup selects bounded batches with `FOR UPDATE SKIP LOCKED`, allowing safe overlap without long table locks. This is why multiple app instances can share runtime state without sticky sessions.

| Table                           | Contents                                                                                            | Expiration behavior                                           |
| ------------------------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| `assessments`                   | Canvas discovery and per-assessment SEB state                                                       | Durable until intentionally changed.                          |
| `courses`                       | Course defaults, setup state, and exam-tool catalog                                                 | Durable until intentionally changed.                          |
| `canvas_oauth_tokens`           | Canvas access and refresh tokens                                                                    | Durable; lifecycle is driven by Canvas authorization/refresh. |
| `admin_course_connections`      | Root-account-scoped cached Canvas course metadata and assessment counts                             | Durable until a course connection is intentionally removed.   |
| `admin_account_settings`        | Root-account operational-term selection                                                             | Durable until an administrator changes it.                    |
| `admin_tool_presets`            | Root-account tool definitions                                                                       | Durable until an administrator changes or deletes the preset. |
| `admin_tool_preset_assignments` | Per-course desired state, rollout status, and retry information                                     | Durable until the preset is deleted or the course is reset.   |
| `sessions`                      | Express session payloads keyed by a hashed session ID                                               | Expired rows are removed by bounded cleanup.                  |
| `transient_states`              | LTI replay claims, OAuth state, configuration grants, proofs, session-handoff records, rate budgets | Expired rows are removed by bounded cleanup.                  |
| `operation_locks`               | Short assessment-update and administrator course-reset leases                                       | Expired rows are removed by bounded cleanup.                  |

Schema changes are explicit, ordered migrations recorded with checksums in `schema_migrations`. The application never mutates schema on ordinary startup; `/ready` fails until all checked-in migrations are applied. A migration job must complete before traffic reaches a new image.

## HTTP and Security Controls

* Security headers are applied before application routes; Express disables `x-powered-by` and trusts one proxy hop.
* Sessions use `HttpOnly` cookies and use `Secure; SameSite=None` when the configured tool URL is HTTPS or the profile is production.
* Sensitive proof, access-code, and password-reveal responses are `no-store` and are bound to the verified session/principal.
* LTI initiation and token validation use process-local and PostgreSQL-backed admission budgets. Configuration-grant minting is rate-limited per principal and IP.
* Canvas API calls are constrained to the configured Canvas origin and `/api/v1` base. Responses have size limits and upstream deadlines; a `401` triggers at most one safe token refresh/retry.
* The public detector script has two stable paths. Debug or diagnostic modes serve a readable, non-cacheable asset; normal production mode serves the built minified asset from the same public path.
* Public JavaScript and CSS responses are compressed; user-specific HTML and API responses remain outside that compression middleware.

<Note>
  The detector's buttons and sidebar are user-interface affordances. The generated SEB URL filter, current configuration fingerprint, server-side proof, and Canvas-authored completion state are the enforcement boundaries. Changing any selected tool or URL policy changes the configuration fingerprint; students must download a new configuration.
</Note>

## Route Reference

The application exposes six groups of public HTTP routes: LTI and health routes, Canvas OAuth routes, SEB student and configuration routes, the Canvas detector and diagnostics routes, instructor assessment routes, and root-account administrator routes. The route handlers are the source of truth for parameters and response schemas; unlisted query parameters and output fields are implementation details.

<CardGroup cols={2}>
  <Card title="LTI Routes" icon="key" href="/api/lti-routes">
    OIDC initiation, signed LTI launch handling, Canvas JSON configuration, JWKS, and health endpoints.
  </Card>

  <Card title="OAuth Routes" icon="lock" href="/api/oauth-routes">
    Canvas OAuth authorization, callback, status, and student-session authorization endpoints.
  </Card>

  <Card title="SEB Student Routes" icon="graduation-cap" href="/api/seb-student-routes">
    Configuration grant minting, encrypted `.seb` download, Config Key proof, access-code redemption, setup check, and exit flows.
  </Card>

  <Card title="Detector Routes" icon="eye" href="/api/seb-detector-routes">
    Stable detector script delivery, Canvas theme loader, compatibility alias, and diagnostic trace endpoint.
  </Card>

  <Card title="Instructor Routes" icon="chalkboard-user" href="/api/instructor-routes">
    Course assessment discovery, SEB settings, enable/disable, refresh, defaults, and password reveal.
  </Card>

  <Card title="Admin Routes" icon="shield-halved" href="/api/admin-routes">
    Root-account course connections, resets, password reveal, term selection, and school tool preset management.
  </Card>
</CardGroup>
