---
title: "Apps"
description: "Crie, implante e controle Beeuni Apps pelo SDK e pelo React."
---

Apps são implantações no escopo do projeto, cada uma com uma URL estável,
releases imutáveis, máquinas por grupo de processos e volumes persistentes. Use `app.capabilities` para definir o fluxo. Esta página aborda a interface do SDK: a
fachada `apps` em um identificador de projeto, as chamadas de artefatos, releases, máquinas e volumes,
tokens de login, os tipos exportados e os hooks do React.

Para saber sobre o modelo, a configuração de release, os tipos de origem, a CLI, a URL estável
e o comportamento de ativação a frio, leia [Apps](/docs/pt/feature-flags/apps).

```ts
const apps = beeuni.project(projectId).apps;
```

:::info[Apps é uma feature flag]
Cada rota de Apps responde `403` com `{ error, code: "feature_disabled", feature: "apps" }`
até que o projeto ative Apps. Use `isFeatureDisabledError(error)` para definir o fluxo
com base nisso. Consulte [Feature flags](/docs/pt/feature-flags).
:::

<a id="the-apps-facade"></a>

## A fachada apps

| Método | Encapsula | O que faz |
|---|---|---|
| `apps.list()` | `GET /projects/:pid/apps` | Lista os Apps que o chamador pode abrir (um gerente de projeto vê todos) |
| `apps.create(input)` | `POST …/apps` | Cria um App e atribui sua URL estável |
| `apps.get(appId)` | `GET …/apps/:id` | Lê um App |
| `apps.update(appId, input)` | `PATCH …/apps/:id` | Renomeia ou altera a máquina, o tempo limite de inatividade, o modo de execução (`always_on`), `uses` ou `labels`. O modo de execução é aplicado em até 5 minutos; uma alteração de máquina é aplicada ao próximo release |
| `apps.remove(appId)` | `DELETE …/apps/:id` | Exclui o App, suas máquinas e todas as imagens de release; retorna `images: {released, pending}`. Seus volumes mantêm um snapshot `final` por 7 dias |
| `apps.start(appId)` | `POST …/apps/:id/start` | Define `desired_state` como `running` e aquece o ambiente de execução. Um App estático responde `409 static_app_no_runtime` |
| `apps.stop(appId)` | `POST …/apps/:id/stop` | Suspende o processamento imediatamente; a próxima requisição o retoma. Um App estático responde `409 static_app_no_runtime`: ele não tem ambiente de execução e atende enquanto houver uma implantação ativa |
| `apps.rollback(appId, deploymentId)` | `POST …/apps/:id/rollback` | Cria um NOVO release a partir do artefato e da configuração de um release pronto e o disponibiliza como uma implantação (`202`). Os volumes mantêm seus dados |

Artefatos são a entrada imutável de uma implantação:

| Método | Encapsula | O que faz |
|---|---|---|
| `apps.artifacts.register(input)` | `POST …/apps/artifacts` | Registra um `archive` ou um `oci_image`; retorna a URL de upload para um arquivo compactado |
| `apps.artifacts.uploadArchive(bytes, options?)` | — | Registra, envia, calcula o hash e finaliza um `.tar.gz` em uma única chamada |
| `apps.artifacts.finalize(artifactId, input)` | `POST …/apps/artifacts/:id/finalize` | Confirma `sha256` e `size_bytes` para um upload manual |

Releases (implantações) são imutáveis e numerados:

| Método | Encapsula | O que faz |
|---|---|---|
| `apps.deployments.create(appId, input)` | `POST …/apps/:id/deployments` | Inicia um release a partir de um artefato, uma origem e um `config` opcional do release |
| `apps.deployments.list(appId)` | `GET …/apps/:id/deployments` | Lista o histórico de implantações |
| `apps.deployments.get(appId, deploymentId)` | `GET …/deployments/:did` | Lê uma implantação e seus eventos |
| `apps.deployments.logs(appId, deploymentId, options?)` | `GET …/deployments/:did/logs` | Lê logs de execução com um cursor |
| `apps.deployments.remove(appId, deploymentId)` | `DELETE …/deployments/:did` | Exclui uma implantação e sua imagem; `409` para a implantação em produção ou uma compilação em andamento. Retorna `image: 'released' \| 'pending' \| 'none'` |

Máquinas, escala e exec (um App servidor; um App sem máquinas responde
`409 app_no_machines`):

| Método | Encapsula | O que faz |
|---|---|---|
| `apps.machines.list(appId)` | `GET …/apps/:id/machines` | Todas as máquinas: `machine_id`, `process_group`, `state`, `release`, `vm`, `volume` |
| `apps.machines.start/stop/restart(appId, machineId)` | `POST …/machines/:mid/{start,stop,restart}` | Atua em uma máquina; resolve com a lista de máquinas |
| `apps.machines.destroy(appId, machineId)` | `DELETE …/machines/:mid` | Destrói uma máquina. Seu volume permanece; seu grupo executa uma máquina a menos |
| `apps.scale(appId, count)` | `PUT …/apps/:id/scale` | Máquinas por grupo de processos (`{ worker: 3 }`, 0–20), aplicado em segundo plano (`202`). Resolve com `scale` e `machines` |
| `apps.exec(appId, { command, machine_id?, timeout_s? })` | `POST …/apps/:id/exec` | Executa um comando em uma máquina em execução (`project.app.admin`; auditado). `timeout_s` 1–300, padrão 60. Resolve com `exit_code`, `stdout`, `stderr` (1 MiB cada), `truncated` |

