sessclone

Configuration

Every variable the deployment and the Collector read, and what each one is for.

The three things a self-hoster supplies are reached by plain configuration, with no provider in the code path: the database is a Postgres URL, storage is any S3-compatible endpoint, and the public base URL is whatever the deployment answers on. Auth is the exception, and is named as one — it is Supabase Auth.

.env.example in the repository is the same list in copyable form. Real values live in .env, which is gitignored, or in your deployment's own secret store.

The server's .env belongs in apps/web

Next.js reads environment files from its own project root, so a .env at the repository root is read by nothing when you run the app directly, and the first page that needs one fails with NEXT_PUBLIC_SUPABASE_URL is not set. Docker Compose is the exception: it reads the root .env.

Server

Database

VariableRequiredWhat it is
DATABASE_URLyesEverything the dashboard reads. Any Postgres; Supabase is not a requirement
INGEST_DATABASE_URLyesWhat the ingest route writes as. The owning role, not sessclone_app

Two variables, because they are two roles. Point DATABASE_URL at sessclone_app, a role that owns nothing. Postgres applies no row-level security to a superuser or to the role that owns the tables, so a dashboard connected as the role that applied the migrations has every policy switched off and no error to say so. Ingest writes tables sessclone_app has no insert grant on, so it gets the owning role through its own variable, and the ingest route is the only code that reads it.

Neither has a default, and neither falls back to the other. A missing one throws DATABASE_URL is not set (or INGEST_DATABASE_URL is not set) on the first request that needs it. A silent fallback would make a misconfigured deployment — both pointed at the owning role, row-level security off — appear to work.

Supabase (sign-in)

VariableRequiredWhat it is
NEXT_PUBLIC_SUPABASE_URLyesProject URL. NEXT_PUBLIC_ because the browser needs it
NEXT_PUBLIC_SUPABASE_ANON_KEYyesAnon key. Public by design; row-level security protects the rows, not this key

Sign-in is GitHub OAuth and a magic link; no password is created or stored. Both are configured in the Supabase project: GitHub needs a client id and secret under Authentication, and both need <NEXT_PUBLIC_APP_URL>/auth/callback in the project's allowed redirect URLs.

The Supabase service role key is not needed: no code reads it. Ingest writes through INGEST_DATABASE_URL instead, so leave the service role key out of the deployment's environment entirely — a key that bypasses every policy is safest where nothing can read it.

The magic-link email is sent by Supabase and configured in the Supabase project's own SMTP settings. The invitation email and the sign-up note are the app's own, below.

Email (SMTP)

VariableRequiredWhat it is
SMTP_URLnoConnection URL, e.g. smtp://user:pass@smtp.example.com:587 or smtps://…:465
SMTP_FROMnoFrom address on the invitation, e.g. SessClone <no-reply@example.com>

Both or neither. With them set, inviting someone emails them the join link; without, the inviter is given the link to pass on themselves — the same link the email would carry. smtp:// uses STARTTLS when the server offers it; smtps:// is TLS from the first byte.

The same two variables send the platform admins a note each time somebody signs up and creates an Org waiting for approval, with a link to that Org under /admin → Orgs. Unset, nothing is sent; the Admin panel lists waiting Orgs first and counts them on its navigation link either way.

Sign-up approval

VariableRequiredDefaultWhat it is
SIGNUP_APPROVALnoonoff lets a new Org in without a platform admin approving it

On by default, self-hosted deployments included. An Org whose subscription is inactive — where every sign-up starts, on the plan it picked — or which has no subscription row, or is cancelled, is locked: the dashboard shows only a waiting page and Sign out, no key can be created, and ingest answers its existing keys with the same 401 as any unknown key. past_due is not locked; it keeps its notice and works. A platform admin approves an Org by setting it active under /admin → Orgs, where waiting Orgs are listed first.

Any value other than off (case-insensitive) leaves it on. With it off, every status behaves as it did before the lock: a notice on every page, collection working. Read per request, so a restart is enough to change it.

Public base URL

VariableRequiredWhat it is
NEXT_PUBLIC_APP_URLyesOrigin this deployment answers on, e.g. https://sessclone.example.com. No trailing slash

Used to build the sign-in redirect, invite links and the install instructions a Member is shown, so your team is told to report to your deployment. It is not derived from request headers: a forwarded Host is attacker-controllable, and an invite link is a credential.

Public pages (optional)

VariableRequiredWhat it is
SEARCH_INDEXINGnoon lets search engines crawl the public pages. Anything else blocks every crawler
NEXT_PUBLIC_CONTACT_EMAILnoAddress the public pages offer for contact. Unset, contact links are hidden
NEXT_PUBLIC_CLOUDFLARE_ANALYTICS_TOKENnoCloudflare Web Analytics site token. Cookieless; marketing pages and docs
NEXT_PUBLIC_CLARITY_PROJECT_IDnoMicrosoft Clarity project id. Marketing pages only, never the dashboard

A self-hosted copy stays out of search results and loads no analytics unless you set these. Canonical links, the sitemap and llms.txt are built from NEXT_PUBLIC_APP_URL. Clarity asks visitors in Europe first and does not load if they decline. The NEXT_PUBLIC_ variables are read at build time, so a change needs a redeploy.

The Privacy and Terms pages describe the hosted service at sessclone.com: its providers and its analytics. Replace them with your own before you open a deployment to people outside your team.

Storage

Transcripts go straight to storage through a presigned PUT; the application never carries the bytes. Only the S3 API is used, so Supabase Storage, Cloudflare R2, AWS S3, Oracle Cloud Object Storage and MinIO are the same code path. With these unset, archival is off and everything else works.

