> ## 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 backend with Next.js

> Create a server-only Next.js App Router boundary for Praxa tasks, preserve idempotency, and test it without leaking credentials.

This tutorial adds one authenticated application route between your browser and
Praxa. The browser never receives the Praxa API key.

```mermaid theme={null}
flowchart LR
  Browser["Browser UI"] -->|"same-origin request"| Route["Next.js Route Handler"]
  Route -->|"server-only API key"| Praxa["Praxa Execution Fabric"]
  Praxa --> Run["Tenant-owned run"]
  Route -->|"safe projection"| Browser
```

## Prerequisites

* Next.js App Router with TypeScript
* Your application session helper
* A personal Praxa key with <code>execute:write</code>
* Node.js 20 or newer

Next.js Route Handlers use the Web Request and Response APIs. Keep
<code>PRAXA\_API\_KEY</code> unprefixed: variables beginning with
<code>NEXT\_PUBLIC\_</code> are eligible for the browser bundle.

## 1. Add the server-only client

```ts lib/praxa.server.ts theme={null}
import "server-only";
import { createHash } from "node:crypto";

const endpoint = "https://api.praxa.io/v1/execute";
const apiKey = process.env.PRAXA_API_KEY;

if (!apiKey) throw new Error("PRAXA_API_KEY is required");

export async function submitTask(input: {
  userId: string;
  requestId: string;
  task: string;
}) {
  const idempotencyKey =
    "next:" +
    createHash("sha256")
      .update(input.userId + "\0" + input.requestId)
      .digest("hex");

  return fetch(endpoint, {
    method: "POST",
    cache: "no-store",
    headers: {
      authorization: "Bearer " + apiKey,
      "content-type": "application/json",
      "idempotency-key": idempotencyKey,
    },
    body: JSON.stringify({
      apiVersion: "v1",
      requestId: idempotencyKey,
      mode: "task",
      task: { input: input.task },
      idempotencyKey,
    }),
  });
}
```

The hash binds the caller's stable application request ID to the authenticated
user without exposing that user ID to Praxa.

## 2. Add the Route Handler

```ts app/api/tasks/route.ts theme={null}
import { submitTask } from "@/lib/praxa.server";

export async function POST(request: Request) {
  const session = await requireSession();
  const body = await request.json();

  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 Response.json({ error: "Invalid request" }, { status: 400 });
  }

  const upstream = await submitTask({
    userId: session.user.id,
    requestId: body.requestId,
    task: body.task,
  });

  const payload = await upstream.json();
  return Response.json(payload, {
    status: upstream.status,
    headers: { "cache-control": "no-store" },
  });
}
```

<Warning>
  Do not accept a Praxa tenant, workspace, owner, scope, or API key in the
  browser request. Your session and server configuration own that authority.
</Warning>

## 3. Call it from the client

```ts theme={null}
export async function startTask(task: string, requestId = crypto.randomUUID()) {
  const response = await fetch("/api/tasks", {
    method: "POST",
    credentials: "same-origin",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ task, requestId }),
  });
  if (!response.ok) throw new Error(await response.text());
  return response.json();
}
```

Persist <code>requestId</code> until the request settles. Reuse it after a
timeout; create a new value only for new logical work.

## 4. Test the boundary

Mock the upstream request in a Route Handler test and prove:

1. No session returns <code>401</code> before the Praxa call.
2. Invalid input returns <code>400</code> before the Praxa call.
3. A valid request forwards one server-owned key and no browser-supplied owner.
4. Repeating the same user and request ID emits the same idempotency key.
5. Changing either value emits a different idempotency key.
6. The response uses <code>Cache-Control: no-store</code>.

Then run one disposable live task and follow its run to a terminal state. A
<code>202</code> response proves admission, not completion.

## Troubleshooting

| Symptom                             | Fix                                                                                 |
| ----------------------------------- | ----------------------------------------------------------------------------------- |
| `PRAXA_API_KEY` is undefined        | Set it in the server runtime and restart; do not add `NEXT_PUBLIC_`                 |
| Route compiles into a client import | Keep the Praxa helper in a `server-only` module and import it only from server code |
| Response is cached                  | Return `Cache-Control: no-store`; mutation Route Handlers are not cached by default |
| Timeout after admission             | Reuse the same request ID/body and reconcile the run                                |
| `403`                               | Verify personal key ownership and exact scopes                                      |
| Browser can choose tenant or scope  | Remove those fields and derive authority from the session/server key                |

## Best practices

* Import `server-only` in credential-owning modules.
* Keep the key unprefixed and scan built client chunks for credential patterns.
* Authenticate and bound the body before calling Praxa.
* Rate-limit per application principal.
* Store the request ID with the draft before the first mutation.
* Add a server-owned run-read route instead of exposing the Praxa key.
* Test Node and edge runtimes separately if your deployment uses both.

<Card title="Next.js Route Handler reference" icon="arrow-up-right-from-square" href="https://nextjs.org/docs/app/getting-started/route-handlers">
  Review the current framework conventions in the official Next.js guide.
</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 App Router keeps credentials server-only and returns a bounded task projection. 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.
