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

# Classroom Architecture

> End-to-end technical architecture for Kralis Classroom, LiveKit, R2, GraphQL, webhooks, and background work.

Kralis Classroom is a standalone Django application and Next.js product area. It owns its domain model and integrates with Kralis core school, user, class, section, subject, year, and term records through explicit foreign keys.

It does not depend on LMS. A future LMS course integration should reference or request a Classroom event; it must not move room or attendance ownership into the LMS domain.

## System boundaries

```mermaid theme={null}
flowchart TB
    Browser[Kralis Web /classroom] -->|Session-authenticated GraphQL| API[Django + Graphene]
    Browser -->|Short-lived scoped JWT| LK[LiveKit Cloud]
    API --> DB[(PostgreSQL)]
    API -->|Provider control API| LK
    LK -->|Signed REST webhooks| API
    API -->|Celery tasks| Queue[Broker]
    Queue --> Worker[Celery worker]
    Beat[Celery Beat] --> Queue
    Worker --> LK
    Worker --> R2[(Private Cloudflare R2)]
    LK -->|Egress MP4 upload| R2
    API -->|Presigned document/playback URL or server read| R2
```

### Kralis owns

* identity, school tenancy, authorization, and role resolution
* events, settings, grants, admissions, participants, and sessions
* expected rosters, provider presence evidence, and attendance results
* chat persistence and presentation state
* recording requests, publication state, and playback authorization
* whiteboard ownership, grants, editor leases, and immutable revisions
* operational limits, notices, audits, incidents, and retention metadata

### LiveKit owns

* WebRTC media transport
* room and participant provider state
* camera, microphone, and screen-share tracks
* restricted data transport for trusted notifications and whiteboard scenes
* room-composite Egress

### R2 owns

* private MP4 recording objects
* private Excalidraw JSON revision objects

Provider identifiers are implementation details. Product URLs and access decisions use Kralis UUIDs.

## Backend organization

```text theme={null}
classroom/
├── models.py                 # standalone product data model
├── graphql/                  # inputs, types, queries, mutations, responses
├── services/                 # domain authorization and lifecycle operations
├── providers/                # LiveKit and R2 adapters and checks
├── tasks.py                  # Celery provider work and reconciliation
├── views.py                  # read-only REST resources and LiveKit webhook
├── urls.py
├── filtering.py
├── serializers.py
└── tests/                    # model, service, GraphQL, provider, security, N+1
```

Business transitions belong in `classroom/services`, not GraphQL resolvers, REST serializers, webhooks, or Celery tasks. Those surfaces resolve inputs and delegate to the same services so manual, scheduled, and provider-driven paths converge.

## Frontend organization

```text theme={null}
app/classroom/
├── page.tsx and classroom.tsx
├── events/new/
├── events/[eventId]/
├── events/[eventId]/attendance/
├── events/[eventId]/room/
├── whiteboards/
├── whiteboards/[whiteboardId]/
├── groups/
├── groups/[groupId]/
└── egress/                   # non-indexed screen-share recording renderer

components/classroom/         # room, lobby, chat, participants, boards, forms
lib/graphql/documents/classroom.ts
lib/classroom/                # client helpers and tested packet parsing
```

The browser uses generated GraphQL types. After changing Classroom GraphQL documents or schema, run the frontend generation command and formatting before type checking.

## Data model relationships

```mermaid theme={null}
erDiagram
    SCHOOL ||--o{ CLASSROOM_EVENT : owns
    CLASSROOM_EVENT ||--|| CLASSROOM_EVENT_SETTINGS : configures
    CLASSROOM_EVENT ||--o{ CLASSROOM_EVENT_ACCESS_GRANT : authorizes
    CLASSROOM_EVENT ||--o{ CLASSROOM_SESSION : runs
    CLASSROOM_EVENT ||--o{ CLASSROOM_ADMISSION : admits
    CLASSROOM_EVENT ||--o{ CLASSROOM_PARTICIPANT : includes
    CLASSROOM_PARTICIPANT ||--o| CLASSROOM_EXPECTED_ATTENDEE : expected
    CLASSROOM_PARTICIPANT ||--o{ CLASSROOM_PRESENCE_INTERVAL : produces
    CLASSROOM_PARTICIPANT ||--o| CLASSROOM_ATTENDANCE : derives
    CLASSROOM_SESSION ||--o{ CLASSROOM_PRESENCE_INTERVAL : contains
    CLASSROOM_SESSION ||--o{ CLASSROOM_RECORDING : records
    CLASSROOM_EVENT ||--o{ CLASSROOM_CHAT_MESSAGE : persists
    CLASSROOM_EVENT ||--o{ CLASSROOM_EVENT_WHITEBOARD : attaches
    CLASSROOM_WHITEBOARD ||--o{ CLASSROOM_EVENT_WHITEBOARD : attached
    CLASSROOM_WHITEBOARD ||--o{ WHITEBOARD_ACCESS_GRANT : authorizes
    CLASSROOM_WHITEBOARD ||--o{ WHITEBOARD_EDIT_SESSION : leases
    CLASSROOM_WHITEBOARD ||--o{ WHITEBOARD_REVISION : snapshots
    COLLABORATION_GROUP ||--o{ COLLABORATION_GROUP_MEMBERSHIP : contains
    COLLABORATION_GROUP ||--o{ CLASSROOM_EVENT_ACCESS_GRANT : receives
    COLLABORATION_GROUP ||--o{ WHITEBOARD_ACCESS_GRANT : receives
```

