Welcome to a calmer internet
# CLAUDE.md
Guidance for AI agents working in this repository. Keep this file lean â it loads
into every session. Deep architecture lives in `ARCHITECTURE.md` (read it when a
change touches the window/service/state/IPC model).
This is **Streamlabs Desktop** (`slobs-client`): an Electron liveâstreaming app
built on OBS. Large, old, and midâmigration on two fronts (Vue â React for UI,
Vuex â Realm/React for state), with several nonâobvious invariants. Read the
relevant service before editing it; prefer targeted reads over broad assumptions.
## Commands
Package manager is **Yarn Berry (3.1.1)** â never use `npm`.
| Task | Command | Notes |
| ------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Lint + format | `yarn eslint` | Prettier runs _through_ ESLint (`eslint-plugin-prettier`). `eslint --fix` formats. |
| Typecheck | `yarn typecheck` | **Fast self-verify.** `tsc --noEmit` for the app + React. Use this to check changes instead of a full build or the e2e suite. |
| Iterative dev build | `yarn watch` | Webpack watch; use this while developing. |
| Oneâshot dev build | `yarn compile` | Slow: clears `bundles/media` and rebuilds everything. Don't run casually. |
| Run the app | `yarn start` | Launches Electron against the last build. |
| Single test file | `yarn test:file <path>` | Compiles tests, runs one file. |
| Full test suite | `yarn test` | **Heavy/slow e2e** â see Testing below. Don't run unless asked. |
For a fast type check without a full build, run **`yarn typecheck`** â it runs
`tsc --noEmit` for the nonâReact app (`tsconfig.json`) and for `app/components-react`
(its stricter config, `strictNullChecks: true`). This is the quickest way to verify
your changes; reserve `yarn compile` and the e2e suite for when you actually need
them. (The build still typechecks via `ts-loader`; tests compile via `tsc -p test`.)
## Code style
- TypeScript, formatted by Prettier via ESLint. **Don't memorize the rules** â
let `yarn eslint` (`--fix`) apply them, and match the surrounding file.
- `strictNullChecks` is **intentionally OFF** globally. Do **not** enable it
repoâwide. A subset of files opts in via `strict-null-check-files/` +
`SLOBS_STRICT_NULLS`; only add to that set deliberately.
## Architecture in one screen
Multiâwindow Electron app. Every window runs the _same_ JS bundle but plays a
different role:
- **worker** â invisible, persistent renderer that runs the **entire services
layer**. All service methods actually execute here.
- **main** â the primary UI window.
- **child** â kept warm in the background for things like Source Properties.
- plus transient **oneâoff** windows (projectors, popâouts), apps, webviews.
The UI windows (main, child, oneâoff) **don't run services** â they call them
remotely. A call from any nonâworker window is sent to the **Electron main
process** (`main.js`), which forwards it to the **worker** window, then routes the
result back to the originating window. Note: the _main process_ (`main.js`, Node)
is the router â **not** the _main window_, which is just another UI client.
**Services** (`app/services/`) are strict singletons holding all domain logic.
Normal application code reaches a service through the `@Inject()` decorator. They're
registered in `app/app-services.ts`.
## State â know which mechanism to use
State management is **midâmigration**. For new code, choose by scope:
- **UIâonly state** â React state (`useState` / hooks), local to the component.
- **Service state that must sync across windows/processes** â **Realm**
(`RealmObject` / `RealmService` in `app/services/realm.ts`), which replicates
across all processes. React reads it via `app/components-react/hooks/realm.ts`.
Much existing service state still lives in **Vuex** via `StatefulService<TState>`
(read through `this.state`, mutated only via `@mutation()` methods). You'll
maintain it where it already exists, but **don't reach for Vuex for new state.**
See `ARCHITECTURE.md` for the full model and the _why_ behind the sharp edges.
## Hard rules (these are easy to get wrong)
1. **Vuex mutations are pure.** When editing an existing `StatefulService`, a
`@mutation()` method may touch only `this.state` and its own arguments â no side
effects, no calling other services, no async. In dev a Proxy enforces this and
throws (`app/services/core/stateful-service.ts:49`).
2. **Calling a service from a UI window â pick the right form:**
- `Service.actions.method()` â **default**. Async, fireâandâforget, returns
`void`.
- `Service.actions.return.method()` â async **with** a return value (resolves
when the worker finishes). Use sparingly.
- `Service.method()` (omit `.actions`) â **synchronous**; it blocks the calling
UI process and logs a console warning. Avoid except where absolutely
necessary â it degrades the user experience.
- Reads: use `views` (Vuex) or read Realm objects directly.
3. **Register new services** in `app/app-services.ts` â it's handâmaintained, not
generated. A service that isn't registered won't resolve.
4. **New UI is React.** Functional components + hooks only, in
`app/components-react/` (`.tsx`). Vue (`app/components/`, `.vue`) is legacy and
frozen â only touch it to migrate a component to React.
5. **Don't edit generated/build output:** `bundles/`, `*.g.less`, `updater/build/`,
`dist/`, `test-dist/`, `docs/dist/`.
6. **Honor inâcode warnings.** Respect `DO NOT CALL` / `@warning` markers â e.g.
`app/services/sources/sources.ts` `updatePropertiesManagerSettingsInStore`,
and the deprecated method in `app/services/video.ts`.
## Conventions
- Filenames map to classes: `foo-bar.ts` â `class FooBarService`.
- Common decorators: `@Inject()` (DI), `@mutation()` (Vuex mutation),
`@InitAfter('OtherService')` (ordered init), `@InheritMutations()`.
- Service lifecycle hooks: `init()` (once per app) â `mounted()` (once per window)
â `afterInit()`.
- Crossâservice / crossâwindow events use **RxJS** `Subject`s.
## Alert type `apiKey` field naming
When adding a new alert type to `alerts-config.ts`, the `apiKey` field must match
the **backend field prefix**, not the internal alert type name. For example,
an alert previously had the alert type `twitter_subscription` in the repository
but because the backend returns settings with keys like `twitter_sub_image_href`,
not `twitter_subscription_image_href`, it requires `apiKey: 'twitter_sub'`,
not `twitter_subscription_*`. The variation field parser in `AlertBoxModule.setData()`
uses `${apiKey}_` prefix matching (see `app/components-react/widgets/useAlertBox.tsx`
line 207). Mismatching this causes variation settings to fail initialization silently,
leading to UI freezes. Also map internal alert types to API endpoints in
`WidgetsService.playAlert()` (`app/services/widgets/widgets.ts`) if the backend uses
a different name there.
## Testing
Tests are **integration/e2e via WebdriverIO** â they launch the real Electron app
and drive it, run **serially**, and are slow. They are not fast unit tests. Run a
single file with `yarn test:file <path>` when iterating; avoid the full `yarn test`
suite (and `yarn package`) unless explicitly asked.
## Where things live
| Path | What |
| ----------------------------------------- | -------------------------------------------------------------- |
| `main.js` | Electron main process: windows, IPC routing, updater, logging. |
| `app/app.ts` | Renderer bootstrap (services, i18n, Sentry). |
| `app/app-services.ts` | Central service registry. |
| `app/services/` | All services (domain logic). |
| `app/services/core/` | Service base, `StatefulService`, DI, mutations. |
| `app/services/realm.ts` | Realmâbacked crossâprocess state. |
| `app/services-manager.ts` | Service instantiation + IPC proxying. |
| `app/services/api/internal-api-client.ts` | Client side of crossâwindow service calls. |
| `app/store/` | Vuex store + crossâwindow mutation sync (legacy). |
| `app/components-react/` | React UI (current). |
| `app/components/` | Vue UI (legacy). |
| `test/` | e2e/stress/screen/performance tests + helpers. |
## Useful env vars (dev)
`SLOBS_REPORT_TO_SENTRY`, `SLOBS_PRODUCTION_DEBUG` (open dev tools on start),
`SLOBS_CACHE_DIR`, `SLOBS_FORCE_AUTO_UPDATE`, `SLOBS_STRICT_NULLS`. See README for
the full list.