Quickstart
Run the Viewer
Get a self-hosted Desde Viewer running locally, from install to your first project.
The Viewer is a single Node process that hosts built prototypes, wraps them in a review shell, and collects comments. You run it yourself. There is no hosted service. This page takes you from a fresh checkout to a working project you own.
Budget about ten minutes. The GitHub App can be created from inside the product in a couple of clicks; registering one by hand is the fallback, and that is what takes the time.
Note The Viewer has never been used by an external team. Treat it as software you run on a trusted network: behind a VPN, or on a machine only your team can reach. Rate limiting covers only the unauthenticated write lanes (sign-in, comments, resolve, invites), and the build runner executes whatever code is in the repository you connect.
Before you start#
- Node 24 or newer. Storage uses Node's built-in
node:sqlite, which does not exist in earlier versions. Check withnode --version. giton your PATH. The build runner clones repositories with it.- A GitHub account you can register an App under.
- A checkout of the Desde monorepo. Nothing is published to npm yet, so the Viewer runs from source.
Install#
The Viewer lives in viewer/ but it is not self-contained. You need two installs.
npm installnpm install --prefix viewerWarning The root install is not optional.
nextandreactlive in the rootpackage.json, not inviewer/package.json. The Viewer's Next.js dashboard resolves them by walking up fromviewer/to the checkout root. Skipping the root install leavesnextunresolvable and the process fails to start.
Build the bridge#
The bridge is the small script the Viewer injects into every prototype it serves: it draws the comment pins, resolves which element a click landed on, and talks to the review shell. It is built once from source.
npm run build:bridgeThat writes dist/bridge-bundle.js at the repo root. The Viewer reads it at boot, unguarded. If the file is missing, the process dies immediately with a raw filesystem error and no hint about the fix:
[viewer] failed to start: Error: ENOENT: no such file or directory, open '.../dist/bridge-bundle.js'If you see that, you skipped this step.
Register a GitHub App#
One GitHub App does two jobs: it signs people in, and it gives the Viewer read access to the repositories you want to build. There is no separate OAuth App.
The quick way, from inside the product#
You do not have to fill in the form below by hand. Boot the Viewer first (see Run it), sign in with the one-time local URL it prints, then go to Settings → GitHub and choose Create GitHub App. That sends a pre-filled manifest to GitHub, GitHub creates the App, and the credentials come back and are saved for you. Nothing is copied into .env.local, and the process does not need restarting.
That is the path to take on a fresh deployment. The rest of this section is the manual fallback.
The manual way#
Register the App by hand when the quick way is unavailable to you. That means GitHub Enterprise, an organization locked down enough to refuse the manifest flow, or a deployment that already has GitHub sign-in configured. In that last case there is no local operator any more, so the admin bearer is the only credential left, and a bearer cannot ride the browser navigation GitHub sends the callback through.
Go to github.com/settings/apps and choose New GitHub App:
- Homepage URL: the origin the Viewer will be reachable at, for example
http://localhost:3100. - Callback URL: that same origin plus
/api/v1/auth/github/callback. - Account permissions → Email addresses: Read-only. Required. Sign-in falls back to reading the user's email list when their GitHub profile has no public email; without this permission, those people cannot sign in at all.
- Repository permissions → Contents: Read-only. Enough to list and clone. Metadata read access is granted implicitly.
- Webhook: leave it inactive for now. It is only needed for push-triggered builds.
Then, on the App's settings page:
- Generate a private key and download the
.pem. - Note the App ID (a number).
- Note the App slug: the last segment of
github.com/apps/<slug>. - Note the App's own Client ID and Client secret. These are what sign-in uses.
Finally, install the App on the account or organization whose repositories you want to build.
Note A signed-in person sees only the App installations their own GitHub account can see, and only the repositories inside them that GitHub grants them personally. The Viewer derives both sets from that person's own sign-in, never from the request, and it answers "not yours" and "does not exist" with the same 404.
The exception is
VIEWER_ADMIN_TOKEN, which bypasses this filter entirely. See the note on it below.
Configure#
Copy the template and fill it in. .env.local is gitignored.
cp viewer/.env.example viewer/.env.local| Variable | Default | What it does |
|---|---|---|
PORT |
3100 |
HTTP port. |
VIEWER_PUBLIC_URL |
http://localhost:$PORT |
The origin the Viewer is reachable at. Must be an absolute http(s) URL and must match the App's callback host exactly, or the sign-in redirect never comes back. It is also the origin the injected bridge posts messages to. |
VIEWER_DATA_DIR |
.desde-viewer |
Where the SQLite database and built prototype assets live. See the warning below. |
VIEWER_GITHUB_CLIENT_ID |
unset | The App's client ID. |
VIEWER_GITHUB_CLIENT_SECRET |
unset | The App's client secret. |
VIEWER_SESSION_SECRET |
unset | Random string used to sign the session cookie. Generate with openssl rand -hex 32. Changing it invalidates every existing session. |
VIEWER_GITHUB_APP_ID |
unset | The App ID. |
VIEWER_GITHUB_APP_PRIVATE_KEY |
unset | The private key, either as a literal PEM or base64-encoded on one line. Parsed and validated at boot. |
VIEWER_GITHUB_APP_SLUG |
unset | The App slug, used to build install links in the UI. |
VIEWER_ADMIN_TOKEN |
unset | An operator bearer token. Optional (see below). |
VIEWER_ALLOWED_EMAIL_DOMAINS |
unset | Comma-separated domains, seeded once into stored domain rules at boot. An admin manages the domain list from Settings after that; the variable itself is then ignored. See members and tokens. |
Two of these groups are all-or-nothing. Setting any one of VIEWER_GITHUB_CLIENT_ID / VIEWER_GITHUB_CLIENT_SECRET / VIEWER_SESSION_SECRET makes all three required, and the same is true for VIEWER_GITHUB_APP_ID / VIEWER_GITHUB_APP_PRIVATE_KEY / VIEWER_GITHUB_APP_SLUG. A missing member throws at boot and names the variable rather than starting half-configured.
The two groups are independent of each other. Sign-in works with no App keys set. You just cannot connect a repository.
About VIEWER_ADMIN_TOKEN. It is an unscoped bearer that reaches every project regardless of membership, and it bypasses the per-user installation filter. You do not need it for the flow on this page, and the flow below is better without it. If you set it, treat it as equivalent to the App private key and do not hand it to reviewers.
The full variable list, including SMTP for mention emails and the prototype content-security-policy, is in viewer configuration.
Warning
VIEWER_DATA_DIRdefaults to the relative path.desde-viewer, which resolves against whatever directory the process was started from. Launching fromviewer/and launching from the repo root silently produce two different databases and two different asset trees, with no warning. Set it to an absolute path if you are ever going to start it from more than one place.
Run it#
cd viewer && npm run dev:localOpen http://localhost:3100.
Warning Use
dev:local, notstart:local.start:localsetsNODE_ENV=production, which makes the process serve the prebuilt.nextdirectory instead of compiling on demand. If that build is stale or was never made, pages and routes are silently missing. You get a 404 on a page that exists in the source.start:localis for a deployment where you have just runnpm run build.
On a successful boot you get one line naming the profile, the bridge version it read, and the public URL:
[viewer] profile=selfhost bridge=<version> → http://localhost:3100If the bridge version reads unknown, the file at dist/bridge-bundle.js is not a bundle this Viewer recognizes. Rebuild it.
Create your first project#
The Viewer is invite-only, but a brand-new instance handles this for you. The first person to sign in becomes Admin automatically. There is no separate step to claim ownership of the deployment.
1. Sign in#
Open the Viewer and sign in with GitHub. This creates your user account. Since you're the first account on this instance, you're an Admin.
2. Create the project#
Click New project on the dashboard. Give it a name, confirm the slug, then press Create.
The project lands with access: all-members, the default, so every member of this instance can already open it. There's no ownership dance: any Editor or Admin can manage any project they can read, and an Admin can manage every project regardless of its access setting. See members and tokens for the three access levels.
Tip Before you connect a repository, set a relative build base if your bundler supports one (Vite:
base: './'; Create React App:"homepage": "."). Most bundlers default to root-absolute asset paths, which can break a prototype for signed-in members under the default path serving. If you skip this, the Viewer scans every build and shows a warning naming the same fix (see Serving for why it matters).
The dashboard takes you straight to the repo-connect wizard next. The project's id is also in the URL and the API response, if you need it to connect the Editor.
From the API instead#
The same thing works from a script, with a write-scoped dsv_ token in place of a browser session:
Mint one at /settings: tick the write scope, since the form defaults to read-only.
curl -X POST http://localhost:3100/api/v1/projects \
-H "Authorization: Bearer dsv_your_token_here" \
-H "Content-Type: application/json" \
-d '{"slug":"acme-checkout","name":"Acme Checkout"}'A 201 comes back with the created project, including its id. The slug becomes the URL the prototype is served at (/p/acme-checkout/); it must be 2 to 63 characters, lowercase letters, digits and hyphens, starting with a letter or digit. name is free text. Only an Editor or an Admin can do this; a Viewer-role token gets 403.
What to do next#
- Connect a repository and build it: the review page's repo panel takes an installation, a repository, a branch, and the install/build commands. See builds.
- Invite people to the instance: an invite link works with no setup, from
/settings→ Members. See members and tokens. - Use it with the Editor: see Editor and Viewer together.
Known limits#
- Prototypes share the Viewer's origin by default, contained by a content-security-policy. Full origin isolation needs
VIEWER_SERVE_DOMAIN, wildcard DNS and a wildcard certificate (see serving). - Rate limiting covers only sign-in, comment writes,
POST /projects/resolveand participant invites. The rest of the API is unthrottled. - There is no built-in backup or retention. Old deployments are not pruned, and the SQLite database is not backed up for you.