Skip to content

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 fileRole
server/app.tsThe relay: allow list, headers, size limit
server/guard.tsThe limits against abuse
src/shared/nextcloud.tsThe calls to Nextcloud
src/shared/recorder.tsRecording from the microphone
src/shared/mp3.tsConversion to MP3 (LAME in JavaScript)
src/shared/drafts.tsDrafts and candidates, in IndexedDB
src/shared/place.tsPersonal or shared device: where secrets are kept
src/shared/lock.tsThe optional local password, Argon2id
src/shared/idle.ts30 minutes without activity: forget or lock
src/shared/deposit.tsSending, alone or in a batch, and publishing in place
src/shared/store.tsClasses and assignments, and their merging
src/shared/vault.tsAES-256-GCM encryption
src/shared/sync.tsThe backup in Nextcloud
src/shared/pairing.tsThe pairing link, Argon2id
src/shared/version.tsThe PWA and its automatic update
src/shared/labels-pdf.tsThe 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.

MethodsPathUse
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>.mp3A memo, by the name the tool gave it
MOVEa memo, or the folder of a class or an assignmentTidying: 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/sharesPublic, read-only link, in the folder
GET/ocs/v2.php/cloud/userCheck 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):

LimitValueAnswer
Requests per address600 per minute429
Refused passwords per address10 in 15 minutes429
Bodies held in memory128 MB in all503
The same, per address40 MB503
Pairing numbers per address5 alive429

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, and pending-pairing.ts removes 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), hence wasm-unsafe-eval in the CSP.

Threat model, in short ​

WhoSeesDoes not see
The relayApp password, sounds, encrypted JSONKey, names
French hostingThe page's requestsEverything else, unless the served code is modified
The ENT's NextcloudSounds, encrypted JSONKey, names
Anyone with the linkA memo's soundThe rest

The accepted limits are described in Where does your data go?.