VibeHost
Guides

Grants and visibility

How VibeHost decides who can see and act on an app. A password applies on every path; share links skip visibility and grants but never the password.

Four mechanisms together decide who can reach an app. The password is the only one every path has to clear when it's set. A share link lets a visitor skip visibility and grants, but if the app has a password, they still have to enter it. That skip is the one documented exception: any new gate adds to the others by default. See How a request gets evaluated for the actual order.

The four mechanisms

        ┌──────────────────────────┐                      ┌──────────────┐
        │  Visibility ∧  Grants    │   ← bypassed by →    │  Share link  │
        │  public/ws/    team or   │                      │  (if used)   │
        │   private      email     │                      │              │
        └──────────────────────────┘                      └──────────────┘
                       │                                         │
                       └─────────────┬───────────────────────────┘
                                     ▼
                              ┌──────────────┐
                              │   Password   │   ← true AND-gate,
                              │   (if set)   │     applies to every path
                              └──────────────┘
                                     │
                                     ▼
                              access granted

A visitor without a share link must pass visibility, grants, and the password if one is set. A visitor with a valid share-link cookie or ?__vh_share= URL parameter skips only visibility and grants. If a password is set, they still have to enter it.

Visibility

Visibility is set per app and decides who can reach the app at all:

VisibilityWho can request
publicAnyone on the internet (no auth required)
workspaceAny workspace member
privateOnly users with an explicit grant
vibehost app visibility my-site public
vibehost app visibility workspace --app my-site
vibehost app visibility private --app my-site

Visibility never lets anyone act on the app. Deploying and changing settings still need a grant.

Slack's crawler is an anonymous visitor too, so a link to a workspace or private app unfurls as a generic card until you connect Slack. See Slack link previews.

Grants

Grants come in two kinds, team and email, and both use the same roles:

RoleCan do
viewerView deployments, see logs
deployerAbove + push new deploys, promote, rollback, and pull the release source
adminAbove + manage grants, settings, custom domains

Exporting an app's source through vibehost pull, the deployment file browser, or the MCP download tool needs deployer, not viewer.

Static apps have one carve-out. A static site's files are exactly what the browser serves, so anyone who can view a static app can also pull it through the CLI. The rules are the same as for visiting the site. You need read access (a grant, or public / workspace visibility), and if the app has a password you must supply it. vibehost pull prompts for it, and agents pass --password or VIBEHOST_APP_PASSWORD. The API never hands out more than a browser with the same credentials could fetch. Next.js apps are different. Their server code never leaves the runtime, so their source stays deployer-only, and pull doesn't support them anyway.

Grants add up. If both your team and your email have grants, the effective role is the max (admin > deployer > viewer).

Team grants apply to everyone in the team. Email grants target one person and need no team. grants ls lists both kinds, and grants self grants your own user, which helps when testing:

vibehost app grants add-team web deployer --app my-site
vibehost app grants remove-team web --app my-site

vibehost app grants add-email contractor@x.com viewer --app my-site
vibehost app grants remove-email contractor@x.com --app my-site

vibehost app grants ls --app my-site

vibehost app grants self admin --app my-site

Email grants work before the invitee signs up. The grant waits, and when someone first signs in with a matching email, it attaches to their account.

Password gate

A password is an optional second check on top of visibility and grants:

vibehost app password set <password> --app my-site
vibehost app password clear --app my-site
vibehost app password status --app my-site

Once it's set, anyone who opens the URL sees a password prompt. After they enter it correctly, a signed cookie remembers them for 7 days. The password applies on every path, so even visitors who came in through a share link must enter it.

This suits a soft launch: the app is public, but only people you give the password to can see it.

Share links are URLs that set a cookie when someone opens them:

vibehost app share-link create --app my-site
vibehost app share-link ls --app my-site

A share link looks like https://my-site-acme.vibehost.space/?__vh_share=<token>. It works as many times as it's opened, until it expires or you revoke it. Visiting it sets a 24-hour cookie tied to that share link, and with that cookie the browser skips the visibility and grant checks. If the app has a password, the visitor still has to enter it. Share links never get past the password gate.

This suits a client preview. You send the link to a client and revoke it when the review is over.

How a request gets evaluated

