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 grantedA 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:
| Visibility | Who can request |
|---|---|
public | Anyone on the internet (no auth required) |
workspace | Any workspace member |
private | Only users with an explicit grant |
vibehost app visibility my-site public
vibehost app visibility workspace --app my-site
vibehost app visibility private --app my-siteVisibility 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:
| Role | Can do |
|---|---|
viewer | View deployments, see logs |
deployer | Above + push new deploys, promote, rollback, and pull the release source |
admin | Above + 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-siteEmail 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-siteOnce 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
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-siteA 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:
- 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. Apublicapp passes, aworkspaceapp needs a workspace member, and aprivateapp needs an explicit team or email grant. - 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.
- 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
| Setup | Anonymous (no link) | Anonymous with valid share link | Workspace member (no grant) | Grant-holder |
|---|---|---|---|---|
public, no password, no share links | view | n/a | view | view |
public, password set | with password | with password | with password | with password |
workspace, no password | ✗ | n/a | view | view |
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 workspaceWorkspace 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-pitchYou'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-launchPut 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-launchAnyone 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-toolYou can add a password as well:
vibehost app password set "$(openssl rand -hex 16)" --app internal-toolThat 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
| Setting | Anon visitor | Workspace member (no grant) | Email-grant viewer | Team-grant deployer | Admin |
|---|---|---|---|---|---|
public | ✓ view | ✓ view | ✓ view | ✓ view + deploy | ✓ all |
public + password | with password | with password | with password | with password | with password |
workspace | ✗ | ✓ view | ✓ view | ✓ view + deploy | ✓ all |
workspace + password | ✗ | ✓ + password | ✓ + password | ✓ + password | ✓ + password |
private | ✗ | ✗ | ✓ view | ✓ view + deploy | ✓ all |
private + share link | with link | with link | ✓ (link not needed) | ✓ (link not needed) | ✓ all |
private + share link + password | with link + password | with link + password | with 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 --jsonThe 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
| Code | Status | Meaning |
|---|---|---|
FORBIDDEN | 403 | You're signed in but lack the required role |
UNAUTHENTICATED | 401 | No / expired session, or password gate not satisfied |
NOT_FOUND | 404 | App doesn't exist OR you lack a grant that would reveal it (privacy: we don't enumerate existence to non-grantees) |
INVALID_GRANT_TARGET | 400 | add-team with a slug that doesn't exist in this workspace |
CROSS_WORKSPACE_GRANT | 400 | Tried to grant a team from a different workspace |
SELF_GRANT_FORBIDDEN | 403 | Can't grants add-email your own email. Use grants self <role> (audited) instead |
ACCESS_REQUEST_PENDING | 409 | Your "request access" was already submitted; waiting on admin |
ACCESS_REQUEST_COOLDOWN | 429 | Your 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 --jsonDisabling 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.
Does a reviewer need an account to open a private preview?
On most platforms, yes, because a private preview works by making the viewer sign in. Compared across Vercel, Netlify, Cloudflare Pages and GitHub Pages, quoted from each vendor's docs, with what VibeHost does instead.
Share links
Cookie-issuing URLs that grant temporary access to private or password-gated apps.