VibeHost
Guides

Share links

Cookie-issuing URLs that grant temporary access to private or password-gated apps.

A share link is a URL that, when visited, sets a cookie in the visitor's browser. The cookie satisfies the share-link gate (see Grants and visibility) for that browser, for as long as the share link is active.

Use a share link when you want to give someone access without making them sign up: a client reviewing a preview, a designer checking a mockup, a journalist previewing a launch under embargo.

Create

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

Output:

{
  "ok": true,
  "data": {
    "id": "qk8m2n4p6r0s3t7u9v1w5x2y",
    "url": "https://my-site-acme.vibehost.space/?__vh_share=vhs_Kx8pQ3nR7tKvL9mX4wZ6yB1cD5fH0jN8",
    "token": "vhs_Kx8pQ3nR7tKvL9mX4wZ6yB1cD5fH0jN8",
    "tokenPrefix": "vhs_Kx8pQ3nR",
    "label": null,
    "expiresAt": null
  }
}

Send data.url to the recipient. The token rides in the __vh_share query parameter. The first time they open it, their browser gets a signed 24-hour cookie (vh_app_share_<appId>) and the server redirects to the same URL with the token removed, so it does not sit in browser history or leak through a Referer header. The cookie satisfies the share-link gate (visibility/grant) on subsequent requests. If the app has a password set, the visitor is still asked for it. The password gate is separate and always applies.

data.token is returned once, at creation, and never stored. The server keeps only its hash. data.tokenPrefix is the first 12 characters, which is what share-link ls displays so you can identify a link without handling the whole token.

Options:

vibehost app share-link create --app my-site \
  --label "Acme review — Q2 pitch" \
  --expires-in 14d
  • --label adds a searchable note that shows up in share-link ls and in the dashboard. Pick something you'll recognize in 6 months.
  • --expires-in takes a duration such as 30m, 6h or 7d. The server caps it at 90 days. Omit it and the link never expires. Once a link has expired its token stops being accepted, and the visitor falls through to the app's ordinary gates, so on a private app they land on the request-access page rather than getting a distinct "link expired" error.

List and revoke

ls lists every link on the app, including revoked and expired ones, with the status of each. Add --json to filter with jq.

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

Revoke by full ID or unique prefix (the second form below is a prefix match):

vibehost app share-link revoke qk8m2n4p6r0s3t7u9v1w5x2y --app my-site
vibehost app share-link revoke vhs_Kx8p --app my-site

Revocation is immediate. The cookie stays in the visitor's browser, but the dispatcher rejects it on the next request.

Three scenarios

You're delivering a landing page, and the client should be able to see it for 2 weeks before the link expires. Setting the app private means only grantees and holders of the share link can reach it.

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

Email the URL to the client. They open it and see the page without signing up.

After 14 days the token stops working and the client sees the private app's request-access page. To extend, mint a new link:

vibehost app share-link create --app client-pitch \
  --label "Acme Q2 review — extension" \
  --expires-in 14d

Revoke the old one (share-link revoke <id>) once the new one is sent.

An internal reviewer isn't in your VibeHost workspace, and you don't want to add them as a workspace member.

Option A keeps the app workspace-scoped and adds the reviewer by email:

vibehost app grants add-email reviewer@partner.com viewer --app new-feature

Option B uses a share link, so the reviewer doesn't need to sign up:

vibehost app visibility new-feature private
vibehost app share-link create --app new-feature \
  --label "Internal review – security team" \
  --expires-in 7d

Option A suits repeat reviewers, because the audit trail records each user. Option B suits a one-off "open this once and look at it".

You're launching with a press embargo. Reporters get the URL 48 hours before the public launch, and the link expires at launch, when the app goes public.

vibehost app visibility upcoming-launch private
vibehost app share-link create --app upcoming-launch \
  --label "NYT embargo" \
  --expires-in 48h
vibehost app share-link create --app upcoming-launch \
  --label "TechCrunch embargo" \
  --expires-in 48h

Two separate links can be revoked and audited separately. At launch, make the app public. Optionally, revoke the embargo links too so press can't keep linking the share URL:

vibehost app visibility upcoming-launch public
vibehost app share-link ls --app upcoming-launch --json | \
  jq -r '.data[] | .id' | \
  xargs -I{} vibehost app share-link revoke {} --app upcoming-launch

How it works on the wire

  1. Visitor opens https://my-site-acme.vibehost.space/?__vh_share=vhs_Kx8pQ3nR7tKvL9mX4wZ6yB1cD5fH0jN8.
  2. The gate hashes the token, looks up the row (not revoked, not expired), and sets a signed 24-hour cookie:
    Set-Cookie: vh_app_share_<appId>=<HMAC-signed-payload>;
                Path=/; Max-Age=86400; Secure; HttpOnly; SameSite=Strict
    The cookie is host-only (no Domain attribute), so the public-suffix listing on vibehost.space can't reject it.
  3. The gate 302-redirects to the same URL with __vh_share removed, which is what the visitor's history and any outbound Referer end up carrying.
  4. The follow-up request carries the share cookie. The share-link gate sees it and satisfies visibility/grant for this browser, skipping those checks on every later request as long as the cookie is valid and the share row is still active. The server then runs the password gate: if the app has a password set and there's no valid password cookie, the visitor gets a 401 password prompt. After they enter the password, the server sets a second cookie (vh_app_pw_<appId>, 7-day TTL).
  5. With both cookies in place, every later request from this browser passes. The share cookie covers visibility/grant for 24 hours, and the password cookie covers the password gate for 7 days. The different lifetimes are deliberate. Share cookies belong to outside parties and stay short, while password cookies belong to viewers returning to an app they use. Whichever expires first re-prompts only for that one factor.

The cookie is bound to one app, and each app issues its own cookie. One share link can't unlock other apps in the same workspace.

Best practices

One link per recipient

Easier to revoke individual access without locking everyone else out.

Always set --expires-in

Links that never expire pile up. 7 to 30 days is a reasonable default.

Combine with private visibility

Set the app private and create share links. The app is invisible to anyone without a link or a grant.

Audit quarterly

Run share-link ls --json and check that nothing long-lived is left over from old projects.

  • They don't authenticate the visitor. The cookie is bound to the share link, not the person. Anyone with the URL can satisfy the gate.
  • They don't bypass the password gate. A valid share-link cookie satisfies the share-link gate (visibility/grant are skipped) for the app it was minted on, including visibility: private apps with no grants. But if the app has a password set, the visitor still has to enter it, because the password cookie is separate and always required. This is by design. Minting a share link is the operator's explicit consent for "anyone with this URL can view (no sign-in required)", but it doesn't override a password the operator set. If "anyone with this URL, no further friction" is what you want, don't set a password. If "anyone with this URL and this password" is what you want, set both.
  • They don't expire by default. Set --expires-in or revoke manually.
  • They don't carry an audit trail of the recipient. If you need "person X opened this on date Y", invite them with a grant instead.

Common errors

CodeStatusMeaning
NOT_FOUND404The link id doesn't exist on this app. If you passed a token prefix instead of an id, no active link matches. Prefix resolution skips revoked links, so revoking one again by prefix lands here.
VALIDATIONNone (CLI only)Raised by the CLI, not the API, when the prefix you passed matches more than one active link. The error lists the matches; pass a longer prefix or the full id.

Revoking an already revoked link by its full id returns 200, because the call is idempotent.

There is no share-link-specific error for an expired token. An expired or revoked token is simply not accepted, and the request falls through to the app's ordinary gates: a private app answers with its request-access page, a password-gated app with the password prompt.

On this page