# metergate 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. `free` or `patron`. - **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 ```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 ```python # 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`](LICENSE). There is also a [`LICENSES/`](LICENSES/) archive, explained (with a mostly straight face) in [`LICENSES/README.md`](LICENSES/README.md).