When a viewer hits my-site-acme.vibehost.space/index.html, the platform runs an authz check in this order:

  1. Share-link gate. If a ?__vh_share=<token> URL parameter resolves to an active share link, or a share cookie checks out and the share is still active, the request skips visibility and grants. It still goes on to step 2 and isn't allowed yet. Without a share token, the platform checks visibility and grants instead. A public app passes, a workspace app needs a workspace member, and a private app needs an explicit team or email grant.
  2. Password gate. If a password is set and there's no valid password cookie, the visitor gets a 401 and a password prompt. This applies on every path, including the share-link path.
  3. Action-level role check (for write endpoints). Reading needs viewer or above. Deploying and exporting source (pull) need deployer or above. Settings and grants need admin. Visibility on its own only ever lets you read, and anything more needs a grant, whatever the app's visibility is. The one read-side exception is the static-app source carve-out above, and the API runs the first two gates (visibility or grant, then password) again before it serves a byte.

So a share link on its own is enough only when the app has no password. If you want anyone with the URL to view the app with nothing else to enter, don't set a password. If you want them to need the URL and a password, set both. That combination is supported: the link gets the visitor past visibility and grants, and the password is still required.

When a password is set, every path has to clear it, whether the visitor came through a share link, workspace membership, a private grant, or public visibility. Skipping visibility and grants with a share link is the one documented exception to the rule that gates add up.

What this means in practice

SetupAnonymous (no link)Anonymous with valid share linkWorkspace member (no grant)Grant-holder
public, no password, no share linksviewn/aviewview
public, password setwith passwordwith passwordwith passwordwith password
workspace, no password✗n/aviewview
private, no extras✗n/a✗view
private + active share link✗view✗ (link required)view (link not needed)
private + password + share link✗with password✗ (no grant)with password

The model has two kinds of gate:

  • Visibility and grants, which a valid share-link cookie skips.
  • The password, which applies on every path when set, including the share-link path. New gates add to the existing ones by default, and the share-link skip is the documented exception.

Choosing who to share with

flowchart TD
  Start{"Who am I sharing with?"} --> Pub["Anyone on the internet<br/>(marketing site, demo)"]
  Start --> Team["Everyone on my team<br/>(internal tool)"]
  Start --> Sub["A specific group<br/>(contractors, web team)"]
  Start --> One["One person<br/>(client, prospect)"]
  Start --> Temp["Anyone with a link<br/>(time-bound)"]
  Pub --> PubCmd["visibility public"]
  Team --> TeamCmd["visibility workspace"]
  Sub --> SubCmd["visibility private<br/>+ grants add-team"]
  One --> OneA{"Do they have a<br/>VibeHost account?"}
  OneA -- yes --> OneCmd["grants add-email viewer"]
  OneA -- no --> OneB["grants add-email viewer<br/>(invite-before-signup works)"]
  Temp --> TempCmd["share-link create<br/>--expires-in 7d"]
  style PubCmd fill:#fff4d8
  style TeamCmd fill:#fff4d8
  style SubCmd fill:#fff4d8
  style OneCmd fill:#fff4d8
  style OneB fill:#fff4d8
  style TempCmd fill:#fff4d8

Common scenarios

You want the whole workspace to see the app, no one outside:

vibehost app visibility staging-tool workspace

Workspace members open the URL and see the app. Everyone else gets a 401, or a 404 if visibility doesn't reveal that the app exists.

You want to send a client a link that works for a week and then stops on its own:

vibehost app visibility client-pitch private
vibehost app share-link create --app client-pitch --expires-in 7d --label "Acme review"

The CLI prints a https://client-pitch-<ws>.vibehost.space/?__vh_share=<token> URL. Send it to the client. After 7 days the link expires and the cookie they got from clicking it stops working.

Revoke earlier if needed:

vibehost app share-link ls --app client-pitch
vibehost app share-link revoke vhs_abc123 --app client-pitch

You're launching to the public but want anyone who sees the app to need a password first:

vibehost app visibility soft-launch public
vibehost app password set "shipmate-2026" --app soft-launch

Put the URL and the password in your launch email. When you're ready to open the app to everyone:

vibehost app password clear --app soft-launch

Anyone signed in to VibeHost can view, but no anonymous visitors:

vibehost app visibility open-beta workspace
vibehost app grants add-team beta-testers viewer --app open-beta

(Workspace alone covers internal; add a team grant for cross-workspace beta if needed.)

Only specific people, no public discovery, no share links:

vibehost app visibility internal-tool private
vibehost app grants add-team admins admin --app internal-tool
vibehost app grants add-email auditor@acme.com viewer --app internal-tool

You can add a password as well:

vibehost app password set "$(openssl rand -hex 16)" --app internal-tool

That makes three gates: private visibility, the grant check, and the password. A visitor has to pass all three, and failing any one denies access.

Access matrix

