How Docly Protects a Published Site — and How a Real Visitor Gets Back In
Published sites sit on the open internet and get the traffic that comes with it. Here is the model Docly uses to decide that an IP has crossed a line, and the self-service ladder that lets a legitimate visitor undo a block without waiting for anyone.
Only flagged events count
The counter behind a block is fed by exactly one thing: an explicit flag, raised by code that saw something specific. Failed sign-ins, path traversal attempts, scanner-style probes, share and profile abuse - and anything your own site chooses to flag.
An ordinary 404 never counts. That is a deliberate choice, and it is the one that makes everything else work. Every site accumulates dead links, moved pages and stale bookmarks. If those fed the counter, a real attack would be a rounding error inside the noise, and the threshold would have to be set so high it stopped meaning anything at all.
The log is per IP, file-based, and survives a restart. It holds a rolling three-day window, and that window is applied every time the count is read - so an IP cannot slowly accumulate its way to a block over months.
Limits follow geography
Not every part of the internet sends the same kind of traffic, so the threshold is not the same everywhere. Each IP is placed in one of three trust classes based on its country, and each class carries its own limit. The limits are configurable.
The country comes from a cached lookup, never a live one - a network round-trip has no business on the request path. An IP with no cached country falls to the middle class rather than the strictest one: when in doubt, be less aggressive.
When an IP reaches its limit a block rule is added, and the duration escalates for repeat offenders. A successful sign-in clears that IP's counter outright.
Getting back in
A blocked IP is not necessarily a hostile one. Shared office connections, mobile carrier NAT, and a colleague running a scanner all produce the same symptom for someone who just wants to read your site. So the unblock flow is a ladder, and it starts as gently as it can:
Step | When | What is required |
|---|---|---|
1 | First self-service unblock for this IP | A captcha - unblocked immediately |
2 | Second time | Captcha plus name, email and reason -> verification link by email |
3 | Third time, or a range block | Manual review |
Step 1 does not ask who you are. The name, email and reason fields are not merely optional there - they are not rendered at all. There is no case for collecting personal data to answer a question a captcha already answers.
At step 2, the emailed link is the credential - not the address it is opened from. Someone whose office connection is blocked can open the link on their phone and it still works. The token carries a short expiry of its own.
The step counter follows the IP over time and survives re-blocking, so the ladder cannot be reset by simply getting blocked again.
Your site can flag, too
The same counter is available to your own code. flagActivity() is a server-side function - browser JavaScript cannot reach it, which is exactly the point.
// #/API/signin.js
if (!passwordOk) {
const blocked = flagActivity("failed app sign-in");
return { error: blocked ? "Too many attempts" : "Invalid credentials" };
} It takes an optional reason and returns true if the IP is now blocked, so you can branch on it. Two details are worth knowing.
One flag per run. The counter counts suspicious requests, not function calls. A script calling it inside a loop could otherwise push a perfectly legitimate visitor past the limit within a single request. Later calls in the same run register nothing but still report the current block status, so if (flagActivity()) behaves exactly as you would expect.
Your reason is prefixed. Whatever you pass is prefixed with site-script: in the security notification, so a site script cannot forge a reason that reads like an internal one.
Image scaling has its own guard
Scaling is CPU-heavy and every distinct size is a new cache entry, so the public scaling path is metered separately: one limit per IP on a site, and one on how many new sizes a site will scale within a window across all IPs. The second is the one that matters against a distributed attempt, where no single IP looks unusual on its own. Over the limit returns 429 with Retry-After, and anything already cached is never affected.
What to do with this
Nothing, if your site is an ordinary one - the protection is on and the defaults are sensible.
If your site has its own sign-in, its own upload form or its own API, add a flagActivity() call where you already detect abuse. The counter is only ever as good as the events it is told about.