{"_id":"@021.is/spine-ratelimit","_rev":"2-9807307d6576c44d9a74c0c214691aa0","name":"@021.is/spine-ratelimit","dist-tags":{"latest":"0.4.3"},"versions":{"0.4.0":{"name":"@021.is/spine-ratelimit","version":"0.4.0","license":"MIT","_id":"@021.is/spine-ratelimit@0.4.0","maintainers":[{"name":"edvone","email":"edvard@edvone.dev"}],"homepage":"https://github.com/021is/spine-ts#readme","bugs":{"url":"https://github.com/021is/spine-ts/issues"},"dist":{"shasum":"2a9f3d6ce88bc8ac0f9d46c087009f7c2e3e9ef6","tarball":"https://registry.npmjs.org/@021.is/spine-ratelimit/-/spine-ratelimit-0.4.0.tgz","fileCount":4,"integrity":"sha512-6kQXfzm6Ibx7OblrjYH+2eOlMpn+FFIG9C88hHoOhQIWrufiy0mDxEPBne+7gAYNztcjEgsJHpaUsaB5ex3WfQ==","signatures":[{"sig":"MEUCIQDkudzIOZdNGG++wf3coiWg2wM3Wsn+xnfAmCJkHf6IlAIgGkVxsuGFzemewUte4H5lC72VrPuq3DAUuD8koGjm3Rs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":8144},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"e4434c6f59db8f586e0f05ffdb3b88a96e68e67b","scripts":{"build":"tsup src/index.ts --format esm --dts --clean","typecheck":"tsc --noEmit"},"_npmUser":{"name":"edvone","email":"edvard@edvone.dev"},"repository":{"url":"git+https://github.com/021is/spine-ts.git","type":"git","directory":"packages/ratelimit"},"_npmVersion":"10.9.4","description":"Postgres-backed sliding-window rate limiter. Sliding-window per-key buckets. No Redis required.","directories":{},"_nodeVersion":"22.22.0","dependencies":{"@021.is/spine-errors":"^0.4.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/spine-ratelimit_0.4.0_1779981968111_0.09835923279342462","host":"s3://npm-registry-packages-npm-production"}},"0.4.3":{"name":"@021.is/spine-ratelimit","version":"0.4.3","license":"MIT","description":"Postgres-backed sliding-window rate limiter. Sliding-window per-key buckets. No Redis required.","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"build":"tsup src/index.ts --format esm --dts --clean","typecheck":"tsc --noEmit"},"dependencies":{"@021.is/spine-errors":"^0.4.3"},"repository":{"type":"git","url":"git+https://github.com/021is/spine-ts.git","directory":"packages/ratelimit"},"publishConfig":{"access":"public"},"_id":"@021.is/spine-ratelimit@0.4.3","gitHead":"b9f9333776fa55686cc2feb05f851aa6000b0e05","bugs":{"url":"https://github.com/021is/spine-ts/issues"},"homepage":"https://github.com/021is/spine-ts#readme","_nodeVersion":"22.22.0","_npmVersion":"10.9.4","dist":{"integrity":"sha512-92Stx/iliJPTRTioIQ2/Wta4k6ZL/s0COnM9UzjysfZ0P0Q901OEZ3ImQmtnrULoREEaZNSEZPLKj61NS+LTNg==","shasum":"cbffe7a0519032f55b78ba3d9aeea055fe101d7b","tarball":"https://registry.npmjs.org/@021.is/spine-ratelimit/-/spine-ratelimit-0.4.3.tgz","fileCount":5,"unpackedSize":9203,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQC5GXKGvQ3aCDp7VN8qGc1CO2Qji3InIy7YvCwBoHuY1QIhAOqHE7r6CupBJVkpgc8XoNZdFeWxR89L9zQCqe4n6sYB"}]},"_npmUser":{"name":"edvone","email":"edvard@edvone.dev"},"directories":{},"maintainers":[{"name":"edvone","email":"edvard@edvone.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/spine-ratelimit_0.4.3_1779995254290_0.5286746704914733"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-28T15:26:07.881Z","modified":"2026-05-28T19:07:34.581Z","0.4.0":"2026-05-28T15:26:08.262Z","0.4.3":"2026-05-28T19:07:34.434Z"},"bugs":{"url":"https://github.com/021is/spine-ts/issues"},"license":"MIT","homepage":"https://github.com/021is/spine-ts#readme","repository":{"type":"git","url":"git+https://github.com/021is/spine-ts.git","directory":"packages/ratelimit"},"description":"Postgres-backed sliding-window rate limiter. Sliding-window per-key buckets. No Redis required.","maintainers":[{"name":"edvone","email":"edvard@edvone.dev"}],"readme":"# @021.is/spine-ratelimit\n\nPostgres-backed sliding-window rate limiter. Sliding-window per-key buckets. Multi-window in parallel (1m + 1h + 1d). No Redis required.\n\n## Use\n\n```ts\nimport { RateLimiter, makePrismaStore } from \"@021.is/spine-ratelimit\";\n\nconst limiter = new RateLimiter({ store: makePrismaStore(prisma) });\n\n// On every API request:\nawait limiter.hit({\n  subject: `api-key:${keyId}`,\n  limits: [\n    { window: \"1m\", max: 60 },\n    { window: \"1d\", max: 100_000 },\n  ],\n}); // throws RateLimitedException → spine-errors maps to 429 + Retry-After\n```\n\n## Prisma schema\n\n```prisma\nmodel RateLimitBucket {\n  subject     String\n  window      String\n  bucketStart DateTime\n  count       Int      @default(0)\n  @@id([subject, window, bucketStart])\n}\n```\n\n## Behavior\n\n- Atomic `UPSERT + increment` per (subject, window, bucketStart). Postgres guarantees concurrency safety.\n- Multi-window in parallel: a single `hit()` checks 1m AND 1d limits at once.\n- `RateLimitedException.retryAfterSec` reflects the **tightest** window — the one that has the least time until reset.\n- Subject isolation: different subjects share zero state.\n\n## In-memory adapter (for tests)\n\n```ts\nimport { makeMemoryStore } from \"@021.is/spine-ratelimit\";\nconst limiter = new RateLimiter({ store: makeMemoryStore() });\n```\n\n## Why Postgres, not Redis\n\n- One less infrastructure component\n- Atomic guarantees come free with Postgres\n- For typical 021 workloads (≤ 1k req/s per app), Postgres handles it without breaking a sweat\n- Cleanup: prune buckets older than your widest window via a daily cron\n","readmeFilename":"README.md"}