SettingAnon visitorWorkspace member (no grant)Email-grant viewerTeam-grant deployerAdmin
public✓ view✓ view✓ view✓ view + deploy✓ all
public + passwordwith passwordwith passwordwith passwordwith passwordwith password
workspace✗✓ view✓ view✓ view + deploy✓ all
workspace + password✗✓ + password✓ + password✓ + password✓ + password
private✗✗✓ view✓ view + deploy✓ all
private + share linkwith linkwith link✓ (link not needed)✓ (link not needed)✓ all
private + share link + passwordwith link + passwordwith link + passwordwith password (link not needed)with password (link not needed)with password

A "✓" means the gate for that row and column passes, and the visitor still has to pass any other active gates. A password always applies when set. A share link doesn't get a visitor past it.

How to check who has access

vibehost app inspect my-site --json

The inspect response includes visibility, passwordSet, all team grants, all email grants, and active share links, so one call shows everything that controls access. Agents should audit an app this way.

For a per-deployment view, use the dashboard's audit log or vibehost audit --target app:my-site --json.

Error codes you might see

CodeStatusMeaning
FORBIDDEN403You're signed in but lack the required role
UNAUTHENTICATED401No / expired session, or password gate not satisfied
NOT_FOUND404App doesn't exist OR you lack a grant that would reveal it (privacy: we don't enumerate existence to non-grantees)
INVALID_GRANT_TARGET400add-team with a slug that doesn't exist in this workspace
CROSS_WORKSPACE_GRANT400Tried to grant a team from a different workspace
SELF_GRANT_FORBIDDEN403Can't grants add-email your own email. Use grants self <role> (audited) instead
ACCESS_REQUEST_PENDING409Your "request access" was already submitted; waiting on admin
ACCESS_REQUEST_COOLDOWN429Your request was denied in the last 24h. Wait before requesting again

Self-grants are audited

vibehost app grants self admin --app my-site is allowed (you might need to deploy your own app after a workspace migration), but it writes an audit row marked self_grant=true. Workspace owners can filter the audit log on this flag to spot escalation patterns.

Resolve app metadata

GET /api/v1/workspaces/:workspaceId/apps/resolve?name=<exact-name> returns {ok:true,data:{id,name,fqdn}} for an app visible in the workspace app list. Use ?id=<exact-id> to resolve an ID instead; provide exactly one selector. A name lookup never falls back to an ID. This endpoint returns no content, deployments, previews, or secrets and does not grant read access.

Workspace admins and owners can resolve private app metadata when adminCanSeePrivateAppMetadata is enabled. Without that policy, the normal list visibility rules apply, including explicit grants, creator access, and owner Admin Mode. CLI self-grant and explicit link use this endpoint so an administrator can resolve an app before obtaining a content grant.

PATs need apps:read; app resource restrictions still apply. Missing, hidden, deleted, cross-workspace, invalid-name, and resource-excluded apps return 404 NOT_FOUND without metadata. Missing or multiple selectors return 400 VALIDATION_FAILED; unauthenticated requests return 401 UNAUTHENTICATED, and insufficient PAT scopes return 403 PAT_SCOPE_INSUFFICIENT. Existing app content endpoints retain their read permission checks.

Search engine indexing

Public visibility lets visitors open a website; it does not enroll it in search. Indexing is off by default for every new and existing app, including custom domains.

Search indexing is rolling out gradually and is not yet available to every workspace. Turning it on for a workspace that is not enabled yet returns 403 Search engine indexing is not available for this workspace yet. It also requires an active Business plan and consent from an app administrator. Existing consent does not bypass this restriction: an app in a workspace that is not enabled is not indexed.

On Business, an app administrator can turn on Allow search engine indexing in the app's Settings. The production website must be public and have no password. Private or workspace visibility, password protection, suspension, and a frozen workspace prevent indexing even when the preference is enabled. Preview channels and historical deployment URLs remain excluded.

vibehost app indexing on --app my-site --json
vibehost app indexing show --app my-site --json
vibehost app indexing off --app my-site --json

Disabling is available on every plan. Downgrading clears consent; after returning to Business, explicitly enable it again. Allowing indexing does not guarantee search inclusion or timing, and a page's own noindex directives still apply.

Indexing applies to HTML/XHTML, XML (including sitemaps), and PDF documents. Images, JavaScript, CSS and other assets retain their normal caching and platform noindex. Candidate documents use no-store and a fresh authorization check. An allowed document without a tenant directive receives index, follow, including 304 revalidation, to replace a previously cached platform noindex.

On this page