Volumes (discos persistentes):

| Método | Encapsula | O que faz |
|---|---|---|
| `apps.volumes.list(appId)` | `GET …/apps/:id/volumes` | Todos os volumes: `name`, `process_group`, `machine_id`, `size_gb`, `status`, `attached`, `snapshot_retention_days` |
| `apps.volumes.create(appId, { name, size_gb, process_group?, snapshot_retention_days? })` | `POST …/volumes` | Cria um volume antecipadamente; uma máquina posterior do seu grupo com uma montagem desse nome o utiliza |
| `apps.volumes.update(appId, volumeId, { size_gb?, snapshot_retention_days? })` | `PATCH …/volumes/:vid` | Expande um volume (nunca reduz; no máximo 100 GB) ou altera sua retenção (1–60 dias) |
| `apps.volumes.destroy(appId, volumeId, confirmName)` | `DELETE …/volumes/:vid?confirm=<name>` | `project.app.admin`. Recusado com `409 volume_attached` enquanto uma máquina o mantém montado. Mantém um snapshot `final` por 7 dias |
| `apps.volumes.snapshots(appId, volumeId)` | `GET …/volumes/:vid/snapshots` | Snapshots `daily`, `manual` e `final` |
| `apps.volumes.snapshot(appId, volumeId)` | `POST …/volumes/:vid/snapshots` | Um snapshot `manual` agora |
| `apps.volumes.restore(appId, volumeId, snapshotId)` | `POST …/volumes/:vid/restore` | `project.app.admin`. A máquina para, o volume retorna ao snapshot e a máquina inicia nele (`202`) |
| `apps.volumes.fork(appId, volumeId, { name, snapshot_id? })` | `POST …/volumes/:vid/fork` | Um novo volume não anexado no mesmo App, a partir de um snapshot ou do conteúdo atual |

Tokens de login (capacidade `member_tokens`; `409 app_capability_unsupported`
quando o App não a tem):

| Método | Encapsula | O que faz |
|---|---|---|
| `apps.token(appId)` | `POST …/apps/:id/token` | Um token de login de 15 minutos para o App, identificando o chamador |

O acesso é a política de autorização do próprio App:

| Método | Encapsula | O que faz |
|---|---|---|
| `apps.access.get(appId)` | `GET …/apps/:id/access` | Lê a política. Exige `project.app.read` |
| `apps.access.update(appId, input)` | `PATCH …/apps/:id/access` | Substitui a política e incrementa sua revisão |
| `apps.access.session(appId)` | `POST …/apps/:id/access-session` | Gera uma URL de cinco minutos que é trocada por um cookie restrito ao host |

<a id="deploy-a-static-site"></a>

## Implante um site estático

`uploadArchive` realiza todo o processo de negociação do artefato: registra o artefato,
verifica-o em relação a `max_bytes`, envia os bytes via `PUT`, calcula o SHA-256 e
finaliza.

```ts
const apps = beeuni.project(projectId).apps;

const app = await apps.create({ slug: 'docs', name: 'Docs' });
const artifact = await apps.artifacts.uploadArchive(tarGzBytes, {
  onProgress: (uploaded, total) => console.log(`${uploaded}/${total}`),
});

const deployment = await apps.deployments.create(app.app_id, {
  artifact_id: artifact.artifact_id,
  source: { kind: 'static', spa: true },
});

console.log(app.url, deployment.status); // https://…apps.beeuni.ai  queued
```

`create` também aceita os campos de máquina: `cpu`, `memory_gb`,
`disk_gb`, `idle_timeout_seconds` e `always_on`. Omita-os para usar os valores padrão.
Apps não têm orçamento mensal: os créditos da conta pagam pelo processamento. Um
App servidor sempre ativo é cobrado 24 horas por dia, 7 dias por semana; um sob demanda, apenas enquanto estiver ativo.
`always_on: true` executa um App servidor 24 horas por dia, 7 dias por semana
(websockets, loops em segundo plano); `false` o interrompe após `idle_timeout_seconds`
e o ativa na próxima requisição. Um App estático não tem ambiente de execução: `always_on` é
`false`.

A API ignora `monthly_budget_usd` se um cliente antigo o enviar (`200`, sem
erro). Nenhuma resposta de App o inclui.

