getting started

quickstart

add bot protection to any website in under two minutes. you need a brickwall account and one line of html.

1. create an account

go to brickwall.onrender.com and register. you'll be taken straight to the dashboard.

2. add your site

in the dashboard, click add site. enter a name and your domain (e.g. yoursite.neocities.org). brickwall will generate a unique site key.

3. add the script tag

paste this into the <head> of every page you want protected:

html
<script src="https://brickwall.onrender.com/js/protect.min.js"
        data-site="YOUR_SITE_KEY"></script>

replace YOUR_SITE_KEY with the key shown in your dashboard under installation.

4. test it

open your site in a private/incognito window. you should be redirected to the brickwall challenge page, solve it in under 2 seconds, then land back on your site. subsequent visits will be instant — the token is cached in localStorage.

once you see requests appearing in your dashboard's all requests panel, everything is working.
getting started

how it works

brickwall is a lightweight bot-gate. it sits between your visitor and your site's content without requiring any server-side code on your end.

the flow

1. visitor arrives → protect.min.js runs and checks localStorage for a valid signed token.

2. no token → visitor is redirected to brickwall.onrender.com/challenge.html with your site key and their return URL in the query string.

3. challenge → the challenge page runs browser fingerprinting checks and a proof-of-work puzzle (SHA-256 hash with leading zeros). takes ~1–2 seconds for a real browser.

4. verification → the solution is sent to the brickwall server, which checks the hash, validates timing (rejects solutions <200ms — too fast for a human), and issues a signed JWT.

5. return → visitor is redirected back to your site with the token appended as ?bw_token=.... the script picks it up, saves it to localStorage, and strips it from the URL.

6. subsequent visits → the token is read from localStorage, the expiry is checked client-side, and the visitor passes through silently. no network request needed.

the token

tokens are JWTs signed with a secret only the brickwall server knows. they contain the site ID and an expiry timestamp. the client-side script decodes the expiry from the JWT payload without a server call to check freshness — it only makes a network request when there's no token at all.

tokens are scoped per site key. a token from one site cannot be used on another, even if both are registered to the same account.
getting started

installation

script tag (recommended)

place this in <head> before other scripts. it must load synchronously to prevent unprotected content from flashing.

html
<head>
  <!-- brickwall — place before other scripts -->
  <script src="https://brickwall.onrender.com/js/protect.min.js"
          data-site="bw_live_xxxxxxxxxxxx"></script>
</head>

attributes

attributedescription
data-siterequiredyour site key from the dashboard. starts with bw_live_.
data-apioptionaloverride the api base url. defaults to https://brickwall.onrender.com. useful if self-hosting.
data-challengeoptionaloverride the challenge page base url. defaults to https://brickwall.onrender.com.

what the script does

on every page load, protect.min.js:

1. checks the URL for a bw_token param (set after a successful challenge) — saves it to localStorage and strips it from the URL using history.replaceState.

2. reads the token from localStorage and decodes the JWT expiry field client-side.

3. if the token is missing or expired, immediately redirects to the challenge page.

4. if the token is valid, does nothing — the user sees the page normally.

stale key warning: if you see "unknown site key" in the challenge page, your embed script has an old key. rotate and copy the current key from your dashboard's installation panel and update the data-site attribute.
configuration

site settings

each site has independent settings accessible from the dashboard under site settings.

settingdefaultdescription
allowCrawlerstrueautomatically pass googlebot, bingbot, and other known crawlers. they're logged as "crawler" with status "passed".
blockTorfalseblock known tor exit node ip ranges before the challenge. blocked visitors see an access denied page.
blockVpnfalseflag or block known datacenter asns and vpn exit ip ranges.
challengeTtl24how many hours a verified token lasts. after this, visitors are re-challenged silently.
activetruewhen false, all visitors pass through without any challenge. useful for maintenance or testing.
configuration

token ttl

the token TTL controls how long a verified visitor goes without being re-challenged. the default is 24 hours. you can set it anywhere from 1 to 720 hours (30 days).

for high-security sites, use a shorter TTL (1–6 hours). for low-friction user experience, use 72–168 hours. the re-challenge is transparent to the user — they'll see the brickwall page for ~1 second and be returned.

token expiry is checked client-side by decoding the JWT payload — no server call required. the token is only validated server-side during the initial challenge flow.
api reference

authentication api

sessions use http-only cookies (bw_session). no bearer tokens needed for dashboard api calls.

POST/api/auth/register

fieldtypedescription
namerequiredstringdisplay name
emailrequiredstringmust be unique
passwordrequiredstringminimum 8 characters

returns {"ok": true, "name": "..."} and sets bw_session cookie.

POST/api/auth/login

fieldtypedescription
emailrequiredstring
passwordrequiredstring

POST/api/auth/logout

clears the session cookie.

GET/api/auth/me

returns {"id", "email", "name"} for the current session. 401 if not logged in.

DELETE/api/auth/account

permanently deletes the account and all associated sites, keys, and request history. clears session cookie.

api reference

sites api

all endpoints require an active session cookie.

GET/api/sites

returns array of sites belonging to the current user.

POST/api/sites

fielddescription
namerequireddisplay name for the site
domainrequireddomain only, e.g. example.com. protocol and path are stripped automatically.

PUT/api/sites/:id

update name, settings, or active state. only provided fields are updated.

json
{
  "name": "my site",
  "active": false,
  "settings": {
    "allowCrawlers": true,
    "blockTor": false,
    "blockVpn": false,
    "challengeTtl": 24
  }
}

DELETE/api/sites/:id

permanently deletes the site, its api key, and all request history.

POST/api/sites/:id/rotate

