Skip to main content
Artifacts use private or organization-shared access. The owner of a private artifact can share it with the current organization from the viewer. That switch does not create, change, or remove a public link. The owner can separately enable a public link, optionally protected by a password. Organization members can read an org_shared artifact without that link. A share URL is the public base origin plus /share/{id}/{token}. Anyone with that URL can ask the API for the public detail. There is no assumption that a particular domain is hosted for you. In the library or viewer, use Share. From the CLI:
Owner HTTP routes:
  • POST /api/v1/artifacts/{id}/share creates the link, or returns the existing one
  • GET /api/v1/artifacts/{id}/share reads the current state
  • DELETE /api/v1/artifacts/{id}/share revokes it
Only the creator can change the share. Another member who can read an org_shared artifact gets the same 404 ARTIFACT_NOT_FOUND as a missing id. A request with no credentials is 401 UNAUTHENTICATED before the artifact is loaded. Local URL:
Placeholder self-hosted URL:
The token is 32 bytes of base64url. The API stores AES-256-GCM ciphertext bound to the artifact id, plus a SHA-256 hash. The encryption key comes from SHARE_TOKEN_KEY when that is set, otherwise from BETTER_AUTH_SECRET. Rotating that input revokes existing public links. Sharing again mints a new token. An invalid id, an invalid token, an unshared artifact, and a revoked token all return the same 404 ARTIFACT_NOT_FOUND body. Those responses do not say whether the artifact exists. A share can carry an optional password. The owner sets it on create or later with POST /share and JSON { "password": "..." }. DELETE /api/v1/artifacts/{id}/share/password keeps the URL and clears the password. Revoking the share also clears the password. Password rules from the shared contracts:
  • 10 to 128 characters after NFC normalization
  • at least four distinct characters
  • not all whitespace
  • not on the bundled common-password list
The API hashes the password with Argon2id and stores only the PHC string. The CLI never prints a password. Use --password to be prompted twice, or pipe it: --password prompts twice and needs a terminal. --password-stdin reads a pipe and removes one trailing newline. Set LINK_PASSWORD before the pipe.
The public page loads GET /api/v1/public/artifacts/{id}/{token} without cookies. If the link has a password and the visitor has no grant, the API returns 401 PASSWORD_REQUIRED and the page shows a password form. A dead link is still 404, so the form does not confirm that an old URL once existed. POST /api/v1/public/artifacts/{id}/{token}/unlock with { "password": "..." }: The grant lasts 15 minutes. The browser keeps it in sessionStorage on the web origin and sends X-Factrove-Share-Grant on the credentials-omitted detail request. It is not a cookie and it is not put in the page URL. The sandboxed iframe cannot send headers, so a granted detail returns contentUrl with a ticket query. That ticket lasts 60 seconds and is honored at most 3 times per API process. Reverse proxies should not log ?ticket=. Protected content without a valid ticket is 401 PASSWORD_REQUIRED. Changing or removing the password, revoking the link, and rotating the share-token key all invalidate existing grants. A plain re-share (POST /share without a password), while the current key can still read the token, keeps the same token and those grants still work. Revoking, or sharing again after that key no longer reads the stored token, mints a new URL.

Rate limits and Retry-After

Current behaviour. This is the only place that calls out the pre-check view charge. A later change may charge a public view only after the link and password check. Until that change, the API does what this section says.
Public detail and public content charge the visitor’s view allowance before the link lookup and before the password or ticket check. A 401 or 404 still uses that allowance. The limit is 60 view units per 60 seconds per client address, in memory on each API process. IPv6 addresses share one bucket by their /64. IPv4 addresses stay separate. A detail that is allowed also grants two companion content loads for 30 seconds. A content request spends one companion when one remains, and otherwise charges another view unit. That choice is also made before lookup. Unlock does not use this limiter. The view 429 is RATE_LIMITED with message Too many requests. Try again later. It does not set Retry-After. Password limits are separate. They are also in memory on each API process, and processes do not share those counters.
  • 10 failures per 10 minutes per client address, using the same IPv6 /64 key (refunded after a match or an infrastructure failure)
  • 30 wrong passwords per 10 minutes per current share token hash
  • 20 verifies per minute and 100 per 10 minutes per IP, with one in-flight verify per address
Every password-throttle 429 RATE_LIMITED (Too many password attempts. Try again later.) sends Retry-After. An exhausted share sends that 429 with no hash verification. Someone who has the link and keeps failing can lock that link for up to ten minutes at a time. A saturated Argon2 gate, or a share whose remaining check slots are all in flight, is 503 UNLOCK_UNAVAILABLE with Retry-After. The owner share payload may include guessesLockedOut when the share budget is exhausted.

Lockout recovery

Revoke clears the stored password. A later plain share creates an unprotected link, including after this recovery and after share-token key rotation. Supply the password again:
--password needs a terminal. The stdin form is printf '%s' "$LINK_PASSWORD" | pnpm factrove share "$artifact_id" --password-stdin. Revoke, then share, mints a new token hash. The previous URL returns 404.

What visitors see

The public JSON is the filename, name, size, upload time, content URL, and passwordProtected. name is null when the artifact has no library name. It omits the creator, organization, access type, and share URL. HTML uses the same sandboxed iframe policy as a signed-in viewer. Inline scripts and styles run. Network requests, external scripts and styles, forms, and nested frames do not. See Prepare an artifact. Markdown is not offered on the public route.