# CLAUDE.md: Video Conferencing Module (Phase 1)

Read this file first in every session. It is the project brief for Claude Code.

## What this repo is

The backend of Rhysley's **Video Conferencing Module**: a company-controlled meetings service built on **self-hosted, unmodified LiveKit**, with every business rule in our own **FastAPI (Python)** backend.

- **First integrating product:** HelixtaHR (Rhysley's HR/ERP product). Users open Meetings from the HelixtaHR left menu, with no second login.
- **Later products:** EazeeSign and others. The module must plug into **any** product without changing meeting logic.
- **Repo:** GitLab, `https://gitlab.rhysley.com/video-conferencing-tool/video-conferencing-module-backend`. Merge requests (not GitHub). No CI pipeline: the GitLab instance has no runner set up for it, so tests and lint run locally.
- **Developer:** Arshdeep Singh. **Engineering lead / reviewer:** Amit Kumar.
- **Jira:** HelixtaHR space, keys `HEL-xxxx`. Phase 1 = Video Conferencing Module stories in Epics E1, E2, E4, E5, E8 (see `docs/STORIES.md`).

## Read these docs (in this order)

| File | What it gives you |
|---|---|
| `docs/WORKPLAN.md` | **The working list.** Tickets in build order, why that order, checkbox tasks, what is blocked and on whom. Tick tasks as you finish them. |
| `docs/STORIES.md` | Every Jira story: user story, current rules, acceptance criteria as amended by Management, exact on-screen messages, out of scope. |
| `docs/DECISIONS.md` | Every decision in force (latest wins), decisions that were **reversed**, and open questions. Check here before implementing any rule. |
| `docs/HLD.md` | The High-Level Design sent to Amit (7 pages). |
| `docs/HELIXTAHR_CONTRACT.md` | What HelixtaHR must build for us (status endpoint, deactivation/role-change notice) and the token format. |
| `docs/HEL-1044_LLD_Access_Gate.docx` | LLD of the access gate (session exchange, token and allowed checks, flow diagrams). |
| `docs/HEL-972_LLD_Meetings_Page.docx` | LLD of the Meetings page: data model, organisation isolation, list rule, history, opening a link. |
| `docs/HEL-973_LLD_Start_Meeting.docx` | LLD of Start meeting and the Host's Join now: audit records, double-click guard, LiveKit room and meeting pass, clean-up of unjoined meetings. |
| `docs/HEL-977_LLD_Join_Checks_Waiting_Room.docx` | LLD of HEL-977 with the backend of HEL-982/981: counting the 25 places and held places, the meeting lock, waiting room, Admit/Deny, Co-hosts, LiveKit webhook, alerts. |
| `docs/HEL-1026_LLD_Leave_End_Meeting.docx` | LLD of HEL-1026: Leave, handover, End meeting, auto-end, attendance records, who may do what. |
| `docs/HEL-1012_LLD_Acting_Host.docx` | LLD of HEL-1012: who manages after an unexpected drop, the Acting Host, stepping down, the notice. |
| `docs/HEL-1136_LLD_Permanent_Meeting_Rooms.docx` | LLD of HEL-1136: rooms and sessions, converting, the address, the grace period, owner settings, turning off. |
| `docs/HEL-980_LLD_Conduct_Meeting.docx` | LLD of HEL-980: one screen share at a time, the waiting request, handing over, host stop, chat without storage. |
| `docs/HEL-1032_LLD_Hands_Reactions.docx` | LLD of HEL-1032 (short): raised hands as a LiveKit attribute set by the backend, hosts lowering any hand, reactions as data messages, nothing stored. |
| `docs/HEL-983_984_LLD_Mute_Remove.docx` | LLD of HEL-983 and HEL-984 (one document): who may act on whom, Mute and Mute all (LiveKit server mute), Remove (shared with the access re-check), records. |
| `docs/Meetings_API_Frontend_Guide.docx` | What the frontend (inside HelixtaHR) calls and how: tokens, all endpoints so far, errors, renewal, waiting room, Admit/Deny, Co-hosts, Leave/End, Acting Host, Permanent Meeting Rooms, screen sharing and chat, raised hands and reactions, Mute and Remove (v1.9: HEL-1044, 972, 973, 977, 982, 981, 1026, 1012, 1136, 980, 1032, 983, 984). `_v1.2` and `_v1.3` are the HEL-973 and HEL-977 stage editions. All built by `guide.js` in the doc generator. |
| `docs/LIVEKIT_SETUP.md` | Running LiveKit locally and how our LiveKit adapter works. |
| `docs/FOLLOWUPS_LOG.md` | Full history of the Jira review rounds (30 Oct to 2 Nov 2026). **History only, never build from it**: it uses old HelixtaHR labels H1–H7 (mapping at its top) and contains notes later replaced. `DECISIONS.md` is the current truth. |

## Status (as of 2 Nov 2026)

- Management (Harpreet Randhawa) reviewed all 11 Phase 1 stories. **9 approved with conditions**, **HEL-1043 and HEL-1026 returned** (on hold until the meeting lifecycle is agreed).
- HLD drafted by Arshdeep and emailed to Amit for review; it ends with 18 open decisions.
- **Start with HEL-1044**, once (a) the BA has recorded Management's corrections in the story and (b) Amit has assigned it.
- Nothing is built yet. Develop **locally first** (local LiveKit server + Postgres, no Docker), then move to dev servers by changing `.env` only.

## Design principles (non-negotiable)

1. **Product-neutral core.** The meeting module knows only: integrating product → organisation → user → role. It never mentions HelixtaHR or its terms (company, CRM, `company_id`, `role_name`); `tests/test_product_neutral.py` fails if anything outside `app/integrations/` does. The module must be movable into any app.
2. **Product specifics live in an adapter** under `app/integrations/<product>/`, with its own settings: token verification, claim mapping, "is Meetings enabled for this organisation", status check, event receiver. HelixtaHR is the first adapter. A new product = a new adapter + one line in `integrations/installed.py`, no core changes.
3. **Everything is keyed by (product, organisation).** For HelixtaHR, organisation = the `company_id` claim (test server: `26` = "Mehar by Rhysley"). Meetings, queries and audit all carry both keys.
4. **The backend enforces every rule.** Hiding a button is never the only protection. Every endpoint goes through one shared dependency that verifies the token and asks the product adapter whether the organisation has Meetings.
5. **LiveKit is unmodified.** Only `app/livekit/adapter.py` talks to LiveKit (via the official `livekit-api` SDK). Tests use a fake adapter.
6. **Secrets stay on the server.** The browser only ever gets a short-lived meeting pass. Never commit secrets, tokens or passwords. `.env` is git-ignored; `.env.example` lists names only.
7. **Phase 1 stores no audio, video, chat, raised hands or reactions.** Audit records (who did what, when) are mandatory.

## Stack

- Python 3.11+, FastAPI, Uvicorn, Pydantic v2 settings
- SQLAlchemy 2 (async) + Alembic, PostgreSQL
- `livekit-api` (server SDK), `pyjwt`, `httpx`
- pytest (+ pytest-asyncio), ruff
- LiveKit server v1.13.11 as a plain program locally (no Docker)
- Frontend: the Meetings screens are built **inside HelixtaHR's own frontend** (decided 3 Nov), with `livekit-client` + `@livekit/components-react`, styled as HelixtaHR (no LiveKit demo look). They already hold `meeting_jwt_token`; they call our `POST /v1/session` first. Not in this repo.

## Planned layout

```
app/
  main.py                  FastAPI app, routers, lifespan (creates LiveKit adapter)
  core/                    config, db, logging, errors, Principal model
  integrations/            everything product-specific; nothing else imports a product
    base.py                interface every product adapter implements
    registry.py            maps token issuer (iss) -> product adapter
    installed.py           product name -> adapter factory (ENABLED_PRODUCTS)
    helixtahr/             HelixtaHR adapter + its settings: verify meeting_jwt_token,
                           map claims, meetings-allowed call (also used by the
                           4-minute re-check; no notices from HelixtaHR)
  access/                  current_principal dependency (token + organisation enabled)
  livekit/                 adapter.py (only LiveKit caller) + webhooks.py
  revocation/              notice handling + re-check job (at least every 4 min)
  audit/                   audit records
  meetings/ admission/ controls/ ...   added story by story
migrations/  tests/  livekit.yaml  .env.example
```

## Core domain rules (cheat sheet; details in STORIES.md / DECISIONS.md)

- **Access:** every user of an **enabled organisation** can use Meetings. No per-role permission. Allowlist = Rhysley companies only in Phase 1; customer companies need Management approval.
- **Org admin:** HelixtaHR `role_name` in {`admin`, `superadmin`} → `org_admin` (Settings tab only). Match on role **name**, never `role_id` (IDs differ per environment).
- **Admission:** only **Host and Co-hosts** join directly. Everyone else, including same-company employees, **waits** for Host / Co-host / Acting Host. A link or a login never bypasses admission. Ordinary participants can never admit. No host present → keep waiting.
- **Waiting users** are never connected to LiveKit: no audio/video/content either way, private preview, not counted toward capacity.
- **Capacity:** 25 connected devices (+ places held for reconnection). One person on two devices = 2 devices, 1 attendee.
- **Meeting pass:** LiveKit token issued only at Join now or on admission; one user, one org, one meeting, **one device** (unique identity per device); ~10 min to connect.
- **Mic defaults:** Host/Co-host join mic **on**; everyone else mic **off**; camera per pre-join choice. **Any reconnect comes back muted.**
- **Reconnection:** admitted participant has **2 minutes** to reconnect without re-admission; their place is held and counts toward 25. Removed/denied/expired → fresh admission.
- **Instant meetings only in Phase 1:** created at **Join now** (not Start meeting) together with an audit record; leaving pre-join creates nothing.
- **Lifecycle (direction 3 Nov, all of Phase 1):** one **Start Meeting** button; creator = Host; secure temporary link. Controls are **Leave** and **End Meeting**. End Meeting disconnects everyone, releases waiting users, and a normal meeting's **link becomes invalid**. The last Host/Co-host leaving on purpose must **hand over**; automatic Acting Host only on unexpected drop.
- **Permanent Meeting Rooms (Phase 1):** only the original Host converts a running meeting (name, custom URL like `/meet/harpreet`, standing Co-hosts, grace period). Owner = creator; only the owner changes settings or disables it. Only owner/standing Co-hosts start sessions; locked otherwise; everyone else waits for admission. No Acting Host; auto-ends after the grace period with no owner/standing Co-host. End Meeting ends the session, not the room. Owner loses access → rooms disabled. Data model: a meeting (the link) has one or more sessions.
- **Screen share:** one at a time, granted by the backend. If A shares and B selects Share, B picks the screen at once (browser rule), A gets a "Stop sharing" request (hosts see it too); when A stops, B's prepared share goes live automatically. Hosts can stop any share. No starting a share from phones.
- **Raised hands:** ordered earliest first; hosts see an indicator even while presenting; hosts can lower any hand → the person sees a message and their hand icon disappears.
- **Revocation:** no notices from HelixtaHR in Phase 1. Our backend re-checks every connected/waiting user with HelixtaHR (`/api/v1/meetings/allowed`, user's own token) **at least every 4 minutes** and fresh at every join → removal always **within 5 minutes**. HelixtaHR unreachable → refuse new joins + alert; keep existing participants until re-checked (pending confirmation).
- **Not signed in** → redirect to HelixtaHR sign-in. Other company → "You can't join this meeting". No refusal screen reveals meeting details.

## Not in Phase 1 (do not build)

External guests and invitations, interviews/scheduled meetings from HelixtaHR, rescheduling, webhooks to HelixtaHR, background blur, recording, transcription, minutes, private chat, file sharing, clickable chat links, pinning, speaker highlight, remembering device choices, joining windows, link reset.

## HelixtaHR facts (adapter only)

- Login: `POST /api/v1/login` returns `meeting_jwt_token` (JWT, HS256, 30-day lifetime, `token_type: "meeting"`), our own token. The Helix AI token (`ai_assist_jwt_token`, same issuer) is refused. HelixtaHR Meetings endpoints live under `/api/v1/meetings/...`.
- Company enablement is set in Rhysley's CRM; HelixtaHR exposes it as `GET /api/v1/meetings/allowed` → `{"meetings_allowed": bool}` (user's own token). No allowlist of our own.
- Claims: `iss`, `sub`, `user_id`, `company_id`, `company_name`, `role_id`, `role_name`, `user_type`, `emp_no`, `name`, `iat`, `exp`, `nbf`, `jti`, `prv`.
- Test issuer: `https://erp-api14.studyineurope.xyz/api/v1/login`. Organisation = `company_id` (not `company_name`, not `prv`). `user_type` meaning is unknown (ask HelixtaHR).
- Known gap: no `aud` claim; secret shared with Helix AI. Accept for Phase 1.

## Local development

No Docker and no CI. PostgreSQL is installed locally (Windows service, port 5432, user `livekit_meetings`, databases `livekit_meetings` for the app and `livekit_meetings_test` for tests); LiveKit runs as the plain `livekit-server` program with `livekit.yaml`. Tests and lint are run by hand before every push. Windows / PowerShell (Arshdeep's machine); see README.md for macOS/Linux and the one-time database setup:

```powershell
Copy-Item .env.example .env       # fill local values; never commit .env
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements-dev.txt
alembic upgrade head
& "$env:USERPROFILE\livekit\livekit-server.exe" --config livekit.yaml   # separate terminal
uvicorn app.main:create_app --factory --reload --port 8001   # http://localhost:8001/health
pytest; ruff check .; ruff format --check .
```

Dependencies live in `requirements.txt` (runtime) and `requirements-dev.txt` (adds test/lint tools); `pyproject.toml` holds tool settings only. The app is built by `create_app()` so tests can pass their own settings and the fake LiveKit adapter.

Env var names: `DATABASE_URL`, `TEST_DATABASE_URL` (tests only; defaults to the local `livekit_meetings_test`), `LIVEKIT_URL` (browser, ws://localhost:7880), `LIVEKIT_API_URL` (server, http://localhost:7880), `LIVEKIT_API_KEY`, `LIVEKIT_API_SECRET`, `ENVIRONMENT`, `LOG_LEVEL`, `ENABLED_PRODUCTS` (comma-separated, e.g. `helixtahr`), `SESSION_SECRET` (ours, 32+ chars, per environment), `SESSION_TTL_SECONDS` (default 900). HelixtaHR adapter (read only in `app/integrations/helixtahr/`): `HELIXTAHR_ISSUER`, `HELIXTAHR_JWT_SECRET`, `HELIXTAHR_BASE_URL`, `HELIXTAHR_TIMEOUT_SECONDS` (default 3). `RECHECK_INTERVAL_SECONDS` (default and maximum 240).

## Working conventions

- Branch per story, cut from `phase-1`: `feature/HEL-1044-access-and-isolation`; merge into `phase-1` via merge request reviewed by Amit. `phase-1` merges into `main` (one merge request) when Phase 1 is done; `main` = stable.
- Commit messages start with the Jira key: `HEL-1044: verify HelixtaHR token`.
- Type hints and async everywhere; routers thin, rules in services, LiveKit only via the adapter.
- Every rule gets a test; every host/admission/removal action writes an audit record.
- When a rule is unclear or listed as open in `DECISIONS.md`, **ask Arshdeep** instead of inventing behaviour. Use the recommended answer only if he says so.
- After finishing work, tick the tasks in `docs/WORKPLAN.md`; if a decision changes in Jira, update `docs/DECISIONS.md`.

## People

| Person | Role |
|---|---|
| Harpreet Randhawa | Management (CEO); reviews and approves stories |
| Harsh Tyagi | Product Manager; owns PRD (v0.5 on RH001ES-735) and story updates |
| Amit Kumar | Engineering lead; assigns developers, owns HLD/LLD approval |
| Deepak Srivastava | QA HOD; story-level QA approval |
| Mukesh | Code review and deployment of this backend; email (SMTP) channel for alerts; helps the frontend engineer integrate |
| Deepak (frontend engineer) | Builds the Meetings screens inside HelixtaHR's frontend from the Frontend Integration Guide. Not Deepak Srivastava (QA HOD) |
| Ishank Verma | Design (prototype, screens) |
| Harshit Yadav | Business Analyst; records story corrections |
| Surinder Chahal | Requested HelixtaHR developer for H1–H3 (not yet confirmed) |
| Arshdeep Singh | Developer of this module |
