VibeHost
Guides

Personal access tokens

Long-lived bearer tokens for CI and external integrations, limited by scope, by app and by workspace.

Callers that can't use a browser session or OAuth authenticate against the VibeHost API with a personal access token (PAT). That covers CI pipelines, custom scripts and integrations that can't run an OAuth flow.

You create and manage PATs from the dashboard only. The PAT management API accepts only a browser session, so a PAT cannot mint more PATs, and a leaked one can't be used to create others.

Create a PAT

Open vibehost.com/settings/access-tokens and click Create token. The dashboard asks for four things.

  • Name identifies the token in audit logs and the token list.
  • Scopes limit what the token can do. Pick the minimum (see below). Leave it empty for full access, equivalent to the dashboard session.
  • Resources optionally restrict the token to specific app IDs.
  • Expiry is 30, 60, 90 or 365 days, or never.

The dashboard shows the plaintext token (vh_pat_…) only once, when you create it. The server only stores sha256(plaintext); there is no recovery. Copy it to your secrets manager immediately.

Scopes

Scope names follow <resource>:<verb>. Pick the minimum. The full list:

The export routes take apps:deploy, not apps:read. apps:read covers metadata (app details, the deployment list, logs) plus one exception for static-app source. The /source routes serve a static release to any caller who clears the full web gate (read access AND the app password when one is set), because a browser with the same credentials could fetch the same bytes anyway. The deploy-class export routes (/download, /files, /files/content) still refuse a read-only token, and Next.js releases are never served at apps:read.

ScopeWhat it unlocks
apps:readRead app metadata, list deployments, read logs, source-read a static release via the /source routes (web-gate rules: read access + app password)
apps:writeCreate, rename and delete apps
apps:deployUpload deploy artifacts, promote and roll back (the most common CI scope). Also export a release with vibehost pull, the deployment file browser or the MCP download tool
domains:readList custom domains
domains:writeAdd, verify and remove custom domains
env:readList environment variables
env:writeSet and remove environment variables
members:readList team and workspace members and per-app grants
members:writeInvite and remove members, add and remove per-app grants
billing:readRead the current plan and usage
workspace:readRead workspace settings
workspace:writeUpdate workspace settings
  • There is no billing:write. Billing changes (subscribe, change plan, cancel) go through Stripe-hosted checkout, which re-authenticates against Stripe directly, and a long-lived API token shouldn't bypass that.
  • apps:deploy is separate from apps:write because deploying is the most common CI action and is a different kind of change from creating, renaming or deleting an app.
  • A PAT created with no scopes has full access, the same as the dashboard session. Pick specific scopes to restrict it.

A PAT for "GitHub Actions that deploys my-app to production" typically needs only apps:deploy and apps:read, restricted to that app. The less a token can do, the less a leaked one can damage.

Resources

The "resources" field restricts a PAT to a specific set of resource IDs:

{
  "apps": ["app_abc123", "app_def456"]
}

A token with that resources field can only act on those two apps. Calls to other apps return 403 TOKEN_RESOURCE_MISMATCH.

v1 only enforces the apps key. The schema accepts domains and env keys for forward compatibility, but they don't restrict anything yet.

Workspace + team binding

Every PAT is bound to one workspace when it is issued: the workspace you were in when you clicked Create token. Calls to URLs under a different workspace return 403 TOKEN_WORKSPACE_MISMATCH.

Similarly, a PAT can optionally be team-bound. Calls to URLs under a different team return 403 TOKEN_TEAM_MISMATCH.

These are the documented exceptions to the rule that errors share one message. The token holder already knows what their token is bound to, so the 403 code tells an attacker without the token nothing.

Use a PAT

curl -H "Authorization: Bearer vh_pat_..." \
  https://api.vibehost.com/api/v1/workspaces/<id>/apps

Or with the CLI:

export VIBEHOST_TOKEN=vh_pat_...
vibehost app list

The CLI reads VIBEHOST_TOKEN if set; otherwise it uses the device-flow token stored in ~/.config/vibehost/config.json for the active profile. VIBEHOST_TOKEN wins over the selected profile's stored token either way.

CI examples

.github/workflows/deploy.yml deploys ./dist to production on every push to main, and fails the job unless the deployment is healthy.

name: Deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with: { node-version: 24 }
      - run: npm ci
      - run: npm run build

      - name: Install VibeHost CLI
        run: curl -fsSL -o vibehost-install.sh https://vibehost.com/install.sh && sh vibehost-install.sh && rm vibehost-install.sh
      - name: Deploy
        env:
          VIBEHOST_TOKEN: ${{ secrets.VIBEHOST_TOKEN }}
        run: |
          ~/.vibehost/cli/vibehost deploy ./dist \
            --app my-site \
            --json > deploy.json
          # Fail unless the platform reports this deployment healthy.
          jq -e '.data.status == "healthy"' deploy.json > /dev/null || { cat deploy.json; exit 1; }
          echo "Deployed: $(jq -r '.data.url' deploy.json)"

The step writes the JSON to a file instead of piping it into tee: a run step without shell: bash has no pipefail, so a failed deploy piped into tee would still pass.

