Docs
Go: net/http
github.com/camada-app/camada-go enforces your snapshot inside a Go process: your ordered custom rules, then allow, block and challenge, decided in-process, with events shipped off the request path. One module, one public package, a net/http middleware that any router built on http.Handler sits behind.
Install
go get github.com/camada-app/camada-goGo 1.23 or newer. No dependencies outside the standard library. Tagged releases are on the Go module proxy (pkg.go.dev).
Mount it
import "github.com/camada-app/camada-go"
mux := http.NewServeMux()
mux.HandleFunc("GET /{$}", home)
http.ListenAndServe(":8080", camada.Handler(mux)) // outermost, so camada answers before routingcamada.Handler shares one lazy engine, built from the environment on the first request:
export CAMADA_KEY=<ingest_token>.<snap_token>
export CAMADA_INGEST_URL=http://localhost:8787 # dev only; defaults to production ingestSet these as environment variables: export them in the shell that starts your app, or add them in your host's settings. Go reads no .env file.
That build starts the snapshot poll on a goroutine and never blocks, so the request that triggered it is answered cold: it passes (fail open), and so does anything that arrives before the first poll lands. To enforce from request 1, build the engine eagerly at startup and wait for the boot poll:
import "github.com/camada-app/camada-go/snapshot"
cam := camada.Default() // builds the engine; the boot poll is already running on its goroutine
if cam.Snap != nil { // nil when CAMADA_KEY is unset or CAMADA_DISABLED=1
deadline := time.Now().Add(5 * time.Second)
for cam.Snap.Verdict(snapshot.MatchInput{IP: "0.0.0.0"}).Reason == "cold" && time.Now().Before(deadline) {
time.Sleep(10 * time.Millisecond) // bounded: an unreachable analyst leaves it cold, and the app still fails open
}
}
defer cam.Stop(context.Background()) // drains the last event batch, at most 500 msAn app that reads its own configuration can build the engine itself and hand it in:
cam := camada.New(camada.Options{
Env: map[string]string{"CAMADA_KEY": myKey, "CAMADA_INGEST_URL": myIngest},
ChallengePath: "/api/__camada/challenge",
})
http.ListenAndServe(":8080", cam.Handler(mux))Quickstart: Go.
The options you will use
| Option | Default | Meaning |
|---|---|---|
| Env | the process environment | Where CAMADA_KEY and friends are read from (a map[string]string). Pass one to configure in code. |
| Transport | net/http | The func(HTTPRequest) HTTPResponse that reaches the analyst; tests hand in an in-process fake. |
| Refresh | server-steered | Poll cadence (a time.Duration). Zero, the tenant config's poll_seconds steers it; set, it is pinned. |
| Challenge | nil (= true) | Serve the proof-of-work page for challenge verdicts (CAMADA_CHALLENGE=0 switches it off too). |
| ChallengePath | /__camada/challenge | Where that page posts its solution. |
| SnapshotVersion | 5 | 4 drops your ordered custom rules; 3 the allow and challenge sections too. |
| ScriptPath | /_cam/b.js | Where the first-party beacon script is served. |
| FPPath | /_cam/fp | Where that script posts the beacon. Keep it in the same directory as ScriptPath: the script derives one from the other. |
Environment: CAMADA_KEY, CAMADA_INGEST_URL, CAMADA_SNAPSHOT_URL, CAMADA_TRUSTED_PROXY, CAMADA_SERVERLESS=1 (no poll goroutine; each stale request kicks one refresh off the request path), CAMADA_CHALLENGE=0, and the kill switch CAMADA_DISABLED=1, checked per request (set at boot, no goroutines start at all). The docs index lists what each one means.
What it does per request
- Keeps the snapshot fresh off the request path: one poll goroutine at the cadence your tenant config sets (gzip on the wire, 304 when nothing changed), asking for snapshot v5, the container that carries your ordered custom rules.
CAMADA_SERVERLESS=1drops the goroutine: each request checks staleness, and a stale one kicks one refresh off the request path. - Resolves the client from the socket peer (
r.RemoteAddr), combined withX-Forwarded-Foronly under your trusted-proxy setting. A raw forwarded header is never trusted without one;net/httpnever rewrites the peer, so a reverse proxy is declared, not inferred. - Runs your ordered custom rules, then the allow, block and challenge lists. A blocked request gets 403 with
x-block-reason(andx-block-rulewhen a rule decided) before your app runs. The event still ships, marked as an SDK block so it is not confused with your own 403s. - A challenged browser gets the proof-of-work page; an API call reaches your app, and its response carries
x-camada-challenge: 1for your frontend to act on. A request whose IP cannot be resolved is let through. See challenge. - Serves the beacon endpoints, then runs your app with
X-Ridand the_sfpsession cookie on its response, and ships one batched, redacted event with its real status, latency and route pattern (r.Pattern) when the response is done. A panic in your handler ships asst: 500and propagates unchanged; a hijacked connection ships asst: 101.
Add the beacon
Bots that never run JavaScript are the cheapest to catch. Put the tag in the <head> of the pages you render and the middleware does the rest:
func home(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "<html><head>%s</head><body>…</body></html>", camada.ScriptTag(r))
}camada.ScriptTag(r) returns a <script src="/_cam/b.js?r=…" async> tag carrying this request's event id, so the beacon is joined to the page view it came from. The middleware serves the script at GET /_cam/b.js (cacheable for an hour) and relays POST /_cam/fp onto the event batch, stamped with the client address camada resolved — never the one the body claims. Both endpoints sit behind the verdict, so a blocked client gets 403 there too. Turn the beacon off in the project's settings and the tag becomes empty and both endpoints fall through to your app.
Report what your app knows
The wire shows a POST /login; only your handler knows whether it failed. Tell camada from the handler:
func login(w http.ResponseWriter, r *http.Request) {
if !signIn(r) {
camada.Track(r, "login_failed", r.FormValue("email")) // hashed in-process; never panics
http.Error(w, "invalid", http.StatusUnauthorized)
return
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}camada.Track(r, event, user) (an empty user when there is no identifier) ships the outcome joined to this request's event and session. The user identifier is hashed in-process with your ingest token before it is queued — the raw value never leaves the process. It never panics; outside the middleware (a request it did not run for) the outcome still ships, with no rid, sid or ip to join on. The event name is free-form; the analyst's rules read this vocabulary:
| Event | When |
|---|---|
| login_failed / login_succeeded | a credential check settled |
| signup | an account was created |
| password_reset | a reset was requested |
| mfa_failed | a second factor was rejected |
| payment_failed / payment_succeeded | a charge settled |
| coupon_failed | a promo code was rejected |
A route you want to gate yourself can serve the challenge on demand; camada.FromRequest(r) exposes the request's RID, SID and resolved IP:
func export(w http.ResponseWriter, r *http.Request) {
if camada.ServeChallenge(w, r) {
return // the proof-of-work page (or 403 JSON) until this browser holds a valid _cch
}
bigFile(w)
}What this position sees
The same position as @camada/node, in a Go process: the final status and latency, the session, the route pattern, the beacon's browser signals and your app's outcomes. net/http parses headers into a map, so the event's header order is the sorted name list, not the wire's — camada knows what this tap can see from the position itself and never scores the absence of header order as evidence against a request. No ASN, country or TLS fingerprint reaches a Go process (a server terminating TLS itself could read the ClientHello, and the event already carries the empty field until that lands), so those conditions fail open in-app and the analyst resolves ASN and country itself. matches patterns are JS regexes read by Go's RE2: named groups are native, [^] and \cX are translated, and a spelling RE2 rejects — lookaround, backreferences — never matches here while it does at the edge. What a rule can key on is on rules and list entries.
Deploying it
- Every process polls its own snapshot (about 5 MB resident) and flushes its own batches; the tenant's
poll_secondskeeps the cadence honest across a fleet. - Pending events drain when you call
cam.Stop(ctx)(deferit inmain), within half a second. No signal handlers are installed — an app owns its own shutdown — so a process killed without one drops its last batch. - Serverless hosts:
CAMADA_SERVERLESS=1. A cold invocation fails open and catches up on the next one. - Behind a reverse proxy or load balancer, declare it:
CAMADA_TRUSTED_PROXY=hops:1(or the tenant config) is what makesX-Forwarded-Forcount.
It fails open
Without a key the engine is inert: one log line, no requests, no enforcement, so an unprovisioned environment behaves exactly as if camada were not installed. Beyond that, a dead ingest (logged at most once a minute, through log.Default() or the logger you pass to camada.SetLogger), a corrupt snapshot or a bug in the package costs you the telemetry, never the request — camada can never turn one of your responses into a 5xx. The whole stance is on the security page.