> ## Documentation Index
> Fetch the complete documentation index at: https://docs.praxa.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a Praxa edge backend with Cloudflare Workers

> Proxy Praxa through an authenticated Worker, store the API key as a secret, and test the edge boundary locally and live.

Cloudflare Workers can provide a small server-side boundary close to your
users. Store the Praxa key as an encrypted Worker secret, never as a plaintext
configuration variable.

## Prerequisites

Before you begin, prepare:

* a trusted server runtime and application authentication boundary;
* a disposable personal workspace Praxa key with only the tutorial's required scopes;
* synthetic input plus a persisted application request ID for replay tests;
* a fake upstream for unit tests and a non-production environment for canaries;
* an acceptance assertion that proves the Worker keeps the key secret, admits one task, and safely projects readback.

## 1. Declare and add the secret

```jsonc wrangler.jsonc theme={null}
{
  "name": "praxa-task-edge",
  "main": "src/index.ts",
  "compatibility_date": "2026-08-01"
}
```

```bash theme={null}
npx wrangler secret put PRAXA_API_KEY
```

For local development, use one ignored <code>.dev.vars</code> or
<code>.env</code> file. Do not define the key under <code>vars</code>.

## 2. Implement the Worker

```ts src/index.ts theme={null}
import { authenticateApplicationRequest } from "./auth";

type Env = { PRAXA_API_KEY: string };

async function deterministicKey(principalId: string, requestId: string) {
  const bytes = new TextEncoder().encode(principalId + "\0" + requestId);
  const digest = await crypto.subtle.digest("SHA-256", bytes);
  const hex = [...new Uint8Array(digest)]
    .map((byte) => byte.toString(16).padStart(2, "0"))
    .join("");
  return "worker:" + hex;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    if (url.pathname !== "/api/tasks" || request.method !== "POST") {
      return new Response("Not found", { status: 404 });
    }

    const principal = await authenticateApplicationRequest(request, env);
    if (!principal) return new Response("Unauthorized", { status: 401 });

    const body = await request.json<{ task?: unknown; requestId?: unknown }>();
    if (
      typeof body.task !== "string" ||
      body.task.length < 1 ||
      body.task.length > 16_000 ||
      typeof body.requestId !== "string" ||
      body.requestId.length < 16 ||
      body.requestId.length > 128
    ) {
      return new Response("Invalid request", { status: 400 });
    }

    const key = await deterministicKey(principal.id, body.requestId);
    const upstream = await fetch("https://api.praxa.io/v1/execute", {
      method: "POST",
      headers: {
        authorization: "Bearer " + env.PRAXA_API_KEY,
        "content-type": "application/json",
        "idempotency-key": key,
      },
      body: JSON.stringify({
        apiVersion: "v1",
        requestId: key,
        mode: "task",
        task: { input: body.task },
        idempotencyKey: key,
      }),
    });

    return new Response(upstream.body, {
      status: upstream.status,
      headers: {
        "content-type": upstream.headers.get("content-type") ?? "application/json",
        "cache-control": "no-store",
      },
    });
  },
};
```

Use Web Crypto in <code>deterministicKey</code> to hash an authenticated
principal plus the stable application request ID. Do not trust an owner or
workspace field in the body.

## 3. Test locally

Use a fetch interceptor or Miniflare to assert:

1. Authentication runs before parsing or forwarding consequential work.
2. Unknown methods and routes fail closed.
3. Oversized input never reaches Praxa.
4. Exact retries preserve the upstream idempotency key.
5. The key never appears in the response, logs, or thrown errors.
6. Every response carrying a run or problem is <code>no-store</code>.

## 4. Verify after deployment

Run two live canaries: an unauthenticated request that must fail before Praxa,
and an authenticated disposable task that must return a run owned by the
configured personal tenant. Follow the run to terminal before declaring the
workflow complete.

## Troubleshooting

| Symptom                            | Fix                                                                                |
| ---------------------------------- | ---------------------------------------------------------------------------------- |
| Secret undefined locally           | Add it to `.dev.vars`, which must remain ignored                                   |
| Secret undefined after deploy      | Run `wrangler secret put PRAXA_API_KEY` for the intended environment               |
| Request exceeds Worker time budget | Use durable admission and asynchronous readback instead of holding the client open |
| `403`                              | Verify personal key ownership and scopes                                           |
| Duplicate work after retry         | Reuse the same application request ID and normalized body                          |
| Upstream response leaks details    | Project a safe response instead of returning arbitrary problem bodies              |

## Best practices

* Store the key as a Worker secret, never a `vars` value.
* Validate authentication and body size before upstream work.
* Reuse one stable idempotency key per logical request.
* Use explicit `AbortSignal` deadlines for upstream calls.
* Rate-limit by the authenticated application principal.
* Test locally with a fake upstream, then run a disposable deployed canary.

<Card title="Cloudflare Workers secrets" icon="arrow-up-right-from-square" href="https://developers.cloudflare.com/workers/configuration/secrets/">
  Review the official secret-binding and local-development guidance.
</Card>

## Optimize for production

* Reuse one configured HTTP or SDK client per process and bound concurrent upstream work.
* Prefer durable admission plus asynchronous readback over holding application requests open.
* Cache only non-sensitive, tenant-scoped reads within their documented freshness window.
* Measure p50/p95 latency, admission-to-terminal time, retries, conflicts, and connection reuse before tuning.

Optimize only after the correctness and isolation matrix passes. Lower latency or cost is not an improvement if verified outcomes, authority checks, or recovery rates regress.

## Cleanup and next steps

1. Revoke the disposable Praxa key and require a later request to fail.
2. Remove synthetic application records and any temporary environment files.
3. Cancel or archive unresolved test runs according to the application policy.
4. Retain only redacted request, run, and verification identifiers needed for the test record.

After cleanup, run the [shared integration test matrix](/tutorials/test-your-integration) and record any environment-specific check that remains pending.

## Frequently asked questions

### What proves this tutorial works?

The minimum observable result is that the Worker keeps the key secret, admits one task, and safely projects readback. A compile, package import, mocked response, or initial admission alone does not prove the complete workflow.

### Can a browser, mobile app, or model prompt hold the credential?

No. Browser and mobile bundles are inspectable. Keep the Praxa credential in the trusted application backend.

### How should an ambiguous mutation be retried?

Persist the exact logical input and idempotency key before the first attempt. Reconcile through authoritative readback or replay the exact request with that same key before creating new work.

### What should we monitor after release?

Monitor application auth failures, upstream status/code, latency, retries, replay conflicts, and terminal run outcomes. Alert on authorization bypass, cross-tenant disclosure, repeated conflicts, or cleanup failure.