Store the PAT as a GitHub repository or environment secret named VIBEHOST_TOKEN. Issue it with the apps:deploy and apps:read scopes, restricted to app_my_site_id.

For PR previews, swap production for the channel:

        run: |
          ~/.vibehost/cli/vibehost deploy ./dist \
            --app my-site \
            --channel pr-${{ github.event.pull_request.number }} \
            --json > deploy.json
          jq -e '.data.status == "healthy"' deploy.json > /dev/null || { cat deploy.json; exit 1; }

.gitlab-ci.yml:

deploy:
  image: node:22
  script:
    - npm ci && npm run build
    - curl -fsSL -o vibehost-install.sh https://vibehost.com/install.sh && sh vibehost-install.sh && rm vibehost-install.sh
    - export PATH="$HOME/.vibehost/cli:$PATH"
    - vibehost deploy ./dist --app my-site --json > deploy.json
    - jq -e '.data.status == "healthy"' deploy.json > /dev/null || { cat deploy.json; exit 1; }
  variables:
    VIBEHOST_TOKEN: $VIBEHOST_TOKEN
  only:
    - main

Set VIBEHOST_TOKEN as a masked and protected CI/CD variable.

.circleci/config.yml:

version: 2.1
jobs:
  deploy:
    docker:
      - image: cimg/node:22.11
    steps:
      - checkout
      - run: npm ci && npm run build
      - run: curl -fsSL -o vibehost-install.sh https://vibehost.com/install.sh && sh vibehost-install.sh && rm vibehost-install.sh
      - run:
          name: Deploy
          command: |
            export PATH="$HOME/.vibehost/cli:$PATH"
            vibehost deploy ./dist --app my-site --json > deploy.json
            jq -e '.data.status == "healthy"' deploy.json > /dev/null || { cat deploy.json; exit 1; }
workflows:
  main:
    jobs:
      - deploy:
          context: vibehost-prod
          filters:
            branches:
              only: main

Add the PAT as VIBEHOST_TOKEN in the vibehost-prod context.

A minimal script that deploys from a cron job on a VM:

#!/usr/bin/env bash
set -euo pipefail
export VIBEHOST_TOKEN="$(cat /run/secrets/vibehost_pat)"

cd /srv/my-site
git pull --ff-only
npm ci && npm run build

vibehost deploy ./dist --app my-site --json > /var/log/vibehost-deploy.json
jq -e '.data.status == "healthy"' /var/log/vibehost-deploy.json > /dev/null
URL=$(jq -r '.data.url' /var/log/vibehost-deploy.json)
echo "$(date -u +%FT%TZ) deployed: $URL" >> /var/log/vibehost-deploy.log

A systemd timer that deploys daily at 03:00:

[Unit]
Description=Daily VibeHost deploy
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target

Each example fails unless the deployment's status is healthy. That confirms the deployment succeeded, not that the URL already serves it: after a redeploy the previous build can keep answering for up to about a minute.

To confirm that the live URL serves the new build, use the smoke check in CI/CD with GitHub Actions. It writes a build-id.txt into the build and polls the live URL until that file matches. The loop is plain shell, so it works in any of these CI systems. It needs an app anyone can open: an app is private by default, and a gated app answers an anonymous request with a redirect to sign in, so don't smoke-check it by status code. curl -f passes on that redirect whether or not your build is live. See Gated apps.

Which PAT each CI job needs

JobMinimum scopesResource restriction
Deploy + smoke (most CI)apps:deploy, apps:readSpecific app IDs
Per-PR preview channelapps:deploy, apps:readSpecific app IDs
Read-only audit / analyticsapps:read, members:read, billing:readWorkspace-wide
Custom domain provisioning automationdomains:read, domains:write, apps:readSpecific app IDs
Env-var rotation from secrets managerenv:read, env:write, apps:readSpecific app IDs
Member onboarding botmembers:write, members:readWorkspace-wide, with no resource restriction, since it invites people to many apps

Give each token the smallest scope that works, restricted to specific app IDs whenever you can. Issue a new PAT rather than widening an existing one.

Rotation

PATs don't rotate automatically. To rotate one:

  1. Create a new PAT with the same scopes + resources.
  2. Switch your secrets manager or CI to the new PAT.
  3. Revoke the old PAT after one deploy cycle confirms the new one works.

You can only revoke from the dashboard too. Open vibehost.com/settings/access-tokens and click Revoke on the row. A revoked PAT stops authenticating immediately, but the row stays for audit, with deletedAt set.

Audit

Every PAT-authenticated request lands in the workspace audit log with the PAT's id and name. Only owners and admins can read it. Read it with vibehost audit or open the Audit tab on your workspace in the dashboard.

If you suspect a leak, revoke immediately, then review the audit log for unexpected actions.

See also

  • MCP server is the OAuth alternative for interactive agents.
  • CLI reference lists every command a PAT can drive. Each CLI command maps to a REST endpoint, if you'd rather call the API directly (Authorization: Bearer vh_pat_… against https://api.vibehost.com/api/v1).

On this page