Skip to content

Self-Hosting

One environment variable is required:

  • PUBLIC_HOSTNAME — your domain, without https:// (e.g. comments.example.com)

This value is baked into the embed script at build time and used to construct OAuth redirect URIs and the OAuth client metadata served at /oauth-client-metadata.json.

A pre-built image is available on the GitHub Container Registry:

Terminal window
docker run -d \
-e PUBLIC_HOSTNAME=comments.example.com \
-p 3000:3000 \
ghcr.io/matteomarjanovic/juttu:latest

ORIGIN is automatically derived as https://$PUBLIC_HOSTNAME. To override it explicitly:

Terminal window
docker run -d \
-e PUBLIC_HOSTNAME=comments.example.com \
-e ORIGIN=https://comments.example.com \
-p 3000:3000 \
ghcr.io/matteomarjanovic/juttu:latest

To build the image yourself instead of pulling the pre-built one:

Terminal window
docker build -t juttu ./juttu
docker run -d -e PUBLIC_HOSTNAME=comments.example.com -p 3000:3000 juttu

Deploy it like any SvelteKit adapter-node app:

  1. Clone the repository.
  2. Create juttu/.env and set PUBLIC_HOSTNAME:
    PUBLIC_HOSTNAME=comments.example.com
  3. Install dependencies and build:
    Terminal window
    cd juttu && npm install && npm run build
  4. Start the server:
    Terminal window
    node build

Set ORIGIN=https://<your-domain> if adapter-node complains about cross-origin requests.

After deploying, update your embed snippet to point at your own domain instead of juttu.app:

<script
defer
src="https://comments.example.com/embed/juttu-embed.min.js"
data-bsky-user-handle="your-handle.bsky.social"
data-article-id="your-article-slug"
></script>

Juttu includes optional, minimal telemetry. It is disabled by default — no data is ever sent unless you explicitly configure an endpoint.

To enable it, set PUBLIC_ANALYTICS_ENDPOINT to the URL of an HTTP endpoint that accepts POST requests with a JSON body:

PUBLIC_ANALYTICS_ENDPOINT=https://your-analytics-server.example.com/event

To opt out entirely: do not set PUBLIC_ANALYTICS_ENDPOINT. If the variable is absent, no analytics code runs.

EventTrigger
page_viewWidget iframe is loaded
likeA user likes a comment
unlikeA user removes a like
repostA user reposts a comment
unrepostA user removes a repost
replyA user posts a reply or root comment

Each request body:

{
"event": "page_view",
"userHandle": "site-owner.bsky.social",
"timestamp": "2026-01-01T00:00:00.000Z"
}

userHandle is the handle of the site owner who embedded the widget. It is never the handle of individual commenters.

Requests are fire-and-forget — they never block the UI and errors are silently ignored.