VibeHost
Guides

Personal access tokens

Long-lived bearer tokens for CI and external integrations. Scoped per resource group, scoped per workspace.

PATs are how non-browser, non-OAuth callers authenticate against the VibeHost API. CI pipelines, custom scripts, integrations that can't run an OAuth dance — they all use PATs.

PATs are created and managed from the dashboard only. The PAT management API itself is browser-session-only, so a PAT cannot mint more PATs — limiting blast radius if one leaks.

Create a PAT

Open vibehost.com/settings/access-tokensCreate token. The dashboard asks for:

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

The plaintext token (vh_pat_…) is shown exactly once on creation. 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:

ScopeWhat it unlocks
apps:readRead app metadata, list deployments, read logs
apps:writeCreate / rename / delete apps
apps:deployUpload deploy artifacts, promote, rollback (the most common CI scope)
domains:readList custom domains
domains:writeAdd + verify + remove custom domains
env:readList environment variables
env:writeSet + remove environment variables
members:readList team + workspace members and per-app grants
members:writeInvite + remove members, add + remove per-app grants
billing:readRead current plan + usage
workspace:readRead workspace settings
workspace:writeUpdate workspace settings

Notes:

  • No billing:write — billing mutations (subscribe, change plan, cancel) go through Stripe-hosted checkout, which re-authenticates against Stripe directly. Long-lived API tokens shouldn't bypass that.
  • apps:deploy is a distinct scope from apps:write because deploying is the most common CI action and is materially different from create/rename/delete.
  • PATs created with empty scopes are full-access — equivalent to the dashboard session. Restrict by picking specific scopes.

A PAT for "GitHub Actions that deploys my-app to production" typically needs only apps:deploy + apps:read, restricted to that specific app. Less surface area = smaller blast radius if leaked.

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. domains and env keys are accepted in the schema for forward-compat but don't gate yet.

Workspace + team binding

Every PAT is bound to one workspace at issue time — the workspace you were in when you clicked Create. 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 unified-error-message rule — the token holder already knows their token's bound scope, so the 403 code reveals nothing an attacker without the token could learn.)

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 in ~/.config/vibehost/config.json.

CI examples

.github/workflows/deploy.yml — deploys ./dist to production on every push to main. Smoke-checks the URL afterwards.

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 https://vibehost.com/install.sh | sh
      - name: Deploy
        env:
          VIBEHOST_TOKEN: ${{ secrets.VIBEHOST_TOKEN }}
        run: |
          ~/.vibehost/bin/vibehost deploy ./dist \
            --app my-site \
            --json | tee deploy.json
          URL=$(jq -r '.data.url' deploy.json)
          echo "Deployed: $URL"
          # Smoke check — fail the job if homepage isn't 200
          curl -fsS -o /dev/null -w "%{http_code}\n" "$URL"

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

For PR previews, swap production for the channel:

        run: |
          ~/.vibehost/bin/vibehost deploy ./dist \
            --app my-site \
            --channel pr-${{ github.event.pull_request.number }} \
            --json

.gitlab-ci.yml:

deploy:
  image: node:22
  script:
    - npm ci && npm run build
    - curl -fsSL https://vibehost.com/install.sh | sh
    - export PATH="$HOME/.vibehost/bin:$PATH"
    - vibehost deploy ./dist --app my-site --json | tee deploy.json
    - URL=$(jq -r '.data.url' deploy.json)
    - curl -fsS -o /dev/null -w "%{http_code}\n" "$URL"
  variables:
    VIBEHOST_TOKEN: $VIBEHOST_TOKEN
  only:
    - main

Set VIBEHOST_TOKEN as a masked + 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 https://vibehost.com/install.sh | sh
      - run:
          name: Deploy
          command: |
            export PATH="$HOME/.vibehost/bin:$PATH"
            vibehost deploy ./dist --app my-site --json
workflows:
  main:
    jobs:
      - deploy:
          context: vibehost-prod
          filters:
            branches:
              only: main

Add the PAT as a context env var (vibehost-prod context → VIBEHOST_TOKEN).

Minimal "deploy 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
URL=$(jq -r '.data.url' /var/log/vibehost-deploy.json)
echo "$(date -u +%FT%TZ) deployed: $URL" >> /var/log/vibehost-deploy.log

# Smoke check
curl -fsS -o /dev/null "$URL"

systemd timer file for daily 03:00 deploys:

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

Scope matrix — which PAT for which CI job

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 (no resource restriction — needs to invite to many apps)

The principle: smallest possible scope, restricted to specific app IDs whenever possible. Re-issue a new PAT rather than widen an existing one.

Rotation

PATs don't auto-rotate. Recommended:

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

Revoking is dashboard-only too — open vibehost.com/settings/access-tokens, click Revoke on the row. Revoked PATs immediately stop authenticating, but the row survives for audit (with deletedAt set).

Audit

Every PAT-authenticated request lands in the workspace audit log with the PAT's id and name — owner / admin only. 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 — OAuth alternative for interactive agents
  • CLI reference — every command a PAT can drive. To call the REST API directly (Authorization: Bearer vh_pat_… against https://api.vibehost.com/api/v1), each CLI command maps to the underlying endpoint.

On this page