Skip to content

Start here

Connect a site you host

Review a site that needs a backend or a database by adding one script tag: allowed origins, deploy labels, your content security policy and the first deploy.

critlab hosts the files a build produces. A project that needs a server or a database at runtime cannot run there (What you can publish). You can host it yourself and connect it instead: one script tag adds critlab's comments to the live site, and the site appears in your folder as an external project. Reviewers open the site where it runs, behind your own login or VPN. critlab keeps the comments, their screenshots and the deploy each one was left on.

When to use it

Use it when the project has to keep a backend: Next.js server rendering, API routes, Server Actions, a database, a login, or live data from your own API. If the project can be exported to static files, publish it instead: nothing to install, and every version gets its own link.

Connect a site

  1. Deploy the site somewhere reviewers can reach, such as a staging or preview environment.
  2. In critlab, choose New, then Connect a site you host. Enter the page's address, add a name and a folder if you like, and list any other origin the snippet will run on, such as a preview domain.
  3. Copy the script tag critlab shows and paste it into the layout of the site, so it loads on every page you want reviewed.
  4. Deploy, then open a page of the site on one of the allowed origins. The critlab bar appears on the page.
  5. Send reviewers the project's link in critlab, or the address of the site itself.

An agent can do step 2 with the CLI:

bash
crit add-url "Acme checkout" https://staging.acme.com/checkout --origin https://preview.acme.com --folder "Acme/Website"

The MCP tool is critlab_add_external_target. It is in the workspace group, which is not loaded by default: see Choose your tools.

The link critlab gives you for the project opens a page that says critlab neither hosts nor controls the site, shows the site's host and has an Open site button. Comments are left on the site itself.

The script tag

html
<script src="https://critlab.app/snippet.js"
        data-critlab-key="sk_..."
        data-branch="main"
        data-commit="9f3c2a1"
        data-env="staging"
        integrity="sha384-..."
        crossorigin="anonymous"
        defer></script>
AttributeWhat it does
srcThe snippet, served by critlab. Its origin is also where the snippet sends its API calls.
data-critlab-keyRequired. Names the project. It is public: it ships in your page source.
data-branch, data-commitWhich build this page is. Comments are filed under it.
data-envThe environment, such as staging or preview. Defaults to staging.
data-reviewerA name your app already knows, used as the reviewer's name so they are not asked.
data-api-baseThe critlab origin, for a snippet file you host yourself. See below.
integrity, crossoriginPin the exact file. See below.

The tag critlab shows has $GIT_BRANCH and $GIT_COMMIT as placeholders. Your build has to replace them: critlab does not. Left as they are, every page load reports the literal text $GIT_BRANCH as the branch.

integrity makes the browser run only the file whose hash you pasted. When critlab ships a new snippet.js, the old hash no longer matches and the browser refuses to run it until you paste the new tag. The current hash is in the project's details, under Site. Leave out integrity and crossorigin to always run the current file.

Allowed origins

The key names the project and authorises nothing. What decides whether a page may start the snippet is the page's origin, checked against the project's list of allowed origins on every page load.

  • Origins are exact: scheme, host and port. https://staging.acme.com does not allow https://acme.com or http://staging.acme.com.
  • There are no wildcards. Each preview deployment with its own hostname has to be listed, or put the previews behind one stable alias and list that.
  • There is always at least one. Creating the project adds the origin of the address you entered.
  • A page on another origin gets a 403 and no critlab bar.

An origin is a header a script outside a browser can send, so it authorises a page, never a person. Keep your site behind its own login or VPN, as you would without critlab.

Change the list in the project's details, under Site (open the card's menu and choose Details), or from a terminal:

bash
crit site acme-checkout --add-origin https://preview.acme.com
crit site acme-checkout --origin https://staging.acme.com --origin https://preview.acme.com

--add-origin adds to the list and --origin replaces it. The MCP tool is critlab_update_site, in the workspace group.

Deploy labels

The first time critlab sees a combination of branch and commit, it records it as a deploy, with the environment the tag reports. Every comment is stored with the deploy it was left on, so a thread says which build it is about. The project's page in critlab lists the deploys, and your agent reads the branch and commit with the thread, so it can check out the right build before it fixes anything.

Fill the attributes from your build, the same place you set any other build-time value. A tag with the same branch and commit on every deploy shows up as one deploy.

Check the first deploy

  1. Open a page of the deployed site on an allowed origin. The critlab bar appears.
  2. Open the project's page in critlab. Until a deploy has reported in, it shows Set up the snippet with your allowed origins and the tag. Afterwards it lists the deploy.

If the bar does not appear, open the browser's console. The snippet prints one line and shows nothing else:

ConsoleCause
snippet did not start: 403 naming an originThe page's origin is not on the allowed list. Add it.
snippet did not start: 404The key is not the project's: a typo, a deleted project or a rotated key. Copy the tag again from Site.
snippet did not start: 403 saying the snippet only runs on prototypes with link accessThe project's access was changed. Set it back to anyone with the link.
snippet did not start: 403 saying external projects are a Team featureThe workspace's plan does not include external projects. See When the beta ends.
snippet could not reach the APIThe browser could not reach critlab. Check your site's security policy below.