`uses` lista os Apps, por slug,
para os quais este App pode obter tokens de login e acessar por meio da montagem de bindings; o
padrão é nenhum, e `update(appId, { uses })` substitui a lista (consulte
[Apps que um App usa](/docs/pt/feature-flags/apps#apps-an-app-uses-sign-in-tokens-and-bindings)).

`create` e `update` retornam `warnings`, que é sempre `[]`.

```ts
const app = await beeuni.project(projectId).apps.create({ slug: 'worker', name: 'Worker', always_on: true });
console.log(app.always_on, app.hosting_type); // true null (until the first deploy)
```

Aguarde a implantação consultando seu status periodicamente:

```ts
async function waitForReady(appId: string, deploymentId: string) {
  for (;;) {
    const { deployment } = await apps.deployments.get(appId, deploymentId);
    if (deployment.status === 'ready') return deployment;
    if (deployment.status === 'failed' || deployment.status === 'cancelled') {
      throw new Error(deployment.error ?? deployment.error_code ?? deployment.status);
    }
    await new Promise((resolve) => setTimeout(resolve, 2000));
  }
}
```

Os valores de status são `queued`, `validating`, `building`, `provisioning`,
`checking`, `ready`, `failed` e `cancelled`.

<a id="deploy-an-oci-image"></a>

## Implante uma imagem OCI

Registre a referência imutável da imagem e declare o comando do processo e
a porta de destino pública:

```ts
const registered = await apps.artifacts.register({
  kind: 'oci_image',
  image: 'ghcr.io/hekmat/service:2026-08-07',
});

await apps.deployments.create(app.app_id, {
  artifact_id: registered.artifact.artifact_id,
  source: {
    kind: 'oci_image',
    image: 'ghcr.io/hekmat/service:2026-08-07',
    command: ['node', 'server.js'],
    port: 3000,
    readiness_path: '/health',
  },
});
```

`register` retorna `upload: null` para um `oci_image`. Apenas um `archive` recebe uma
URL de upload.

`CreateAppDeploymentInput` também aceita `environment` (valores de execução
não secretos), `secrets` (chave de execução para nome de segredo do projeto) e `config`
(`AppReleaseConfigInput`, a configuração do release). Um `config` inválido
responde `400 app_config_invalid` com `issues: [{ path, message }]`; uma montagem
em um ambiente de execução sem volumes responde `400 app_volumes_unsupported`.

```ts
await apps.deployments.create(app.app_id, {
  artifact_id: registered.artifact.artifact_id,
  source: { kind: 'oci_image', image, command: ['node', 'server.js'] },
  config: {
    processes: { app: '', worker: 'node worker.js' },
    http_service: { internal_port: 3000, access: 'app', processes: ['app'] },
    mounts: [{ source: 'data', destination: '/data', initial_size_gb: 10, processes: ['app'] }],
    deploy: { release_command: 'node migrate.js' },
  },
});
```

<a id="read-runtime-logs"></a>

## Leia os logs de execução

```ts
let cursor = 0;
for (;;) {
  const page = await apps.deployments.logs(app.app_id, deployment.deployment_id, {
    after: cursor,
    limit: 200,
  });
  for (const entry of page.entries) console.log(entry.source, entry.line);
  cursor = page.next_cursor;
  if (page.entries.length === 0) break;
}
```

Cada entrada contém `cursor`, `time`, `source` (`app`, `appd`, `caddy`) e
`line`. Uma linha `caddy` é o log de acesso: método, URI, status, duração e
cabeçalhos de resposta. Ela nunca contém cabeçalhos de requisição, pois eles contêm as
credenciais do gateway (o token e a identidade do visitante, o token de entrada do provedor).

<a id="machines-scale-and-volumes"></a>

## Máquinas, escala e volumes

```ts
const apps = beeuni.project(projectId).apps;

const machines = await apps.machines.list(app.app_id);
await apps.scale(app.app_id, { worker: 3 });                     // 202: applied in the background

const { exit_code, stdout } = await apps.exec(app.app_id, { command: ['ls', '/data'] });

const [data] = await apps.volumes.list(app.app_id);
const snapshot = await apps.volumes.snapshot(app.app_id, data.volume_id);
await apps.volumes.restore(app.app_id, data.volume_id, snapshot.snapshot_id);   // project.app.admin
const copy = await apps.volumes.fork(app.app_id, data.volume_id, { name: 'data_copy' });
```

Uma máquina mantém seu `machine_id` entre versões; cada versão substitui sua
box (`instance_id`). Uma máquina monta um volume por vez. Uma restauração é uma
nova versão do volume: o conteúdo substituído permanece no histórico dele. Um
snapshot de um volume montado pode não incluir o último ~1 s de gravações.

<a id="a-convex-backend"></a>

## Um backend Convex

Um backend Convex é um App de servidor comum com um volume, criado a partir do
modelo `convex` (`beeuni apps launch --template convex --name db`; veja
[Modelo `convex`](/docs/pt/feature-flags/apps#template-convex-a-convex-backend)).
O SDK não tem nenhuma chamada específica para Convex. Leia a chave de administrador da máquina com
`exec` e dê a um App de interface acesso ao backend com `uses`:

```ts
const db = (await apps.list()).find((a) => a.slug === 'db')!;
const { stdout } = await apps.exec(db.app_id, { command: ['sh', '-c', 'cd /convex && ./generate_admin_key.sh'] });
// The admin key controls all code and data: never log it or ship it to a browser.

await apps.update(site.app_id, { uses: ['db'] }); // the site may mint db tokens and bind to it
```

`db.services` contém as URLs do backend: `http` (a URL do cliente Convex) e
`site` (ações HTTP).

<a id="your-app-already-knows-who-is-looking"></a>

## Seu App já sabe quem está acessando

Um App hospedado no Beeuni é aberto por alguém que **já fez login** no Beeuni. O
gateway de Apps autentica essa pessoa antes de servir o primeiro byte, então seu App não precisa
de um login próprio — sem segunda senha, sem tela de consentimento, sem redirecionamento.

No navegador:

```ts
import { createBeeuni, beeuniAppViewerToken } from '@beeuni/sdk';
import { useBeeuniAppViewer } from '@beeuni/sdk/react';

const beeuni = createBeeuni({
  backendUrl: '/_beeuni/api/v1',             // the gate, on your App's own origin
  getToken: beeuniAppViewerToken(),          // the viewer's own App-scoped token
});

function Header() {
  const { status, viewer } = useBeeuniAppViewer();
  return <span>{status === 'viewer' ? viewer.email : 'Signed out'}</span>;
}
```

O navegador chama `/_beeuni/api/v1/*` na própria origem do seu App, nunca
`https://api.beeuni.ai/v1` diretamente: a API não responde a preflight CORS de
uma origem de App, então uma chamada direta falha no navegador. O gateway encaminha cada
chamada à API do Beeuni como o visitante autenticado, com o token do visitante restrito ao
App. Regras:

- Ela precisa de `viewer_token_scope: 'api'`. Caso contrário, responde
  `403 viewer_api_disabled`.
- Ela atua apenas para um visitante autenticado pelo gateway (login do Beeuni ou um link
  de acesso). Sem um, responde `401 no_viewer_identity`.
- Ela responde apenas a requisições da própria origem do App. Uma requisição de outro site
  recebe `403 cross_site_request`.
- Ela encaminha `/v1/*`, exceto `/v1/oauth/*` e endpoints MCP, que respondem
  `404`. O cookie do seu App e seu próprio cabeçalho `Authorization` nunca chegam à
  API, e a API não pode definir um cookie no seu App.

No servidor do seu App, o gateway inclui a identidade assinada em cada requisição:

```ts
import { readAppViewer, createAppViewerBeeuni } from '@beeuni/sdk/server';

const viewer = await readAppViewer(request);
// { userId, email, name, picture, groupIds, groups, role, projectId, accountId, appId, accessMode, token }
if (!viewer) return new Response('Not found', { status: 404 });

// and, for an `api`-scoped App, act as them:
const beeuni = await createAppViewerBeeuni(request, { backendUrl });
await beeuni.projects.list();               // their projects, their role
```

`readAppViewer` verifica um HMAC do cabeçalho com `BEEUNI_APP_VIEWER_SECRET`,
que o Beeuni injeta no seu App na implantação. Um cabeçalho falsificado nunca passa: o
gateway exclui qualquer cópia enviada pelo cliente antes de encaminhar, e a assinatura é
feita com um segredo derivado para cada App.

<a id="how-much-your-app-is-told"></a>

### Quanto seu App recebe de informação

Uma configuração na política de acesso do App — **Settings → Access** no Beeuni,
`beeuni apps access <app> --viewer off|identity|api` ou `viewer_token_scope`
em `PATCH /projects/:id/apps/:appId/access`:

| Escopo | Seu App recebe |
|---|---|
| `identity` (padrão) | O id, email, nome, foto, função na conta e grupos do Beeuni do visitante (ids e nomes, apenas na conta deste App), além de um token `profile email`. O suficiente para mostrar a cada pessoa seus próprios dados e controlar o acesso por grupo. |
| `api` | Tudo acima e um token que atua **como** essa pessoa na API do Beeuni — limitado pela função dela. |
| `off` | Nada. |

O token nunca é a sessão do usuário no Beeuni: ele dura uma hora, contém apenas
esses escopos, e todo token emitido por um App deixa de valer quando o App é excluído ou sua
política de acesso muda.

<a id="using-the-viewer-token"></a>

### Como usar o token do visitante

- **No seu servidor, leia-o a cada requisição.** O gateway coloca um token válido em
  `x-beeuni-app-viewer-token` em cada requisição encaminhada pelo proxy. Envie-o como
  `Authorization: Bearer` para a API do Beeuni — `createAppViewerBeeuni(request)`
  faz exatamente isso. Crie um cliente por requisição; não armazene o token.
- **Após uma mudança na política de acesso, os tokens antigos param de funcionar.** A próxima requisição
  pelo gateway contém um novo token. No navegador, `beeuniAppViewerToken()`
  se recupera sozinho: quando a API recusa seu token, o SDK relê
  `/_beeuni/viewer` e repete a chamada uma vez.
- **A função do visitante é o limite.** O token pode fazer exatamente o que o
  visitante pode fazer no Beeuni, nunca mais. Em um projeto com permissões de agente, um
  `member` precisa de uma concessão de acesso ao agente que seu App inicia: sem ela,
  `POST /projects/:id/sessions` responde `403 no_agent_access`. Conceda aos seus
  visitantes acesso ao agente, não uma função mais ampla.
- **Quem tem um visitante.** Uma pessoa que abre o App com seu login do Beeuni — ou
  com um link de acesso, que a autentica como essa pessoa — tem um. Um
  App `password` não tem nenhum: a senha comprova um segredo compartilhado, não uma pessoa. Um
  App `public` tem um apenas para um visitante que chegou por um link de acesso;
  a URL direta continua anônima.
- **É uma credencial.** O Beeuni omite os cabeçalhos de requisição do log de acesso
  do App (`beeuni apps logs`). Não registre o cabeçalho no seu próprio código.

Um App servido em seu **próprio domínio** (não `*.apps.beeuni.ai`) não tem um gateway
à frente — use [Entrar com Beeuni](/docs/pt/sdk/sign-in) nesse caso.

<a id="one-member-on-every-runtime"></a>

## Um membro, em todos os ambientes de execução

O Beeuni atesta a mesma pessoa em três lugares: o navegador consulta o gateway
(`/_beeuni/viewer`), o servidor do App lê o cabeçalho assinado, e um App que
verifica tokens de login (um backend Convex ou seu próprio servidor) recebe um
token assinado pelo Beeuni. `readBeeuniMember` transforma cada um desses formatos em um
`BeeuniMember`, e `requireBeeuniMember` decide quem pode prosseguir. Sem
framework e sem dependência: o mesmo código roda em um navegador, Node, Bun, um
Worker e uma função Convex.

```ts
import {
  readBeeuniMember, requireBeeuniMember, verifyBeeuniToken,
  beeuniToken, beeuniBinding, fetchBeeuniAppViewer, BeeuniMemberError,
} from '@beeuni/sdk';

// Browser: who is looking (null when nobody).
const me = readBeeuniMember(await fetchBeeuniAppViewer());

// Browser → an App this App uses, on the same origin: its URL and a token fetcher.
const db = beeuniBinding('db');
const convex = new ConvexReactClient(db.url);
convex.setAuth(db.token);
// React: read the member with useQuery(api.members.me), never useConvexAuth() (see below).

// Browser: a token for this App itself, or for an App it uses.
const token = await beeuniToken()();
const dbToken = beeuniToken({ audience: 'db' });

// A Convex function: the runtime verified the token; enforce the rules.
const member = requireBeeuniMember(await ctx.auth.getUserIdentity(), { groups: ['Finance'] });

// A server App: verify the bearer. No options = the BEEUNI_AUTH_* env Beeuni injects.
// Any other server: pass the App's `auth` values.
const caller = await verifyBeeuniToken(bearer, { issuer: app.auth.issuer, audience: app.auth.audience, jwks: app.auth.jwks_uri });
```

No React, não chame `useConvexAuth()`: ele lança uma exceção sob um
`ConvexProvider` simples (precisa de `ConvexProviderWithAuth`, e `beeuniBinding` não é
verificado com esse provedor). Leia o membro autenticado com uma consulta:

```ts
// convex/members.ts
export const me = query({ args: {}, handler: async (ctx) => readBeeuniMember(await ctx.auth.getUserIdentity()) });
```

```tsx
const me = useQuery(api.members.me);   // undefined: loading. null: signed out. Otherwise the member.
```

`readBeeuniMember` retorna `null` para um chamador anônimo. Use
`requireBeeuniMember` apenas onde a função deve lançar uma exceção.

| Campo | Valor |
|---|---|
| `userId` | Id de usuário do Beeuni. Estável; use-o como chave das linhas. |
| `email`, `name`, `picture` | Do perfil do membro. `null` quando desconhecido. |
| `groups`, `groupIds` | Os grupos do Beeuni do membro nesta conta, por nome e por id. |
| `role` | Função na conta: `owner`, `admin` ou `member`. |
| `accountId`, `projectId` | Onde o App reside. |

`requireBeeuniMember(claims, { groups, roles })` retorna o membro ou lança
`BeeuniMemberError` com `code` `unauthenticated` (ninguém, ou um token que
falhou em uma verificação) ou `forbidden` (fora dos grupos ou funções). Cada lista significa
"qualquer um destes"; um grupo corresponde por nome ou id; uma lista vazia não admite ninguém.

<a id="sign-in-tokens"></a>

### Tokens de login

Um emissor por projeto assina os tokens de todos os Apps: `iss` =
`<API origin>/v1/projects/<projectId>`, `aud` = o id do App ao qual o token se
destina, 15 minutos. Seu conjunto de chaves é público em `<issuer>/jwks.json`, identificado por
`<issuer>/.well-known/openid-configuration`. Cada App contém os valores que
verificam seus tokens: `app.auth.issuer`, `app.auth.audience`, `app.auth.jwks_uri`.
Um token contém `sub`, `email`, `name`, `picture`, `groups`, `group_ids`,
`role`, `account_id` e `project_id`; o token de um agente contém
`kind: 'agent'` e nenhum grupo ou função.

| Função | O que faz |
|---|---|
| `beeuniToken({ audience? })` | Um buscador de tokens (o formato que os clientes em tempo real usam como callback de autenticação) para o próprio App (padrão) ou um App que ele usa. Lê `/_beeuni/token` na origem do próprio App. Armazenado em cache até pouco antes da expiração; `{ forceRefreshToken: true }` solicita novamente. Retorna `null` sem um usuário conectado, e `null` mais um aviso no console para um App que ele não usa (`403 app_not_linked`). Nunca lança exceções. |
| `beeuniBinding(slug, { origin? })` | `{ url, token }` para um App que este App usa: `url` = `<origin>/_beeuni/apps/<slug>` (HTTP e WebSocket, mesma origem, sem CORS), `token` = `beeuniToken({ audience: slug })`. |
| `verifyBeeuniToken(bearer, { issuer?, audience?, jwks? })` | Verifica a assinatura ES256, a expiração (tolerância de 60 s), o emissor e o público-alvo. Padrões: `BEEUNI_AUTH_ISSUER`, `BEEUNI_AUTH_AUDIENCE`, `BEEUNI_AUTH_JWKS`. Beeuni injeta os três no ambiente de execução de cada App de servidor (emissor: o projeto; público-alvo: o id do próprio App; conjunto de chaves: um URI `data:`), portanto `verifyBeeuniToken(bearer)` funciona ali sem opções. Nunca defina `BEEUNI_AUTH_*` no `env` de um App de servidor: o prefixo `BEEUNI_` é reservado e `beeuni validate` o rejeita. O conjunto de chaves é um objeto, texto JSON, um URI `data:` ou uma URL https (`auth.jwks_uri`, buscada uma vez por processo). Toda falha lança `BeeuniMemberError`, inclusive a ausência de um conjunto de chaves ou de um público-alvo: uma única chave do projeto assina os tokens de todos os Apps, então `aud` é o que impede a entrada do token de outro App. `audience: false` aceita um token para qualquer App do projeto. |
| `apps.token(appId)` | Um token que identifica o chamador, obtido da API (`POST …/apps/:id/token`). Uma sessão de agente recebe um que identifica seu agente. |

Um membro que você remove do projeto ou de um grupo perde o acesso em até um período
de validade do token; o controle de acesso do App o rejeita no próximo carregamento de página. Os grupos do Beeuni exigem
o plano Enterprise; as funções de acesso funcionam em todos os planos.

<a id="manage-access"></a>

## Gerenciar acesso

```ts
await apps.access.update(app.app_id, {
  mode: 'restricted',
  member_ids: [memberId],
  group_ids: [groupId],
});

const preview = await apps.access.session(app.app_id);
window.open(preview.url); // valid for five minutes
```

`AppAccessConfig` informa `password_configured`, nunca a senha nem seu
hash. Defina uma senha com `{ mode: 'password', password }`. Cada atualização
incrementa `revision`, o que revoga os cookies existentes do App.

<a id="types"></a>

## Tipos

Todos os tipos abaixo são exportados de `@beeuni/sdk`.

| Tipo | O que contém |
|---|---|
| `App` | Identidade, `capabilities`, `labels`, `scale`, `services` (`AppServiceSummary[]`), `machines` (`total`, `running`, `stopped`), `volumes` (`count`, `size_gb`), `config` (o `AppReleaseConfig` da versão em produção, ou `null`), `uses`, `used_by`, `auth`, `url`, `access_mode`, `access_revision`, `desired_state`, `active_deployment_id`, `hosting_type` (da versão ativa: `static`, `sandbox` ou `null` antes da primeira implantação), `machine` (o tamanho do grupo de serviço na versão em produção), `idle_timeout_seconds`, `always_on`, `retained_deployments`, `warnings` (somente criação e atualização; sempre `[]`), `last_request_at`, `viewer_can_access` |
| `AppCapability` | `'deployments' \| 'rollback' \| 'preview' \| 'sleep' \| 'static' \| 'member_tokens' \| 'machines' \| 'scale' \| 'exec' \| 'volumes' \| 'snapshots'` |
| `AppReleaseConfig` · `AppReleaseConfigInput` | A configuração resolvida da versão (`processes`, `services`, `mounts`, `vm`, `deploy`) · a forma escrita (`processes`, `http_service`, `services`, `mounts`, `vm`, `deploy` e as formas abreviadas `port`, `command`, `readiness_path`, `always_on`, `resources`) |
| `AppServiceConfig` · `AppServiceAccess` · `AppAutoStop` · `AppCheck` | `name`, `internal_port`, `protocol`, `access`, `auto_stop_machines`, `auto_start_machines`, `min_machines_running`, `processes`, `checks` · `'public' \| 'app' \| 'admins'` · `'off' \| 'stop' \| 'suspend'` · `type`, `port`, `path`, `interval_s`, `timeout_s`, `grace_period_s` |
| `AppMountConfig` · `AppVmConfig` · `AppDeployStrategy` | `source`, `destination`, `initial_size_gb`, `snapshot_retention_days`, `processes` · `cpu`, `memory_gb`, `disk_gb`, `processes` · `'rolling' \| 'immediate' \| 'bluegreen' \| 'canary'` |
| `AppServiceSummary` | `name`, `protocol`, `access`, `internal_port`, `processes`, `url` (`null` quando o rótulo do host seria longo demais) |
| `AppMachine` · `AppMachineState` | `machine_id`, `instance_id`, `process_group`, `state`, `release` (`deployment_id`, `version`), `vm`, `volume`, `created_at`, `started_at`, `stopped_at` · `'started' \| 'starting' \| 'stopped' \| 'stopping' \| 'provisioning'` |
| `AppVolume` · `AppVolumeSnapshot` · `CreateAppVolumeInput` | `volume_id`, `name`, `process_group`, `machine_id`, `size_gb`, `status`, `attached`, `snapshot_retention_days`, `created_at` · `snapshot_id`, `kind` (`daily`, `manual`, `final`), `commit_id`, `created_at` · `name`, `size_gb`, `process_group`, `snapshot_retention_days` |
| `AppExecInput` · `AppExecResult` | `command`, `machine_id`, `timeout_s` · `machine_id`, `exit_code`, `stdout`, `stderr`, `truncated` |
| `AppAuth` | `issuer`, `audience` (o id do App), `jwks_uri`: os valores públicos que verificam os tokens de login do App |
| `AppToken` | `token`, `expires_at` |
| `BeeuniTokenOptions` · `BeeuniTokenFetcher` · `BeeuniBinding` · `VerifyBeeuniTokenOptions` | Os auxiliares de tokens de login acima |
| `AppDeployment` | `version`, `status`, `source_kind`, `hosting_type` (`static`: servido a partir do armazenamento, sem ambiente de execução; `sandbox`: máquinas), `hosting_provider`, `runtime_spec`, `build_spec`, `error_code`, `attempt_count`, `created_by`, `actor_type`, `source_session_id`, `config` (a configuração resolvida da versão), `strategy`, `release_command_status` (`running`, `succeeded`, `failed` ou `null`) |
| `AppRollback` | O valor com que `apps.rollback` é resolvido: o `App` mais a versão na fila em `deployment` |
| `AppDeploymentDetail` | Um `deployment` mais seu `events`: eventos do ciclo de vida e o log de build (linhas `build_log` e um evento `log_truncated` quando o build imprimiu mais de 5.000 linhas) |
| `AppAccessConfig` | `mode`, `revision`, `member_ids`, `group_ids`, `password_configured` |
| `AppAccessMode` | `'private' \| 'project' \| 'restricted' \| 'public' \| 'password'` |
| `AppSource` | `StaticAppSource \| BundleAppSource \| DockerfileAppSource \| OciImageAppSource` |
| `AppArtifact` | `kind`, `status`, `sha256`, `size_bytes`, `image_reference` |
| `AppLogEntry` · `AppLogsResponse` | Uma linha de log e uma página mais `next_cursor` |

`apps.list()` e `useProjectApps` retornam apenas os Apps que o chamador pode abrir
conforme a política de acesso de cada App (privado: o criador; restrito: os membros
e grupos listados; projeto, senha e público: a equipe do projeto). Um gerente de projeto
vê todos os Apps, então um App privado continua gerenciável quando seu criador
sai. Portanto, `viewer_can_access` é `true` em todos os Apps listados. Um `get` de
um App que o chamador não pode abrir responde `404`, não `403`. Trate `undefined` como
desconhecido, não como negado.

`AppAccessMode` é uma configuração de visibilidade por recurso sobre o modelo de funções de acesso,
não uma função de acesso. `restricted` identifica usuários e grupos — os mesmos tipos de entidades que o
modelo de funções de acesso usa. Consulte
[Contas e acesso](/docs/pt/accounts#per-feature-access-settings).

`DockerfileAppSource` e `OciImageAppSource` exigem `command`. `port` é a
porta do serviço principal: omita-a apenas para uma versão somente com workers (um `config`
com `processes` e sem serviço). `StaticAppSource` e `BundleAppSource` não recebem
nenhum dos dois.

<a id="react-hooks"></a>

## Hooks do React

`@beeuni/sdk/react` exporta sete hooks para Apps.

### useProjectApps(projectId)

Os Apps que o chamador pode abrir, mais suas mutações de ciclo de vida. Cada mutação
invalida o inventário em caso de sucesso.

```tsx
import { useProjectApps } from '@beeuni/sdk/react';

function AppList({ projectId }: { projectId: string }) {
  const apps = useProjectApps(projectId);
  if (!apps.data) return null;

  return (
    <ul>
      {apps.data.map((app) => (
        <li key={app.app_id}>
          <a href={app.url}>{app.slug}</a>
          <button onClick={() => apps.stop.mutate(app.app_id)}>Stop</button>
        </li>
      ))}
    </ul>
  );
}
```

Retorna os campos da consulta mais `create`, `update`, `start`, `stop` e
`remove`.

### useAppDeployments(projectId, appId)

O histórico imutável de implantações, buscado novamente a cada 5 s para que um build em execução
avance por conta própria.

```tsx
const deployments = useAppDeployments(projectId, appId);

await deployments.deploy.mutateAsync({
  artifact_id: artifact.artifact_id,
  source: { kind: 'static', spa: true },
});
await deployments.rollback.mutateAsync(previousDeploymentId);
```

Ambas as mutações invalidam a lista de implantações e o inventário de Apps.

### useAppDeployment(projectId, appId, deploymentId)

Uma implantação e seus eventos, incluindo o log de build (`AppDeploymentDetail`). Faz
consultas a cada 2 s até que a implantação esteja `ready`, `failed` ou `cancelled`. Passe
`null` como `deploymentId` para mantê-lo inativo.

```tsx
const detail = useAppDeployment(projectId, appId, deploymentId);
const log = (detail.data?.events ?? [])
  .map((event) => (event.type === 'build_log' ? event.message : `[${event.type}] ${event.message}`))
  .join('\n');
```

Um build mantém suas primeiras 2.500 e suas últimas 2.500 linhas. As linhas são gravadas em
lotes, no máximo 1 s após serem impressas. Uma implantação com falha ou cancelada
mantém seu log de build por 14 dias.

### useAppAccess(projectId, appId, options?)

A política de acesso e uma sessão de acesso de curta duração. As duas partes são consultas
separadas, e cada uma é opcional.

```tsx
const access = useAppAccess(projectId, appId, {
  policy: canEditAccess,
  session: app.viewer_can_access,
});

access.policy.data;  // AppAccessConfig
access.session.data; // { url, expires_at }
await access.update.mutateAsync({ mode: 'project' });
```

| Opção | Padrão | Use `false` quando |
|---|---|---|
| `policy` | `true` | A interface apenas exibe uma prévia do App. `GET …/access` é uma leitura administrativa e retorna `403` para um chamador sem permissões de gerente de projeto. |
| `session` | `true` | O chamador não pode abrir o App. Passe `app.viewer_can_access` (sempre `true` para um App listado). |

Uma grade de Apps que deixa ambas as opções em `true` dispara uma leitura de política e uma
emissão de sessão por App, e cada uma resulta em `403` para um membro que não pode abrir esse
App.

### useAppMachines(projectId, appId)

As máquinas de um App, consultadas a cada 5 s, mais `start`, `stop`, `restart`,
`destroy` (cada uma recebe um `machineId`) e `scale` (recebe
`{ [group]: count }`). Cada mutação atualiza as máquinas e o inventário de
Apps. `appMachinesKey(projectId, appId)` é a chave da consulta.

```tsx
const machines = useAppMachines(projectId, app.app_id);
await machines.scale.mutateAsync({ worker: 2 });
await machines.restart.mutateAsync(machineId);
```

### useAppVolumes(projectId, appId)

Os volumes de um App, mais `snapshot` (`volumeId`), `restore`
(`{ volumeId, snapshotId }`), `fork` (`{ volumeId, name, snapshotId? }`),
`extend` (`{ volumeId, sizeGb }`) e `destroy` (`{ volumeId, name }`). Cada
mutação atualiza os volumes. `appVolumesKey(projectId, appId)` é a chave da
consulta.

```tsx
const volumes = useAppVolumes(projectId, app.app_id);
await volumes.snapshot.mutateAsync(volumeId);
await volumes.restore.mutateAsync({ volumeId, snapshotId });
```

### useAppVolumeSnapshots(projectId, appId, volumeId)

Os snapshots de um volume. Passe `null` como `volumeId` para manter a consulta inativa.

<a id="errors"></a>

## Erros

```ts
import { featureDisabledKey, isFeatureDisabledError } from '@beeuni/sdk';

try {
  await beeuni.project(projectId).apps.list();
} catch (error) {
  if (isFeatureDisabledError(error)) {
    console.log(`${featureDisabledKey(error)} is off for this project`);
  }
}
```

Outras respostas que você deve tratar:

- `409` para um slug duplicado, `402` com `app_quota_exceeded` quando a conta
  está no limite de Apps, e `400` com `app_machine_out_of_range` para uma especificação fora dos limites.
- `400 app_not_found` (`slugs`) para um slug desconhecido em `uses`.
- `409 app_capability_unsupported` (`capability`) para uma chamada que o App não
  oferece.
- Uma implantação: `400 app_config_invalid` (`issues`), `400 app_volumes_unsupported`.
  Uma reversão: `409 release_not_ready`.
- Máquinas e exec: `409 app_no_machines`, `404 machine_not_found`,
  `409 machine_not_running`, `400 process_group_unknown`,
  `400 scale_out_of_range`, `400 command_required`.
- Volumes: `400 volume_name_invalid`, `volume_size_invalid`,
  `volume_shrink_refused`, `volume_retention_invalid`, `snapshot_invalid`,
  `confirmation_required`; `404 volume_not_found`; `409 volume_attached`.

`token()` responde com `Cache-Control: no-store`.
