Aplicaciones
Crea, despliega y controla Beeuni Apps desde el SDK y desde React.
Las Apps son despliegues de ámbito de proyecto, cada uno con una URL estable,
versiones inmutables, máquinas por grupo de procesos y volúmenes persistentes. Bifurca según app.capabilities. Esta página cubre la interfaz del SDK: la
fachada apps en un identificador de proyecto, las llamadas de artefactos, versiones, máquinas y volúmenes,
los tokens de inicio de sesión, los tipos exportados y los hooks de React.
Para conocer el modelo, la configuración de versiones, los tipos de origen, la CLI, la URL estable y el comportamiento de activación en frío, consulta Apps.
const apps = beeuni.project(projectId).apps;
La fachada apps
| Método | Encapsula | Qué hace |
|---|---|---|
apps.list() |
GET /projects/:pid/apps |
Enumera las Apps que el solicitante puede abrir (un administrador del proyecto ve todas) |
apps.create(input) |
POST …/apps |
Crea una App y le asigna su URL estable |
apps.get(appId) |
GET …/apps/:id |
Lee una App |
apps.update(appId, input) |
PATCH …/apps/:id |
La renombra o cambia la máquina, el tiempo de inactividad, el modo de ejecución (always_on), uses o labels. El modo de ejecución se aplica en un plazo de 5 minutos; un cambio de máquina se aplica a la siguiente versión |
apps.remove(appId) |
DELETE …/apps/:id |
Elimina la App, sus máquinas y todas las imágenes de versiones; devuelve images: {released, pending}. Sus volúmenes conservan una instantánea final durante 7 días |
apps.start(appId) |
POST …/apps/:id/start |
Establece desired_state en running y prepara el entorno de ejecución. Una App estática responde 409 static_app_no_runtime |
apps.stop(appId) |
POST …/apps/:id/stop |
Suspende el cómputo de inmediato; la siguiente solicitud lo reanuda. Una App estática responde 409 static_app_no_runtime: no tiene entorno de ejecución y sirve contenido mientras tenga un despliegue activo |
apps.rollback(appId, deploymentId) |
POST …/apps/:id/rollback |
Crea una versión NUEVA a partir del artefacto y la configuración de una versión lista y la publica como un despliegue (202). Los volúmenes conservan sus datos |
Los artefactos son la entrada inmutable de un despliegue:
| Método | Encapsula | Qué hace |
|---|---|---|
apps.artifacts.register(input) |
POST …/apps/artifacts |
Registra un archive o un oci_image; devuelve la URL de carga para un archivo |
apps.artifacts.uploadArchive(bytes, options?) |
— | Registra, carga, calcula el hash y finaliza un .tar.gz en una sola llamada |
apps.artifacts.finalize(artifactId, input) |
POST …/apps/artifacts/:id/finalize |
Confirma sha256 y size_bytes para una carga manual |
Las versiones (despliegues) son inmutables y están numeradas:
| Método | Encapsula | Qué hace |
|---|---|---|
apps.deployments.create(appId, input) |
POST …/apps/:id/deployments |
Inicia una versión a partir de un artefacto, un origen y un config de versión opcional |
apps.deployments.list(appId) |
GET …/apps/:id/deployments |
Enumera el historial de despliegues |
apps.deployments.get(appId, deploymentId) |
GET …/deployments/:did |
Lee un despliegue junto con sus eventos |
apps.deployments.logs(appId, deploymentId, options?) |
GET …/deployments/:did/logs |
Lee los registros de ejecución con un cursor |
apps.deployments.remove(appId, deploymentId) |
DELETE …/deployments/:did |
Elimina un despliegue y su imagen; 409 para el despliegue activo o una compilación en curso. Devuelve image: 'released' | 'pending' | 'none' |
Máquinas, escalado y exec (una App de servidor; una App sin máquinas responde
409 app_no_machines):
| Método | Encapsula | Qué hace |
|---|---|---|
apps.machines.list(appId) |
GET …/apps/:id/machines |
Todas las máquinas: machine_id, process_group, state, release, vm, volume |
apps.machines.start/stop/restart(appId, machineId) |
POST …/machines/:mid/{start,stop,restart} |
Actúa sobre una máquina; se resuelve con la lista de máquinas |
apps.machines.destroy(appId, machineId) |
DELETE …/machines/:mid |
Destruye una máquina. Su volumen se conserva; su grupo ejecuta una máquina menos |
apps.scale(appId, count) |
PUT …/apps/:id/scale |
Máquinas por grupo de procesos ({ worker: 3 }, 0–20), aplicado en segundo plano (202). Se resuelve con scale y machines |
apps.exec(appId, { command, machine_id?, timeout_s? }) |
POST …/apps/:id/exec |
Ejecuta un comando en una máquina en ejecución (project.app.admin; auditado). timeout_s 1–300, valor predeterminado 60. Se resuelve con exit_code, stdout, stderr (1 MiB cada uno), truncated |
Volúmenes (discos persistentes):
| Método | Encapsula | Qué hace |
|---|---|---|
apps.volumes.list(appId) |
GET …/apps/:id/volumes |
Todos los volúmenes: 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 |
Crea un volumen de antemano; una máquina posterior de su grupo con un montaje de ese nombre lo toma |
apps.volumes.update(appId, volumeId, { size_gb?, snapshot_retention_days? }) |
PATCH …/volumes/:vid |
Amplía un volumen (nunca lo reduce; máximo 100 GB) o cambia su retención (1–60 días) |
apps.volumes.destroy(appId, volumeId, confirmName) |
DELETE …/volumes/:vid?confirm=<name> |
project.app.admin. Se rechaza con 409 volume_attached mientras una máquina lo tenga montado. Conserva una instantánea final durante 7 días |
apps.volumes.snapshots(appId, volumeId) |
GET …/volumes/:vid/snapshots |
Instantáneas daily, manual y final |
apps.volumes.snapshot(appId, volumeId) |
POST …/volumes/:vid/snapshots |
Una instantánea manual ahora |
apps.volumes.restore(appId, volumeId, snapshotId) |
POST …/volumes/:vid/restore |
project.app.admin. La máquina se detiene, el volumen vuelve a la instantánea y la máquina arranca con él (202) |
apps.volumes.fork(appId, volumeId, { name, snapshot_id? }) |
POST …/volumes/:vid/fork |
Un volumen nuevo, sin conectar, en la misma App, a partir de una instantánea o del contenido actual |
Tokens de inicio de sesión (capacidad member_tokens; 409 app_capability_unsupported
cuando la App carece de ella):
| Método | Encapsula | Qué hace |
|---|---|---|
apps.token(appId) |
POST …/apps/:id/token |
Un token de inicio de sesión de 15 minutos para la App, que identifica al solicitante |
El acceso es la política de autorización propia de la App:
| Método | Encapsula | Qué hace |
|---|---|---|
apps.access.get(appId) |
GET …/apps/:id/access |
Lee la política. Requiere project.app.read |
apps.access.update(appId, input) |
PATCH …/apps/:id/access |
Sustituye la política e incrementa su revisión |
apps.access.session(appId) |
POST …/apps/:id/access-session |
Genera una URL de cinco minutos que se canjea por una cookie exclusiva del host |
Desplegar un sitio estático
uploadArchive realiza todo el protocolo del artefacto: registra el artefacto,
lo verifica frente a max_bytes, envía los bytes mediante PUT, calcula el SHA-256 y
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 también acepta los campos de máquina: cpu, memory_gb,
disk_gb, idle_timeout_seconds y always_on. Omítelos para usar los valores predeterminados.
Las Apps no tienen presupuesto mensual: los créditos de la cuenta pagan el cómputo. Una App de servidor
siempre activa se factura las 24 horas del día; una bajo demanda, solo mientras está activa.
always_on: true ejecuta una App de servidor las 24 horas del día
(websockets, bucles en segundo plano); false la detiene después de idle_timeout_seconds
y la activa con la siguiente solicitud. Una App estática no tiene entorno de ejecución: always_on es
false.
La API ignora monthly_budget_usd si un cliente antiguo lo envía (200, sin
error). Ninguna respuesta de App lo incluye.
uses enumera, por slug, las Apps
para las que esta App puede obtener tokens de inicio de sesión y a las que puede acceder mediante su montaje de enlaces; el
valor predeterminado es ninguna, y update(appId, { uses }) sustituye la lista (consulta
Apps que usa una App).
create y update devuelven warnings, que siempre es [].
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)
Espera al despliegue consultando periódicamente su estado:
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));
}
}
Los valores de estado son queued, validating, building, provisioning,
checking, ready, failed y cancelled.
Desplegar una imagen OCI
Registra la referencia inmutable de la imagen y luego declara el comando del proceso y el puerto de destino público:
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 devuelve upload: null para un oci_image. Solo un archive obtiene una
URL de carga.
CreateAppDeploymentInput también acepta environment (valores de ejecución
no secretos), secrets (clave de ejecución a nombre de secreto del proyecto) y config
(AppReleaseConfigInput, la configuración de la versión). Un config no válido
responde 400 app_config_invalid con issues: [{ path, message }]; un montaje
en un entorno de ejecución sin volúmenes 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' },
},
});
Leer los registros de ejecución
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 contiene cursor, time, source (app, appd, caddy) y
line. Una línea caddy es el registro de acceso: método, URI, estado, duración y
cabeceras de respuesta. Nunca contiene cabeceras de solicitud, porque estas incluyen las
credenciales de la puerta de acceso (el token y la identidad del visitante, el token de entrada del proveedor).
Máquinas, escalado y volúmenes
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' });
Una máquina conserva su machine_id entre versiones; cada versión sustituye su
contenedor (instance_id). Una máquina monta un volumen a la vez. Una restauración es una
nueva versión del volumen: el contenido sustituido permanece en su historial. Una
instantánea de un volumen montado puede omitir el último ~1 s de escrituras.
Un backend de Convex
Un backend de Convex es una App de servidor normal con un volumen, creada a partir de la
plantilla convex (beeuni apps launch --template convex --name db; consulta
Plantilla convex).
El SDK no tiene ninguna llamada específica de Convex. Lee la clave de administrador de la máquina con
exec y da acceso al backend a una App de interfaz con 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 contiene las URL del backend: http (la URL del cliente de Convex) y
site (acciones HTTP).
Tu App ya sabe quién la está viendo
Quien abre una App alojada en Beeuni es alguien que ya ha iniciado sesión en Beeuni. La puerta de acceso de Apps autentica a esa persona antes de que se sirva el primer byte, por lo que tu App no necesita un inicio de sesión propio: ni una segunda contraseña, ni una pantalla de consentimiento, ni una redirección.
En el 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>;
}
El navegador llama a /_beeuni/api/v1/* en el origen de tu propia App, nunca a
https://api.beeuni.ai/v1 directamente: la API no responde a solicitudes de comprobación previa CORS desde
el origen de una App, por lo que una llamada directa falla en el navegador. La puerta de acceso reenvía cada
llamada a la API de Beeuni como el visitante que ha iniciado sesión, con su token
limitado a la App. Reglas:
- Necesita
viewer_token_scope: 'api'. De lo contrario, responde403 viewer_api_disabled. - Actúa solo para un visitante al que la puerta de acceso haya iniciado sesión (mediante el inicio de sesión de Beeuni o un enlace
de acceso). Si no hay ninguno, responde
401 no_viewer_identity. - Solo responde a solicitudes del origen de la propia App. Una solicitud desde otro sitio
recibe
403 cross_site_request. - Reenvía
/v1/*excepto/v1/oauth/*y los endpoints de MCP, que responden404. La cookie de tu App y su propia cabeceraAuthorizationnunca llegan a la API, y la API no puede establecer una cookie en tu App.
En el servidor de tu App, la puerta de acceso incluye la identidad firmada en cada solicitud:
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 un HMAC de la cabecera con BEEUNI_APP_VIEWER_SECRET,
que Beeuni inyecta en tu App al desplegarla. Una cabecera falsificada nunca supera la verificación: la
puerta de acceso elimina cualquier copia proporcionada por el cliente antes de reenviarla, y la firma se
crea con un secreto derivado para cada App.
Cuánta información recibe tu App
Un ajuste de la política de acceso de la App: Settings → Access en Beeuni,
beeuni apps access <app> --viewer off|identity|api o viewer_token_scope
en PATCH /projects/:id/apps/:appId/access:
| Ámbito | Tu App recibe |
|---|---|
identity (predeterminado) |
El id, correo electrónico, nombre, imagen, rol de cuenta y grupos de Beeuni del visitante (ids y nombres, solo en la cuenta de esta App), además de un token profile email. Suficiente para mostrar a cada persona sus propios datos y controlar el acceso por grupo. |
api |
Lo anterior y un token que actúa como esa persona en la API de Beeuni, limitado por su propio rol. |
off |
Nada. |
El token nunca es la sesión de Beeuni del usuario: dura una hora, incluye solo esos ámbitos y todos los tokens emitidos por una App dejan de funcionar cuando se elimina la App o cambia su política de acceso.
Uso del token del visitante
- En tu servidor, léelo en cada solicitud. La puerta de acceso coloca un token válido en
x-beeuni-app-viewer-tokenen cada solicitud enviada a través del proxy. Envíalo comoAuthorization: Bearera la API de Beeuni:createAppViewerBeeuni(request)hace exactamente eso. Crea un cliente por solicitud; no almacenes el token. - Tras un cambio en la política de acceso, los tokens antiguos dejan de funcionar. La siguiente solicitud
a través de la puerta de acceso lleva uno nuevo. En el navegador,
beeuniAppViewerToken()se recupera por sí solo: cuando la API rechaza su token, el SDK vuelve a leer/_beeuni/viewery repite la llamada una vez. - El rol del visitante es el límite. El token puede hacer exactamente lo que el
visitante puede hacer en Beeuni, nunca más. En un proyecto con permisos de agente, un
membernecesita un permiso sobre el agente que inicia tu App: sin él,POST /projects/:id/sessionsresponde403 no_agent_access. Concede a tus visitantes acceso al agente, no un rol más amplio. - Quién tiene un visitante identificado. Una persona que abre la App con su inicio de sesión de Beeuni, o
con un enlace de acceso que inicia sesión como esa persona, lo tiene. Una
App
passwordno lo tiene: la contraseña demuestra un secreto compartido, no una identidad personal. Una Apppublicsolo lo tiene para un visitante que haya llegado mediante un enlace de acceso; la URL sin más sigue siendo anónima. - Es una credencial. Beeuni excluye las cabeceras de solicitud del registro de acceso de la App
(
beeuni apps logs). No registres la cabecera en tu propio código.
Una App servida en su propio dominio (no en *.apps.beeuni.ai) no tiene una puerta de acceso
por delante: usa Iniciar sesión con Beeuni allí en su lugar.
Un miembro, en todos los entornos de ejecución
Beeuni avala a la misma persona en tres lugares: el navegador consulta a la puerta de acceso
(/_beeuni/viewer), el servidor de la App lee la cabecera firmada y una App que
verifica tokens de inicio de sesión (un backend de Convex o tu propio servidor) recibe un
token firmado por Beeuni. readBeeuniMember convierte cada uno de esos formatos en un
BeeuniMember y requireBeeuniMember decide quién puede continuar. Sin
framework ni dependencias: el mismo código se ejecuta en un navegador, Node, Bun, un
Worker y una función de 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 });
En React, no llames a useConvexAuth(): lanza una excepción con un
ConvexProvider simple (necesita ConvexProviderWithAuth y beeuniBinding no se
verifica con ese proveedor). Lee el miembro que ha iniciado sesión mediante una 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 devuelve null para una llamada anónima. Usa
requireBeeuniMember solo cuando la función deba lanzar una excepción.
| Campo | Valor |
|---|---|
userId |
Id de usuario de Beeuni. Estable; úsalo como clave de las filas. |
email, name, picture |
Del perfil del miembro. null cuando se desconoce. |
groups, groupIds |
Los grupos de Beeuni del miembro en esta cuenta, por nombre y por id. |
role |
Rol de cuenta: owner, admin o member. |
accountId, projectId |
Dónde reside la App. |
requireBeeuniMember(claims, { groups, roles }) devuelve el miembro o lanza
BeeuniMemberError con code unauthenticated (nadie, o un token que
no superó una comprobación) o forbidden (fuera de los grupos o roles). Cada lista significa
“cualquiera de estos”; un grupo coincide por nombre o id; una lista vacía no admite a nadie.
Tokens de inicio de sesión
Un emisor por proyecto firma los tokens de todas las Apps: iss =
<API origin>/v1/projects/<projectId>, aud = el id de la App a la que está destinado el token,
15 minutos. Su conjunto de claves es público en <issuer>/jwks.json, identificado por
<issuer>/.well-known/openid-configuration. Cada App incluye los valores que
verifican sus tokens: app.auth.issuer, app.auth.audience, app.auth.jwks_uri.
Un token contiene sub, email, name, picture, groups, group_ids,
role, account_id y project_id; el token de un agente contiene
kind: 'agent' y ningún grupo ni rol.
| Función | Qué hace |
|---|---|
beeuniToken({ audience? }) |
Un proveedor de tokens (con la forma que los clientes en tiempo real aceptan como callback de autenticación) para la propia App (por defecto) o una App que utiliza. Lee /_beeuni/token en el origen de la propia App. Se almacena en caché hasta poco antes de caducar; { forceRefreshToken: true } vuelve a solicitarlo. Devuelve null si no hay un usuario con sesión iniciada, y null más una advertencia en la consola para una App que no utiliza (403 app_not_linked). Nunca lanza excepciones. |
beeuniBinding(slug, { origin? }) |
{ url, token } para una App que esta App utiliza: url = <origin>/_beeuni/apps/<slug> (HTTP y WebSocket, mismo origen, sin CORS), token = beeuniToken({ audience: slug }). |
verifyBeeuniToken(bearer, { issuer?, audience?, jwks? }) |
Comprueba la firma ES256, la caducidad (60 s de tolerancia), el emisor y la audiencia. Valores predeterminados: BEEUNI_AUTH_ISSUER, BEEUNI_AUTH_AUDIENCE, BEEUNI_AUTH_JWKS. Beeuni inyecta los tres en el entorno de ejecución de cada App de servidor (emisor: el proyecto; audiencia: el id de la propia App; conjunto de claves: un URI data:), por lo que verifyBeeuniToken(bearer) funciona allí sin opciones. Nunca establezcas BEEUNI_AUTH_* en env de una App de servidor: el prefijo BEEUNI_ está reservado y beeuni validate lo rechaza. El conjunto de claves es un objeto, texto JSON, un URI data: o una URL https (auth.jwks_uri, obtenida una vez por proceso). Cada fallo lanza BeeuniMemberError, incluidos la ausencia de un conjunto de claves o de una audiencia: una sola clave del proyecto firma los tokens de todas las Apps, por lo que aud es lo que impide aceptar el token de otra App. audience: false acepta un token para cualquier App del proyecto. |
apps.token(appId) |
Un token que identifica al invocador, procedente de la API (POST …/apps/:id/token). Una sesión de agente recibe uno que identifica a su agente. |
Un miembro que elimines del proyecto o de un grupo pierde el acceso en el plazo de una vida útil del token; el control de acceso de la App lo rechaza en la siguiente carga de página. Los grupos de Beeuni requieren el plan Enterprise; los roles funcionan en todos los planes.
Gestionar el acceso
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 de password_configured, nunca de la contraseña ni de su
hash. Establece una contraseña con { mode: 'password', password }. Cada actualización
incrementa revision, lo que revoca las cookies existentes de la App.
Tipos
Todos los tipos siguientes se exportan desde @beeuni/sdk.
| Tipo | Qué contiene |
|---|---|
App |
Identidad, capabilities, labels, scale, services (AppServiceSummary[]), machines (total, running, stopped), volumes (count, size_gb), config (AppReleaseConfig de la versión en producción, o null), uses, used_by, auth, url, access_mode, access_revision, desired_state, active_deployment_id, hosting_type (de la versión activa: static, sandbox o null antes del primer despliegue), machine (el tamaño del grupo que atiende las solicitudes en la versión en producción), idle_timeout_seconds, always_on, retained_deployments, warnings (solo creación y actualización; siempre []), last_request_at, viewer_can_access |
AppCapability |
'deployments' | 'rollback' | 'preview' | 'sleep' | 'static' | 'member_tokens' | 'machines' | 'scale' | 'exec' | 'volumes' | 'snapshots' |
AppReleaseConfig · AppReleaseConfigInput |
La configuración resuelta de la versión (processes, services, mounts, vm, deploy) · la forma redactada (processes, http_service, services, mounts, vm, deploy y las 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 cuando la etiqueta del host sería demasiado larga) |
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 (el id de la App), jwks_uri: los valores públicos que verifican los tokens de inicio de sesión de la App |
AppToken |
token, expires_at |
BeeuniTokenOptions · BeeuniTokenFetcher · BeeuniBinding · VerifyBeeuniTokenOptions |
Las funciones auxiliares para tokens de inicio de sesión descritas arriba |
AppDeployment |
version, status, source_kind, hosting_type (static: servido desde el almacenamiento, sin entorno de ejecución; sandbox: máquinas), hosting_provider, runtime_spec, build_spec, error_code, attempt_count, created_by, actor_type, source_session_id, config (la configuración resuelta de la versión), strategy, release_command_status (running, succeeded, failed o null) |
AppRollback |
El valor con el que se resuelve apps.rollback: App más la versión en cola en deployment |
AppDeploymentDetail |
Un deployment más su events: eventos del ciclo de vida y el registro de compilación (líneas build_log y un evento log_truncated cuando la compilación imprimió más de 5.000 líneas) |
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 |
Una línea de registro y una página más next_cursor |
apps.list() y useProjectApps devuelven solo las Apps que el invocador puede abrir
según la política de acceso de cada App (privada: el creador; restringida: los
miembros y grupos indicados; proyecto, contraseña y pública: el equipo del proyecto). Un responsable del proyecto
ve todas las Apps, por lo que una privada sigue siendo administrable cuando su creador
se marcha. Por tanto, viewer_can_access es true en todas las Apps enumeradas. Un get de
una App que el invocador no puede abrir responde con 404, no con 403. Trata undefined como
desconocido, no como denegado.
AppAccessMode es un ajuste de visibilidad por recurso que se superpone al modelo de roles,
no un rol. restricted identifica usuarios y grupos: los mismos tipos de entidades que
usa el modelo de roles. Consulta
Cuentas y acceso.
DockerfileAppSource y OciImageAppSource requieren command. port es el
puerto del servicio principal: omítelo solo para una versión que tenga únicamente workers (un config
con processes y sin servicio). StaticAppSource y BundleAppSource no aceptan
ninguno de los dos.
Hooks de React
@beeuni/sdk/react exporta siete hooks para Apps.
useProjectApps(projectId)
Las Apps que el invocador puede abrir, más sus mutaciones del ciclo de vida. Cada mutación invalida el inventario cuando se completa correctamente.
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>
);
}
Devuelve los campos de la consulta más create, update, start, stop y
remove.
useAppDeployments(projectId, appId)
El historial inmutable de despliegues, que se vuelve a obtener cada 5 s para que una compilación en curso avance por sí sola.
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 mutaciones invalidan la lista de despliegues y el inventario de Apps.
useAppDeployment(projectId, appId, deploymentId)
Un despliegue y sus eventos, incluido el registro de compilación (AppDeploymentDetail). Realiza
consultas cada 2 s hasta que el despliegue sea ready, failed o cancelled. Pasa
null como deploymentId para mantenerlo inactivo.
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');
Una compilación conserva sus primeras 2.500 líneas y sus últimas 2.500 líneas. Las líneas se escriben en lotes, como máximo 1 s después de imprimirse. Un despliegue fallido o cancelado conserva su registro de compilación durante 14 días.
useAppAccess(projectId, appId, options?)
La política de acceso y una sesión de acceso de corta duración. Ambas partes son consultas independientes y cada una es 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' });
| Opción | Valor predeterminado | Usa false cuando |
|---|---|---|
policy |
true |
La interfaz solo muestra una vista previa de la App. GET …/access es una lectura administrativa y responde 403 a quien realiza la llamada sin permisos de administrador del proyecto. |
session |
true |
Quien realiza la llamada no tiene permiso para abrir la App. Pasa app.viewer_can_access (siempre true para una App incluida en la lista). |
Una cuadrícula de Apps que deja ambas opciones en true ejecuta una lectura de la política y una
creación de sesión por App, y cada una devuelve 403 para un miembro que no tiene permiso para abrir esa
App.
useAppMachines(projectId, appId)
Las máquinas de una App, consultadas cada 5 s, junto con start, stop, restart,
destroy (cada una recibe un machineId) y scale (recibe
{ [group]: count }). Cada mutación actualiza las máquinas y el inventario de
Apps. appMachinesKey(projectId, appId) es la clave de consulta.
const machines = useAppMachines(projectId, app.app_id);
await machines.scale.mutateAsync({ worker: 2 });
await machines.restart.mutateAsync(machineId);
useAppVolumes(projectId, appId)
Los volúmenes de una App, junto con snapshot (volumeId), restore
({ volumeId, snapshotId }), fork ({ volumeId, name, snapshotId? }),
extend ({ volumeId, sizeGb }) y destroy ({ volumeId, name }). Cada
mutación actualiza los volúmenes. appVolumesKey(projectId, appId) es la clave de
consulta.
const volumes = useAppVolumes(projectId, app.app_id);
await volumes.snapshot.mutateAsync(volumeId);
await volumes.restore.mutateAsync({ volumeId, snapshotId });
useAppVolumeSnapshots(projectId, appId, volumeId)
Las instantáneas de un volumen. Pasa null como volumeId para mantener la consulta inactiva.
Errores
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`);
}
}
Otras respuestas que debes gestionar:
409para un slug duplicado,402conapp_quota_exceededcuando la cuenta alcanza su límite de Apps y400conapp_machine_out_of_rangepara una especificación fuera de sus límites.400 app_not_found(slugs) para un slug desconocido enuses.409 app_capability_unsupported(capability) para una llamada que la App no ofrece.- Un despliegue:
400 app_config_invalid(issues),400 app_volumes_unsupported. Una reversión:409 release_not_ready. - Máquinas y 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. - Volúmenes:
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 con Cache-Control: no-store.