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

# Published Sites and hosted frontends

> Bundle @arg/sdk into a Site: opened inside Arg it reaches the signed-in viewer after consent; outside Arg it talks to a gateway your own app server mounts.

A published [Site](/guides/sdk/actions#workspace-resources) is a static or server-rendered frontend Arg hosts for you. Its frontend can use `@arg/sdk` in two different situations, with two different sources of authority.

## Opened inside Arg

When a viewer opens the Site from **Apps**, or from a `.app` launcher, on web, desktop, iOS or Android, Arg wraps it with a bridge. Bundle the SDK into the Site like any dependency:

```bash theme={null}
npm install @arg/sdk
```

```ts theme={null}
import { arg } from "@arg/sdk";

await arg.ready;
const viewer = await arg.auth.principal();
const settings = await arg.fs.readJSON("./settings.json");
```

The Site sees the **signed-in viewer**, not its publisher, and only after that viewer enables **Permissions → Workspace access** for it. The grant starts off for every Site and every account, and works like an app preview's:

* Folder scope reads relative to the Site's source folder in the workspace; Workspace scope, **Read and write** and **Actions** are separate choices.
* It is remembered for that Site, origin and source folder, including published updates.
* Revoking it, or changing account, replaces the guest so nothing survives in memory.

Credentials stay in Arg: the page never holds a token, and its normal `fetch()` calls and assets keep going to the Site's own network. The bridge serves files, embedded SQLite, identity and members, chats, workspace discovery and Actions with their usual gates.

Two limits: a shared-origin development URL does not get a bridge, only the Site's canonical origin does; and Android needs a current system WebView.

### Outside Arg, without a gateway

The same Site opened in an ordinary browser tab renders anonymously if it is public, or behind its normal viewing authorization if it is workspace-only. There, `arg.ready` rejects with `bridge_unavailable` and nothing attaches a viewer or publisher account. Handle it with your app's signed-out state:

```ts theme={null}
try {
  await arg.ready;
} catch (error) {
  if (error.code === "bridge_unavailable") showSignedOut();
  else throw error;
}
```

## Hosted frontends outside Arg

A frontend that must work outside Arg - a Site with its own users, or any app you host - reaches the SDK through a **gateway your app server mounts on the same origin**. The browser half:

```ts theme={null}
import { createHostedClient } from "@arg/sdk/hosted";

const arg = await createHostedClient({ endpoint: "/api/arg-sdk" });
const principal = await arg.auth.principal();
```

`createHostedClient` connects to that endpoint and nothing else: it does not log visitors in, does not exchange a Site's viewing authorization for account access, and never attaches the publisher's identity. Your server already owns an authenticated session for the visitor (or a service identity) and decides what that session may do.

### The gateway

`createGateway` from `@arg/sdk/gateway` returns a `(request: Request) => Promise<Response>` handler for any standard Request/Response server:

```ts theme={null}
import { createGateway } from "@arg/sdk/gateway";
import { createServerTransport } from "@arg/sdk/server";

export const handleSdk = createGateway({
  origin: "https://app.example.com",
  path: "/api/arg-sdk",
  async authenticate(request) {
    const session = await sessions.fromCookie(request); // your own session store
    if (!session || session.revoked) return null;
    return {
      transport: createServerTransport({
        workspaceId: session.workspaceId,
        auth: { kind: "api-key", key: keyFor(session) },
      }),
      operations: ["auth.principal", "fs.read", "fs.readJSON", "fs.list", "actions.run"],
      authorize: ({ operation, args }) =>
        operation !== "actions.run" || ALLOWED_ACTIONS.has(String(args[0])),
    };
  },
});
```

`authenticate` runs on every request and returns the server-owned session: a workspace-bound `transport`, an explicit list of permitted `operations`, and an `authorize` check per call. Return `null` for an expired or revoked session and `false` from `authorize` to deny a call. Never pick the credential, workspace or role from anything the guest sent.

The gateway itself checks one exact origin, the JSON content type and the protocol header, emits no CORS headers, rejects oversized bodies and hides internal error messages, following the [non-simple request and origin defences for CSRF](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/CSRF). It is a transport adapter: cookie verification, installation consent, audience binding, expiry, revocation and resource authorization belong in your `authenticate` and `authorize`.

<Warning>
  Allowing `actions.run` is not a narrow file grant. Authorize the Action id **and** what it can do
  downstream: an Action that executes code or starts an agent must not receive a broader workspace
  or integration scope than the app was granted.
</Warning>

### Servers with capability tokens

A Server (a long-running process on an HTTPS URL) receives its own Action capability token. On that server, use [`createCapabilityClient`](/guides/sdk/server#sites-and-servers-with-their-own-tokens) rather than an account credential; its frontend, if it needs the SDK, goes through a gateway exactly as above.
