git clone https://git.noctentrion.com/egor/metergate.gitgit clone ssh://git@git.noctentrion.com:2222/egor/metergate.gitmetergate
Per-subject usage quotas with groups, per-user overrides, an automatic
monthly reset, and capability gating — i.e. "you've spent your media
allowance for the month, so uploads are off, but you can still type."
Standalone, zero required dependencies, and you can drive it from Python, a
CLI, or an HTTP API.
Initially developed for Noctentrion (a continuwuity-based Matrix homeserver).
Why this exists
Synapse enforces per-user rate limits and per-user media quotas natively.
conduwuit — and continuwuity, the fork it became — does not. It is a lightweight,
RocksDB-backed homeserver: it has a global media size cap and admin-side media
pruning, but no per-user rate-limit or media-quota subsystem, and no admin
surface to configure one. That gap has held across conduwuit's lifetime and
through continuwuity's current 0.5.x line (0.5.9 as of writing).
So the limit has to live outside the homeserver. metergate is that: a small,
generic quota engine you point at whatever per-user usage signal you already
collect — here, upload bytes — and let it decide what is still allowed.
Packaging, tests, the HTTP API, and final refinement done with Claude.
Install
pip install metergate # core: zero dependencies
pip install "metergate[postgres]" # or [redis]
Concepts
- resource — a quantity you count:
media_bytes,requests,messages. - group — a tier with per-resource limits, e.g.
freeorpatron. - override — a per-user limit that takes precedence over the group.
- period — when the counter resets:
month(default),week,day, or
never. Usage is bucketed by period, so the reset is automatic. - capability — an action gated by one or more resources. If any of them is
over its limit, the capability is denied and nothing else is affected.
Limit resolution is override → group → unlimited.
Use it from Python
from metergate import MeterGate, SQLiteStorage
mg = MeterGate(SQLiteStorage("quota.db"), period="month")
mg.define_group("free", {"media_bytes": "500MB"})
mg.define_group("patron", {"media_bytes": "50GB"})
mg.define_capability("upload_media", ["media_bytes"]) # gated by media_bytes
mg.set_subject("@alice", group="free")
mg.set_override("@alice", "media_bytes", "1GB") # @alice gets a bump
mg.use("@alice", "media_bytes", "300MB") # record an upload
mg.allowed("@alice", "upload_media") # True — still under 1GB
mg.check("@alice", "media_bytes").remaining # bytes left this month
mg.over_quota("media_bytes") # everyone who's blown it
…or the CLI
metergate group set free media_bytes=500MB requests=10000
metergate cap set upload_media media_bytes
metergate user set @alice --group free
metergate use @alice media_bytes 300MB
metergate check @alice --cap upload_media # prints ALLOWED/DENIED, exits 0/1
metergate over media_bytes # who's over this month
metergate top media_bytes -n 10 # biggest users
metergate export > quota.json # config out; `import` puts it back
Pick the backend with --storage (or $METERGATE_STORAGE):
memory · sqlite:quota.db · postgresql://… · redis://….
…or over HTTP, for everything that isn't Python
metergate serve --port 8400 --token s3cret
curl -H 'Authorization: Bearer s3cret' \
'http://localhost:8400/v1/check?subject=@alice&capability=upload_media'
# {"subject":"@alice","capability":"upload_media","allowed":true}
curl -H 'Authorization: Bearer s3cret' -X POST localhost:8400/v1/use \
-d '{"subject":"@alice","resource":"media_bytes","amount":"300MB"}'
Full surface: GET /v1/check · GET /v1/usage · GET /v1/over · GET /v1/top
· GET|POST /v1/groups · POST /v1/subjects · POST /v1/use · GET /healthz.
JSON in, JSON out, optional bearer token — so any platform that can hold a
conversation over HTTP can integrate it. (That was the point of the question,
yes — it has a real API now.)
The continuwuity use-case, concretely
# in your media-upload path:
if not mg.allowed(user_id, "upload_media"):
return forbidden("monthly media quota reached — text still works")
mg.use(user_id, "media_bytes", len(blob))
License
MIT — see LICENSE. There is also a LICENSES/ archive,
explained (with a mostly straight face) in LICENSES/README.md.