> For the complete documentation index, see [llms.txt](https://docs.goassay.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.goassay.ai/developers/list-your-api-advanced/run-in-production.md).

# Run the verifier in production

Keep signature verification reliable across replicas, key changes and proxies.

Start with a working [in-code verifier](/developers/list-your-api-get-started/verify-in-your-code.md) or [Worker](/developers/list-your-api-get-started/cloudflare-worker.md). Before adding replicas, give every instance a shared replay store and check that proxies preserve the request Assay signed.

## Share replay protection

A replay store remembers a signed request's nonce, its one-time identifier. Checking for the nonce and inserting it must be one atomic operation. A separate read followed by a write can let concurrent requests both pass.

Use the same store and key prefix across all instances that protect your endpoint. Keep entries until the signature's expiry plus clock tolerance. Size the store to retain them for that whole period; eviction or a restart that loses entries can admit a replay.

Each library ships a Redis replay store that uses `SET NX EX`, rounds the lifetime up to whole seconds and passes Redis errors on, so a store failure rejects the request. These examples add shared storage to the servers in [Verify in your code](/developers/list-your-api-get-started/verify-in-your-code.md). Set `REDIS_URL` in your deployment's secret configuration. In production, pin the package version you tested.

{% tabs %}
{% tab title="Node" %}
Requires Node 20 or newer.

```sh
npm install @goassay-ai/provider-verify express redis
```

The adapter works with node-redis v4 or newer and with ioredis. Save as `server.mjs`, then run `node server.mjs`:

```js
import express from "express";
import { createClient } from "redis";
import { assayVerify } from "@goassay-ai/provider-verify/express";
import { redisReplayStore } from "@goassay-ai/provider-verify/redis";

if (!process.env.REDIS_URL) throw new Error("REDIS_URL is required");
const redis = createClient({ url: process.env.REDIS_URL });
redis.on("error", () => console.error("replay_store_connection_error"));
await redis.connect();

const app = express();
app.use(assayVerify({
  authority: "api.example.com", // Replace with the public host Assay calls.
  replayStore: redisReplayStore(redis, "assay:nonce:"),
}));
app.post("/v1/search", (req, res) => res.json({ ok: true }));
app.use((err, req, res, next) => {
  console.error("verification_or_handler_error");
  res.status(503).json({ error: "service_unavailable" });
});
app.listen(8080, "127.0.0.1");
```

{% endtab %}

{% tab title="Python" %}
Requires Python 3.9 or newer. In a virtual environment:

```sh
python -m pip install "assay-provider-verify[flask,redis]"
```

`redis_replay_store` works with redis-py clients. Save as `server.py`, then run `python server.py`:

```python
import os
import redis
from flask import Flask, jsonify
from assay_provider_verify import AssayVerifyWSGI, redis_replay_store

client = redis.Redis.from_url(os.environ["REDIS_URL"], socket_timeout=2)

app = Flask(__name__)
app.wsgi_app = AssayVerifyWSGI(
    app.wsgi_app,
    authority="api.example.com",  # Replace with the public host Assay calls.
    replay_store=redis_replay_store(client, "assay:nonce:"),
)

@app.post("/v1/search")
def search():
    return jsonify(ok=True)

if __name__ == "__main__":
    app.run("127.0.0.1", 8080)
```

{% endtab %}

{% tab title="Go" %}
Requires Go 1.21 or newer.

```sh
go get github.com/assay-ai/assay-provider-verify/go github.com/redis/go-redis/v9
```

Go takes any `ReplayStore` with `CheckAndStore(ctx, nonce, ttl)`. This one wraps go-redis `SetNX`. Save as `main.go`, then run `go run .`:

```go
package main

import (
    "context"
    "io"
    "log"
    "net/http"
    "os"
    "time"

    assayverify "github.com/assay-ai/assay-provider-verify/go"
    "github.com/redis/go-redis/v9"
)

type redisStore struct {
    client *redis.Client
    prefix string
}

// CheckAndStore returns Redis errors, so verification fails closed.
func (s redisStore) CheckAndStore(ctx context.Context, nonce string, ttl time.Duration) (bool, error) {
    if ttl < time.Second {
        ttl = time.Second
    }
    return s.client.SetNX(ctx, s.prefix+nonce, "1", ttl).Result()
}

func main() {
    opt, err := redis.ParseURL(os.Getenv("REDIS_URL"))
    if err != nil {
        log.Fatal(err)
    }
    verify, err := assayverify.Middleware(assayverify.Options{
        Authority:   "api.example.com", // Replace with the public host Assay calls.
        ReplayStore: redisStore{client: redis.NewClient(opt), prefix: "assay:nonce:"},
    })
    if err != nil {
        log.Fatal(err)
    }
    mux := http.NewServeMux()
    mux.HandleFunc("/v1/search", func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodPost {
            w.WriteHeader(http.StatusMethodNotAllowed)
            return
        }
        w.Header().Set("Content-Type", "application/json")
        _, _ = io.WriteString(w, `{"ok":true}`)
    })
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", verify(mux)))
}
```

{% endtab %}
{% endtabs %}

Replace the handler with your tool. Deploy behind HTTPS. Set connection and command deadlines in your Redis client and infrastructure so a store outage cannot leave requests waiting. Keep requests rejected when storage fails.

The Cloudflare Worker can use its `NONCE_STORE` Durable Object instead; see [Verify with a Cloudflare Worker](/developers/list-your-api-get-started/cloudflare-worker.md).

## Keep key rotation working

Use the default public key set, or JWKS, at `https://api.goassay.ai/.well-known/assay-request-keys.json`. Allow outbound HTTPS to that host. Select keys by the signed `keyid`; do not pin a single current key in application code.

| Key-fetch setting    | Library behavior                                                               |
| -------------------- | ------------------------------------------------------------------------------ |
| Cache lifetime       | One hour                                                                       |
| Unknown key          | Can trigger a refetch after at least 60 seconds since the last successful load |
| Fetch deadline       | 5 seconds                                                                      |
| Failed fetch         | 5-second retry pause; existing cached keys remain usable                       |
| Static `jwks` option | Disables automatic fetching; your deployment must supply key updates           |

Reuse verifier instances and their caches. An unfamiliar key can remain unknown during the refetch interval. Alert on sustained `UNKNOWN_KEY` failures and restore key fetching. Do not bypass verification during a key change or network outage.

## Preserve the public request

| Boundary                     | Production rule                                                                                                                                               |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Proxy or load balancer       | Preserve method, raw path, query, call ID, signature headers and body bytes until verification.                                                               |
| `Host` rewrite               | Configure the known public `authority` (`Authority` in Go), including a non-default port. Do not take it from an arbitrary client-supplied forwarding header. |
| JSON parser or decompression | Verify before changing the signed body.                                                                                                                       |
| Redirect                     | Register the final HTTPS URL in Assay so the signed request reaches its destination directly.                                                                 |
| Origin behind a gateway      | Restrict direct access so requests cannot bypass the verifier.                                                                                                |
| Proxy retries                | Do not resend an accepted signature. Its nonce is already used.                                                                                               |

## Clocks and timeouts

Keep server clocks synchronized. The libraries allow 60 seconds of clock tolerance and reject signed windows longer than 300 seconds. Increasing tolerance also extends how long replay entries must live. Do not leave a fixed fixture clock in production.

Key fetching, replay storage and your handler all consume the caller's time budget. The mapping tester uses the listing's timeout, capped at 30 seconds. Use measured end-to-end latency to set your handler and infrastructure deadlines. The verifier adapters buffer the body, with a default cap of 5 MiB; a body cap is not a read timeout.

## Logging: what to keep

Keep a structured record of receipt time, verification outcome or reason, latency and service instance. For accepted calls, keep the verified call ID and key ID to correlate your logs with Assay. Treat IDs read from rejected requests as untrusted and sanitize them before logging.

Avoid routine logging of API keys, authorization headers, full signed headers, nonces, bodies or query strings containing user data. If you retain payload evidence for debugging, restrict access and define your own retention period. These are operational recommendations; the verifier does not install a logging or retention policy for you.

## Check it worked

In a controlled integration test, deliver the same valid signed request to different replicas: only the first should pass. Test a shared-store outage and confirm no handler runs. Check a valid call through your actual proxy and confirm unsigned calls cannot reach the handler or bypass the gateway.

## Errors

| Symptom                                      | Action                                                                      |
| -------------------------------------------- | --------------------------------------------------------------------------- |
| Same signed call passes on separate replicas | Check that they use the same store and prefix, with no early eviction.      |
| `EXPIRED` across many calls                  | Check clock synchronization and queue delays.                               |
| `BAD_SIGNATURE` only through the proxy       | Compare public authority and raw request target before and after the proxy. |
| Store failures or slow requests              | Restore storage and enforce client deadlines. Keep failures closed.         |

## Go further

* [Reject reasons](/developers/list-your-api-get-started/reject-reasons.md).
* List your API (optional): [List a catalogue tool](https://docs.goassay.ai/list-your-api/list-a-catalogue-tool).

## Next

* [Write your own verifier](/developers/list-your-api-advanced/write-your-own-verifier.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.goassay.ai/developers/list-your-api-advanced/run-in-production.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