VariableRequiredDefaultWhat it is
STORAGE_ENDPOINTfor archival—S3 API endpoint URL. The provider's S3 endpoint, not its dashboard
STORAGE_REGIONnoautoRegion. auto suits R2 and Supabase; AWS needs the real one, e.g. eu-west-2
STORAGE_BUCKETfor archival—Bucket holding transcripts. Must not be public: reads are presigned
STORAGE_ACCESS_KEY_IDfor archival—Access key id
STORAGE_SECRET_ACCESS_KEYfor archival—Secret access key
STORAGE_FORCE_PATH_STYLEnotruePath-style addressing. Required by MinIO and Supabase; AWS accepts it
STORAGE_PRESIGN_TTL_SECONDSno300Life of an issued URL. A presigned URL is a bearer credential — keep it short

Self-hosting has the script that checks a bucket speaks every part of the S3 API this uses, and the CORS rule the transcript viewer needs. That script reads the same variables, plus STORAGE_COMPAT_CREATE_BUCKET=true to create the bucket before checking it.

Retention sweep

VariableRequiredWhat it is
RETENTION_SWEEP_SECRETnoShared secret for POST /api/retention/sweep. Unset means the route refuses every call

Retention is a window per Org, in days, set by an Owner or an Admin under Org settings and capped by the Org's Tier. Nothing enforces it on a schedule, because your scheduler is yours: POST /api/retention/sweep with Authorization: Bearer <secret> removes transcripts past their window — the rows and the stored objects together, oldest first, at most 500 per call — and answers { removed, remaining } so a backlog can be drained by calling again. Turns are never touched: the spend history survives every transcript it describes.

Set the windows before the first sweep

Upgrading an existing deployment sets every Org to 90 days, which no Org chose. The first sweep on a deployment that has been collecting for longer destroys every transcript older than that. Set the windows first — update orgs set retention_days = 365; or per Org — then configure the secret.

The route answers 401 for a wrong secret and 503 when the secret is unset, when storage is not configured, or when the sweep failed (which rolls the rows back).

Published pricing

VariableRequiredWhat it is
PRICING_URLnoMarkdown pricing page, e.g. https://platform.claude.com/docs/en/about-claude/pricing.md
PRICING_REVALIDATE_SECRETnoShared secret for POST /api/pricing/revalidate. Unset means the route refuses every call

PRICING_URL is read by Fetch latest pricing on /admin/rates. Nothing fetched is written until a platform admin approves it.

The public pricing pages cache the Tier table. Saving a Tier in the admin panel clears that cache itself; POST /api/pricing/revalidate with Authorization: Bearer <secret> is for a Tier changed outside the panel. The cache is per instance, so several instances or a serverless deployment each refresh on their own schedule unless you add a shared cache handler.

Docker Compose

Read by compose itself, from the .env at the repository root.

VariableRequiredDefaultWhat it is
WEB_PORTno3000Host port the dashboard is published on. Deliberately not PORT
POSTGRES_PASSWORDwith db only—Password for the sessclone role in the bundled Postgres (--profile db)
POSTGRES_PORTno5432Host port the bundled Postgres is published on, bound to loopback

WEB_PORT is not PORT because next start reads PORT as the port it listens on inside the container, and the same file is handed to both.

Collector

The Collector reads its configuration from the environment, or from the answers given when the plugin was enabled, so your team can point at your deployment without editing the plugin. An exported variable wins over the answer.

VariableRequiredDefaultWhat it is
SESSCLONE_URLin practicehttp://127.0.0.1:3000Base URL of the deployment. Matches the server's NEXT_PUBLIC_APP_URL
SESSCLONE_API_KEYyes—The Member's API key. Identifies the Member and the Org
SESSCLONE_STATE_DIRnoper platform, belowWhere the cursors and the retry queue are kept
SESSCLONE_DEVICEnoderivedPins this environment's Device key instead of deriving one
SESSCLONE_DEBUGno—Set to anything to print a hook's swallowed failure to stderr. Off, a hook is silent

SESSCLONE_URL is required by anyone whose deployment is not on their own laptop — which is everyone — but the code falls back rather than refusing, and says nothing about it. A value that is not an http or https URL is refused at session start.

The Device key is normally host:<hostname> on a machine and cloud:<account uuid> in Claude Code on the web. Set SESSCLONE_DEVICE only when one account runs several environments that should be counted separately — a CI fleet beside a laptop, say. The value is used verbatim, so it also pins an identity across a rename.

The state directory deliberately never lands under ~/.claude, because Claude Code's own cleanupPeriodDays sweep deletes under ~/.claude/projects/ after 30 days by default and would take the cursors and the queue with it:

PlatformDefault state directory
Linux$XDG_STATE_HOME/sessclone, else ~/.local/state/sessclone
macOS~/Library/Application Support/sessclone
Windows%LOCALAPPDATA%\sessclone, else %USERPROFILE%\AppData\Local\sessclone

Only the Linux row has been observed on a real machine so far; treat the other two as the intended behaviour.

HTTPS_PROXY (or https_proxy) is honoured: when one is set, the Collector restarts its hook once with NODE_USE_ENV_PROXY=1 so Node's fetch goes through the proxy. That is what a Claude Code cloud environment needs, since its egress proxy is what adds the credential. You never set NODE_USE_ENV_PROXY yourself.

CLAUDE_CONFIG_DIR is Claude Code's variable. The Collector reads it — CLAUDE_CONFIG_DIR, else ~/.claude, exactly as Claude Code does — to find transcripts, and never writes inside it.

Not configured here

  • Rates. Prices ship as reviewed migrations and are edited in the platform admin area, never read from the environment. A price in an env var is a price nobody reviewed.
  • Tiers and retention windows. Rows in the database, set per Org.
  • The Org timezone. An Org setting, because two Orgs on one deployment bucket their days differently.

On this page