Hi all,
I am considering running CryptPad self-hosted for a small German business and have run
into an architectural limitation that I would like to check with the maintainers before opening a PR.
What we want to do
Our subscription service (a separate Rust process) needs to update a user's storage quota in CryptPad whenever the user upgrades or downgrades their allowances. The user expects the new limit to be live within seconds of changing the external allowance, not "next time the operator restarts the container".
Why we cannot do it cleanly today
The natural place to write a SET_QUOTA decree is from a small plugin endpoint (we added one under lib/plugins/xxx/quota/ exposing POST /v1/xxx/set-quota with a shared-secret header). The plugin runs inside http-worker, which is where we hit two problems:
The first problem is that Decrees.handleCommand(Env, decree) from http-worker only mutates the JSON snapshot of Env that the worker got at fork time (http-worker.js line 29: var Env = JSON.parse(process.env.Env)). That mutation is invisible to the main worker, which is the one that actually serves quota checks.
The second problem is that Decrees.write (the helper that admin-rpc uses) calls Env.scheduleDecree.ordered(...) which is undefined in the serialised snapshot, so it throws.
So our plugin currently uses Fs.appendFile to write the decree directly to decree.ndjson. That makes the change durable on disk, but the live Env keeps the old quota until the next CryptPad restart.
What we have working locally
We added a small file watcher in our plugin's main-worker initialize hook. After startup it tracks the file size, sets up Fs.watch, and on every change reads bytes from the last known offset, splits on \n, and calls Decrees.handleCommand(Env, decree) for each newly parsed line. CryptPad's own throttledEnvChange in server.js then propagates the updated Env to all http-workers within 250 ms, and we call Env.flushCache() to rotate FRESH_KEY.
End-to-end measured latency from "external service hits our endpoint" to "main-worker has applied the new customLimit" is about 50 ms in production. We have run it for a few weeks across several quota changes with no observed issues. The implementation is around 100 lines of Node and includes:
- an offset cursor so we never re-apply old decrees,
- a 50 ms debounce on
Fs.watch since inotify often fires multiple events per append,
- a pending-buffer for partial trailing lines in case the watcher fires mid-write,
- a truncation guard that resets the cursor instead of read-looping forever, and
- per-line try/catch around
JSON.parse and handleCommand so a single malformed entry cannot kill the watcher.
What we are checking before doing the PR work
We would happily clean this up and submit it as an upstream PR (likely in lib/api.js right after the existing Decrees.load(Env, ...) block, or as a small helper in lib/decrees.js). Before spending the time, three open questions:
First, security model. Are you comfortable treating decree.ndjson appends as authoritative when they did not pass through admin-rpc's admin-key check? In our deployment the file is owned by the cryptpad user and only writable by code running as that user, so any append is authorised at the OS level. That assumption may not hold for every operator.
Second, opt-in or always-on? An always-on watcher feels right because it costs nothing when no one writes to the file, but a config flag like watchDecreesForLiveReload: true is also fine if you want existing deployments to behave exactly as before.
Third, would you accept a PR that does this, or do you prefer a different mechanism for the underlying need ("external service updates a per-user customLimit live")? An alternative would be to add a addAdminCommands-style entry that authenticates via service bearer secret instead of operator pubkey, but that is a bigger change and fel unnecessary for us.
Happy to paste the actual changes into a follow-up if that helps the discussion. If you would rather see a draft PR straight away, I can do that too.
Thanks for your time, and thanks for CryptPad!