# App server (/docs/deploy/app-server)



Your **app server** is your own application. In production it does two jobs:

1. **Runs your tools.** `connectAgents` registers agents and executes their
   tools when the Runtime asks.
2. **Serves your users.** It signs people in and calls the Runtime for each
   person.

The starter project does the first job. Add the second when people use your
agents.

## Build and start [#build-and-start]

```sh
npm run build
npm start          # node dist/src/main.js
```

Set three variables, or print them for the linked project with
`eval "$(npx @nylorun/cli env)"`:

| Variable              | Value                                                                                          |
| --------------------- | ---------------------------------------------------------------------------------------------- |
| `NYLORUN_RUNTIME_URL` | Where your app reaches the Runtime. See [Docker](/docs/deploy/docker#connect-your-app-server). |
| `NYLORUN_TENANT`      | The Tenant id.                                                                                 |
| `NYLORUN_SERVER_KEY`  | The Tenant's application key. Server-side only.                                                |

The app does not start the stack, Studio, or a file watcher. Start the Runtime
first (`npx nylorun up`), under the same supervisor if you use one. You can
also create Tenants with [`@nylorun/admin`](/docs/reference/admin).

### How the connection is found [#how-the-connection-is-found]

`createClient()` and `connectAgents()` use exactly one complete source, in this
order:

1. Explicit `{ url, key, tenant }` options.
2. `NYLORUN_RUNTIME_URL`, `NYLORUN_SERVER_KEY` or `NYLORUN_EXECUTOR_KEY`, and
   `NYLORUN_TENANT`.
3. The nearest `.nylorun/link.json` plus `credentials.json`.

Sources never mix. If any variable is set, all of them must be, or the call
fails with `connection_missing`.

## Executors [#executors]

The compiled app connects every exported agent and workflow:

```ts title="src/main.ts"
import { connectAgents } from "@nylorun/agents";
import { agents } from "../agents/index.js";

const connection = connectAgents({
  agents,
  implementationVersion: process.env.NYLORUN_IMPLEMENTATION_VERSION ?? "app-1",
  runtime: {
    url: process.env.NYLORUN_RUNTIME_URL!,
    tenant: process.env.NYLORUN_TENANT!,
    key: process.env.NYLORUN_SERVER_KEY!,
  },
  onError: console.error,
});

await connection.ready;
```

Restarts and replicas re-register idempotently. Credentials, retries, and
uncertain work are on [Run → Executors](/docs/run/executors).

## Serve people [#serve-people]

Your server signs people in and keeps the Tenant key. For each request it
names the person with `client.as()`. The Runtime then limits the call to that
person's sessions and vaults:

```ts
import { createClient } from "@nylorun/agents";

const app = createClient(); // the Tenant key, on the server only

// Per request, after your own sign-in:
const person = app.as(`app:${user.id}`, { scopes: ["sessions:own", "vaults:own"] });
const session = await person.createSession({ agentId: "support", ownerUserId: `app:${user.id}` });
await person.listSessions(); // only this person's sessions
```

`as()` sends `Nylorun-Subject` and `Nylorun-Scopes` on every call, event streams
included. Nothing is minted or cached, so calling it per request is cheap.
Another person's session answers `404`, like a missing one; a route outside the
scopes answers `403` (`scope_required`).

| Scope             | Allows                                                                                   |
| ----------------- | ---------------------------------------------------------------------------------------- |
| `sessions:own`    | The person's own sessions: create, list, read, stream, message, approve, respond, cancel |
| `vaults:own`      | The person's own vaults and credentials                                                  |
| `agents:read`     | Listing the Tenant's agents                                                              |
| `agents:write`    | Saving agents; listing agents, models, and providers                                     |
| `tenant:settings` | The Tenant's status, model provider, and sandbox settings                                |

The default is `["sessions:own"]`. Full options are in
[Reference → client](/docs/reference/agents/client#as).

### Serve a chat UI with AG-UI [#serve-a-chat-ui-with-ag-ui]

For a web or desktop chat UI, mount the AG-UI handler. It maps each AG-UI
thread to one session per person, agent, and thread, and calls the Runtime as
that person:

```ts
import { createServer } from "node:http";
import { createAgUiHandler, toNodeListener } from "@nylorun/agents/ag-ui";
import support from "./agents/support.js";

const agui = createAgUiHandler({
  basePath: "/api/agui",
  agents: [support], // nothing else is reachable
  subject: async (request) => (await getSignedInUser(request))?.id, // undefined → 401
});

createServer(toNodeListener(agui)).listen(3000);
// Next.js, Hono, Bun, Deno, Workers: export or mount agui.fetch directly.
```

Point CopilotKit or `@ag-ui/client`'s `HttpAgent` at `/api/agui/support`. A
complete web backend with a test is in the
[AG-UI example](https://github.com/nylorun/agents/tree/main/examples/src/ag-ui).
Routes, approvals, and limitations are in
[Reference → ag-ui](/docs/reference/agents/ag-ui).

### Rules for an app server [#rules-for-an-app-server]

* Browsers always go through your server. Never let one call the Runtime.
* Drop every `Nylorun-*` header your own clients send. Never forward `Origin`;
  the Runtime refuses browser requests.
* Terminate TLS for your clients.
* Keep the admin key and application keys on the server. A server that holds
  the admin key can derive its Tenant key instead of storing one
  (`deriveTenantKey(adminKey, tenantId, principalId)` from `@nylorun/admin`,
  for a Tenant created with that derived principal).
* Removing a person is your decision: stop acting for them and close their open
  streams. There is no per-person credential to revoke.
* Keep the Runtime off the network. How your server reaches it is on
  [Docker](/docs/deploy/docker#connect-your-app-server).

## Next step [#next-step]

<Cards>
  <Card title="Docker" description="Run the Runtime and connect your app server to it." href="/docs/deploy/docker" />

  <Card title="Sessions" description="Everything a session client can do." href="/docs/run/sessions" />

  <Card title="Agents SDK reference" description="createClient, as(), connectAgents, and the AG-UI handler." href="/docs/reference/agents" />
</Cards>
