Skip to content
BeeuniBeeuni
English
Esc
↑↓navigate↵open⌘Jpreview
On this page

Apps

Create, deploy, and control Beeuni Apps from the SDK and from React.

Apps are project-scoped deployments with one stable URL each, immutable releases, machines per process group, and persistent volumes. Branch on app.capabilities. This page covers the SDK surface: the apps facade on a project handle, the artifact, release, machine, and volume calls, sign-in tokens, the exported types, and the React hooks.

For the model, the release config, the source kinds, the CLI, the stable URL, and cold-wake behavior, read Apps.

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

The apps facade

Method Wraps What it does
apps.list() GET /projects/:pid/apps Lists the Apps the caller may open (a project manager sees all)
apps.create(input) POST …/apps Creates an App and assigns its stable URL
apps.get(appId) GET …/apps/:id Reads one App
apps.update(appId, input) PATCH …/apps/:id Renames it or changes machine, idle timeout, run mode (always_on), uses, or labels. Run mode applies within 5 minutes; a machine change applies to the next release
apps.remove(appId) DELETE …/apps/:id Deletes the App, its machines, and every release image; returns images: {released, pending}. Its volumes keep a final snapshot for 7 days
apps.start(appId) POST …/apps/:id/start Sets desired_state to running and warms the runtime. A static App answers 409 static_app_no_runtime
apps.stop(appId) POST …/apps/:id/stop Suspends compute now; the next request resumes it. A static App answers 409 static_app_no_runtime: it has no runtime and serves while it has an active deployment
apps.rollback(appId, deploymentId) POST …/apps/:id/rollback Creates a NEW release from a ready release’s artifact and config and rolls it out like a deploy (202). Volumes keep their data

Artifacts are the immutable input to a deployment:

Method Wraps What it does
apps.artifacts.register(input) POST …/apps/artifacts Registers an archive or an oci_image; returns the upload URL for an archive
apps.artifacts.uploadArchive(bytes, options?) — Registers, uploads, hashes, and finalizes one .tar.gz in a single call
apps.artifacts.finalize(artifactId, input) POST …/apps/artifacts/:id/finalize Confirms sha256 and size_bytes for a manual upload

Releases (deployments) are immutable and numbered:

Method Wraps What it does
apps.deployments.create(appId, input) POST …/apps/:id/deployments Starts a release from an artifact, a source, and an optional release config
apps.deployments.list(appId) GET …/apps/:id/deployments Lists the deployment history
apps.deployments.get(appId, deploymentId) GET …/deployments/:did Reads one deployment plus its events
apps.deployments.logs(appId, deploymentId, options?) GET …/deployments/:did/logs Reads runtime logs with a cursor
apps.deployments.remove(appId, deploymentId) DELETE …/deployments/:did Deletes one deployment and its image; 409 for the live deployment or an in-progress build. Returns image: 'released' | 'pending' | 'none'

Machines, scale, and exec (a server App; an App with no machines answers 409 app_no_machines):

Method Wraps What it does
apps.machines.list(appId) GET …/apps/:id/machines Every machine: machine_id, process_group, state, release, vm, volume
apps.machines.start/stop/restart(appId, machineId) POST …/machines/:mid/{start,stop,restart} Acts on one machine; resolves with the machine list
apps.machines.destroy(appId, machineId) DELETE …/machines/:mid Destroys one machine. Its volume stays; its group runs one machine fewer
apps.scale(appId, count) PUT …/apps/:id/scale Machines per process group ({ worker: 3 }, 0–20), applied in the background (202). Resolves with scale and machines
apps.exec(appId, { command, machine_id?, timeout_s? }) POST …/apps/:id/exec Runs one command in a running machine (project.app.admin; audited). timeout_s 1–300, default 60. Resolves with exit_code, stdout, stderr (1 MiB each), truncated

Volumes (persistent disks):

