Documentation
Acrossed API Reference
One endpoint, one decision, sub-millisecond. Every request is HMAC-SHA-256 signed. Rules live in memory. Your traffic data is never stored.
1. Install
npm install acrossed # Node / TypeScript
pip install acrossed # Python
go get github.com/acrossed-com/sdk-go # Go2. Quickstart — Express
import { createClient } from "acrossed";
import express from "express";
const app = express();
const ac = createClient({
apiKey: process.env.ACROSSED_KEY!,
signingSecret: process.env.ACROSSED_SECRET!,
});
app.use(async (req, res, next) => {
const result = await ac.checkRequest({
ip: req.ip,
method: req.method,
path: req.path,
headers: Object.fromEntries(
Object.entries(req.headers).map(([k, v]) => [k, String(v ?? "")])
),
});
if (result.decision === "deny") {
return res.status(403).json({ error: result.reason });
}
next();
});3. Next.js middleware
Drop a middleware.ts at your project root. It runs before RSCs, API routes, and Clerk auth.
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { createClient } from "acrossed";
const ac = createClient({
apiKey: process.env.ACROSSED_KEY!,
signingSecret: process.env.ACROSSED_SECRET!,
timeoutMs: 800,
});
export async function middleware(req: NextRequest) {
const ip =
req.headers.get("x-forwarded-for")?.split(",")[0].trim() ??
req.headers.get("x-real-ip") ??
"unknown";
const result = await ac.checkRequest({
ip,
method: req.method,
path: req.nextUrl.pathname,
headers: Object.fromEntries(req.headers.entries()),
});
if (result.decision === "deny") {
return new NextResponse(
JSON.stringify({ error: result.reason }),
{ status: 403, headers: { "content-type": "application/json" } }
);
}
return NextResponse.next();
}
export const config = {
matcher: ["/((?!_next\/static|_next\/image|favicon.ico).*)"],
};With Clerk
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
import { createClient } from "acrossed";
import { NextResponse } from "next/server";
const ac = createClient({
apiKey: process.env.ACROSSED_KEY!,
signingSecret: process.env.ACROSSED_SECRET!,
});
const isPublic = createRouteMatcher(["/", "/sign-in(.*)", "/sign-up(.*)"]);
export default clerkMiddleware(async (auth, req) => {
const ip = req.headers.get("x-forwarded-for")?.split(",")[0].trim() ?? "unknown";
const result = await ac.checkRequest({ ip, method: req.method, path: req.nextUrl.pathname });
if (result.decision === "deny") return new NextResponse(null, { status: 403 });
if (!isPublic(req)) await auth.protect();
});
export const config = {
matcher: ["/((?!_next\/static|_next\/image|favicon.ico).*)"],
};4. Fastify
import Fastify from "fastify";
import { createClient } from "acrossed";
const app = Fastify({ trustProxy: true });
const ac = createClient({
apiKey: process.env.ACROSSED_KEY!,
signingSecret: process.env.ACROSSED_SECRET!,
});
// Global hook — runs before every route handler
app.addHook("onRequest", async (req, reply) => {
const result = await ac.checkRequest({
ip: req.ip,
method: req.method,
path: req.url,
headers: req.headers as Record<string, string>,
});
if (result.decision === "deny") {
reply.code(403).send({ error: result.reason });
}
});5. Edge functions
Vercel Edge Middleware
// middleware.ts (runtime: "edge")
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { createClient } from "acrossed";
export const runtime = "edge";
const ac = createClient({
apiKey: process.env.ACROSSED_KEY!,
signingSecret: process.env.ACROSSED_SECRET!,
timeoutMs: 600,
});
export async function middleware(req: NextRequest) {
const ip = req.headers.get("x-forwarded-for")?.split(",")[0].trim() ?? "unknown";
const result = await ac.checkRequest({ ip, method: req.method, path: req.nextUrl.pathname });
if (result.decision === "deny") return new NextResponse(null, { status: 403 });
return NextResponse.next();
}Cloudflare Workers
import { createClient } from "acrossed";
const ac = createClient({
apiKey: ACROSSED_KEY,
signingSecret: ACROSSED_SECRET,
timeoutMs: 600,
});
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const ip = request.headers.get("cf-connecting-ip") ?? "unknown";
const url = new URL(request.url);
const result = await ac.checkRequest({
ip,
method: request.method,
path: url.pathname,
headers: Object.fromEntries(request.headers.entries()),
});
if (result.decision === "deny") {
return new Response(JSON.stringify({ error: result.reason }), {
status: 403,
headers: { "content-type": "application/json" },
});
}
return fetch(request);
},
};6. Python
Flask
from acrossed import Acrossed
from flask import Flask, request, abort
import os
ac = Acrossed(
api_key=os.environ["ACROSSED_KEY"],
signing_secret=os.environ["ACROSSED_SECRET"],
)
app = Flask(__name__)
@app.before_request
def gate():
d = ac.check_request(request)
if d.deny:
abort(403, description=d.reason)Django middleware
# myapp/middleware.py
from acrossed import Acrossed
from django.http import JsonResponse
import os
class AcrossedMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.ac = Acrossed(
api_key=os.environ["ACROSSED_KEY"],
signing_secret=os.environ["ACROSSED_SECRET"],
)
def __call__(self, request):
ip = (request.META.get("HTTP_X_FORWARDED_FOR", "").split(",")[0].strip()
or request.META.get("REMOTE_ADDR", ""))
result = self.ac.check(ip=ip, method=request.method, path=request.path)
if result.deny:
return JsonResponse({"error": result.reason}, status=403)
return self.get_response(request)
# settings.py — add to MIDDLEWARE list:
# "myapp.middleware.AcrossedMiddleware",7. Go
package main
import (
"net/http"
"os"
"github.com/acrossed-com/sdk-go"
)
func main() {
client, err := acrossed.New(acrossed.Config{
APIKey: os.Getenv("ACROSSED_KEY"),
SigningSecret: os.Getenv("ACROSSED_SECRET"),
})
if err != nil { panic(err) }
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
})
http.ListenAndServe(":8080", gate(client, mux))
}
func gate(c *acrossed.Client, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
result, err := c.Check(r.Context(), acrossed.CheckPayload{
IP: r.RemoteAddr,
Method: r.Method,
Path: r.URL.Path,
})
if err != nil || result.Decision == "deny" {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}8. Rule schema
Rules are a JSON array evaluated priority-order (lowest number first). First match wins. Empty array = allow all.
[
{
"id": "login-throttle",
"priority": 10,
"match": { "path": "/login", "method": "POST" },
"ip_block": ["1.2.3.4", "10.0.0.0/8"],
"ip_allow": ["203.0.113.10"],
"country_block": ["RU", "KP"],
"country_allow": ["US", "DE"],
"require_header": "x-auth-token",
"forbid_header": "x-bot-flag",
"time": { "after": "09:00", "before": "21:00", "days": [1,2,3,4,5] },
"limit": { "requests": 10, "window": "1m", "by": "ip" },
"action": "deny",
"reason": "too_many_requests"
}
]9. Wire format
If signing requests yourself (not using an SDK), the canonical signing string is timestamp.rawBody and the signature is hex(HMAC-SHA-256(signingSecret, signingString)).
// Required request headers:
X-Acrossed-Key: <apiKey>
X-Acrossed-Timestamp: <unix seconds as string>
X-Acrossed-Signature: hex(HMAC-SHA256(signingSecret, timestamp + "." + rawBody))
Content-Type: application/json
// Timestamps older than 10 seconds are rejected.
// Response shape:
{
"decision": "allow" | "deny",
"reason": "string",
"matchedRule": "rule-id or undefined",
"latencyUs": 480
}