[{"data":1,"prerenderedAt":1322},["ShallowReactive",2],{"navigation_docs":3,"-gotrue-client-ip-and-rate-limits":172,"-gotrue-client-ip-and-rate-limits-surround":1317},[4,26,67,98,124,140,161],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":25},"Getting Started","i-lucide-rocket","\u002Fgetting-started","1.getting-started",[10,15,20],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fgetting-started\u002Fintroduction","1.getting-started\u002F1.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Installation","\u002Fgetting-started\u002Finstallation","1.getting-started\u002F2.installation","i-lucide-download",{"title":21,"path":22,"stem":23,"icon":24},"Quick Start","\u002Fgetting-started\u002Fquick-start","1.getting-started\u002F3.quick-start","i-lucide-play",false,{"title":27,"icon":28,"path":29,"stem":30,"children":31,"page":25},"Core","i-lucide-box","\u002Fcore","2.core",[32,37,42,47,52,57,62],{"title":33,"path":34,"stem":35,"icon":36},"Routing","\u002Fcore\u002Frouting","2.core\u002F1.routing","i-lucide-route",{"title":38,"path":39,"stem":40,"icon":41},"Handlers","\u002Fcore\u002Fhandlers","2.core\u002F2.handlers","i-lucide-braces",{"title":43,"path":44,"stem":45,"icon":46},"Request Binding","\u002Fcore\u002Frequest-binding","2.core\u002F3.request-binding","i-lucide-tags",{"title":48,"path":49,"stem":50,"icon":51},"Validation","\u002Fcore\u002Fvalidation","2.core\u002F4.validation","i-lucide-check-check",{"title":53,"path":54,"stem":55,"icon":56},"Context","\u002Fcore\u002Fcontext","2.core\u002F5.context","i-lucide-file-input",{"title":58,"path":59,"stem":60,"icon":61},"Errors","\u002Fcore\u002Ferrors","2.core\u002F6.errors","i-lucide-triangle-alert",{"title":63,"path":64,"stem":65,"icon":66},"Middleware","\u002Fcore\u002Fmiddleware","2.core\u002F7.middleware","i-lucide-layers",{"title":68,"icon":69,"path":70,"stem":71,"children":72,"page":25},"Auth","i-lucide-shield-check","\u002Fauth","3.auth",[73,78,83,88,93],{"title":74,"path":75,"stem":76,"icon":77},"Overview","\u002Fauth\u002Foverview","3.auth\u002F1.overview","i-lucide-shield",{"title":79,"path":80,"stem":81,"icon":82},"Auth Rules","\u002Fauth\u002Fauth-rules","3.auth\u002F2.auth-rules","i-lucide-lock",{"title":84,"path":85,"stem":86,"icon":87},"JWT Providers","\u002Fauth\u002Fjwt-providers","3.auth\u002F3.jwt-providers","i-lucide-key",{"title":89,"path":90,"stem":91,"icon":92},"Roles & Permissions","\u002Fauth\u002Froles-and-permissions","3.auth\u002F4.roles-and-permissions","i-lucide-users",{"title":94,"path":95,"stem":96,"icon":97},"Audit Logging","\u002Fauth\u002Faudit-logging","3.auth\u002F5.audit-logging","i-lucide-scroll-text",{"title":99,"icon":100,"path":101,"stem":102,"children":103,"page":25},"GoTrue","i-lucide-key-round","\u002Fgotrue","4.gotrue",[104,109,114,119],{"title":105,"path":106,"stem":107,"icon":108},"Proxy","\u002Fgotrue\u002Fproxy","4.gotrue\u002F1.proxy","i-lucide-shuffle",{"title":110,"path":111,"stem":112,"icon":113},"Endpoints","\u002Fgotrue\u002Fendpoints","4.gotrue\u002F2.endpoints","i-lucide-list",{"title":115,"path":116,"stem":117,"icon":118},"Client IP & Rate Limits","\u002Fgotrue\u002Fclient-ip-and-rate-limits","4.gotrue\u002F3.client-ip-and-rate-limits","i-lucide-network",{"title":120,"path":121,"stem":122,"icon":123},"Deployment","\u002Fgotrue\u002Fdeployment","4.gotrue\u002F4.deployment","i-lucide-server",{"title":125,"icon":126,"path":127,"stem":128,"children":129,"page":25},"API Docs","i-lucide-file-text","\u002Fapi-docs","5.api-docs",[130,135],{"title":131,"path":132,"stem":133,"icon":134},"OpenAPI","\u002Fapi-docs\u002Fopenapi","5.api-docs\u002F1.openapi","i-lucide-file-json",{"title":136,"path":137,"stem":138,"icon":139},"Scalar UI","\u002Fapi-docs\u002Fscalar","5.api-docs\u002F2.scalar","i-lucide-book-open-text",{"title":141,"icon":142,"path":143,"stem":144,"children":145,"page":25},"Guides","i-lucide-compass","\u002Fguides","6.guides",[146,151,156],{"title":147,"path":148,"stem":149,"icon":150},"Testing","\u002Fguides\u002Ftesting","6.guides\u002F1.testing","i-lucide-flask-conical",{"title":152,"path":153,"stem":154,"icon":155},"Performance","\u002Fguides\u002Fperformance","6.guides\u002F2.performance","i-lucide-gauge",{"title":157,"path":158,"stem":159,"icon":160},"Releasing","\u002Fguides\u002Freleasing","6.guides\u002F3.releasing","i-lucide-tag",{"title":162,"icon":163,"path":164,"stem":165,"children":166,"page":25},"Reference","i-lucide-code","\u002Freference","7.reference",[167],{"title":168,"path":169,"stem":170,"icon":171},"API Reference","\u002Freference\u002Fapi-reference","7.reference\u002F1.api-reference","i-lucide-list-tree",{"id":173,"title":115,"body":174,"description":1310,"extension":1311,"links":1312,"meta":1313,"navigation":1314,"path":116,"seo":1315,"stem":117,"__hash__":1316},"docs\u002F4.gotrue\u002F3.client-ip-and-rate-limits.md",{"type":175,"value":176,"toc":1302},"minimark",[177,181,186,206,215,351,354,361,365,418,421,439,442,452,456,471,662,675,678,743,746,749,753,759,824,1052,1058,1233,1237,1240,1272,1279,1283,1298],[178,179,180],"p",{},"The proxy needs to know who is calling, for three reasons: GoTrue's own security features want the real client IP, rate limiting is keyed by it, and audit records are worth little without it. Getting this wrong in either direction is a security problem, so the resolution rule is explicit.",[182,183,185],"h2",{"id":184},"understand-the-two-modes","Understand the two modes",[178,187,188,192,193,197,198,201,202,205],{},[189,190,191],"strong",{},"Edge mode"," is the default, when ",[194,195,196],"code",{},"TrustedProxies"," is empty. The connection peer is the client, and inbound ",[194,199,200],{},"X-Forwarded-For"," and ",[194,203,204],{},"X-Real-IP"," headers are ignored entirely. Use this when your process is directly exposed, because otherwise any client could claim any address.",[178,207,208,211,212,214],{},[189,209,210],{},"Trusted proxy mode"," applies when you list the ranges of the proxies in front of you. If the connection peer is one of them, the ",[194,213,200],{}," chain is walked from right to left, skipping trusted hops, and the first untrusted address is the client. If the peer is not trusted, its address is used and the headers are ignored.",[216,217,222],"pre",{"className":218,"code":219,"language":220,"meta":221,"style":221},"language-go shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","gt := auth.NewGoTrue(auth.GoTrueConfig{\n    InternalURL:    \"http:\u002F\u002Fgotrue:9999\",\n    JWTSecret:      secret,\n    TrustedProxies: []string{\"10.0.0.0\u002F8\", \"172.16.0.0\u002F12\", \"192.168.1.10\"},\n})\n","go","",[194,223,224,262,284,297,345],{"__ignoreMap":221},[225,226,229,233,237,240,243,247,250,254,256,259],"span",{"class":227,"line":228},"line",1,[225,230,232],{"class":231},"sTEyZ","gt ",[225,234,236],{"class":235},"sMK4o",":=",[225,238,239],{"class":231}," auth",[225,241,242],{"class":235},".",[225,244,246],{"class":245},"s2Zo4","NewGoTrue",[225,248,249],{"class":235},"(",[225,251,253],{"class":252},"sBMFI","auth",[225,255,242],{"class":235},[225,257,258],{"class":252},"GoTrueConfig",[225,260,261],{"class":235},"{\n",[225,263,265,268,271,274,278,281],{"class":227,"line":264},2,[225,266,267],{"class":231},"    InternalURL",[225,269,270],{"class":235},":",[225,272,273],{"class":235},"    \"",[225,275,277],{"class":276},"sfazB","http:\u002F\u002Fgotrue:9999",[225,279,280],{"class":235},"\"",[225,282,283],{"class":235},",\n",[225,285,287,290,292,295],{"class":227,"line":286},3,[225,288,289],{"class":231},"    JWTSecret",[225,291,270],{"class":235},[225,293,294],{"class":231},"      secret",[225,296,283],{"class":235},[225,298,300,303,305,308,312,315,317,320,322,325,328,331,333,335,337,340,342],{"class":227,"line":299},4,[225,301,302],{"class":231},"    TrustedProxies",[225,304,270],{"class":235},[225,306,307],{"class":235}," []",[225,309,311],{"class":310},"spNyl","string",[225,313,314],{"class":235},"{",[225,316,280],{"class":235},[225,318,319],{"class":276},"10.0.0.0\u002F8",[225,321,280],{"class":235},[225,323,324],{"class":235},",",[225,326,327],{"class":235}," \"",[225,329,330],{"class":276},"172.16.0.0\u002F12",[225,332,280],{"class":235},[225,334,324],{"class":235},[225,336,327],{"class":235},[225,338,339],{"class":276},"192.168.1.10",[225,341,280],{"class":235},[225,343,344],{"class":235},"},\n",[225,346,348],{"class":227,"line":347},5,[225,349,350],{"class":235},"})\n",[178,352,353],{},"Entries can be CIDR blocks or bare IPs, IPv4 or IPv6. A malformed entry panics at construction, so a typo is caught at startup.",[355,356,357,358,360],"warning",{},"List only proxies you actually control. Every hop you trust is a hop that can forge the client address. If your load balancer appends to ",[194,359,200],{}," rather than replacing it, that is exactly the behaviour this walk expects.",[182,362,364],{"id":363},"what-the-resolved-ip-is-used-for","What the resolved IP is used for",[366,367,368,381],"table",{},[369,370,371],"thead",{},[372,373,374,378],"tr",{},[375,376,377],"th",{},"Use",[375,379,380],{},"Detail",[382,383,384,398,406],"tbody",{},[372,385,386,390],{},[387,388,389],"td",{},"Forwarding",[387,391,392,393,201,395,397],{},"Sent to GoTrue as ",[194,394,200],{},[194,396,204],{}," on every proxied call",[372,399,400,403],{},[387,401,402],{},"Rate limiting",[387,404,405],{},"Part of the limiter key",[372,407,408,411],{},[387,409,410],{},"Audit",[387,412,413,414,417],{},"Recorded as ",[194,415,416],{},"ip"," in the details of each audit event",[178,419,420],{},"GoTrue only honours the forwarded address when it is configured to:",[216,422,426],{"className":423,"code":424,"language":425,"meta":221,"style":221},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","GOTRUE_SECURITY_SB_FORWARDED_FOR_ENABLED=true\n","bash",[194,427,428],{"__ignoreMap":221},[225,429,430,433,436],{"class":227,"line":228},[225,431,432],{"class":231},"GOTRUE_SECURITY_SB_FORWARDED_FOR_ENABLED",[225,434,435],{"class":235},"=",[225,437,438],{"class":276},"true\n",[178,440,441],{},"Without that, GoTrue's own limits and logs see your service's address instead of the user's.",[443,444,445,448,449,451],"note",{},[194,446,447],{},"ctx.GetClientIP()"," on a regular ezz route is a simpler thing: it takes the first ",[194,450,200],{}," value with no trust check. It is fine for logging, but do not use it for security decisions. The trusted-proxy logic described here applies to the GoTrue proxy only.",[182,453,455],{"id":454},"add-redis-rate-limiting","Add Redis rate limiting",[178,457,458,459,462,463,466,467,470],{},"The proxy calls a limiter on sensitive endpoints only: sign-up, token, OTP, recovery, resend, OAuth token, dynamic client registration, and passkey authentication. Read-only routes such as ",[194,460,461],{},"\u002Fhealth",", ",[194,464,465],{},"\u002Fsettings",", and ",[194,468,469],{},"\u002Fuser"," are not throttled.",[216,472,474],{"className":218,"code":473,"language":220,"meta":221,"style":221},"import (\n    \"github.com\u002Fredis\u002Fgo-redis\u002Fv9\"\n    \"github.com\u002Fsulv-io\u002Fezz\u002Fauth\u002Fratelimit\"\n)\n\nrdb := redis.NewClient(&redis.Options{Addr: \"redis:6379\"})\n\ngt := auth.NewGoTrue(auth.GoTrueConfig{\n    InternalURL: \"http:\u002F\u002Fgotrue:9999\",\n    JWTSecret:   secret,\n    RateLimiter: ratelimit.NewRedis(rdb, 10, time.Minute),   \u002F\u002F 10 requests per minute\n})\n",[194,475,476,485,495,504,509,515,558,563,586,601,613,657],{"__ignoreMap":221},[225,477,478,482],{"class":227,"line":228},[225,479,481],{"class":480},"s7zQu","import",[225,483,484],{"class":235}," (\n",[225,486,487,489,492],{"class":227,"line":264},[225,488,273],{"class":235},[225,490,491],{"class":252},"github.com\u002Fredis\u002Fgo-redis\u002Fv9",[225,493,494],{"class":235},"\"\n",[225,496,497,499,502],{"class":227,"line":286},[225,498,273],{"class":235},[225,500,501],{"class":252},"github.com\u002Fsulv-io\u002Fezz\u002Fauth\u002Fratelimit",[225,503,494],{"class":235},[225,505,506],{"class":227,"line":299},[225,507,508],{"class":235},")\n",[225,510,511],{"class":227,"line":347},[225,512,514],{"emptyLinePlaceholder":513},true,"\n",[225,516,518,521,523,526,528,531,534,537,539,542,544,547,549,551,554,556],{"class":227,"line":517},6,[225,519,520],{"class":231},"rdb ",[225,522,236],{"class":235},[225,524,525],{"class":231}," redis",[225,527,242],{"class":235},[225,529,530],{"class":245},"NewClient",[225,532,533],{"class":235},"(&",[225,535,536],{"class":252},"redis",[225,538,242],{"class":235},[225,540,541],{"class":252},"Options",[225,543,314],{"class":235},[225,545,546],{"class":231},"Addr",[225,548,270],{"class":235},[225,550,327],{"class":235},[225,552,553],{"class":276},"redis:6379",[225,555,280],{"class":235},[225,557,350],{"class":235},[225,559,561],{"class":227,"line":560},7,[225,562,514],{"emptyLinePlaceholder":513},[225,564,566,568,570,572,574,576,578,580,582,584],{"class":227,"line":565},8,[225,567,232],{"class":231},[225,569,236],{"class":235},[225,571,239],{"class":231},[225,573,242],{"class":235},[225,575,246],{"class":245},[225,577,249],{"class":235},[225,579,253],{"class":252},[225,581,242],{"class":235},[225,583,258],{"class":252},[225,585,261],{"class":235},[225,587,589,591,593,595,597,599],{"class":227,"line":588},9,[225,590,267],{"class":231},[225,592,270],{"class":235},[225,594,327],{"class":235},[225,596,277],{"class":276},[225,598,280],{"class":235},[225,600,283],{"class":235},[225,602,604,606,608,611],{"class":227,"line":603},10,[225,605,289],{"class":231},[225,607,270],{"class":235},[225,609,610],{"class":231},"   secret",[225,612,283],{"class":235},[225,614,616,619,621,624,626,629,631,634,636,640,642,645,647,650,653],{"class":227,"line":615},11,[225,617,618],{"class":231},"    RateLimiter",[225,620,270],{"class":235},[225,622,623],{"class":231}," ratelimit",[225,625,242],{"class":235},[225,627,628],{"class":245},"NewRedis",[225,630,249],{"class":235},[225,632,633],{"class":231},"rdb",[225,635,324],{"class":235},[225,637,639],{"class":638},"sbssI"," 10",[225,641,324],{"class":235},[225,643,644],{"class":231}," time",[225,646,242],{"class":235},[225,648,649],{"class":231},"Minute",[225,651,652],{"class":235},"),",[225,654,656],{"class":655},"sHwdD","   \u002F\u002F 10 requests per minute\n",[225,658,660],{"class":227,"line":659},12,[225,661,350],{"class":235},[178,663,664,666,667,670,671,674],{},[194,665,628],{}," is a fixed-window counter. Each request increments ",[194,668,669],{},"ratelimit:\u003Cclient-ip>:\u003Croute>"," and sets the key's expiry to the window, so every client gets its own budget per endpoint. It accepts a ",[194,672,673],{},"redis.UniversalClient",", which means a single node, a sentinel setup, or a cluster all work.",[178,676,677],{},"Exceeding the limit returns:",[216,679,683],{"className":680,"code":681,"language":682,"meta":221,"style":221},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{\n  \"error\": true,\n  \"message\": \"too many requests\",\n  \"status\": 429\n}\n","json",[194,684,685,689,704,724,738],{"__ignoreMap":221},[225,686,687],{"class":227,"line":228},[225,688,261],{"class":235},[225,690,691,694,697,699,701],{"class":227,"line":264},[225,692,693],{"class":235},"  \"",[225,695,696],{"class":310},"error",[225,698,280],{"class":235},[225,700,270],{"class":235},[225,702,703],{"class":235}," true,\n",[225,705,706,708,711,713,715,717,720,722],{"class":227,"line":286},[225,707,693],{"class":235},[225,709,710],{"class":310},"message",[225,712,280],{"class":235},[225,714,270],{"class":235},[225,716,327],{"class":235},[225,718,719],{"class":276},"too many requests",[225,721,280],{"class":235},[225,723,283],{"class":235},[225,725,726,728,731,733,735],{"class":227,"line":299},[225,727,693],{"class":235},[225,729,730],{"class":310},"status",[225,732,280],{"class":235},[225,734,270],{"class":235},[225,736,737],{"class":638}," 429\n",[225,739,740],{"class":227,"line":347},[225,741,742],{"class":235},"}\n",[443,744,745],{},"The limiter fails open. If Redis is unreachable, the request is allowed rather than rejected, so a cache outage does not take down sign-in. If you would rather fail closed, wrap the limiter and return the error path you want.",[178,747,748],{},"Because the window is fixed rather than sliding, a client can send up to twice the limit across a window boundary. For login endpoints that is usually acceptable; if it is not, implement a sliding window or token bucket behind the same interface.",[182,750,752],{"id":751},"write-your-own-limiter","Write your own limiter",[178,754,755,756,758],{},"The interface is one method, and the ",[194,757,253],{}," package does not import any Redis code, so nothing forces you into a particular store:",[216,760,762],{"className":218,"code":761,"language":220,"meta":221,"style":221},"type RateLimiter interface {\n    Allow(ctx context.Context, key string) (bool, error)\n}\n",[194,763,764,778,820],{"__ignoreMap":221},[225,765,766,769,772,775],{"class":227,"line":228},[225,767,768],{"class":235},"type",[225,770,771],{"class":252}," RateLimiter",[225,773,774],{"class":235}," interface",[225,776,777],{"class":235}," {\n",[225,779,780,783,785,789,792,794,796,798,801,804,807,810,813,815,818],{"class":227,"line":264},[225,781,782],{"class":245},"    Allow",[225,784,249],{"class":235},[225,786,788],{"class":787},"sHdIc","ctx",[225,790,791],{"class":252}," context",[225,793,242],{"class":235},[225,795,53],{"class":252},[225,797,324],{"class":235},[225,799,800],{"class":787}," key",[225,802,803],{"class":310}," string",[225,805,806],{"class":235},")",[225,808,809],{"class":235}," (",[225,811,812],{"class":310},"bool",[225,814,324],{"class":235},[225,816,817],{"class":310}," error",[225,819,508],{"class":235},[225,821,822],{"class":227,"line":286},[225,823,742],{"class":235},[216,825,827],{"className":218,"code":826,"language":220,"meta":221,"style":221},"type postgresLimiter struct{ db *sql.DB }\n\nfunc (l postgresLimiter) Allow(ctx context.Context, key string) (bool, error) {\n    var count int\n    err := l.db.QueryRowContext(ctx, `\n        INSERT INTO rate_limits (key, window_start, count)\n        VALUES ($1, date_trunc('minute', now()), 1)\n        ON CONFLICT (key, window_start)\n        DO UPDATE SET count = rate_limits.count + 1\n        RETURNING count`, key).Scan(&count)\n    if err != nil {\n        return false, err\n    }\n    return count \u003C= 10, nil\n}\n",[194,828,829,856,860,908,919,948,953,958,963,968,993,1009,1023,1029,1047],{"__ignoreMap":221},[225,830,831,833,836,839,842,845,848,850,853],{"class":227,"line":228},[225,832,768],{"class":235},[225,834,835],{"class":252}," postgresLimiter",[225,837,838],{"class":235}," struct{",[225,840,841],{"class":231}," db ",[225,843,844],{"class":235},"*",[225,846,847],{"class":252},"sql",[225,849,242],{"class":235},[225,851,852],{"class":252},"DB",[225,854,855],{"class":235}," }\n",[225,857,858],{"class":227,"line":264},[225,859,514],{"emptyLinePlaceholder":513},[225,861,862,865,867,870,873,875,878,880,882,884,886,888,890,892,894,896,898,900,902,904,906],{"class":227,"line":286},[225,863,864],{"class":235},"func",[225,866,809],{"class":235},[225,868,869],{"class":787},"l ",[225,871,872],{"class":252},"postgresLimiter",[225,874,806],{"class":235},[225,876,877],{"class":245}," Allow",[225,879,249],{"class":235},[225,881,788],{"class":787},[225,883,791],{"class":252},[225,885,242],{"class":235},[225,887,53],{"class":252},[225,889,324],{"class":235},[225,891,800],{"class":787},[225,893,803],{"class":310},[225,895,806],{"class":235},[225,897,809],{"class":235},[225,899,812],{"class":310},[225,901,324],{"class":235},[225,903,817],{"class":310},[225,905,806],{"class":235},[225,907,777],{"class":235},[225,909,910,913,916],{"class":227,"line":299},[225,911,912],{"class":235},"    var",[225,914,915],{"class":231}," count ",[225,917,918],{"class":310},"int\n",[225,920,921,924,926,929,931,934,936,939,941,943,945],{"class":227,"line":347},[225,922,923],{"class":231},"    err ",[225,925,236],{"class":235},[225,927,928],{"class":231}," l",[225,930,242],{"class":235},[225,932,933],{"class":231},"db",[225,935,242],{"class":235},[225,937,938],{"class":245},"QueryRowContext",[225,940,249],{"class":235},[225,942,788],{"class":231},[225,944,324],{"class":235},[225,946,947],{"class":235}," `\n",[225,949,950],{"class":227,"line":517},[225,951,952],{"class":276},"        INSERT INTO rate_limits (key, window_start, count)\n",[225,954,955],{"class":227,"line":560},[225,956,957],{"class":276},"        VALUES ($1, date_trunc('minute', now()), 1)\n",[225,959,960],{"class":227,"line":565},[225,961,962],{"class":276},"        ON CONFLICT (key, window_start)\n",[225,964,965],{"class":227,"line":588},[225,966,967],{"class":276},"        DO UPDATE SET count = rate_limits.count + 1\n",[225,969,970,973,976,978,980,983,986,988,991],{"class":227,"line":603},[225,971,972],{"class":276},"        RETURNING count",[225,974,975],{"class":235},"`",[225,977,324],{"class":235},[225,979,800],{"class":231},[225,981,982],{"class":235},").",[225,984,985],{"class":245},"Scan",[225,987,533],{"class":235},[225,989,990],{"class":231},"count",[225,992,508],{"class":235},[225,994,995,998,1001,1004,1007],{"class":227,"line":615},[225,996,997],{"class":480},"    if",[225,999,1000],{"class":231}," err ",[225,1002,1003],{"class":235},"!=",[225,1005,1006],{"class":235}," nil",[225,1008,777],{"class":235},[225,1010,1011,1014,1018,1020],{"class":227,"line":659},[225,1012,1013],{"class":480},"        return",[225,1015,1017],{"class":1016},"sfNiH"," false",[225,1019,324],{"class":235},[225,1021,1022],{"class":231}," err\n",[225,1024,1026],{"class":227,"line":1025},13,[225,1027,1028],{"class":235},"    }\n",[225,1030,1032,1035,1037,1040,1042,1044],{"class":227,"line":1031},14,[225,1033,1034],{"class":480},"    return",[225,1036,915],{"class":231},[225,1038,1039],{"class":235},"\u003C=",[225,1041,639],{"class":638},[225,1043,324],{"class":235},[225,1045,1046],{"class":235}," nil\n",[225,1048,1050],{"class":227,"line":1049},15,[225,1051,742],{"class":235},[178,1053,1054,1055,270],{},"To vary limits per endpoint, switch on the key, which is ",[194,1056,1057],{},"\u003Cip>:\u003Croute>",[216,1059,1061],{"className":218,"code":1060,"language":220,"meta":221,"style":221},"func (l tieredLimiter) Allow(ctx context.Context, key string) (bool, error) {\n    limit := 60\n    switch {\n    case strings.HasSuffix(key, \":\u002Ftoken\"):\n        limit = 10\n    case strings.HasSuffix(key, \":\u002Fsignup\"):\n        limit = 5\n    }\n    return l.allowWithLimit(ctx, key, limit)\n}\n",[194,1062,1063,1108,1118,1125,1155,1165,1190,1199,1203,1229],{"__ignoreMap":221},[225,1064,1065,1067,1069,1071,1074,1076,1078,1080,1082,1084,1086,1088,1090,1092,1094,1096,1098,1100,1102,1104,1106],{"class":227,"line":228},[225,1066,864],{"class":235},[225,1068,809],{"class":235},[225,1070,869],{"class":787},[225,1072,1073],{"class":252},"tieredLimiter",[225,1075,806],{"class":235},[225,1077,877],{"class":245},[225,1079,249],{"class":235},[225,1081,788],{"class":787},[225,1083,791],{"class":252},[225,1085,242],{"class":235},[225,1087,53],{"class":252},[225,1089,324],{"class":235},[225,1091,800],{"class":787},[225,1093,803],{"class":310},[225,1095,806],{"class":235},[225,1097,809],{"class":235},[225,1099,812],{"class":310},[225,1101,324],{"class":235},[225,1103,817],{"class":310},[225,1105,806],{"class":235},[225,1107,777],{"class":235},[225,1109,1110,1113,1115],{"class":227,"line":264},[225,1111,1112],{"class":231},"    limit ",[225,1114,236],{"class":235},[225,1116,1117],{"class":638}," 60\n",[225,1119,1120,1123],{"class":227,"line":286},[225,1121,1122],{"class":480},"    switch",[225,1124,777],{"class":235},[225,1126,1127,1130,1133,1135,1138,1140,1143,1145,1147,1150,1152],{"class":227,"line":299},[225,1128,1129],{"class":480},"    case",[225,1131,1132],{"class":231}," strings",[225,1134,242],{"class":235},[225,1136,1137],{"class":245},"HasSuffix",[225,1139,249],{"class":235},[225,1141,1142],{"class":231},"key",[225,1144,324],{"class":235},[225,1146,327],{"class":235},[225,1148,1149],{"class":276},":\u002Ftoken",[225,1151,280],{"class":235},[225,1153,1154],{"class":235},"):\n",[225,1156,1157,1160,1162],{"class":227,"line":347},[225,1158,1159],{"class":231},"        limit ",[225,1161,435],{"class":235},[225,1163,1164],{"class":638}," 10\n",[225,1166,1167,1169,1171,1173,1175,1177,1179,1181,1183,1186,1188],{"class":227,"line":517},[225,1168,1129],{"class":480},[225,1170,1132],{"class":231},[225,1172,242],{"class":235},[225,1174,1137],{"class":245},[225,1176,249],{"class":235},[225,1178,1142],{"class":231},[225,1180,324],{"class":235},[225,1182,327],{"class":235},[225,1184,1185],{"class":276},":\u002Fsignup",[225,1187,280],{"class":235},[225,1189,1154],{"class":235},[225,1191,1192,1194,1196],{"class":227,"line":560},[225,1193,1159],{"class":231},[225,1195,435],{"class":235},[225,1197,1198],{"class":638}," 5\n",[225,1200,1201],{"class":227,"line":565},[225,1202,1028],{"class":235},[225,1204,1205,1207,1209,1211,1214,1216,1218,1220,1222,1224,1227],{"class":227,"line":588},[225,1206,1034],{"class":480},[225,1208,928],{"class":231},[225,1210,242],{"class":235},[225,1212,1213],{"class":245},"allowWithLimit",[225,1215,249],{"class":235},[225,1217,788],{"class":231},[225,1219,324],{"class":235},[225,1221,800],{"class":231},[225,1223,324],{"class":235},[225,1225,1226],{"class":231}," limit",[225,1228,508],{"class":235},[225,1230,1231],{"class":227,"line":603},[225,1232,742],{"class":235},[182,1234,1236],{"id":1235},"rate-limit-your-own-routes-too","Rate limit your own routes too",[178,1238,1239],{},"The proxy limiter covers proxied auth endpoints. For the rest of your API, use the middleware:",[216,1241,1243],{"className":218,"code":1242,"language":220,"meta":221,"style":221},"app.Use(middleware.IPRateLimit(300))\n",[194,1244,1245],{"__ignoreMap":221},[225,1246,1247,1250,1252,1254,1256,1259,1261,1264,1266,1269],{"class":227,"line":228},[225,1248,1249],{"class":231},"app",[225,1251,242],{"class":235},[225,1253,377],{"class":245},[225,1255,249],{"class":235},[225,1257,1258],{"class":231},"middleware",[225,1260,242],{"class":235},[225,1262,1263],{"class":245},"IPRateLimit",[225,1265,249],{"class":235},[225,1267,1268],{"class":638},"300",[225,1270,1271],{"class":235},"))\n",[178,1273,1274,1275,1278],{},"That one is per process and in memory. See ",[1276,1277,63],"a",{"href":64}," for the alternatives.",[182,1280,1282],{"id":1281},"next-steps","Next steps",[1284,1285,1286,1292],"ul",{},[1287,1288,1289,1291],"li",{},[1276,1290,120],{"href":121}," for the GoTrue side of the configuration.",[1287,1293,1294,1297],{},[1276,1295,1296],{"href":95},"Audit logging"," for what the resolved IP ends up in.",[1299,1300,1301],"style",{},"html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}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 pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":221,"searchDepth":264,"depth":264,"links":1303},[1304,1305,1306,1307,1308,1309],{"id":184,"depth":264,"text":185},{"id":363,"depth":264,"text":364},{"id":454,"depth":264,"text":455},{"id":751,"depth":264,"text":752},{"id":1235,"depth":264,"text":1236},{"id":1281,"depth":264,"text":1282},"Recover the real client IP behind a load balancer and throttle sensitive auth endpoints with Redis.","md",null,{},{"icon":118},{"title":115,"description":1310},"q1qeUF-ptmGpvAG9c2hdP1pZAnyGxno9bl_6USCenV4",[1318,1320],{"title":110,"path":111,"stem":112,"description":1319,"icon":113,"children":-1},"Every route gt.Mount registers, with its request and response types.",{"title":120,"path":121,"stem":122,"description":1321,"icon":123,"children":-1},"Configure GoTrue to sit behind ezz, run it locally with Docker, and refresh the golden fixtures.",1784970048376]