Method Wraps What it does
apps.volumes.list(appId) GET …/apps/:id/volumes Every volume: 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 Pre-creates a volume; a later machine of its group with a mount of that name takes it
apps.volumes.update(appId, volumeId, { size_gb?, snapshot_retention_days? }) PATCH …/volumes/:vid Grows a volume (never shrinks; at most 100 GB) or changes its retention (1–60 days)
apps.volumes.destroy(appId, volumeId, confirmName) DELETE …/volumes/:vid?confirm=<name> project.app.admin. Refused with 409 volume_attached while a machine mounts it. Keeps a final snapshot 7 days
apps.volumes.snapshots(appId, volumeId) GET …/volumes/:vid/snapshots daily, manual, and final snapshots
apps.volumes.snapshot(appId, volumeId) POST …/volumes/:vid/snapshots A manual snapshot now
apps.volumes.restore(appId, volumeId, snapshotId) POST …/volumes/:vid/restore project.app.admin. The machine stops, the volume returns to the snapshot, the machine starts on it (202)
apps.volumes.fork(appId, volumeId, { name, snapshot_id? }) POST …/volumes/:vid/fork A new, unattached volume in the same App, from a snapshot or the current contents

Sign-in tokens (capability member_tokens; 409 app_capability_unsupported when the App lacks it):

Method Wraps What it does
apps.token(appId) POST …/apps/:id/token A 15-minute sign-in token for the App, naming the caller

Access is the App’s own authorization policy:

Method Wraps What it does
apps.access.get(appId) GET …/apps/:id/access Reads the policy. Needs project.app.read
apps.access.update(appId, input) PATCH …/apps/:id/access Replaces the policy and bumps its revision
apps.access.session(appId) POST …/apps/:id/access-session Mints a five-minute URL that exchanges into a host-only cookie

Deploy a static site

uploadArchive does the whole artifact handshake: it registers the artifact, checks it against max_bytes, PUTs the bytes, computes the SHA-256, and finalizes.

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 accepts the machine fields too: cpu, memory_gb, disk_gb, idle_timeout_seconds, and always_on. Omit them for the defaults. Apps have no monthly budget: account credits pay for compute. An always-on server App bills 24/7; an on-demand one only while awake. always_on: true runs a server App 24/7 (websockets, background loops); false stops it after idle_timeout_seconds and wakes it on the next request. A static App has no runtime: always_on is false.

The API ignores monthly_budget_usd if an older client sends it (200, no error). No App response carries it.

uses lists the Apps, by slug, this App may get sign-in tokens for and reach through its bindings mount; the default is none, and update(appId, { uses }) replaces the list (see Apps an App uses).

create and update return warnings, which is always [].

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)

Wait for the deployment by polling its status:

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));
  }
}

The status values are queued, validating, building, provisioning, checking, ready, failed, and cancelled.

Deploy an OCI image

Register the immutable image reference, then declare the process command and the public target port:

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 returns upload: null for an oci_image. Only an archive gets an upload URL.

CreateAppDeploymentInput also accepts environment (non-secret runtime values), secrets (runtime key to project secret name), and config (AppReleaseConfigInput, the release config). An invalid config answers 400 app_config_invalid with issues: [{ path, message }]; a mount on a runtime without volumes answers 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' },
  },
});

Read runtime logs

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;
}

Each entry carries cursor, time, source (app, appd, caddy), and line. A caddy line is the access log: method, URI, status, duration and response headers. It never contains request headers, because those carry the gate’s credentials (the viewer token and identity, the provider ingress token).

Machines, scale, and 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' });

A machine keeps its machine_id across releases; each release replaces its box (instance_id). One machine mounts a volume at a time. A restore is a new version of the volume: the replaced contents stay in its history. A snapshot of a mounted volume can miss its last ~1 s of writes.

A Convex backend

A Convex backend is an ordinary server App with a volume, made from the convex template (beeuni apps launch --template convex --name db; see Template convex). The SDK has no Convex-specific call. Read the admin key from the machine with exec, and give a UI App access to the backend with 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 holds the backend’s URLs: http (the Convex client URL) and site (HTTP actions).

Your App already knows who is looking