Every domain row inherits school scoping through the Kralis base model. A session is an execution of an event, not the event itself. A recording belongs to both the event and, when available, the session that produced it.

## API split

### GraphQL

GraphQL is the application control plane. It covers:

* event CRUD-like lifecycle mutations and cursor-paginated queries;
* access grants, collaboration groups, and membership;
* lobby requests and decisions;
* join credentials and recording-notice acknowledgement;
* participant moderation, live settings, chat, and presentation;
* attendance, sessions, recordings, playback, and publication;
* whiteboard creation, grants, attachments, revisions, and realtime access;
* school operational policy, usage, audit events, and issue resolution.

Root queries and sensitive nested relationships are school- and role-scoped. Clients must not infer authorization from a previously fetched object; each mutation rechecks current access.

### REST

Classroom REST resources are read-only and school-acronym scoped:

```text theme={null}
/classroom/<school-acronym>/events/
/classroom/<school-acronym>/sessions/
/classroom/<school-acronym>/participants/
/classroom/<school-acronym>/attendance/
/classroom/<school-acronym>/recordings/
/classroom/<school-acronym>/whiteboards/
/classroom/<school-acronym>/collaboration-groups/
```

The provider webhook is REST because LiveKit posts signed event payloads:

```text theme={null}
POST /classroom/webhooks/livekit/
```

It intentionally has no Kralis user authentication. The receiver verifies the LiveKit authorization signature using the API key and secret before processing anything.

## Authentication and LiveKit authorization

Kralis users do not receive LiveKit accounts. The flow is:

1. authenticate the Kralis web request;
2. load the event inside the user’s school;
3. resolve the strongest event role;
4. enforce event state, recording notice, removal state, admission, and capacity;
5. create or update the Kralis participant;
6. issue a short-lived LiveKit JWT for one opaque room.

The token contains the Kralis user UUID as identity and school/event/session metadata. It permits subscription and only the media sources authorized by the current role and overrides. It does not grant room administration, arbitrary data publishing, or metadata mutation.

Lobby and whiteboard tokens are even narrower data-room credentials. The backend remains authoritative when an old token is replayed: signed join webhooks recheck access and admission before creating attendance evidence and queue disconnection when authorization is no longer valid.

## Webhook inbox and presence

Accepted provider events are stored in `ProviderWebhookEvent` using LiveKit’s event ID as an idempotency key. Duplicate processed events return success without applying the transition again.

Supported provider events include:

* `room_started`
* `participant_joined`
* `participant_left`
* `room_finished`
* `egress_started`
* `egress_updated`
* `egress_ended`

The handler maps the opaque provider room to a known Kralis session. Unknown rooms are safely ignored. Processing failures remain in the inbox with an attempt count and operational issue.

Participant SID, rather than user ID alone, makes presence intervals reconnect safe. Attendance calculation clamps and merges those intervals before deriving duration and status.

An early `room_finished` for a scheduled event ends only that session; the scheduler may create a replacement. An on-time/provider-driven completion uses the shared full event finalizer.

## Realtime application data

The browser cannot publish arbitrary application commands in the media room. Chat, hand, settings, moderation, and presentation changes first pass through authenticated GraphQL and persistent services. After commit, the server sends a small trusted LiveKit packet prompting connected clients to refetch or apply the authoritative state.

Separate topics exist for chat, participant state, admission, event settings, presentation, whiteboard scenes, and the active whiteboard cursor.

Whiteboard editor tokens are the exception: exactly one leased editor may publish scene/cursor data in the board’s restricted data-only room. Durable state still comes from immutable R2 revisions.

## Recording architecture

Manual start creates a `REQUESTED` row synchronously and queues provider work. Celery starts room-composite Egress. Provider webhooks update normal state, and the minute reconciler repairs delayed or missed transitions.

Camera-grid recording uses LiveKit’s standard `grid` layout. Screen-share recording uses the public Kralis egress renderer at `LIVEKIT_EGRESS_TEMPLATE_URL`, the `kralis-classroom` custom layout, and the standard LiveKit egress query-token mechanism. The configured base URL must not contain a query or fragment. The renderer reads credentials through the LiveKit Egress SDK, removes them from browser history, and starts capture only after media is available.

