The proxy
Overview
You change one DNS record. Traffic to that hostname reaches Cladior, gets metered, and continues to your server. You do not import anything, call anything, or check anything, and your application does not need to know Cladior exists.
Setup
Add the app in the dashboard with the address your server already answers on, then:
Type CNAME
Name app (or any hostname you like)
Value proxy.cladior.comCertificates are issued and renewed automatically. If your DNS is already on Cloudflare, set that record to DNS only, the grey cloud, rather than proxied.
Where the record goes
Wherever your DNS is hosted, which is not always where your site is hosted. The two get confused constantly, and the answer is decided by one thing: whichever service your domain's nameservers point at is the one holding your DNS.
| If your nameservers are at | Add the CNAME in |
|---|---|
| Your registrar (Namecheap, GoDaddy, Porkbun) | The registrar's DNS or Advanced DNS panel |
| Netlify | Domains, your domain, DNS records, Add new record |
| Vercel | Project settings, Domains, or the account-level Domains tab |
| Cloudflare | DNS, Records. Set it to DNS only, the grey cloud |
| AWS | Route 53, hosted zone, Create record |
Hosting your site on Netlify does not by itself mean Netlify holds your DNS. If you pointed the domain at Netlify with an A record from your registrar, the registrar is still where records are added. If you moved the nameservers to Netlify, it is Netlify.
If you are unsure, look up your domain's nameservers: whatever they say is where your
records live. whois yourdomain.com in a terminal, or any online whois, lists
them.
Putting it on the root domain
The record above uses a subdomain, and a subdomain CNAME works at every provider. A root
domain, written @ or as the bare name, is the awkward case: the DNS standard
does not allow a CNAME there, so providers each solve it their own way and one of them
cannot solve it at all.
| Provider | Root domain |
|---|---|
| Cloudflare | Add the CNAME normally. Cloudflare flattens it for you |
| Namecheap | Use an ALIAS record, not a CNAME |
| Vercel | Use an ALIAS record |
| Route 53 | Not possible. Its ALIAS records only point at AWS resources, so use a subdomain |
Namecheap will refuse an ALIAS while a CNAME, A, AAAA or URL redirect exists on the same name, and a URL redirect on the root is a common default, so remove that first.
How long it takes
Minutes to hours, occasionally longer, and none of it is under our control or yours: it depends on how the record's TTL was set and which resolvers have already cached the old answer. Your registration draft is saved throughout, so you can close the tab after adding the record and come back to the step you left.
The certificate is issued automatically once the record resolves to us, and cannot be issued before then, because the certificate authority checks the same DNS you are waiting on. So a domain that shows as pending is usually waiting on propagation rather than on anything that needs fixing.
Letting us add it for you
If your DNS is hosted at Cloudflare, you can authorise Cladior and we will write the record into your zone. It is offered during registration and again on the app afterwards, and it is only a shortcut: the result is the same record you would add by hand.
Netlify and Vercel are not available yet. Both support the kind of scoped authorisation this needs, so both are possible; neither is built. Every other provider, including Porkbun, Namecheap, GoDaddy and Route 53, offers only a long-lived API key that would give us control of your whole zone, mail records included. We are not willing to hold that to save you one copy and paste, so those stay manual by choice rather than by omission.
That is for a domain you own. If you took a cladior.com subdomain instead,
there is no record to add: the hostname is inside our own zone, so it is already pointed at
the proxy and already carries a certificate. The dashboard marks it Ready as soon as it exists.
What we do to your responses
HTML gets the Cladior script added before </head>, which is what draws
the balance and account controls. Everything else, JSON, images, stylesheets, downloads,
passes through untouched.
Redirects to your origin's own hostname are rewritten to yours, and cookies your origin scopes to its own domain are rescoped, because a browser would otherwise discard them. Without both, a login on a proxied site fails with nothing in any log to explain it.
Keeping traffic honest
Your server stays reachable at its own address. Anyone who finds it can use it without passing through Cladior, and those requests are not metered. Closing that is optional, and it is the same five lines wherever you host.
We send a secret header on every request we forward. Reject anything without it. This runs on every host and every plan including free ones, and it goes in one place: the middleware you already have. No route, endpoint or piece of application logic changes.
const expected = process.env.CLADIOR_ORIGIN_SECRET;
if (expected && request.headers.get('x-cladior-secret') !== expected) {
return new Response('Not found', { status: 404 });
}Generate the secret in the dashboard under Origin protection, choosing Check it in my own app. Set the same value as CLADIOR_ORIGIN_SECRET in your host's environment variables.
The same check written for Express, Next.js, SvelteKit, Django, Laravel and Go, plus the health-check exemption that catches people out, is in Locking your server.
You can generate a replacement at any time. The new secret takes effect on the very next request we forward, and the old one stops working at that moment, so there is no overlap window. Set the new value on your server in the same sitting: between generating it and deploying it, every request we forward carries a secret your server does not recognise, and if your check is in place those requests are refused.
Keep it server side. A variable named PUBLIC_, NEXT_PUBLIC_ or VITE_ is compiled into your browser bundle, where
anybody can read it, and the check then protects nothing at all. This is the most common way
this gets broken.
The expected && is deliberate. Unset, the site serves normally, so a
deploy that forgets the variable does not take you down. Set, the door is shut. Failing
closed on a missing variable turns a configuration slip into an outage.
Where to put it
| Framework | One place |
|---|---|
| SvelteKit | hooks.server.ts, inside handle |
| Next.js | middleware.ts at the project root |
| Express | one app.use() above your routes |
| FastAPI | @app.middleware("http") |
| Django | a middleware class in MIDDLEWARE |
| Laravel | global middleware |
| Rails | Rack middleware, or before_action in ApplicationController |
| Go | wrap the mux once |
| nginx, no app code | if ($http_x_cladior_secret != "…") { return 404; } |
Once per deployed service, not once per endpoint. Two services, an API and a web app, means two copies. A webhook receiver is the one thing to exempt: Stripe will not send our header, so that path needs an exception.
Every managed host sells a version of this as a paid feature: Deployment Protection on Vercel, password protection on Netlify, Access on Cloudflare. They all do the same job as the five lines above, they all need a plan, and none of them is stronger. If you are already paying for one, keep using it, and set its credential as the header value.
Free pages and search engines
Everything behind your hostname is paid by default. That is the right default and the wrong one to leave alone, because a search engine has no credit: left as it is, a crawler receives the paywall, indexes nothing, and your pages do not rank.
Free pages
Set path prefixes in the dashboard that are served without credit and never billed. A
prefix covers what sits under it, so /about also covers /about/team, and / means the home page alone rather than the
whole site.
Stylesheets, scripts and fonts are free wherever free pages exist, because a landing page without its stylesheet is not a free page. PDFs, video and archives are not covered and stay paid, since those are usually the product rather than the page.
Images sit between the two, so their folder decides. An image in the same folder as a free
page is free, which covers the logo, the icons and the social card, all of which live at
the root next to your home page. An image in a subfolder is not, so a site selling photos
from /photos/ or scans from /archive/ keeps them behind the
meter. If your product is images served from the root, move them into a folder.
This lives in the dashboard rather than your own code, and it has to: we decide whether to charge before forwarding the request, so the same rule in your middleware would run after the charge had already happened.
Search engines
| Setting | What a crawler receives |
|---|---|
| Nothing | The paywall. Paid pages do not rank. This is the default. |
| Free pages | Only what is already free. No structured data needed, weaker rankings. |
| Everything | The full page, with the paywall declared in the markup. Best rankings. |
Crawlers are verified by address against the ranges Google and Bing publish, never by User-Agent. Anyone can claim to be Googlebot in a header, so a User-Agent check would be a paywall in name only. An unverified request is treated as an ordinary visitor.
On Everything, we add "isAccessibleForFree": false to the page for you. That declaration
is what separates a permitted paywall from cloaking: serving a crawler more than a reader
without it can have a site removed from the index. You do not have to write it, and you
should not write your own alongside ours.
Google dropped its First Click Free rule in 2017, so a full paywall is allowed to rank and no free sample is required. A metered approach still tends to rank better, because pages everybody bounces off send poor signals, but that is a decision about conversion rather than a rule you are breaking.
What the visitor is asked
The first time somebody hits a paid page on your site, the proxy sends them to a Cladior consent screen. It shows three things:
- Your app name, and the public domain it is registered to
- The rate per kilobyte they will be charged
- Any scopes you asked for, described in Scopes
The domain is shown next to the name because a name on its own can be copied by anyone. Visitors are told to check it matches the site they are actually on, so register the domain your readers will recognise.
They approve once. Every other Cladior site they visit afterwards is silent, and coming back to yours does not ask again.
The rate is fixed at that moment
Whatever rate the screen showed is the rate that visitor pays, permanently. Changing your rate later applies only to people connecting after the change: nobody is ever charged a figure they did not see and agree to.
If they revoke you
A visitor can revoke your app from their Cladior dashboard at any time. Access ends immediately, the next request through the proxy is refused, and they meet the consent screen again if they return. There is nothing for you to handle: it is the same path as a first visit.
Offering a sign-in link
Usually you do not need one. A visitor who opens a paid page meets the paywall and signs in from there, which is one fewer thing on your page and one fewer decision for them.
When you do want an explicit link, point it at /__cladior/connect on your
own domain. It takes the visitor to consent in the same tab and returns them to where
they were.
<a href="/__cladior/connect">Sign in</a>Add ?to=/wherever to choose where they land afterwards. Only paths on your
own site are accepted: sending somebody who has just authenticated to an arbitrary
address would be an open redirect that also hands over a session.
This is the only sign-in route. It is an ordinary navigation rather than a popup, so it works the same on mobile and cannot be eaten silently by a blocker.
Status codes
| Code | Meaning |
|---|---|
| 200 | Forwarded and billed. |
| 401 | No session. A browser is sent to sign in instead of seeing this. |
| 402 | The reader's balance cannot cover it. |
| 404 | That hostname is not registered with Cladior. |
| 501 | A WebSocket upgrade. Not supported yet, and refused rather than half-forwarded. |
| 502 | Your origin did not answer, or redirected a request we could not safely replay. |
| 503 | No origin address configured for this app. |
Known limits
WebSocket connections are refused. Forwarding them would be worse than refusing: traffic after the handshake never returns through us, so you would have a working socket and an invoice that never mentions it.
A large upload sent without a declared length cannot follow a redirect, because the body is consumed as it is forwarded and replaying it would upload nothing while looking successful. That returns 502.
Responses your origin marks cacheable are stored by the reader's browser and served without
reaching us, so they are not metered. If every request must be counted, do not send
long Cache-Control lifetimes.