Architecture
For an introduction without jargon, see How it works. This page is for people who want to read or change the code.
Principle: everything that can be done in the browser is done there. The server keeps nothing, and the lasting storage is the teacher's Nextcloud.
The code
| Folder or file | Role |
|---|---|
server/app.ts | The relay: allow list, headers, size limit |
server/guard.ts | The limits against abuse |
src/shared/nextcloud.ts | The calls to Nextcloud |
src/shared/recorder.ts | Recording from the microphone |
src/shared/mp3.ts | Conversion to MP3 (LAME in JavaScript) |
src/shared/drafts.ts | Drafts and candidates, in IndexedDB |
src/shared/place.ts | Personal or shared device: where secrets are kept |
src/shared/lock.ts | The optional local password, Argon2id |
src/shared/idle.ts | 30 minutes without activity: forget or lock |
src/shared/deposit.ts | Sending, alone or in a batch, and publishing in place |
src/shared/store.ts | Classes and assignments, and their merging |
src/shared/vault.ts | AES-256-GCM encryption |
src/shared/sync.ts | The backup in Nextcloud |
src/shared/pairing.ts | The pairing link, Argon2id |
src/shared/version.ts | The PWA and its automatic update |
src/shared/labels-pdf.ts | The labels PDF |
What the relay lets through
The relay refuses whatever does not come from the page's origin and whatever has no credentials. Any other request gets 403, 404 or 405.
| Methods | Path | Use |
|---|---|---|
MKCOL | /remote.php/dav/files/<id>/Corico[/<class>[/<assignment>]] | Create the folders |
GET PUT | /remote.php/dav/files/<id>/Corico/<file> | Files of the folder itself |
GET PUT DELETE | /remote.php/dav/files/<id>/Corico/[<class>/[<assignment>/]]<uuid>.mp3 | A memo, by the name the tool gave it |
MOVE | a memo, or the folder of a class or an assignment | Tidying: a memo keeps its name, a folder its depth; inside the folder, same account, never over anything (Overwrite: F); never the saved content |
POST | /ocs/v2.php/apps/files_sharing/api/v1/shares | Public, read-only link, in the folder |
GET | /ocs/v2.php/cloud/user | Check the account |
It only passes on the useful headers, never a cookie, and follows no redirect. The body is kept in memory while being sent, and its size is capped at 20 MB.
What limits abuse
server/guard.ts, in memory only, with salted and hashed addresses (15 minutes at most):
| Limit | Value | Answer |
|---|---|---|
| Requests per address | 600 per minute | 429 |
| Refused passwords per address | 10 in 15 minutes | 429 |
| Bodies held in memory | 128 MB in all | 503 |
| The same, per address | 40 MB | 503 |
| Pairing numbers per address | 5 alive | 429 |
The page tells the teacher what to do on these answers, rather than an error code (src/shared/failure.ts).
An IPv6 address counts by its /64 network, the counts start again every 15 minutes under a new secret, and no body is read before the origin, the route, the credentials and the limits have been checked. A share link is only accepted for a name the tool gives its sounds, each setting once, in a plain form, with nothing else in the address.
An origin can be forged from a script: these limits are what stops it. They also protect other teachers, as Nextcloud would otherwise slow this server's address down for all.
Changing the key
After a device is lost (src/LostDevicePage.vue). The new key is first remembered apart, the backup is merged and written with it under If-Match, and only then does it replace the old one. If the page is closed in between, the next opening tries it on the backup and adopts it if it opens it. Other devices become locked until they are paired again, and write nothing in the meantime. The memos are not encrypted, and their links do not change.
Checked online
On November 5, 2026, from outside, on the page and on the relay: TLS 1.2 and 1.3 only, weak ciphers refused, Let's Encrypt certificates, HTTP redirected to HTTPS, HSTS for one year, the host's sensitive files refused, and a CSP, nosniff and frame refusal on the page. The relay adds to every answer nosniff, Content-Security-Policy: default-src 'none' and Referrer-Policy: no-referrer. The documentation's CSP keeps 'unsafe-inline' for scripts, as VitePress needs it: it holds no data, calls nothing, and has its own origin, so it cannot reach what the app keeps in the browser.
Sending a memo
A redone memo does a PUT on the same path, without creating a link. Since Nextcloud attaches the link to the file, the QR code stays valid (checked on the ENT).
The encrypted backup
The content (classes, assignments, grid) forms a single file, Corico/donnees-chiffrees.bin: IV (12 bytes) + AES-256-GCM(JSON).
The merge combines elements by identifier. What was removed is recorded in removed, with when it was, so that another device does not bring it back: an assignment, a class, or assignment/student, negative once brought back, the latest counting. A class removed stays if another device has just made an assignment of it. Of two versions of a name, the latest is kept, by the date at of each element. For the same student, the most recent memo is the one kept.
A removal deletes the files first (src/shared/removal.ts): on a failure, what is left stays listed and can be tried again. A memo sent by another device at the very moment it is removed would stay in the folder, unknown to the tool, to be deleted by hand.
Pairing
- The data follows the
#, which is never sent to the server, andpending-pairing.tsremoves it from the address before the router reads it. - 8 characters in base 32 make 40 bits: Argon2id makes trying every code offline very slow.
- The relay's number (
server/pairing.ts,POST /pairing) is drawn by the device making the link and taken once by the one opening it (POST /pairing/<id>, then 410): in memory, 10 minutes at most, 5 alive per address and 2,000 in all. A date in the link could not do this: whoever holds the link and the code would decrypt it outside the app. Without the number, nothing opens. - The opening device takes the number at the first try and keeps it in memory: a mistyped code is typed again; a reload loses it, and a new link is needed.
- Argon2id is loaded on demand, in WebAssembly (
hash-wasm), hencewasm-unsafe-evalin the CSP.
Threat model, in short
| Who | Sees | Does not see |
|---|---|---|
| The relay | App password, sounds, encrypted JSON | Key, names |
| French hosting | The page's requests | Everything else, unless the served code is modified |
| The ENT's Nextcloud | Sounds, encrypted JSON | Key, names |
| Anyone with the link | A memo's sound | The rest |
The accepted limits are described in Where does your data go?.