Your site's security policy

If your site sends a Content-Security-Policy, it has to allow the snippet. These rows were checked in Chrome by loading the snippet, with its integrity hash, on a page that sends default-src 'none' plus exactly this list, and running a whole review on it: comment mode, a comment with its pin and thread, the screenshot in the thread, a voice note, and the live connection to other reviewers.

DirectiveAllow
script-srcThe critlab origin in the tag's src.
connect-srcThe critlab origin for the API, and the wss: address of the realtime service (the realtimeUrl in the first session response, visible in the browser's network panel).
img-srcThe critlab origin, for a comment's screenshot in the thread, and data:, which the snippet takes the screenshot through and draws comment mode's pointer from.
media-srcThe critlab origin, if reviewers leave voice notes: a voice note plays from there.
style-src-elem'unsafe-inline'. See below.

None of those flows needed unsafe-eval or blob:; the snippet makes blob: images only for pictures attached to a comment, which it does not offer on a host page. Each row matters: without data: comments are saved with no screenshot, without the critlab origin in img-src the screenshot in the thread does not load, without the realtime address reviewers do not see each other, and without media-src a voice note cannot be played. In every case the browser's console names the directive that blocked it.

Inline styles

The snippet draws its interface from two <style> elements its script inserts: one inside its own Shadow DOM, and one small one in your page for comment mode's pointer. A style-src governs both exactly as it governs an inline style in your own markup. Shadow DOM is no exemption, and the snippet adds no nonce.

With style-src 'self' and nothing for inline styles, the snippet still starts, connects and saves comments and screenshots, but it is unstyled: the bar sits in the flow of your page with the browser's default buttons, and comment mode keeps the default pointer. The console shows two Applying inline style violates the following Content Security Policy directive errors.

Allowing inline <style> elements is enough to fix that, and it can be done without loosening style-src itself:

text
style-src 'self'; style-src-elem 'self' 'unsafe-inline'

Style attributes stay refused: the snippet needed none in any of the flows above. style-src 'self' 'unsafe-inline' works too and is the blunter row. Hashes are not a practical way out: the console prints them, but the large Shadow DOM stylesheet can change with any release of snippet.js, and the hash would stop matching. If your policy cannot allow inline <style> elements at all, the snippet stays unstyled on that site today.

What the snippet sees

  • The snippet sends no cookies to critlab, critlab sets none, and the snippet never reads yours. It runs only with link access: a password or members-only project cannot work over it, because that needs a cookie.
  • When a reviewer comments, critlab stores a screenshot of the viewport, a crop of the element they clicked, a snapshot of the page's structure with form values removed, the element's selector and text, the page's path and the reviewer's name. The path is kept without its query string.
  • When a visitor chooses Follow on someone in the room, the snippet shares the visitor's page, as a path without its query string, and their scroll position with the room, and moves them along with the presenter's path and scroll. Clicks and typed input are never shared. A change of path loads the new page on the follower's side, and the follow ends there: the follower chooses Follow again. Scrolling, hash-router routes and changes that keep the same path follow without a reload.
  • The snippet does not read what anyone types. Do not review against real customer data: anything visible on the page can end up in a screenshot. Use staging data.

Single-page apps

A client-side router that changes the address with history.pushState needs nothing: the snippet follows the route. If your code replaces the whole document, call window.critlab.destroy() first. It removes the critlab bar and every listener the snippet added, and it is the only function the snippet offers a page.

Host the snippet yourself

If your policy allows only your own scripts, serve a copy of snippet.js from your origin, byte for byte so the integrity hash still matches, and tell it where critlab is:

html
<script src="/vendor/critlab-snippet.js"
        data-critlab-key="sk_..."
        data-api-base="https://critlab.app"
        defer></script>

connect-src and img-src still have to allow the critlab origin, because the API calls and the screenshots do not move.

Rotate the key

If a key ended up somewhere it should not be, rotate it: Rotate key in the project's details, crit site <slug> --rotate-key, or critlab_update_site with rotateKey. Every old script tag stops at its next page load. The project's link and its comments stay. Because the origins are the real gate, a copied key does nothing on a page you did not list.

When the beta ends

The snippet asks critlab on every page load whether the workspace may use external projects. That is how a downgrade stops it, and it is what will happen to a project on a free workspace on the day the beta ends, unless its plan includes external projects by then. Your site is not affected: the page loads as before and the console shows the line from the table above.

Which plan includes external projects after the beta, and what happens to existing ones on that day, is decided together with pricing. It is not decided yet, and this page will say when it is.

Self-hosted installs

On a self-hosted install the licence decides, not the beta: the key has to name staging_snippet. See Licence. Everything else on this page is the same, with your own origin in place of critlab's.

What the snippet does not do

Spotlight, password or members-only access and anchoring into your own nested iframes are not available on a connected site. Images from other sites that do not send CORS headers can show as grey boxes in a screenshot, and a comment is never blocked because its screenshot could not be taken. The security overview covers the rest.