An App hosted by Beeuni is opened by someone Beeuni already signed in. The Apps gate authenticates them before your first byte is served, so your App needs no login of its own — no second password, no consent screen, no redirect.

In the browser:

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>;
}

The browser calls /_beeuni/api/v1/* on your App’s own origin, never https://api.beeuni.ai/v1 directly: the API answers no CORS preflight from an App origin, so a direct call fails in the browser. The gate forwards each call to the Beeuni API as the signed-in viewer, with the viewer’s App-scoped token. Rules:

  • It needs viewer_token_scope: 'api'. Otherwise it answers 403 viewer_api_disabled.
  • It acts only for a viewer the gate signed in (Beeuni login or an access link). With none, it answers 401 no_viewer_identity.
  • It answers a request from the App’s own origin only. A cross-site request answers 403 cross_site_request.
  • It forwards /v1/* except /v1/oauth/* and MCP endpoints, which answer 404. Your App’s cookie and its own Authorization header never reach the API, and the API cannot set a cookie on your App.

On your App’s server, the gate signs the identity into every request:

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 verifies an HMAC over the header with BEEUNI_APP_VIEWER_SECRET, which Beeuni injects into your App at deploy. A forged header never passes: the gate deletes any client-supplied copy before forwarding, and the signature is made with a secret derived per App.

How much your App is told

One setting on the App’s access policy — Settings → Access in Beeuni, beeuni apps access <app> --viewer off|identity|api, or viewer_token_scope on PATCH /projects/:id/apps/:appId/access:

Scope Your App receives
identity (default) The viewer’s id, email, name, picture, account role, and Beeuni groups (ids and names, in this App’s account only), plus a profile email token. Enough to show each person their own data and to gate by group.
api The above, and a token that acts as that person on the Beeuni API — bounded by their own role.
off Nothing.

The token is never the user’s Beeuni session: it lasts an hour, carries only those scopes, and every token an App minted dies when the App is deleted or its access policy changes.

Using the viewer token

  • On your server, read it per request. The gate puts a live token in x-beeuni-app-viewer-token on every proxied request. Send it as Authorization: Bearer to the Beeuni API — createAppViewerBeeuni(request) does exactly that. Build one client per request; do not store the token.
  • After an access-policy change, old tokens stop working. The next request through the gate carries a new one. In the browser, beeuniAppViewerToken() recovers by itself: when the API refuses its token, the SDK re-reads /_beeuni/viewer and replays the call once.
  • The viewer’s role is the ceiling. The token can do exactly what the viewer can do in Beeuni, never more. On a project with agent permissions, a member needs a grant on the agent your App starts: without one, POST /projects/:id/sessions answers 403 no_agent_access. Grant your viewers the agent, not a broader role.
  • Who has a viewer. A person who opens the App with their Beeuni login — or with an access link, which signs them in as that person — has one. A password App has none: the password proves a shared secret, not a person. A public App has one only for a visitor who arrived through an access link; the bare URL stays anonymous.
  • It is a credential. Beeuni leaves request headers out of the App’s access log (beeuni apps logs). Do not log the header in your own code.

An App served on its own domain (not *.apps.beeuni.ai) has no gate in front of it — use Sign in with Beeuni there instead.

One member, on every runtime

Beeuni vouches for the same person in three places: the browser asks the gate (/_beeuni/viewer), the App’s server reads the signed header, and an App that verifies sign-in tokens (a Convex backend, or your own server) receives a Beeuni-signed token. readBeeuniMember turns each of those shapes into one BeeuniMember, and requireBeeuniMember decides who may proceed. No framework and no dependency: the same code runs in a browser, Node, Bun, a Worker and a Convex function.

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 });

In React, do not call useConvexAuth(): it throws under a plain ConvexProvider (it needs ConvexProviderWithAuth, and beeuniBinding is not verified with that provider). Read the signed-in member with a query:

// 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 returns null for an anonymous caller. Use requireBeeuniMember only where the function must throw.

Field Value
userId Beeuni user id. Stable; key rows on it.
email, name, picture From the member’s profile. null when unknown.
groups, groupIds The member’s Beeuni groups in this account, by name and by id.
role Account role: owner, admin or member.
accountId, projectId Where the App lives.

requireBeeuniMember(claims, { groups, roles }) returns the member or throws BeeuniMemberError with code unauthenticated (nobody, or a token that failed a check) or forbidden (outside the groups or roles). Each list means “any one of these”; a group matches by name or id; an empty list admits nobody.

Sign-in tokens

One issuer per project signs every App’s tokens: iss = <API origin>/v1/projects/<projectId>, aud = the id of the App the token is for, 15 minutes. Its key set is public at <issuer>/jwks.json, named by <issuer>/.well-known/openid-configuration. Every App carries the values that verify its tokens: app.auth.issuer, app.auth.audience, app.auth.jwks_uri. A token carries sub, email, name, picture, groups, group_ids, role, account_id and project_id; an agent’s token carries kind: 'agent' and no groups or role.

Function What it does
beeuniToken({ audience? }) A token fetcher (the shape realtime clients take as their auth callback) for the App itself (default) or an App it uses. Reads /_beeuni/token on the App’s own origin. Cached until shortly before expiry; { forceRefreshToken: true } asks again. Yields null without a signed-in viewer, and null plus one console warning for an App it does not use (403 app_not_linked). Never throws.
beeuniBinding(slug, { origin? }) { url, token } for an App this App uses: url = <origin>/_beeuni/apps/<slug> (HTTP and WebSocket, same origin, no CORS), token = beeuniToken({ audience: slug }).
verifyBeeuniToken(bearer, { issuer?, audience?, jwks? }) Checks the ES256 signature, expiry (60 s skew), issuer and audience. Defaults: BEEUNI_AUTH_ISSUER, BEEUNI_AUTH_AUDIENCE, BEEUNI_AUTH_JWKS. Beeuni injects all three into every server App’s runtime (issuer: the project; audience: the App’s own id; key set: a data: URI), so verifyBeeuniToken(bearer) works there with no options. Never set BEEUNI_AUTH_* in a server App’s env: the BEEUNI_ prefix is reserved and beeuni validate rejects it. The key set is an object, JSON text, a data: URI, or an https URL (auth.jwks_uri, fetched once per process). Every failure throws BeeuniMemberError, a missing key set or a missing audience included: one project key signs every App’s tokens, so aud is what keeps another App’s token out. audience: false accepts a token for any App of the project.
apps.token(appId) A token naming the caller, from the API (POST …/apps/:id/token). An agent session gets one naming its agent.

A member you remove from the project or a group loses access within one token lifetime; the App gate refuses them on the next page load. Beeuni groups need the Enterprise plan; roles work on every plan.

Manage access

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 reports password_configured, never the password or its hash. Set a password with { mode: 'password', password }. Each update increments revision, which revokes existing App cookies.

Types

Every type below is exported from @beeuni/sdk.

Type What it holds
App Identity, capabilities, labels, scale, services (AppServiceSummary[]), machines (total, running, stopped), volumes (count, size_gb), config (the live release’s AppReleaseConfig, or null), uses, used_by, auth, url, access_mode, access_revision, desired_state, active_deployment_id, hosting_type (of the active release: static, sandbox, or null before the first deploy), machine (the serving group’s size in the live release), idle_timeout_seconds, always_on, retained_deployments, warnings (create and update only; always []), last_request_at, viewer_can_access
AppCapability 'deployments' | 'rollback' | 'preview' | 'sleep' | 'static' | 'member_tokens' | 'machines' | 'scale' | 'exec' | 'volumes' | 'snapshots'
AppReleaseConfig · AppReleaseConfigInput The resolved release config (processes, services, mounts, vm, deploy) · the authored form (processes, http_service, services, mounts, vm, deploy, and the shorthands 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 when the host label would be too long)
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 (the App id), jwks_uri: the public values that verify the App’s sign-in tokens
AppToken token, expires_at
BeeuniTokenOptions · BeeuniTokenFetcher · BeeuniBinding · VerifyBeeuniTokenOptions The sign-in token helpers above
AppDeployment version, status, source_kind, hosting_type (static: served from storage, no runtime; sandbox: machines), hosting_provider, runtime_spec, build_spec, error_code, attempt_count, created_by, actor_type, source_session_id, config (the release’s resolved config), strategy, release_command_status (running, succeeded, failed or null)
AppRollback What apps.rollback resolves with: the App plus the queued release in deployment
AppDeploymentDetail One deployment plus its events: lifecycle events and the build log (build_log lines, and one log_truncated event when the build printed more than 5,000 lines)
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 One log line, and one page plus next_cursor

apps.list() and useProjectApps return only the Apps the caller may open under each App’s access policy (private: the creator; restricted: the listed members and groups; project, password and public: the project team). A project manager sees every App, so a private one stays manageable when its creator leaves. viewer_can_access is therefore true on every listed App. A get of an App the caller cannot open answers 404, not 403. Treat undefined as unknown, not as denied.

AppAccessMode is a per-resource visibility setting on top of the role model, not a role. restricted names users and groups — the same principal types the role model uses. See Accounts & access.

DockerfileAppSource and OciImageAppSource require command. port is the primary service’s port: omit it only for a worker-only release (a config with processes and no service). StaticAppSource and BundleAppSource take neither.

React hooks

@beeuni/sdk/react exports seven hooks for Apps.

useProjectApps(projectId)

The Apps the caller may open, plus its lifecycle mutations. Every mutation invalidates the inventory on success.

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>
  );
}

It returns the query fields plus create, update, start, stop, and remove.

useAppDeployments(projectId, appId)

The immutable deployment history, refetched every 5 s so a running build advances on its own.

const deployments = useAppDeployments(projectId, appId);

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

Both mutations invalidate the deployment list and the App inventory.

useAppDeployment(projectId, appId, deploymentId)

One deployment and its events, build log included (AppDeploymentDetail). It polls every 2 s until the deployment is ready, failed or cancelled. Pass null as deploymentId to keep it idle.

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');

A build keeps its first 2,500 and its last 2,500 lines. Lines are written in batches, at most 1 s after they are printed. A failed or cancelled deployment keeps its build log for 14 days.

useAppAccess(projectId, appId, options?)

The access policy and a short-lived access session. Both halves are separate queries, and each one is optional.

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' });
Option Default Use false when
policy true The surface only previews the App. GET …/access is an administrative read and answers 403 for a caller without project-manager permissions.
session true The caller may not open the App. Pass app.viewer_can_access (always true for a listed App).

A grid of Apps that leaves both options at true fires one policy read and one session mint per App, and each is a 403 for a member who may not open that App.

useAppMachines(projectId, appId)

An App’s machines, polled every 5 s, plus start, stop, restart, destroy (each takes a machineId) and scale (takes { [group]: count }). Each mutation refreshes the machines and the App inventory. appMachinesKey(projectId, appId) is the query key.

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

useAppVolumes(projectId, appId)

An App’s volumes, plus snapshot (volumeId), restore ({ volumeId, snapshotId }), fork ({ volumeId, name, snapshotId? }), extend ({ volumeId, sizeGb }), and destroy ({ volumeId, name }). Each mutation refreshes the volumes. appVolumesKey(projectId, appId) is the query key.

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

useAppVolumeSnapshots(projectId, appId, volumeId)

One volume’s snapshots. Pass null as volumeId to keep it idle.

Errors

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`);
  }
}

Other answers you should handle:

  • 409 for a duplicate slug, 402 with app_quota_exceeded when the account is at its App limit, and 400 with app_machine_out_of_range for a spec outside its bounds.
  • 400 app_not_found (slugs) for an unknown slug in uses.
  • 409 app_capability_unsupported (capability) for a call the App does not offer.
  • A deploy: 400 app_config_invalid (issues), 400 app_volumes_unsupported. A rollback: 409 release_not_ready.
  • Machines and 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() answers with Cache-Control: no-store.

Was this page helpful?