## Prerequisites

- **Rust** (latest stable) — [rustup.rs](https://rustup.rs)
- **Dioxus CLI** — `curl -sSL http://dioxus.dev/install.sh | sh`
- **Bun** — [bun.sh](https://bun.sh), for Tailwind CSS
- **Docker** — for PostgreSQL and Mailpit
- **just** — `cargo install just`

## Quick start

This page builds Thermite from source, which is what you want to work *on* it. To just run it,
pull the image instead: [Self-hosting](/docs/getting-started/self-hosting).

1. **Clone the repository**
```bash
git clone https://github.com/hauju/thermite-rs.git
cd thermite-rs
```

2. **Bootstrap your env file**
```bash
just bootstrap
```

Copies `.env.example` to `.env` and fills in a freshly generated `SESSION_SECRET`. It refuses
to overwrite an existing `.env`.

The defaults work as-is for local development. Set `THERMITE_ADMIN_EMAIL` and
`THERMITE_ADMIN_PASSWORD` and that is your sign-in — no identity provider to stand up. An
external one is optional; see [Authentication](/docs/guides/authentication).

3. **Start the infrastructure**
```bash
just init
```

Brings up **PostgreSQL 18** on `5433` and **Mailpit** with SMTP on `1028` and its web UI on
`8028`. The ports are offset off the defaults so the stack runs alongside other projects'
containers, and every one is bound to `127.0.0.1` — a dev database with default credentials is
never reachable from the network.

Migrations live in `migrations/`, are embedded in the binary, and apply automatically on first
boot. There is no separate migrate step.

4. **Install frontend dependencies**
```bash
bun install
```

5. **Run the development server**
```bash
just serve
```

Thermite comes up on `http://localhost:8080`, hot-reloading on change and compiling Tailwind
as it goes.

## Signing in

Thermite signs people in itself by default. Set the operator credential and there is nothing
else to run:

```bash
THERMITE_ADMIN_EMAIL=you@example.com
THERMITE_ADMIN_PASSWORD=a-long-random-one
```

The password is hashed once at boot and never stored — the value in the environment is the only
copy. Your user row is created on the first successful sign-in, and once you are in you can
enroll a passkey and stop typing it. With SMTP configured, the login page also offers an emailed
code, which is a way in on its own, so an instance may set SMTP and no admin credential.

Setting `FERRISKEY_URL` switches authentication to [FerrisKey](https://ferriskey.dev) OIDC
instead, for a deployment that already runs an identity provider — see
[Authentication](/docs/guides/authentication). That variable is the whole switch; there is no
mode to set.

> **Warning:** An instance with none of these refuses to boot, naming what to set. A server nobody can log into
is a misconfiguration, not a deployment.

> **Note:** **The first account owns everything.** With no allowlist configured, only the first account may
register on a fresh instance, and registration closes as soon as any user exists. Register
yours before you expose the instance to anything.

## Send it a first event

Create a project in the dashboard, copy its DSN from **Project settings → DSN keys**, and point
any Sentry SDK at it. Thermite speaks Sentry's wire protocol, so there is nothing to install
beyond the SDK you would have used anyway — see the [SDKs guide](/docs/guides/sdks).

## Project commands

| Command | Description |
| --- | --- |
| `just serve` | Development server with hot reload |
| `just init` | Start Postgres and Mailpit |
| `just check` | Everything CI runs: `fmt`, `clippy`, `test` |
| `just test` | Workspace tests, plus the app crate under its `server` feature |
| `just clippy` | `cargo clippy --workspace --all-targets -- -D warnings` |
| `just prepare` | Regenerate the committed sqlx query metadata |
| `just tw` | Build Tailwind CSS manually |
| `just icons` | Re-render the favicon and PWA icons from their SVG sources |

> **Warning:** The app crate's default feature builds the wasm client, so plain `cargo test` does not test the
server. Anything server-side needs `--no-default-features --features server`, which is what
`just test` does for you.

## Environment variables

### Core
- `DATABASE_URL` — PostgreSQL connection string
- `DATABASE_MAX_CONNECTIONS` / `DATABASE_INGEST_MAX_CONNECTIONS` — pool sizes. Ingest runs on
  its own pool so an error storm backpressures the SDKs instead of stalling logins and the
  dashboard; background sweepers get a separate 2-connection pool.
- `BASE_URL` — public URL of the instance. Also the OAuth issuer and resource identifier
  advertised to MCP clients, so in production this must be the public HTTPS URL.
- `SESSION_SECRET` — 64+ byte hex secret for signing session cookies
- `SECURE_COOKIES` — `true` in production (HTTPS)
- `TRUST_PROXY_HEADERS` — `true` behind a reverse proxy, so rate limiting trusts the forwarded
  client IP
- `THERMITE_SITE` — `true` also serves the marketing site (the landing page, `/pricing`, the
  legal pages). Off by default, which is what a self-hosted instance wants: `/` redirects to
  `/dashboard` and those pages answer `404`. The docs, `/llms.txt` and the application are
  unaffected either way.

### Authentication
- `THERMITE_ADMIN_EMAIL` / `THERMITE_ADMIN_PASSWORD` — the operator credential for local
  sign-in. Set both or neither; the password is hashed at boot and never stored.
- `FERRISKEY_URL` — _(optional)_ FerrisKey base API URL. **Setting it is what selects
  FerrisKey OIDC** instead of local sign-in; unset, Thermite signs people in itself.
- `FERRISKEY_ISSUER_URL` — _(optional)_ public OIDC issuer base URL, falls back to
  `FERRISKEY_URL`
- `FERRISKEY_REALM` / `FERRISKEY_CLIENT_ID` / `FERRISKEY_CLIENT_SECRET` — required when
  `FERRISKEY_URL` is set
- `THERMITE_ALLOWED_EMAILS` / `THERMITE_ALLOWED_EMAIL_DOMAINS` — who may self-register
- `DEV_LOGIN` — a local-only login shortcut. Compiled out of release builds entirely.
- `ENCRYPTION_KEY` — _(optional)_ 32-byte base64 key for AES-256-GCM

### Email (SMTP)
- `SMTP_HOST` / `SMTP_PORT` / `SMTP_FROM` / `SMTP_USER` / `SMTP_PASSWORD`
- `SMTP_SECURITY` — `tls` (465), `starttls` (587), or `none`. Unset means `none` for
  localhost/Mailpit and `tls` everywhere else.

Optional. `SMTP_HOST` is the switch: with it unset, alerting delivers to webhooks only and
the login page offers no emailed code. FerrisKey mode requires it, because its own login
mails a verification code.

### Ingest tuning
- `THERMITE_MAX_ENVELOPE_BYTES` — hard cap on a request body, before decompression
- `THERMITE_RATE_LIMIT_PER_MINUTE` — per-project ingest quota, events per rolling minute
- `THERMITE_SCRUB_FIELDS` — extra payload keys to scrub on top of the built-in denylist.
  Scrubbing happens before the event is stored, so these values are never written.

### Retention
- `THERMITE_RETENTION_DAYS` / `THERMITE_MAX_EVENTS_PER_PROJECT` — events are dropped by
  whichever rule bites first. Age alone does not bound disk (a traffic spike fills the window),
  so the per-project cap is the rule that actually protects you.
- `THERMITE_RETENTION_BATCH` — rows per delete statement, so a sweep cannot hold locks long
  enough to stall ingest

Rollups, issue rows and agent analyses survive eviction, so counts and charts stay true after
the individual payloads are gone.

### Public demo
- `THERMITE_DEMO_PROJECT` — slug of one project anyone may read **without signing in**: its
  board and its issues, stack traces, breadcrumbs and user context included. The DSN and
  alert routing stay hidden, and nothing can be changed. Point it only at a project that
  receives synthetic or public data — the instance keeps it alive by raising a playground
  event into it every 10 to 30 minutes, and recreates and seeds it if it goes missing.
- `THERMITE_DEMO_URL` — where the landing page's "See the live demo" button goes when the demo
  lives on another instance. Unset: the `THERMITE_DEMO_PROJECT` board here.
- `THERMITE_DEMO_AUTOLOGIN` — `true` makes `GET /demo` sign anyone in as the shared `demo`
  user, with write access to every project. A public sandbox: run it on a throwaway
  instance only. Links to it preview with the demo share card, and every page is `noindex`.
- `THERMITE_WAITLIST` — `true` puts hosted signup behind a waitlist: the landing and pricing
  pages collect addresses (into the `waitlist` table) instead of sending visitors to a closed
  registration.

### Web analytics
- `UMAMI_HOST` — base URL of your own [Umami](https://umami.is) instance. The tracker is
  proxied from your origin (`/stats.js` and `/api/send`), because the ad-block lists carry
  `umami.*` hostnames and a cross-origin tag would be dropped for a large share of visitors
  without anything looking broken.
- `UMAMI_WEBSITE_ID` — the id Umami issued for this site, written into the page's tracker
  tag at render time.

Both are read at runtime, and with either unset the page carries no tracker and the server
calls nothing: the published image is the same one every self-hoster pulls.

### Alerting
- `THERMITE_ALERT_EMAIL` — comma-separated recipients
- `THERMITE_ALERT_WEBHOOK` — one JSON object POSTed per alert

Instance-wide defaults; a project can override both with its own routing. With neither set,
alerting is off.

### Self-reporting
- `THERMITE_DSN` — Thermite watches itself with its own SDK. Point this at a **different**
  Thermite, or at least a different project: an instance reporting its own ingest failures
  into the ingest that is failing loses exactly the events worth having.
- `THERMITE_RELEASE` — the deployed git SHA. Triage hands an agent the release an error came
  from, and `git diff` only works when that name is a revision.
- `ENVIRONMENT` — environment tag

## Changing the database schema

Queries use `sqlx`'s compile-time-checked macros, so column names and types are verified against
the schema when you build. The metadata lives in `.sqlx/` and is committed, which is what lets a
fresh clone build with no database running.

> **Warning:** After editing any SQL or adding a migration, regenerate the metadata:

```bash
just init      # a database must be running
just prepare
```

Editing a query without re-preparing fails the next build. Editing a **migration** without
re-preparing does not — cached entries are keyed by the query text, so untouched queries keep
matching now-stale metadata and compile fine, failing only at runtime. CI guards this by building
against a real database.

> **Note:** Migrations are immutable once applied: sqlx records a checksum, so editing an applied file — even
a comment — fails the next boot. Add a new numbered migration instead.

