[{"data":1,"prerenderedAt":237},["ShallowReactive",2],{"navigation":3,"\u002Fplugins\u002Frate-limiting":189,"\u002Fplugins\u002Frate-limiting-surround":232},[4,23,40,51,74,116,157,177],{"title":5,"path":6,"stem":7,"children":8,"icon":22},"Get Started","\u002Fget-started","1.get-started\u002F1.index",[9,12,17],{"title":10,"path":6,"stem":7,"icon":11},"Introduction","i-lucide-house",{"title":13,"path":14,"stem":15,"icon":16},"Prerequisites","\u002Fget-started\u002Fprerequisites","1.get-started\u002F2.prerequisites","i-lucide-list-checks",{"title":18,"path":19,"stem":20,"icon":21},"Installation","\u002Fget-started\u002Finstallation","1.get-started\u002F3.installation","i-lucide-settings","i-lucide-rocket",{"title":24,"icon":25,"path":26,"stem":27,"children":28,"page":39},"Develop","i-lucide-code","\u002Fdevelop","2.develop",[29,34],{"title":30,"path":31,"stem":32,"icon":33},"Version Control","\u002Fdevelop\u002Fversion-control","2.develop\u002F1.version-control","i-lucide-git-branch",{"title":35,"path":36,"stem":37,"icon":38},"Claude Code","\u002Fdevelop\u002Fclaude-code","2.develop\u002F2.claude-code","i-lucide-sparkles",false,{"title":41,"icon":42,"path":43,"stem":44,"children":45,"page":39},"Launch","i-lucide-globe","\u002Flaunch","3.launch",[46],{"title":47,"path":48,"stem":49,"icon":50},"Cloudflare","\u002Flaunch\u002Fcloudflare","3.launch\u002F1.cloudflare","i-lucide-cloud-upload",{"title":52,"path":53,"stem":54,"children":55,"icon":73},"Plugins","\u002Fplugins","4.plugins\u002F1.index",[56,58,63,68],{"title":52,"path":53,"stem":54,"icon":57},"i-lucide-list",{"title":59,"path":60,"stem":61,"icon":62},"Public API","\u002Fplugins\u002Fapi-keys","4.plugins\u002F2.api-keys","i-lucide-key",{"title":64,"path":65,"stem":66,"icon":67},"Cron Jobs","\u002Fplugins\u002Fcron-jobs","4.plugins\u002F4.cron-jobs","i-lucide-clock",{"title":69,"path":70,"stem":71,"icon":72},"Rate Limiting","\u002Fplugins\u002Frate-limiting","4.plugins\u002F5.rate-limiting","i-lucide-gauge","i-lucide-puzzle",{"title":75,"path":76,"stem":77,"children":78,"icon":115},"Examples","\u002Fexamples","5.examples\u002F1.index",[79,80,85,90,95,100,105,110],{"title":75,"path":76,"stem":77,"icon":57},{"title":81,"path":82,"stem":83,"icon":84},"Job Management","\u002Fexamples\u002Fjob-management","5.examples\u002F2.job-management","i-lucide-briefcase",{"title":86,"path":87,"stem":88,"icon":89},"Kanban \u002F To-Do List","\u002Fexamples\u002Fkanban-todo","5.examples\u002F3.kanban-todo","i-lucide-kanban",{"title":91,"path":92,"stem":93,"icon":94},"Inventory Management","\u002Fexamples\u002Finventory-management","5.examples\u002F4.inventory-management","i-lucide-package",{"title":96,"path":97,"stem":98,"icon":99},"Mini CRM","\u002Fexamples\u002Fmini-crm","5.examples\u002F5.mini-crm","i-lucide-users",{"title":101,"path":102,"stem":103,"icon":104},"Sales Orders & Invoices","\u002Fexamples\u002Fsales-invoices","5.examples\u002F6.sales-invoices","i-lucide-receipt",{"title":106,"path":107,"stem":108,"icon":109},"Calendar & Booking","\u002Fexamples\u002Fcalendar-booking","5.examples\u002F7.calendar-booking","i-lucide-calendar",{"title":111,"path":112,"stem":113,"icon":114},"Support Tickets","\u002Fexamples\u002Fsupport-tickets","5.examples\u002F8.support-tickets","i-lucide-life-buoy","i-lucide-book-open",{"title":117,"icon":118,"path":119,"stem":120,"children":121,"page":39},"Reference","i-lucide-file-text","\u002Freference","6.reference",[122,127,132,137,142,147,152],{"title":123,"path":124,"stem":125,"icon":126},"Architecture","\u002Freference\u002Farchitecture","6.reference\u002F1.architecture","i-lucide-layers",{"title":128,"path":129,"stem":130,"icon":131},"Permissions","\u002Freference\u002Fpermissions","6.reference\u002F2.permissions","i-lucide-shield",{"title":133,"path":134,"stem":135,"icon":136},"Invitations","\u002Freference\u002Finvitations","6.reference\u002F3.invitations","i-lucide-mail",{"title":138,"path":139,"stem":140,"icon":141},"Webhooks","\u002Freference\u002Fwebhooks","6.reference\u002F4.webhooks","i-lucide-webhook",{"title":143,"path":144,"stem":145,"icon":146},"AI Chat","\u002Freference\u002Fai-chat","6.reference\u002F5.ai-chat","i-lucide-message-square",{"title":148,"path":149,"stem":150,"icon":151},"Activity Log","\u002Freference\u002Factivity-log","6.reference\u002F6.activity-log","i-lucide-scroll",{"title":153,"path":154,"stem":155,"icon":156},"Manual Setup","\u002Freference\u002Fmanual-setup","6.reference\u002F7.manual-setup","i-lucide-wrench",{"title":158,"icon":159,"path":160,"stem":161,"children":162,"page":39},"Legal","i-lucide-scale","\u002Flegal","7.legal",[163,168,172],{"title":164,"path":165,"stem":166,"icon":167},"License","\u002Flegal\u002Flicense","7.legal\u002F1.license","i-lucide-file-check",{"title":169,"path":170,"stem":171,"icon":118},"Terms and Conditions","\u002Flegal\u002Fterms","7.legal\u002F2.terms",{"title":173,"path":174,"stem":175,"icon":176},"Privacy Policy","\u002Flegal\u002Fprivacy","7.legal\u002F3.privacy","i-lucide-lock",{"title":178,"path":179,"stem":180,"children":181,"icon":183},"Upgrades","\u002Fupgrades","8.upgrades\u002F1.index",[182,184],{"title":178,"path":179,"stem":180,"icon":183},"i-lucide-arrow-up-circle",{"title":185,"path":186,"stem":187,"icon":188},"\u002Fapp\u002F* gated subtree routing","\u002Fupgrades\u002Fapp-subtree-routing","8.upgrades\u002F2.app-subtree-routing","i-lucide-route",{"id":190,"title":69,"body":191,"description":225,"extension":226,"links":227,"meta":228,"navigation":229,"path":70,"seo":230,"stem":71,"__hash__":231},"docs\u002F4.plugins\u002F5.rate-limiting.md",{"type":192,"value":193,"toc":221},"minimark",[194,198,201],[195,196,197],"p",{},"This template does not ship with rate limiting. If you are deploying as a public-facing SaaS (or any environment where abuse is a concern), you can add a Postgres-backed fixed-window rate limiter with zero external dependencies.",[195,199,200],{},"To install this plugin, copy the prompt below, paste it into Claude Code (or Codex, or any AI coding tool), and let it run.",[202,203,205,210],"steps",{"level":204},"3",[206,207,209],"h3",{"id":208},"rate-limiter","Rate Limiter",[211,212,217],"pre",{"className":213,"code":215,"language":216},[214],"language-text","Add a Postgres-backed fixed-window rate limiter to this project. Here is the\nfull specification:\n\n### 1. Database migration\n\nCreate a Supabase migration that:\n\n- Creates a `rate_limits` table:\n  - `key` (text, not null) — arbitrary string identifying the resource being\n    limited (e.g. \"invite-create:\u003Cteam_id>\")\n  - `window_start` (timestamptz, not null) — the start of the current time\n    window\n  - `count` (int, not null, default 0) — number of requests in this window\n  - Primary key: `(key, window_start)`\n- Adds an index on `window_start` for cleanup queries\n- Enables RLS with zero policies (table is only accessed by the service role)\n- Creates an `enforce_rate_limit(p_key text, p_window_seconds int)` function\n  (SECURITY DEFINER, search_path = public, pg_temp) that:\n  - Snaps to the current fixed window: `to_timestamp(floor(extract(epoch from\n    now()) \u002F p_window_seconds) * p_window_seconds)`\n  - Upserts into `rate_limits` with `ON CONFLICT DO UPDATE SET count =\n    rate_limits.count + 1`\n  - Returns the new count\n- Creates a `cleanup_rate_limits()` function that deletes rows older than 1 day\n\n### 2. Server utility\n\nCreate `server\u002Futils\u002FrateLimit.ts` with an `enforceRateLimit` function:\n\n```ts\nimport type { SupabaseClient } from \"@supabase\u002Fsupabase-js\";\nimport type { Database } from \"~~\u002Fshared\u002Ftypes\u002Fdatabase.types\";\nimport { createError } from \"h3\";\n\ntype RateLimitOptions = {\n  key: string;\n  limit: number;\n  windowSeconds: number;\n};\n\nexport async function enforceRateLimit(\n  client: SupabaseClient\u003CDatabase>,\n  { key, limit, windowSeconds }: RateLimitOptions,\n) {\n  const { data, error } = await client.rpc(\"enforce_rate_limit\", {\n    p_key: key,\n    p_window_seconds: windowSeconds,\n  });\n\n  if (error || data == null) {\n    console.error(\"[rateLimit] enforce_rate_limit failed:\", error);\n    return; \u002F\u002F fail open\n  }\n\n  if (data > limit) {\n    throw createError({\n      statusCode: 429,\n      statusMessage: \"Too many requests. Please slow down and try again shortly.\",\n    });\n  }\n}\n```\n\n### 3. Add rate limits to these server routes\n\nEach `enforceRateLimit` call goes right after the auth check, before any\nbusiness logic:\n\n- `server\u002Fapi\u002Fteams\u002Findex.post.ts` — 10 per hour per user:\n  `{ key: \\`team-create:${user.sub}\\`, limit: 10, windowSeconds: 60 * 60 }`\n\n- `server\u002Fapi\u002Fteams\u002F[teamId]\u002Finvitations\u002Findex.post.ts` — two limits:\n  - 10 per minute per team:\n    `{ key: \\`invite-create:${teamId}:m\\`, limit: 10, windowSeconds: 60 }`\n  - 100 per hour per team:\n    `{ key: \\`invite-create:${teamId}:h\\`, limit: 100, windowSeconds: 60 * 60 }`\n\n- `server\u002Fapi\u002Finvitations\u002F[token].get.ts` — 30 per minute per IP (this is an\n  unauthenticated endpoint, so key on IP instead of user):\n  ```ts\n  const ip = getRequestIP(event, { xForwardedFor: true }) ?? \"unknown\";\n  await enforceRateLimit(client, { key: \\`invite-lookup:${ip}\\`, limit: 30, windowSeconds: 60 });\n  ```\n\n- `server\u002Fapi\u002Finvitations\u002F[token]\u002Faccept.post.ts` — 20 per minute per user:\n  `{ key: \\`invite-accept:${user.sub}\\`, limit: 20, windowSeconds: 60 }`\n\n- `server\u002Fapi\u002Fteams\u002F[teamId]\u002Favatar.post.ts` — 10 per minute per team:\n  `{ key: \\`team-avatar-upload:${teamMember.teamId}\\`, limit: 10, windowSeconds: 60 }`\n\n- `server\u002Fapi\u002Fauth\u002Fprofile\u002Favatar.post.ts` — 10 per minute per user:\n  `{ key: \\`avatar-upload:${user.sub}\\`, limit: 10, windowSeconds: 60 }`\n\n### 4. Regenerate database types\n\nRun `npx supabase gen types typescript` to add the `rate_limits` table and\nthe `enforce_rate_limit` \u002F `cleanup_rate_limits` functions to\n`shared\u002Ftypes\u002Fdatabase.types.ts`.\n","text",[218,219,215],"code",{"__ignoreMap":220},"",{"title":220,"searchDepth":222,"depth":223,"links":224},1,2,[],"Add a Postgres-backed fixed-window rate limiter with zero external dependencies.","md",null,{},{"icon":72},{"title":69,"description":225},"Kpf4mLF2cd8ygqbD2cmd7Nn354uZxfBRA63TzFjUEsQ",[233,235],{"title":64,"path":65,"stem":66,"description":234,"icon":67,"children":-1},"Scheduled tasks that run on a recurring basis.",{"title":75,"path":76,"stem":77,"description":236,"icon":57,"children":-1},"How to build full features from the example tutorials.",1777092169440]