generates a new api key for the site. all existing visitor tokens are immediately invalidated — visitors will be re-challenged on their next visit. returns {"key": "bw_live_..."}.

GET/api/sites/:id/requests

returns the last 500 verification attempts for the site, newest first.

GET/api/sites/:id/stats

returns {"total", "passed", "blocked", "flagged"} counts for all recorded requests.

api reference

challenge api

these endpoints are called by the challenge page and the embed script. you don't need to call them directly unless you're building something custom.

POST/api/challenge/init

initialize a challenge for a site key. returns a challenge ID and proof-of-work difficulty, or skips straight to a token for known crawlers.

fielddescription
siteKeyrequiredthe bw_live_... key from the embed script
returnUrloptionalurl to redirect to after challenge passes

POST/api/challenge/verify

submit a proof-of-work solution. returns a signed JWT on success.

fielddescription
challengeIdrequiredid from /init
noncerequiredthe nonce that produces a hash with the required leading zeros
elapsedrequiredmilliseconds taken to solve. must be ≥ 200ms or the solve is rejected as automated.

POST/api/challenge/check

verify a token server-side. returns {"valid": true/false}. useful for backend verification — see server-side verification.

fielddescription
tokenrequiredthe JWT from localStorage / the bw_token url param
siteKeyrequiredyour site key
guides

neocities / static sites

brickwall was built with static hosts in mind. there's no server to configure — just paste the script tag.

html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <!-- add this line -->
  <script src="https://brickwall.onrender.com/js/protect.min.js"
          data-site="bw_live_your_key_here"></script>
  <title>my site</title>
</head>
<body>
  ...
</body>
</html>

paste this into every .html file you want protected. on neocities, you can edit files directly in the browser or upload them via the dashboard.

if you have many pages, add the script to a shared header/template if your site uses one. otherwise add it to each file individually.
guides

single-page apps

for SPAs (react, vue, svelte, etc.), add the script tag to your index.html. since brickwall redirects at the browser level before js frameworks initialize, it works regardless of your routing setup.

html — index.html
<head>
  <meta charset="UTF-8">
  <!-- brickwall must load before your app bundle -->
  <script src="https://brickwall.onrender.com/js/protect.min.js"
          data-site="bw_live_your_key_here"></script>
  <link rel="stylesheet" href="/dist/bundle.css">
</head>
<body>
  <div id="root"></div>
  <script src="/dist/bundle.js"></script>
</body>

the redirect happens synchronously before your app mounts — if there's no valid token, the browser never renders your app content.

guides

server-side verification

if you have an api or backend that should only respond to verified visitors, you can verify tokens server-side using the /api/challenge/check endpoint.

javascript — express middleware
async function requireBrickwall(req, res, next) {
  const token = req.headers['x-brickwall-token']
  if (!token) return res.status(401).json({ error: 'not verified' })

  const r = await fetch('https://brickwall.onrender.com/api/challenge/check', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ token, siteKey: process.env.BRICKWALL_SITE_KEY })
  })
  const { valid } = await r.json()
  if (!valid) return res.status(403).json({ error: 'invalid token' })
  next()
}

on the client side, pass the token in a request header:

javascript — client
const token = localStorage.getItem('bw_token_YOUR_SITE_KEY')
fetch('/api/protected-route', {
  headers: { 'x-brickwall-token': token }
})
guides

multiple sites

each site in your dashboard gets its own unique key. tokens are scoped to a site key — a visitor verified on site A cannot use that token on site B.

to protect multiple sites, add each domain in the dashboard and use the corresponding key in each site's script tag. they'll all show up as separate entries in your dashboard with their own request logs and settings.

there's no limit on the number of sites per account during early access.
reference

edge cases

scenariobehavior
googlebot / bingbotauto-bypassed if "allow crawlers" is on. logged as crawler/passed. token issued immediately.
tor exit nodesblocked before challenge if "block tor" is on. blocked visitors see an access denied screen.
vpn / datacenter ipsflagged in the request log. can be configured to block outright.
headless chrome / puppeteerblocked by navigator.webdriver === true check on the challenge page.
phantomjs / nightmareblocked by artifact detection on the challenge page.
javascript disablednoscript fallback message shown. content is not accessible without js.
pow solved in <200msrejected as automated. a human browser cannot solve it that fast.
challenge expires (2min)user is redirected back to the challenge page cleanly on retry.
token expiredcleared from localStorage, visitor re-challenged transparently.
rate limited (15 req/min)ip is rate limited for 60 seconds. user sees a countdown message.
site set to inactiveall visitors pass through without a challenge.
reference

troubleshooting

"unknown site key"

the key in your embed script doesn't match any site in the database. this usually happens after a server restart when data was previously stored in memory (pre-database). go to your dashboard → installation, copy the current key, and update the data-site attribute in your html.

"automated browser detected"

the challenge page's headless browser detection triggered. this should only happen for actual automation tools. if you're seeing this on a real browser, open a github issue — it may be a false positive we haven't seen before.

infinite redirect loop

the most common cause is the token being saved to the wrong origin. make sure you're using the latest version of protect.min.js — older versions saved the token to brickwall's localStorage instead of your site's. re-deploy with the latest script tag.

requests not showing in dashboard

requests are only logged during /api/challenge/init and /api/challenge/verify. if the redirect from your site to the challenge page never happens, no requests are logged. check the browser console for errors from protect.min.js.

can't scroll in dashboard

clear your browser cache and hard-reload (ctrl+shift+r / cmd+shift+r). if you deployed before the scroll fix, you may be serving an old cached version.

login says "invalid credentials"

if you registered before the postgres migration, your account was stored in memory and lost on restart. register a new account — this time it'll persist in the database.