Egress uploads H.264 720p MP4 directly to private R2 using S3-compatible credentials. Kralis validates the returned filename before marking the record ready.

## R2 layout

R2 keys use immutable UUIDs rather than acronyms:

```text theme={null}
classroom/recordings/<school-uuid>/<event-uuid>/<recording-uuid>.mp4
classroom/whiteboards/<school-uuid>/<whiteboard-uuid>/revisions/<sequence>-<opaque-id>.excalidraw.json
classroom/provider-checks/r2-<opaque-id>.json
```

`R2_ENDPOINT` must be an account endpoint such as:

```text theme={null}
https://<account-id>.r2.cloudflarestorage.com
```

It must not include `/kralis`, a bucket, path, query, or fragment. Configure the bucket independently.

## Environment configuration

```dotenv theme={null}
# Cloudflare R2
R2_ENDPOINT=https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com
R2_ACCESS_KEY_ID=...
R2_SECRET_ACCESS_KEY=...
R2_BUCKET_NAME=kralis-private
R2_PRESIGNED_URL_TTL_SECONDS=900

# LiveKit Cloud
LIVEKIT_URL=wss://YOUR_PROJECT.livekit.cloud
LIVEKIT_API_KEY=...
LIVEKIT_API_SECRET=...
LIVEKIT_WEBHOOK_URL=https://api.example.com/classroom/webhooks/livekit/
LIVEKIT_TOKEN_TTL_SECONDS=300
LIVEKIT_EGRESS_TEMPLATE_URL=https://app.example.com/classroom/egress

# Celery broker used by Classroom and other Kralis tasks
CELERY_BROKER_URL=amqp://...
```

All secrets are server-side. The frontend public domain is required for the screen-share Egress renderer. The API public domain is required for LiveKit webhooks.

## Celery and lifecycle convergence

Run an API process, Celery worker, and Celery Beat. The configured periodic work is:

```text theme={null}
every 30 seconds  reconcile_classroom_schedule_task
every minute      reconcile_classroom_recordings_task
daily at 02:00    enforce_classroom_retention_task
```

Provider preparation, recording start, chat/participant/settings publication, moderation, disconnection, and room cleanup are also Celery tasks. Mutations commit Kralis state before queuing side effects.

## Operational safeguards

* per-school concurrent room, recording, and storage limits
* idempotent schedule and webhook transitions
* capacity reservations for concurrent joins
* mutation rate limits for admission, chat, hand, joins, whiteboard leases, heartbeats, and revisions
* recording and webhook retention
* append-only audit events for sensitive actions
* coalesced operational issues and support email alerts
* explicit publication before ordinary recording playback
* presigned private playback URLs
* role-aware GraphQL nested resolvers

Default mutation limits are evaluated per authenticated scope and operation:

| Operation                 | Default limit  |
| ------------------------- | -------------- |
| Admission requests        | 20 per minute  |
| Chat messages             | 30 per minute  |
| Hand-state changes        | 30 per minute  |
| Join-credential requests  | 30 per minute  |
| Begin whiteboard editing  | 30 per minute  |
| Whiteboard heartbeats     | 120 per minute |
| Whiteboard revision saves | 30 per minute  |

The GraphQL error includes retry information so the frontend can disable the action temporarily and show a countdown instead of encouraging repeated calls.

## Provider and staging checks

Verify configured providers with isolated temporary resources:

```bash theme={null}
python manage.py check_classroom_providers
python manage.py check_classroom_providers --skip-r2
python manage.py check_classroom_providers --skip-livekit
```

The R2 check performs create, read, update, and delete. The LiveKit check creates a temporary room and validates the configured signed webhook path.

Run the disposable end-to-end command only in staging:

```bash theme={null}
python manage.py check_classroom_staging \
  --school-acronym DEMO \
  --actor-id <user-uuid> \
  --confirm-staging
```

## Deployment position

The current product is designed for managed LiveKit Cloud first, with private Cloudflare R2 storage. This avoids operating TURN, SFU, Egress, autoscaling, and regional media capacity while Classroom usage is being established.

The provider adapters preserve a path to self-hosted LiveKit later. That change requires capacity planning for signaling, Redis, TURN, regional bandwidth, Egress workers, observability, upgrades, and failure recovery; it is an infrastructure decision, not a frontend rewrite.

## LMS integration rule

When LMS requests a live class for a course:

1. the LMS course explicitly requests creation or attachment;
2. Classroom creates or owns the event and its settings;
3. LMS stores the association on the course side;
4. Classroom remains independently visible and usable;
5. Classroom remains authoritative for access, sessions, attendance, recordings, chat, and boards.

Do not automatically create a room for every course. Avoid placing course IDs inside LiveKit room names or making Classroom authorization depend on LMS enrollment without an explicit integration contract.
