Apps
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.
const apps = beeuni.project(projectId).apps;
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 |
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.
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).
create e update retornam warnings, que é sempre [].
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:
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.
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:
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.
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' },
},
});
Leia os logs de execução
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).
Máquinas, escala e volumes
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.
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).
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:
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).
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:
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, responde403 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 respondem404. O cookie do seu App e seu próprio cabeçalhoAuthorizationnunca 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:
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.
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.
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-tokenem cada requisição encaminhada pelo proxy. Envie-o comoAuthorization: Bearerpara 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/viewere 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
memberprecisa de uma concessão de acesso ao agente que seu App inicia: sem ela,POST /projects/:id/sessionsresponde403 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
passwordnão tem nenhum: a senha comprova um segredo compartilhado, não uma pessoa. Um Apppublictem 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 nesse caso.
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.
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:
// convex/members.ts
export const me = query({ args: {}, handler: async (ctx) => readBeeuniMember(await ctx.auth.getUserIdentity()) });
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.
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.
Gerenciar acesso
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Erros
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:
409para um slug duplicado,402comapp_quota_exceededquando a conta está no limite de Apps, e400comapp_machine_out_of_rangepara uma especificação fora dos limites.400 app_not_found(slugs) para um slug desconhecido emuses.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.