[{"data":1,"prerenderedAt":828},["ShallowReactive",2],{"navigation:en":3,"\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Frate-limiting:en":311},[4],{"title":5,"path":6,"stem":7,"children":8,"icon":10},"Nuxfire Docs","\u002Fdocs","docs\u002F0.index",[9,11,152],{"title":5,"path":6,"stem":7,"icon":10},"i-ph-squares-four",{"title":12,"path":13,"stem":14,"children":15,"icon":151},"SaaS Starter Kit","\u002Fdocs\u002Fsaas-starter-kit","docs\u002F1.saas-starter-kit\u002F0.index",[16,19,63,129],{"title":17,"path":13,"stem":14,"icon":18},"What is the SaaS Starter Kit?","i-ph-info",{"title":20,"icon":21,"hide":22,"path":23,"stem":24,"children":25,"page":62},"Getting Started","i-ph-play",true,"\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started","docs\u002F1.saas-starter-kit\u002F1.getting-started",[26,30,34,38,42,46,50,54,58],{"title":27,"path":28,"stem":29},"Quick Start","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fquick-start","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F0.quick-start",{"title":31,"path":32,"stem":33},"Installation","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Finstallation","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F1.installation",{"title":35,"path":36,"stem":37},"Cloudflare Access","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fcloudflare-access","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F2.cloudflare-access",{"title":39,"path":40,"stem":41},"Configuration","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fconfiguration","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F3.configuration",{"title":43,"path":44,"stem":45},"Secrets","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fsecrets","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F4.secrets",{"title":47,"path":48,"stem":49},"Environment Variables","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fenvironment-variables","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F5.environment-variables",{"title":51,"path":52,"stem":53},"Deploy","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fdeploy","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F6.deploy",{"title":55,"path":56,"stem":57},"Local Development","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Flocal-development","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F7.local-development",{"title":59,"path":60,"stem":61},"Project Structure","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fproject-structure","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F8.project-structure",false,{"title":64,"icon":65,"hide":22,"path":66,"stem":67,"children":68,"page":62},"Features","i-ph-cube","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures","docs\u002F1.saas-starter-kit\u002F2.features",[69,73,77,81,85,89,93,97,101,105,109,113,117,121,125],{"title":70,"path":71,"stem":72},"Authentication","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fauth","docs\u002F1.saas-starter-kit\u002F2.features\u002F1.auth",{"title":74,"path":75,"stem":76},"Rate Limiting","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Frate-limiting","docs\u002F1.saas-starter-kit\u002F2.features\u002F10.rate-limiting",{"title":78,"path":79,"stem":80},"WebSockets","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fwebsockets","docs\u002F1.saas-starter-kit\u002F2.features\u002F11.websockets",{"title":82,"path":83,"stem":84},"Blog","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fblog","docs\u002F1.saas-starter-kit\u002F2.features\u002F14.blog",{"title":86,"path":87,"stem":88},"Documentation","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fdocumentation","docs\u002F1.saas-starter-kit\u002F2.features\u002F15.documentation",{"title":90,"path":91,"stem":92},"Analytics","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fanalytics","docs\u002F1.saas-starter-kit\u002F2.features\u002F16.analytics",{"title":94,"path":95,"stem":96},"Platform Admin Console","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fplatform-admin","docs\u002F1.saas-starter-kit\u002F2.features\u002F17.platform-admin",{"title":98,"path":99,"stem":100},"Database","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fdatabase","docs\u002F1.saas-starter-kit\u002F2.features\u002F2.database",{"title":102,"path":103,"stem":104},"Payments & Billing","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fpayments","docs\u002F1.saas-starter-kit\u002F2.features\u002F3.payments",{"title":106,"path":107,"stem":108},"Emails","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Femails","docs\u002F1.saas-starter-kit\u002F2.features\u002F4.emails",{"title":110,"path":111,"stem":112},"Notifications","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fnotifications","docs\u002F1.saas-starter-kit\u002F2.features\u002F5.notifications",{"title":114,"path":115,"stem":116},"Role-Based Access Control","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Frbac","docs\u002F1.saas-starter-kit\u002F2.features\u002F6.rbac",{"title":118,"path":119,"stem":120},"Async Jobs","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fasync-jobs","docs\u002F1.saas-starter-kit\u002F2.features\u002F7.async-jobs",{"title":122,"path":123,"stem":124},"Storage","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fstorage","docs\u002F1.saas-starter-kit\u002F2.features\u002F8.storage",{"title":126,"path":127,"stem":128},"Internationalization","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Finternationalization","docs\u002F1.saas-starter-kit\u002F2.features\u002F9.internationalization",{"title":130,"icon":131,"path":132,"stem":133,"children":134,"page":62},"Concepts","i-ph-graduation-cap","\u002Fdocs\u002Fsaas-starter-kit\u002Fconcepts","docs\u002F1.saas-starter-kit\u002F3.concepts",[135,139,143,147],{"title":136,"path":137,"stem":138},"Technology Stack","\u002Fdocs\u002Fsaas-starter-kit\u002Fconcepts\u002Ftech-stack","docs\u002F1.saas-starter-kit\u002F3.concepts\u002F0.tech-stack",{"title":140,"path":141,"stem":142},"SST.dev","\u002Fdocs\u002Fsaas-starter-kit\u002Fconcepts\u002Fsst","docs\u002F1.saas-starter-kit\u002F3.concepts\u002F1.sst",{"title":144,"path":145,"stem":146},"NuxtHub","\u002Fdocs\u002Fsaas-starter-kit\u002Fconcepts\u002Fnuxt-hub","docs\u002F1.saas-starter-kit\u002F3.concepts\u002F2.nuxt-hub",{"title":148,"path":149,"stem":150},"Design System & Branding","\u002Fdocs\u002Fsaas-starter-kit\u002Fconcepts\u002Fdesign-system","docs\u002F1.saas-starter-kit\u002F3.concepts\u002F3.design-system","i-ph-lightning",{"title":153,"path":154,"stem":155,"children":156,"icon":159},"Fireskills","\u002Fdocs\u002Ffireskills","docs\u002F2.fireskills\u002F0.index",[157,160,219,249,275,293],{"title":158,"path":154,"stem":155,"icon":159},"What is Fireskills?","i-ph-fire",{"title":20,"icon":21,"hide":22,"path":161,"stem":162,"children":163,"page":62},"\u002Fdocs\u002Ffireskills\u002Fgetting-started","docs\u002F2.fireskills\u002F1.getting-started",[164,167,171,175,179,183,187,191,195,199,203,207,211,215],{"title":27,"path":165,"stem":166},"\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fquick-start","docs\u002F2.fireskills\u002F1.getting-started\u002F00.quick-start",{"title":168,"path":169,"stem":170},"Workflow & Governance","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fworkflow","docs\u002F2.fireskills\u002F1.getting-started\u002F01.workflow",{"title":172,"path":173,"stem":174},"Project Context","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fproject-context","docs\u002F2.fireskills\u002F1.getting-started\u002F02.project-context",{"title":176,"path":177,"stem":178},"Step 0: Setup","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-setup","docs\u002F2.fireskills\u002F1.getting-started\u002F03.step-setup",{"title":180,"path":181,"stem":182},"Step 1: Inbox","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-inbox","docs\u002F2.fireskills\u002F1.getting-started\u002F04.step-inbox",{"title":184,"path":185,"stem":186},"Step 2: Backlog","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-backlog","docs\u002F2.fireskills\u002F1.getting-started\u002F05.step-backlog",{"title":188,"path":189,"stem":190},"Step 3: Specify","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-specify","docs\u002F2.fireskills\u002F1.getting-started\u002F06.step-specify",{"title":192,"path":193,"stem":194},"Step 4: Validate","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-validate","docs\u002F2.fireskills\u002F1.getting-started\u002F07.step-validate",{"title":196,"path":197,"stem":198},"Step 5: Tasks","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-tasks","docs\u002F2.fireskills\u002F1.getting-started\u002F08.step-tasks",{"title":200,"path":201,"stem":202},"Step 6: TDD\u002FBDD","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-tdd-bdd","docs\u002F2.fireskills\u002F1.getting-started\u002F09.step-tdd-bdd",{"title":204,"path":205,"stem":206},"Step 7: Implement","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-implement","docs\u002F2.fireskills\u002F1.getting-started\u002F10.step-implement",{"title":208,"path":209,"stem":210},"Step 8: Documentator","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-documentator","docs\u002F2.fireskills\u002F1.getting-started\u002F11.step-documentator",{"title":212,"path":213,"stem":214},"Late Changes","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Flate-changes","docs\u002F2.fireskills\u002F1.getting-started\u002F12.late-changes",{"title":216,"path":217,"stem":218},"Other Entry Points","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fother-entry-points","docs\u002F2.fireskills\u002F1.getting-started\u002F13.other-entry-points",{"title":220,"icon":221,"hide":22,"path":222,"stem":223,"children":224,"page":62},"Methodology","i-ph-git-branch","\u002Fdocs\u002Ffireskills\u002Fmethodology","docs\u002F2.fireskills\u002F2.methodology",[225,229,233,237,241,245],{"title":226,"path":227,"stem":228},"The Three Acts","\u002Fdocs\u002Ffireskills\u002Fmethodology\u002Fthree-acts","docs\u002F2.fireskills\u002F2.methodology\u002F0.three-acts",{"title":230,"path":231,"stem":232},"The Three Gates","\u002Fdocs\u002Ffireskills\u002Fmethodology\u002Fthree-gates","docs\u002F2.fireskills\u002F2.methodology\u002F1.three-gates",{"title":234,"path":235,"stem":236},"The Canonical Spec","\u002Fdocs\u002Ffireskills\u002Fmethodology\u002Fcanonical-spec","docs\u002F2.fireskills\u002F2.methodology\u002F2.canonical-spec",{"title":238,"path":239,"stem":240},"Lifecycle States","\u002Fdocs\u002Ffireskills\u002Fmethodology\u002Flifecycle-states","docs\u002F2.fireskills\u002F2.methodology\u002F3.lifecycle-states",{"title":242,"path":243,"stem":244},"Effort Scale","\u002Fdocs\u002Ffireskills\u002Fmethodology\u002Feffort-scale","docs\u002F2.fireskills\u002F2.methodology\u002F4.effort-scale",{"title":246,"path":247,"stem":248},"Milestones & Governance","\u002Fdocs\u002Ffireskills\u002Fmethodology\u002Fgovernance-and-milestones","docs\u002F2.fireskills\u002F2.methodology\u002F5.governance-and-milestones",{"title":250,"icon":251,"hide":22,"path":252,"stem":253,"children":254,"page":62},"Skills","i-ph-stack","\u002Fdocs\u002Ffireskills\u002Fskills","docs\u002F2.fireskills\u002F3.skills",[255,259,263,267,271],{"title":256,"path":257,"stem":258},"Catalog","\u002Fdocs\u002Ffireskills\u002Fskills\u002Fcatalog","docs\u002F2.fireskills\u002F3.skills\u002F0.catalog",{"title":260,"path":261,"stem":262},"Act I: Define","\u002Fdocs\u002Ffireskills\u002Fskills\u002Fact-1-define","docs\u002F2.fireskills\u002F3.skills\u002F1.act-1-define",{"title":264,"path":265,"stem":266},"Act II: Design & Prove","\u002Fdocs\u002Ffireskills\u002Fskills\u002Fact-2-design-prove","docs\u002F2.fireskills\u002F3.skills\u002F2.act-2-design-prove",{"title":268,"path":269,"stem":270},"Act III: Deliver","\u002Fdocs\u002Ffireskills\u002Fskills\u002Fact-3-deliver","docs\u002F2.fireskills\u002F3.skills\u002F3.act-3-deliver",{"title":272,"path":273,"stem":274},"Governance & Decisions","\u002Fdocs\u002Ffireskills\u002Fskills\u002Fgovernance-and-decisions","docs\u002F2.fireskills\u002F3.skills\u002F4.governance-and-decisions",{"title":276,"icon":277,"hide":22,"path":278,"stem":279,"children":280,"page":62},"Specialists","i-ph-star-four","\u002Fdocs\u002Ffireskills\u002Fspecialists","docs\u002F2.fireskills\u002F4.specialists",[281,285,289],{"title":282,"path":283,"stem":284},"Overview","\u002Fdocs\u002Ffireskills\u002Fspecialists\u002Foverview","docs\u002F2.fireskills\u002F4.specialists\u002F0.overview",{"title":286,"path":287,"stem":288},"Nuxfire Stack","\u002Fdocs\u002Ffireskills\u002Fspecialists\u002Fnuxfire-stack","docs\u002F2.fireskills\u002F4.specialists\u002F1.nuxfire-stack",{"title":290,"path":291,"stem":292},"Engineering Specialists","\u002Fdocs\u002Ffireskills\u002Fspecialists\u002Fengineering-specialists","docs\u002F2.fireskills\u002F4.specialists\u002F2.engineering-specialists",{"title":294,"icon":295,"hide":22,"path":296,"stem":297,"children":298,"page":62},"CLI & Tooling","i-ph-terminal","\u002Fdocs\u002Ffireskills\u002Fcli","docs\u002F2.fireskills\u002F5.cli",[299,303,307],{"title":300,"path":301,"stem":302},"CLI Overview","\u002Fdocs\u002Ffireskills\u002Fcli\u002Foverview","docs\u002F2.fireskills\u002F5.cli\u002F0.overview",{"title":304,"path":305,"stem":306},"Commands","\u002Fdocs\u002Ffireskills\u002Fcli\u002Fcommands","docs\u002F2.fireskills\u002F5.cli\u002F1.commands",{"title":308,"path":309,"stem":310},"Safety Scripts","\u002Fdocs\u002Ffireskills\u002Fcli\u002Fgovernance-scripts","docs\u002F2.fireskills\u002F5.cli\u002F2.governance-scripts",{"id":312,"title":74,"body":313,"description":823,"extension":824,"meta":825,"navigation":22,"path":75,"seo":826,"stem":76,"__hash__":827},"docs\u002Fdocs\u002F1.saas-starter-kit\u002F2.features\u002F10.rate-limiting.md",{"type":314,"value":315,"toc":820},"minimark",[316,325,356,359,672,689,707,712,813,816],[317,318,319,320,324],"p",{},"A fixed-window rate limiter backed by Cloudflare KV. The algorithm lives once, in ",[321,322,323],"code",{},"packages\u002Frate-limit",", over a small counter store. Each Worker ties it to its own KV through a thin wrapper:",[326,327,328,339],"ul",{},[329,330,331,334,335,338],"li",{},[321,332,333],{},"apps\u002Fapp\u002Fserver\u002Futils\u002Frate-limit.ts"," — binds the algorithm to ",[321,336,337],{},"hubKV()"," inside the App Worker's Nitro runtime.",[329,340,341,344,345,348,349,352,353,355],{},[321,342,343],{},"apps\u002Ffunctions\u002Fsrc\u002Frate-limit.ts"," — binds it to the raw ",[321,346,347],{},"KVNamespace"," (",[321,350,351],{},"AuthKV","), since the auth Worker has no Nitro\u002FNuxtHub to call ",[321,354,337],{}," from.",[317,357,358],{},"The two Workers keep separate counters: same algorithm, different namespaces.",[360,361,367],"pre",{"className":362,"code":363,"filename":364,"language":365,"meta":366,"style":366},"language-typescript shiki shiki-themes material-theme-lighter github-light github-dark monokai","export async function rateLimit(store, key, { limit, windowSeconds }, now = Date.now) {\n  \u002F\u002F Cloudflare KV rejects a TTL under 60s, so the window can't be shorter.\n  const window = Math.max(windowSeconds, MIN_WINDOW_SECONDS);\n  const storeKey = `ratelimit:${key}:${Math.floor(now() \u002F 1000 \u002F window)}`;\n  const count = ((await store.get(storeKey)) ?? 0) + 1;\n  await store.set(storeKey, count, window);\n  return { allowed: count \u003C= limit, count, limit };\n}\n","packages\u002Frate-limit\u002Fcore.ts","typescript","",[321,368,369,443,450,486,557,606,635,666],{"__ignoreMap":366},[370,371,374,378,382,386,390,394,398,401,404,406,409,412,414,417,420,423,427,431,434,437,440],"span",{"class":372,"line":373},"line",1,[370,375,377],{"class":376},"sRxSC","export",[370,379,381],{"class":380},"sTNss"," async",[370,383,385],{"class":384},"srJo8"," function",[370,387,389],{"class":388},"sD0ED"," rateLimit",[370,391,393],{"class":392},"swvn1","(",[370,395,397],{"class":396},"sQgqH","store",[370,399,400],{"class":392},",",[370,402,403],{"class":396}," key",[370,405,400],{"class":392},[370,407,408],{"class":392}," {",[370,410,411],{"class":396}," limit",[370,413,400],{"class":392},[370,415,416],{"class":396}," windowSeconds",[370,418,419],{"class":392}," },",[370,421,422],{"class":396}," now",[370,424,426],{"class":425},"sGXK2"," =",[370,428,430],{"class":429},"ss--_"," Date",[370,432,433],{"class":392},".",[370,435,436],{"class":429},"now",[370,438,439],{"class":392},")",[370,441,442],{"class":392}," {\n",[370,444,446],{"class":372,"line":445},2,[370,447,449],{"class":448},"ss7Ak","  \u002F\u002F Cloudflare KV rejects a TTL under 60s, so the window can't be shorter.\n",[370,451,453,456,460,462,465,467,470,473,476,478,481,483],{"class":372,"line":452},3,[370,454,455],{"class":384},"  const",[370,457,459],{"class":458},"s91G_"," window",[370,461,426],{"class":425},[370,463,464],{"class":429}," Math",[370,466,433],{"class":392},[370,468,469],{"class":388},"max",[370,471,393],{"class":472},"squCx",[370,474,475],{"class":429},"windowSeconds",[370,477,400],{"class":392},[370,479,480],{"class":458}," MIN_WINDOW_SECONDS",[370,482,439],{"class":472},[370,484,485],{"class":392},";\n",[370,487,489,491,494,496,500,504,508,511,514,517,519,522,525,528,531,533,536,539,543,546,548,550,552,555],{"class":372,"line":488},4,[370,490,455],{"class":384},[370,492,493],{"class":458}," storeKey",[370,495,426],{"class":425},[370,497,499],{"class":498},"siCPE"," `",[370,501,503],{"class":502},"sLACW","ratelimit:",[370,505,507],{"class":506},"slF1C","${",[370,509,510],{"class":429},"key",[370,512,513],{"class":506},"}",[370,515,516],{"class":502},":",[370,518,507],{"class":506},[370,520,521],{"class":429},"Math",[370,523,433],{"class":524},"s00De",[370,526,527],{"class":388},"floor",[370,529,393],{"class":530},"s-euU",[370,532,436],{"class":388},[370,534,535],{"class":530},"() ",[370,537,538],{"class":425},"\u002F",[370,540,542],{"class":541},"sYThS"," 1000",[370,544,545],{"class":425}," \u002F",[370,547,459],{"class":429},[370,549,439],{"class":530},[370,551,513],{"class":506},[370,553,554],{"class":498},"`",[370,556,485],{"class":392},[370,558,560,562,565,567,570,573,576,578,581,583,586,589,592,595,598,601,604],{"class":372,"line":559},5,[370,561,455],{"class":384},[370,563,564],{"class":458}," count",[370,566,426],{"class":425},[370,568,569],{"class":472}," ((",[370,571,572],{"class":376},"await",[370,574,575],{"class":429}," store",[370,577,433],{"class":392},[370,579,580],{"class":388},"get",[370,582,393],{"class":472},[370,584,585],{"class":429},"storeKey",[370,587,588],{"class":472},")) ",[370,590,591],{"class":425},"??",[370,593,594],{"class":541}," 0",[370,596,597],{"class":472},") ",[370,599,600],{"class":425},"+",[370,602,603],{"class":541}," 1",[370,605,485],{"class":392},[370,607,609,612,614,616,619,621,623,625,627,629,631,633],{"class":372,"line":608},6,[370,610,611],{"class":376},"  await",[370,613,575],{"class":429},[370,615,433],{"class":392},[370,617,618],{"class":388},"set",[370,620,393],{"class":472},[370,622,585],{"class":429},[370,624,400],{"class":392},[370,626,564],{"class":429},[370,628,400],{"class":392},[370,630,459],{"class":429},[370,632,439],{"class":472},[370,634,485],{"class":392},[370,636,638,641,643,646,648,650,653,655,657,659,661,663],{"class":372,"line":637},7,[370,639,640],{"class":376},"  return",[370,642,408],{"class":392},[370,644,645],{"class":472}," allowed",[370,647,516],{"class":392},[370,649,564],{"class":429},[370,651,652],{"class":425}," \u003C=",[370,654,411],{"class":429},[370,656,400],{"class":392},[370,658,564],{"class":429},[370,660,400],{"class":392},[370,662,411],{"class":429},[370,664,665],{"class":392}," };\n",[370,667,669],{"class":372,"line":668},8,[370,670,671],{"class":392},"}\n",[317,673,674,675,677,678,680,681,684,685,688],{},"The store is a two-method port (",[321,676,580],{},", ",[321,679,618],{}," with a TTL). ",[321,682,683],{},"kvNamespaceStore"," and ",[321,686,687],{},"hubKvStore"," in the same package adapt the two KV shapes to it. Read, increment and write are not atomic, so concurrent calls can undercount: the limit is a brake, not an exact quota.",[317,690,691,692,684,695,698,699,702,703,706],{},"The App Worker wrapper exports ",[321,693,694],{},"rateLimit(key, rule)",[321,696,697],{},"assertNotRateLimited(event, routeKey, options)",". The second one is for HTTP routes: it keys by ",[321,700,701],{},"\u003CrouteKey>:\u003Cip>"," and throws ",[321,704,705],{},"429 Too Many Requests"," directly when the limit is exceeded.",[708,709,711],"h2",{"id":710},"whats-currently-protected","What's currently protected",[713,714,715,731],"table",{},[716,717,718],"thead",{},[719,720,721,725,728],"tr",{},[722,723,724],"th",{},"Where",[722,726,727],{},"Keyed by",[722,729,730],{},"Purpose",[732,733,734,751,764,776,788,801],"tbody",{},[719,735,736,745,748],{},[737,738,739,348,742,439],"td",{},[321,740,741],{},"apps\u002Ffunctions\u002Fsrc\u002Fauth.ts",[321,743,744],{},"enforceCredentialRateLimit",[737,746,747],{},"path + IP + email",[737,749,750],{},"Password\u002Fcode login on the auth Worker",[719,752,753,758,761],{},[737,754,755],{},[321,756,757],{},"layers\u002Faccount\u002Fserver\u002Fapi\u002Fmfa\u002Fverify.post.ts",[737,759,760],{},"user id",[737,762,763],{},"Account MFA enrollment attempts",[719,765,766,771,773],{},[737,767,768],{},[321,769,770],{},"layers\u002Faccount\u002Fserver\u002Fapi\u002Fmfa\u002Fchallenge.post.ts",[737,772,760],{},[737,774,775],{},"Account MFA challenge attempts",[719,777,778,783,785],{},[737,779,780],{},[321,781,782],{},"layers\u002Faccount\u002Fserver\u002Fapi\u002Fmfa\u002Frecovery-codes.post.ts",[737,784,760],{},[737,786,787],{},"Recovery code regeneration",[719,789,790,795,798],{},[737,791,792],{},[321,793,794],{},"layers\u002Fbilling\u002Fserver\u002Ftrpc\u002Fcheckout.ts",[737,796,797],{},"IP",[737,799,800],{},"Starting a purchase without an account",[719,802,803,808,810],{},[737,804,805],{},[321,806,807],{},"layers\u002Fbilling\u002Fserver\u002Fapi\u002Fwebhooks\u002Fstripe.post.ts",[737,809,797],{},[737,811,812],{},"Stripe webhook, on top of signature validation",[317,814,815],{},"This isn't a general-purpose API throttle — it's applied specifically where credential\u002Fsignature validation alone doesn't stop a high-volume replay: authentication attempts, MFA codes and unauthenticated webhook or checkout endpoints. There's no rate limiting on ordinary tRPC procedures today.",[817,818,819],"style",{},"html pre.shiki code .sRxSC, html code.shiki .sRxSC{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit;--shiki-sepia:#F92672;--shiki-sepia-font-style:inherit}html pre.shiki code .sTNss, html code.shiki .sTNss{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583;--shiki-sepia:#F92672}html pre.shiki code .srJo8, html code.shiki .srJo8{--shiki-light:#9C3EDA;--shiki-light-font-style:inherit;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit;--shiki-sepia:#66D9EF;--shiki-sepia-font-style:italic}html pre.shiki code .sD0ED, html code.shiki .sD0ED{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0;--shiki-sepia:#A6E22E}html pre.shiki code .swvn1, html code.shiki .swvn1{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8;--shiki-sepia:#F8F8F2}html pre.shiki code .sQgqH, html code.shiki .sQgqH{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit;--shiki-sepia:#FD971F;--shiki-sepia-font-style:italic}html pre.shiki code .sGXK2, html code.shiki .sGXK2{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583;--shiki-sepia:#F92672}html pre.shiki code .ss--_, html code.shiki .ss--_{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8;--shiki-sepia:#F8F8F2}html pre.shiki code .ss7Ak, html code.shiki .ss7Ak{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit;--shiki-sepia:#88846F;--shiki-sepia-font-style:inherit}html pre.shiki code .s91G_, html code.shiki .s91G_{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#F8F8F2}html pre.shiki code .squCx, html code.shiki .squCx{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8;--shiki-sepia:#F8F8F2}html pre.shiki code .siCPE, html code.shiki .siCPE{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF;--shiki-sepia:#E6DB74}html pre.shiki code .sLACW, html code.shiki .sLACW{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF;--shiki-sepia:#E6DB74}html pre.shiki code .slF1C, html code.shiki .slF1C{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF;--shiki-sepia:#F92672}html pre.shiki code .s00De, html code.shiki .s00De{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF;--shiki-sepia:#F8F8F2}html pre.shiki code .s-euU, html code.shiki .s-euU{--shiki-light:#90A4AE;--shiki-default:#032F62;--shiki-dark:#9ECBFF;--shiki-sepia:#F8F8F2}html pre.shiki code .sYThS, html code.shiki .sYThS{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#AE81FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"title":366,"searchDepth":445,"depth":445,"links":821},[822],{"id":710,"depth":445,"text":711},"Fixed-window rate limiting backed by Cloudflare KV, protecting login, MFA, checkout, and payment webhook endpoints.","md",{},{"title":74,"description":823},"DEy2HA3IgHobUpfcaNL7SZs2jeHPTH0h25AagFJ8Pu8",1792616803886]