Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

JamStream is a desktop app that lets a band play together over the internet, with latency low enough to actually play.

  • One of you hosts. The app starts a small server, on your own computer or in your own cloud account, that exists only for the length of the session.
  • Everyone else joins with a link. Paste a personal invite into the app; there are no accounts and no JamStream servers in the middle.
  • Every packet is encrypted, and the server shuts itself down ten minutes after the last musician leaves.
  • The cost lands on you, not JamStream. A cloud session runs a few cents on your own provider account; nobody else is paid anything.

A four piece playing. One strip per musician, the click, and the latency in the bar.

What it costs

Sessions hosted on your own computer cost nothing. A three hour cloud session with four musicians costs about $0.08 on DigitalOcean, paid to your provider by the second. The preview shown before every launch is the number to trust; see Understanding cost.

Streaming

The host can put a session on air to Twitch and YouTube Live, either one or both at once, and one platform dropping out never interrupts the other. Stream keys are masked, never shown back, and kept in this computer's keychain. See Streaming to Twitch and YouTube.

Project status

JamStream is in beta. Download it for macOS, Windows, and Linux from the Download page.

  • The desktop app is the product. Its wizard hosts real sessions on your own computer or in your cloud account, joins you automatically, and manages the invites while the session runs.
  • Every app build bundles its own jamstreamd session server, so hosting locally needs nothing else installed.
  • Screenshots on this site are from the current build and will change.
  • The jamstream command line tool hosts, monitors, and ends the same sessions for automation, scripting, and headless use; see the CLI reference.

If something here does not match what the software does, that is a bug in one of them. Report it.

Download

Most people want the desktop app: pick your platform below, download it, and open it. Every app build bundles its own jamstreamd session server, so hosting and joining need nothing else installed.

The jamstream CLI, for terminals and automation, installs in one line at the bottom of this page.

Every link on this page points at the latest release by a stable name, so a new release updates them all in place. All artifacts are listed on the releases page.

Building from source also works on every platform: clone the repository and run cargo install --path crates/cli.

macOS

Windows

Extract the app zip and run jamstream-app.exe. The zip's other file, jamstreamd.exe, is the bundled session server: keep it beside the app, or hosting on this computer stops working.

The binaries are plain zips and are not code signed. SmartScreen may show "Windows protected your PC" the first time you run the app: click More info, then Run anyway.

Right-clicking the downloaded zip, opening Properties, and ticking Unblock before extracting clears the mark for everything inside; without that, the app zip's two exes can each raise the warning once. Verifying the download confirms you have the real file.

Linux

The session server downloads are only for hosting on your own computer with the CLI alone; the desktop app bundles its own.

Architecture availability

PlatformAppCLISession server
macOSApple silicon + Intel (universal)Apple silicon + Intel (universal)bundled in app
Windowsx86_64x86_64bundled in app
Linuxx86_64x86_64x86_64 and aarch64

No arm64 builds of the app or CLI are published yet for Windows or Linux; build from source there.

Verifying a download

Every release ships a SHA256SUMS file covering every artifact:

$ curl -fsSLO https://github.com/sean-reid/jamstream/releases/latest/download/SHA256SUMS
$ sha256sum --check --ignore-missing SHA256SUMS
jamstream-cli-linux-x86_64.tar.gz: OK

On macOS that command is shasum -a 256. In PowerShell:

irm https://github.com/sean-reid/jamstream/releases/latest/download/SHA256SUMS -OutFile SHA256SUMS
(Get-FileHash jamstream-app-windows-x86_64.zip).Hash -eq (Select-String jamstream-app-windows-x86_64.zip SHA256SUMS).Line.Split(' ')[0]

True means the file matches. Get-FileHash prints uppercase and the sums file is lowercase; PowerShell's -eq ignores case, so that does not matter.

Package managers

Homebrew (macOS and Linux) and Scoop (Windows) are live, and both follow new releases, so an upgrade comes from the package manager rather than from this page. winget and the AUR are planned.

$ brew install --cask sean-reid/jamstream/jamstream   # desktop app
$ brew install sean-reid/jamstream/jamstream-cli      # CLI, with completions
scoop bucket add jamstream https://github.com/sean-reid/scoop-jamstream
scoop install jamstream-app   # desktop app, with a Start Menu shortcut
scoop install jamstream       # CLI

Install the CLI in one line

The CLI suits scripts, automation, and machines without a display; the CLI reference documents every command.

On macOS and Linux:

curl -fsSL https://sean-reid.github.io/jamstream/install.sh | sh

The script:

  • Detects your platform, downloads the matching archive, and verifies its sha256 against SHA256SUMS.
  • Installs jamstream to /usr/local/bin when that is writable, otherwise to ~/.local/bin. Set JAMSTREAM_INSTALL_DIR to pick the directory yourself.
  • With -s -- --with-server, also installs the jamstreamd session server on Linux x86_64, which local mode with the CLI alone uses.
  • With -s -- --tag v0.2.0, installs that release instead of the newest one.

Uninstalling is the same shape:

curl -fsSL https://sean-reid.github.io/jamstream/uninstall.sh | sh

It removes what install.sh installed and nothing else. A session still running makes it stop and say so, since the binary being removed is what ends sessions; session records are kept unless you pass --purge, and credentials stay in your OS keychain either way.

On Windows:

powershell -ExecutionPolicy Bypass -c "irm https://sean-reid.github.io/jamstream/install.ps1 | iex"

The script installs to %LOCALAPPDATA%\Programs\jamstream (set JAMSTREAM_INSTALL_DIR to pick the directory yourself) and adds that directory to your user Path; open a new terminal to pick it up.

Flags need the script saved first, because iex cannot pass them:

irm https://sean-reid.github.io/jamstream/install.ps1 -OutFile install.ps1
powershell -ExecutionPolicy Bypass -File .\install.ps1 -WithApp

-WithApp installs the desktop app beside the CLI. -Tag v0.2.0 installs that release instead of the newest one.

Uninstalling on Windows is the same shape:

powershell -ExecutionPolicy Bypass -c "irm https://sean-reid.github.io/jamstream/uninstall.ps1 | iex"

It removes the binaries install.ps1 put in place and nothing else. A session still running makes it stop and say so. Your session records and local recordings are kept unless you pass -Purge (saved-script form again), which deletes both; credentials stay in Credential Manager either way.

If you installed by extracting a zip yourself, there is nothing to run: delete the extracted folder. Session data lives at %LOCALAPPDATA%\jamstream, and saved credentials are in Windows Credential Manager; search for jamstream there to remove them.

Quickstart: host your first session

Two paths from nothing to a running session, both in the desktop app.

PathTimeNeedsGood for
LocalAbout 5 minutesNo cloud accountMusicians in the same room or on the same network
InternetAbout 20 minutes the first timeA DigitalOcean accountBandmates anywhere

Most of that 20 minutes is setting up the DigitalOcean account, not the app. This page walks the local path first, then the internet path.

1. Get the app

Download the desktop app for your platform from the Download page and open it. The app carries its own jamstreamd session server, so there is nothing else to install.

The home screen: paste an invite to join, or host a session.

2. Host on this computer

Click Host a session. The wizard's first step asks where the session server should run:

Step 1 of 4. Local needs no account; a cloud shows ready once credentials are saved.

Pick local and click Continue. Local has no region to pick, so the wizard jumps to a "Before you start" step where you set the seats and the timers, and it confirms the session costs nothing. Click Start the session.

The app starts a real jamstreamd process on your machine, completes a full encrypted handshake with it before showing you anything, joins you automatically, and opens Settings on the Invites tab.

On Windows, the first local host raises a Defender Firewall prompt for jamstreamd.exe. Allow it on both Private and Public networks, or bandmates on your network time out after 10 seconds when they join.

3. Share the invites

The Invites tab, open the moment you are hosting. Each link admits one person.

  • Each row is one seat. Click Copy link and send that link to exactly one person, over any channel you trust.
  • Rows read not joined, connected, or free as people come and go.
  • Mint invite adds seats mid-session. Details in Hosting a session.

4. Bandmates join

Everyone else opens the app on their own machine, types their name, pastes their invite into the Join a session field on the home screen, and clicks Join or presses Enter. A malformed or expired invite shows the reason under the field instead of joining.

Local invites carry your machine's network address (192.168.1.12 style), so they work from any machine on the same network. They do not work across the internet; bandmates elsewhere need the DigitalOcean path below, or router port forwarding, which Playing on the same network explains honestly.

5. End it

On the Invites tab, click End session for everyone. The server process is killed and every invite is dead from that moment. A forgotten local session costs nothing, and the server also exits on its own after 10 minutes with no musicians connected.

That is the whole local loop. The rest of this page is the internet path.

Put it on air

  • Any session, local or cloud, can stream live to Twitch, YouTube Live, or both at once while you play.
  • The Broadcast tab of Settings takes a stream key per platform; ON AIR lights in the status bar for everyone in the session.
  • Dropping one platform leaves the other streaming.

See Streaming to Twitch and YouTube.

Host on the internet with DigitalOcean

The flow is the same wizard; the server runs on a small droplet in your own DigitalOcean account instead of on your machine, and the invites work from anywhere.

Connect your account

In the wizard's first step, pick digitalocean. With no saved credentials the row reads setup needed and the Connect DigitalOcean pane opens under it:

The in-app credential pane. The token is checked with a real API call before it is saved.

  1. Sign up at digitalocean.com and add a payment method. The DigitalOcean setup page has every step from zero, including the exact token scopes.
  2. Click Open the token page and generate a token scoped to droplet and tag operations only; copy it, it is shown once.
  3. Paste it into the API token field and click Check credentials. On success the pane says "Works. Saved on this computer." and the row flips to ready.

The token is saved on this computer, so this is a one-time step; next session the row is ready from the start.

Pick a region and launch

Click Continue. The app fetches live prices and times the network from your computer to each of the provider's regions, then sorts them by worst round trip in 5 ms steps, with price breaking ties inside a step.

Take the top row unless you know your bandmates sit far from you; Hosting a session explains how to pick fairly.

The next step is the cost preview: set the expected hours and seats, read the estimate (a three hour four musician session on DigitalOcean is about $0.08), and click Launch.

The wizard boots the machine, waits for its address, and proves the server answers a real encrypted handshake before it joins you and opens Settings on the Invites tab. There is no server to find or upload: release builds carry their own jamstreamd build pinned in, and the machine verifies the download at boot.

The meter is now running. The droplet bills by the second until you end the session, and it shuts itself down after 10 minutes with no musicians connected, or at the 12 hour hard cap, whichever comes first.

Share, check, end

The Invites tab works exactly as in the local path; the links now work from anywhere. What you have spent so far sits at the right end of the status bar, beside the elapsed time and the session id.

End session for everyone destroys the droplet and confirms with DigitalOcean that nothing tagged with the session is still listed. If you ever doubt everything is gone, Understanding cost covers the sweeper.

From the terminal

The jamstream CLI hosts, monitors, and ends the same sessions, for scripts, automation, and machines without a display. One line installs it on macOS and Linux (the Download page has the Windows line and every artifact):

$ curl -fsSL https://sean-reid.github.io/jamstream/install.sh | sh

Cloud credentials come from the environment (for example DIGITALOCEAN_TOKEN); local hosting with the CLI alone also needs a jamstreamd on this computer, which the install script's --with-server flag provides on Linux.

host shows the region table and the cost preview, asks for confirmation, and prints one invite per seat once the server answers a real handshake:

$ jamstream host --provider digitalocean
...
Launch this session? [y/N] y

Session 3f2a9c01 is running.
server       203.0.113.10:43210
host         jamstream://join/r6edH1LCtlT3vPPiILRRVAEACgAAAcrRAjiV...
musician 1   jamstream://join/r6edH1LCtlT3vPPiILRRVAEACgAAAcrRAjiW...
...
End the session with: jamstream end 3f2a9c01

$ jamstream status
SESSION    PROVIDER/REGION      STATUS      ELAPSED      ACCRUED      PROJECTED TAKES
3f2a9c01   digitalocean/nyc3    running  1 h 04 min    $0.028576 $0.08037 at 3.0 h -

$ jamstream end 3f2a9c01
Session 3f2a9c01 ended. Instance 512190713 is destroyed.

$ jamstream sweep --dry-run
No jamstream-tagged instances found.

The invite strings are shortened here; real ones are about 220 characters. A headless client can even join with a WAV file as its instrument; see jamstream join.

Sessions live in shared state files: the CLI sees sessions the app hosted and can end them, and the app can end sessions the CLI hosted. Every command and flag is in the CLI reference.

Hosting a session

Hosting means launching one small server, minting the invites, and ending the session when you are done. In the app, Host a session on the home screen walks four steps: where the server runs, which region, a cost preview, and the launch itself.

Hosting does not have to involve a cloud at all: picking local in the wizard's first step runs the server on your own computer, costs nothing, and is the right choice when everyone is on the same network. This page covers the cloud path; Playing on the same network covers local mode.

The provider step

The first step lists where the server can run: local, then the three clouds, each with its status. A cloud with saved credentials reads ready; one without reads setup needed.

Selecting an unset provider opens an inline pane that takes the credential, checks it with a real API call, and saves it on this computer. Provider setup walks each provider from zero.

The region table

Before launching, JamStream measures the round trip from your machine to each of the provider's regions and shows a table:

Step 2 of 4, with prices fetched live and latencies probed from the host's machine. The CLI prints the same table.

ColumnWhat it means
worst rttThe slowest measured round trip, in milliseconds, among the people probing. At create time that is only you; your bandmates' distances are not known yet. The probes time a TCP handshake against each region's public endpoints, which tracks the UDP path closely enough to rank regions.
hourlyThe machine's current price per hour, fetched live where the provider offers it.
egressWhat the provider charges per GB of outbound audio.

Regions are sorted by worst round trip in 5 ms steps, with price breaking ties inside a step, best first. The top row is preselected if a probe answered for it; click another to override it (--region in the CLI).

A region that did not answer a probe reads no probe and sorts last. If none answered, nothing is preselected and Continue stays off until you pick one.

A region under 30 ms from everyone keeps the network's share of latency in single digits each way, which is what makes the total playable.

If the band spans a continent, pick the region that is mediocre for everyone over the one that is perfect for you; the person with the worst round trip sets the feel. See Troubleshooting for what the numbers mean in the ear.

The cost preview and the seats

Step 3 shows the expected hours, the seat counts, and the resulting estimate, all editable in place:

Step 3 of 4. The same lines jamstream host prints, from the same live prices.

FieldWhat it does
hoursShapes the estimate only; the real bill is metered. Play longer and you pay for the time played.
musicians, including youPlaying seats: 4 means your own seat plus three invites to hand out. The cap is 10, which is also what the server admits.
listenersPeople who only hear the mix, up to 20.
stream destinationsHow many platforms you expect to broadcast to, up to 2; it is the number here that moves the estimate most. It configures nothing: platforms are set up in the session itself, in Streaming to Twitch and YouTube.
Recordingoff, mix only, or mix and stems. It is the one choice here that is fixed for the session: a session launched with it off cannot record later. A cloud take needs a bucket, set up once in Settings; Recording a session walks it.

Understanding cost explains every line of the estimate.

Clicking Launch boots the machine, waits for its address, proves the server answers a real encrypted handshake, and joins you.

Invites are minted at launch

One invite per seat is minted up front, on your machine, and the wizard opens Settings on the Invites tab the moment you are in:

Each link admits one person. Copy, revoke, or mint more, and end the session for everyone from here.

  • Each row is one seat with a live status: not joined, connected, or free. Copy link puts that person's invite on the clipboard; send it to exactly one person over a channel you trust.
  • Revoking a seat frees it: the status reads free, was Ben and New link mints a replacement into the same chair.
  • Revoke ejects that member and kills their invite, with a confirmation step. The host also sees a Revoke button on each mixer strip.
  • Mint invite adds a musician or listener seat mid-session, up to 10 musicians (you included) and 20 listeners. Unused invites cost nothing.
  • The for field names the next link you mint. The name rides inside the invite, so the roster and any recorded stems say "Ana" from that person's first packet instead of "musician 2".
  • An unused seat named this way reads not joined, for Ana. People can also set their own name when they join, which wins over the invite's.

See Joining a session for how invites behave on the other end.

The panel belongs to the app that launched the session. A session hosted from the CLI cannot have seats added later, from either side, and joining it in the app is an ordinary join with no Invites tab.

The safety knobs

Every session carries two timers, both set at launch:

TimerDefaultWizard fieldCLI flagRange
Idle exit10 minutesidle exit--idle-min1 to 120 minutes in the wizard; any value from the CLI
Hard cap12 hourshard cap--max-hours1 to 24 hours in the wizard; any value from the CLI

With no musicians connected for the idle window, the server shuts itself down. On DigitalOcean and AWS the machine is destroyed with it; on GCP it stops serving and is deleted when you end the session, on your next sweep, or at the hard cap.

The machine is destroyed at the hard cap no matter what, and invites expire with it.

There is no way to extend a running session; host a new one. The point of the caps is that a forgotten session costs a bounded, small amount, not a month of billing. Understanding cost covers the other guardrails.

While it runs

The Broadcast tab of Settings holds both halves of streaming: Stream mix sets what the platforms and the listeners hear, and Destinations puts the session on air to Twitch and YouTube Live.

Record is the one performance action in the bar itself, and Recording a session covers takes.

Devices and buffer size can change mid-session from Settings in the top bar, and the change applies immediately; Joining a session walks the whole screen.

jamstream status lists the same sessions from the terminal, with elapsed time, cost accrued so far, and a projection.

If tagged machines already exist in your account when you host again, the CLI warns at host time, so a stray session does not hide behind a new one.

Ending

End session for everyone on the Invites tab destroys the machine, confirms with the provider that nothing tagged with the session is still listed, and marks the local record ended, with a progress sheet until the provider confirms. The invites are dead from that moment.

Leaving is not ending: Leave disconnects you and the server keeps running until the host ends it or the idle exit fires.

The machine takes its own log with it, so the app keeps a copy of the last of it while the session runs, in jamstream/sessions/logs/<id>.log under your platform's data directory; jamstream end prints the path.

That file is where the reason a broadcast or a take failed is written down, and stream keys are stripped from it before it is written.

Closing the app window while your session runs asks the same question rather than deciding for you:

  • Cancel
  • Keep it running and quit; the band plays on, and the server stops itself 10 minutes after the last musician leaves, at its hard cap, with Stop strays on the home screen, or with jamstream end
  • End session and quit

No dialog appears when nothing you launched is running.

From the terminal

The CLI runs the same flow without a screen:

$ jamstream host --provider digitalocean
$ jamstream status
$ jamstream end 3f2a9c01        # any unambiguous prefix, or --last

host prints the region table and cost preview, asks for confirmation, and prints one invite per seat; end confirms with the provider that nothing tagged with the session is still listed. Every flag is in the CLI reference.

Joining a session

You need exactly two things: the app, from the Download page, and your invite, a string starting with jamstream://join/, sent to you by the host.

What an invite is

Each invite admits one person to one seat in one session, signed by the host's key so it cannot be forged.

  • Do not share your invite; two people cannot use the same one. If someone else needs in, the host mints another seat on the Invites tab.
  • Losing your connection does not burn the invite. Close the app, move to another machine, rejoin with the same string; the seat is yours until the session ends or the host revokes it.
  • Invites die with the session. There is nothing to clean up or keep secret afterward.
  • Send invites over a channel you trust. Anyone holding your invite can join as you until it is revoked.

Joining

  1. Type your name into the your name field. It is what the roster and any recorded stems say instead of "musician 2", it is remembered for next time, and it wins over whatever name the host put on your invite.
  2. Paste the invite into the Join a session field (hint text: "paste an invite, jamstream://join/...") and click Join, or press Enter.
  3. A malformed or expired invite shows the reason under the field instead of joining.
  4. If every seat for your role is taken, the screen reads "the session is full; waiting for a seat to free" and keeps trying, so you are in as soon as somebody leaves.

The session screen

Once connected you are in the session screen:

A four piece session in the current build. One strip per musician; chat on the right; latency and meters on the left of the status bar.

What you are looking at:

  • One mixer strip per musician: an avatar disc, a presence dot, a name, a fader, its dB readout, a pan slider, and a Mute button. This is your personal monitor mix; moving Ana's fader changes what you hear, not what anyone else hears.
  • The avatar disc shows a member's picture if they set one, and their initials on a color hashed from their name if they have not. The same disc and color appear on the card the broadcast renders, so a member looks the same in the app and on a stream.
  • Your own strip is dimmed with a "you" tag and its controls do nothing: there is no separate gain for your own channel, because self monitoring is local. The Audio tab's Hear yourself through the server puts your own sound in the mix too, on the band's timeline instead of off it.
  • The host additionally sees a Revoke button on every other strip. Revoking ejects that member and kills their invite, with a confirmation step.
  • Chat, with timestamps. The metronome panel shows tempo, beats per bar, and click state; the host sets them, and "hear the click" is your own choice.
  • The ON AIR lamp is absent until the host starts a broadcast and lit while one is running; hover it to see how many destinations are receiving it. Only the host can start or stop one, but everyone sees the lamp, because everyone is in it. See Streaming to Twitch and YouTube.

The status bar sits in the same place every session: mouth to ear latency and your meters on the left, the lamps that say what the session is doing in the middle, the session id and Leave on the right.

Hover the latency number for rtt, both jitter buffer depths, what each device buffer costs, and loss in each direction, which Troubleshooting turns into actions. Leave asks for a confirmation; leaving does not end the session, and your seat is kept.

A member who stops responding shows it in two stages. The dot beside their name goes amber after 2 seconds, which is 800 missed frames and far past anything a working client does; hovering it reads "gone quiet". After 10 seconds the server gives up on them: the strip grays out and reads disconnected.

The dot stays nearly silent while somebody is playing, so the one to notice mid song is the one that changed. Their seat is held either way, and reconnecting with the same invite puts them back in it.

At ten musicians the strips extend past the window and scroll horizontally:

A session at the 10 musician cap in the current build, with 10 listeners connected.

Devices and buffer size, mid-session

Settings in the top bar opens over the session without covering the strips or the status bar. The tabs, in order:

  • Audio, for devices and buffer size.
  • Broadcast and Invites, for a host whose own app launched the session.
  • Recording, for where takes go; a setting of this computer, present whatever the session is.
  • You, for your avatar and the theme.

Settings mid-session: device and buffer changes apply immediately; the stream reopens in place.

  • Buffer size offers 120, 240, or 480 frames (2.5, 5, or 10 ms). Start at the smallest. The status bar's figure, which the drawer never covers, moves by three times the pick, paid once going in and twice coming out.
  • Add extra depth automatically lets a machine that keeps nearly breaking up hold a little more than the size asks for, and say how much. Untick it to get the size you picked and nothing else; the answer is kept for this computer.
  • A choice outside what the selected device can deliver is annotated with the device's own minimum or maximum, because that is what you will really get.
  • The Input level meter should move when you play. If it is still, the wrong capture device is selected or the operating system has not granted microphone access.
  • Hear yourself through the server puts your own sound in your mix, on the band's timeline. Off by default, needs headphones, and offered once above the control when mouth to ear sits above about 30 ms. See The band can't keep together.
  • Capture and Playback list a System default entry first, then your machine's real audio devices. Changing one mid-session reopens the audio stream on the new device without leaving the session, and Rescan picks up an interface plugged in after launch.
  • On Windows an Allow exclusive access checkbox sits under the pickers; Troubleshooting has what it costs either way.
  • Device and buffer choices are remembered across launches; a remembered device that is not connected at startup falls back to the system default until it returns.
  • Any sample rate works. Sessions run at 48 kHz, and an interface at another rate joins anyway; a note under the pickers says how, per direction. Troubleshooting has the four ways that can go and which one costs milliseconds.
  • Your avatar, on the You tab, opens a file picker: pick a PNG or JPEG and everyone in the session gets the picture over the same encrypted link as the audio; a photo straight off a phone works as it is.
  • The avatar choice lasts for this run of the app; set it again after a restart. Remove drops it here and on your next join, though members already in the session keep the picture you sent them.

From the terminal

For test rigs and machines without a display, the CLI joins headlessly with a WAV file as its instrument and writes what it heard:

$ jamstream join 'jamstream://join/...' --headless \
    --input take.wav --output mix.wav --duration-secs 120 \
    --chat "bot checking in"
joined
roster: 3 members
chat from 1: heard you
left after 120 s; wrote mix.wav

Input must be a 48 kHz WAV, mono or stereo; after the file ends, the client sends silence. The received stereo mix lands in --output. Flags and details in the CLI reference.

Playing on the same network

Picking local in the host wizard's first step runs the session server as a process on your own computer instead of a cloud machine. Every desktop app build bundles its own jamstreamd, so there is nothing to install and no account to create.

Local is selectable the moment the app opens, its row reads no account needed, and the wizard skips the region step. The step that would price the session reads "Before you start" instead, and the launch button is Start the session.

Everything else is the same flow: invites are minted up front, the app completes a real encrypted handshake with the server before showing them, joins you automatically, and the Invites tab ends the session.

When local is the right choice

  • Everyone is in the same room or on the same network. The invites carry your machine's network address (for example 192.168.1.12), so any machine on the same network can join.
  • You want the lowest possible latency. The audio path is one hop across your own network, with no internet in it.
  • You want to try JamStream before creating a cloud account. Local sessions need no credentials and cost nothing.

Playing alone on one machine also works.

The limits, honestly

Broadcasting to Twitch or YouTube Live from a local session needs ffmpeg and mediamtx on your PATH, which the app does not bundle, and does not work on Windows yet. Streaming has the details.

Everything else about a local session, including recording, needs nothing installed.

An invite that names 192.168.1.12 means nothing outside your network. Reaching a local session across the internet would take:

  • forwarding the session's UDP port to this computer in your router's admin pages
  • a public IP, which many home connections behind carrier NAT do not have
  • an invite carrying that public address, which the current build cannot mint: a local invite carries loopback and the LAN address it discovered, and nothing else

JamStream automates none of that. If anyone is joining from outside your network, host in the cloud instead; see Provider setup.

Ending and the idle exit

End session for everyone on the Invites tab kills the server process, and the same shared state means jamstream end from a terminal does too; for local sessions the instance id shown in status and end output is the process id.

Closing the app window mid-session asks the same question a cloud session asks, because the server deliberately outlives its launcher. If you forget, the server watches its own activity and exits after 10 minutes with no musicians connected. There is no bill either way.

The local server also exits at the 12 hour hard cap, and the invites expire with it.

If a laptop dies mid-session or a state file is lost, jamstream sweep finds local strays the same way it finds cloud ones: the local provider keeps an on-disk registry of the processes it spawned, so a later sweep from a fresh shell still sees and kills them.

From the terminal

jamstream host --provider local runs the same flow (local is the default provider, so the flag is optional):

  • it prints one invite per seat after the handshake check
  • it takes --musicians, --listeners, --idle-min, and --port; the wizard offers the same seats and windows
  • the CLI defaults to port 43210; the app picks a free UDP port for each local session instead

See the host reference.

Unlike the app, the CLI does not bundle the server, so local mode needs a jamstreamd binary already on your machine; nothing is downloaded. It is taken from JAMSTREAMD_PATH, then from beside the running executable, then from your PATH.

host fails before starting anything with an error naming every place it looked. On Linux the install script's --with-server flag puts one in the second of those places.

A session is only handed to you once the server it started can be asked to end cleanly, so a jamstreamd too old to answer that request is stopped again and the error says what it did not do. Use the server from the same release as the CLI.

If a local server exits at startup, the first place to look is its server.log, in the session's own directory under your platform's data directory in jamstream/sessions/; on Windows that is %LOCALAPPDATA%\jamstream\sessions\<id>\server.log.

Streaming to Twitch and YouTube

The host can put a session on air to Twitch and YouTube Live, either one alone or both at once. A platform that refuses its key or drops its connection does not take the other one with it; one encode feeds both, so a failure in that encode stops both.

Only the host can start or stop a broadcast. Everyone in the session sees the ON AIR lamp in the middle of the status bar, because everyone in the session is in the broadcast.

On air to both platforms.

What goes out

The broadcast mix, over a card of the musicians: an avatar or their initials, their name, and a level meter each, with the listeners as a count in the footer. The host shapes that mix at the top of the Broadcast tab in Settings, and it is not the same as anyone's monitor mix.

To hear exactly what the stream carries, switch on audition stream mix there. It replaces the host's monitor mix with the stream's, own voice included, and lights an AUDITION lamp for as long as it is on.

Getting a stream key

Both platforms give you a key that keeps working session after session.

PlatformWhere to find it
TwitchCreator Dashboard, Settings, Stream, Primary Stream key
YouTube LiveYouTube Studio, Go Live, Stream, Stream key

A YouTube channel needs live streaming enabled once before that key appears, which can take 24 hours on a new account. Do it before the band is waiting.

Add key under Destinations shows these same steps, with a button that opens the platform's page.

Going live

Settings, then the Broadcast tab: the stream mix is at the top and Destinations under it.

  1. Add key, paste the key, Save key. The row reads asking, then ready once the server has it.
  2. Repeat for the second platform if you want both.
  3. Go live.

Adding a Twitch key. The field never shows the key back, so a paste is checked by the character count.

ON AIR lights for everyone in the session once a platform is actually taking the broadcast, which is a few seconds after the press. You can add or drop a platform while you are on air; Remove stops that one and leaves the others streaming.

Your key is treated as a credential. Leave keep this key in this computer's keychain on and the next session starts with Use saved key instead of a paste; Forget key deletes it again.

When a session cannot stream

A broadcast goes out through a relay that runs on the session machine, alongside the server. It is downloaded when the session boots, and a session runs perfectly well without it: the band plays, listeners listen, takes record. Only the broadcast needs it.

The Broadcast tab says so when it is missing, with the reason, and every control that would send a key somewhere is off, because it would have nowhere to go.

Nothing on this computer can fix this one: the relay is on the session machine.

The session server checks the relay every five seconds for as long as the session lasts. This appears if the relay dies mid-session too, and clears if it comes back; nothing on your computer affects it, so to broadcast, start another session.

A relay that never answered at all is given three minutes before it is reported, so a fresh session says nothing about it either way for that long.

A session hosted on your own machine is the other way round, because the session machine is yours.

It broadcasts through ffmpeg and mediamtx on your own PATH, and the app installs neither: brew install ffmpeg mediamtx on macOS, apt install ffmpeg plus a mediamtx release on Linux.

Without them the reason says which one is missing. Broadcasting from a local session does not work on Windows at all yet; host in the cloud for that.

While you are on air

Each row says what that platform is actually doing:

Row readsWhat it means
no keynothing configured for this platform
key saveda key is on this computer, one click from being used
askingthe server has not answered yet
readyconfigured, and goes live when you press Go live
connectingstarting up; nothing is reaching the platform yet
livethe platform is receiving the broadcast
failedit stopped, with the reason on the next line

Under the rows, one line names the encode every destination shares: 1280x720 at 30 fps, 2628 kbps. Under that are two frame counts, also one pair for the whole broadcast, so every row shows the same two, and each changes color as it rises:

TermWhat it countsWhat it means
repeatedframes the machine had no time to draw; sent again as the last picturesound stays in step but the video stutters; a climbing count means the machine is at its limit
droppedframes the encoder would not take; gone for goodthe video falls that many pictures short of the sound

Repeats come first, and losses only once the machine is well past keeping up, so any dropped frame is worth acting on. Removing a destination brings neither count down, because one encode feeds every platform. What helps is a shorter session, a smaller machine load, or one platform instead of two.

When a platform fails

One platform's connection failed; the other kept streaming.

A stream that dies quietly is worse than one that never started, so a failure shows in three places: the row goes red with the reason under it, the tab counts the failures, and the status bar lights STREAM FAILED even with Settings closed.

A destination that stops is retried on its own, on a backoff that starts at 500 ms and doubles to 16 seconds: the row goes back to connecting, and to live once the platform is taking the broadcast again.

A key the platform rejects fails every time, and the reason says so. Remove that destination, then Forget key before Add key: while the key is still saved the row offers Use saved key, which sends the rejected one again.

The reason is what the encoder or the pusher printed, quoted, up to two lines of it. Read the front of it first.

Reason starts withWhere it broke
push failed:sending the encode to the platform
encoder down:making the encode in the first place, before any platform is involved

Then read what follows. Failed to connect to rtmps://<redacted> with Connection refused is the platform saying no, so the key is wrong, was reset, or belongs to another channel. Everything after the :// is stripped, host included, because the key is in there.

Failed to connect to <local relay> is the session machine failing to talk to itself, which no key change will fix; restart the session.

Stop streaming takes everything off air at once, with no confirmation step, because a host who needs the stream to stop needs it to stop now. Ending the session stops it too.

What it costs

Broadcasting is the one part of a session that moves real traffic. Set stream destinations on the wizard's cost preview and the estimate counts it; what egress is has the numbers.

Twitch and YouTube Live are the two platforms this build supports, because they are the two tested end to end. Both hand out a key that keeps working, which is what lets a session be set up before the band arrives.

Recording a session

A take is the mix your listeners heard, written as 16 bit 48 kHz FLAC.

Nothing is captured by surprise. Recording is armed at launch, and a take runs only while the host holds it open: each Record to Stop is one take.

Set up a bucket once

A cloud session records to a bucket in your own account, because the machine deletes itself when the session ends and a take on its disk goes with it. Open Settings, then Recording:

Set up once per computer. The key is masked and never shown again.

  1. Pick the provider holding the bucket, and name the bucket and the region it is in. Host in that region and the upload costs nothing.
  2. Paste the storage key pair. This is not the credential that launches machines. Launching a recorded session writes this key into the session machine, so scope it to writing the recordings prefix of one bucket. The last section of your provider's page makes exactly that key.
  3. Click Check. It writes one small object to the bucket and deletes it. A pass saves the key in this computer's keychain; a failure says what to change and saves nothing, so a wrong key fails while you are pasting rather than mid-song.
  4. keep takes for sets the default retention for new sessions: 7, 30 or 90 days, or forever. It saves as you pick it, unlike the bucket and the key, which save on a passing Check. The default is 30 days, and it is a rule on the bucket itself, so it keeps being enforced long after the machine is gone.

A session on your own computer records to your own disk and needs none of this.

Arm it at launch

Whether a session can record, and whether stems are captured alongside the mix, are fixed before anyone plays and cannot change once the session is running. Both choices sit on the host wizard's cost preview:

Off, the mix, or the mix and stems, with what each costs before you launch.

ChoiceCapturesSize, three hours, four piece
offnothing; every launch starts herenone
mix onlythe stereo mix listeners hearabout 1.2 GB
mix and stemsthe mix, plus one stereo file per musicianabout 6.2 GB

The size sits beside each row in the wizard, and the estimate below moves as you pick, because that is the moment the difference matters.

With no bucket set up, the two recording rows are disabled and say so, pointing at the Recording tab. A local session has no such requirement: the rows are live and the takes land on this computer.

Launching proves the key can write this session's own prefix, and sets the retention rule before the machine is paid for.

Setting retention needs a key that can also read the bucket's existing rules, not just write to it.

Without that permission the session still records, but retention can't be set: takes are kept indefinitely and go on costing storage until you delete them yourself. jamstream host says so in the line it prints after the bucket check.

The fix is the read permission in the recording step of your provider's page.

From the terminal

--record records a local session; --bucket names a bucket and implies it.

$ jamstream host --provider local --record --yes
$ jamstream host --provider aws --region eu-west-1 --bucket my-jams --yes

The CLI reads the storage key from JAMSTREAM_RECORDING_ACCESS_KEY_ID and JAMSTREAM_RECORDING_SECRET_ACCESS_KEY rather than from the keychain; jamstream recordings says why it is those and not a provider's launch pair.

--record-stems captures stems and implies --record; --retention takes 7d, 30d, 90d or forever. Every flag is in the host reference.

A session launched without recording cannot be talked into it later. Press Record on one and it answers recording is not configured for this session straight away, so you find out before the song rather than after it.

Starting a take

Record in the session's status bar opens the Record sheet. Only the host has either.

The sheet shows the take's state, a line saying whether stems are being captured, Record to start a take, Stop to end it, and Close. Closing the sheet does not stop a take, and neither does leaving the session; a take stops when the host presses Stop or when the session ends.

While a take runs, REC lights in the middle of the bar for everyone in the session. Hover it and it reads "this session is being recorded". Nothing lights while the recorder is idle.

Where takes land

Files carry the take's date and time in UTC, and stems carry the player's name:

jamstream-2026-07-28-1930-mix.flac
jamstream-2026-07-28-1930-Ana.flac
WhereLocationExpires
This computerrecordings folder under your platform's data directorynever; only you delete a take
Bucketjamstream/recordings/ plus the session id, in the bucket you namedthe retention choice: 7, 30 or 90 days, or forever (default 30)

On this computer

Named in the wizard when you arm a local take, and printed at launch by the CLI:

$ jamstream host --provider local --record --yes

Session 3f2a9c01 is running.
server       192.168.1.12:43210
record dir   /Users/you/Library/Application Support/jamstream/recordings
host         jamstream://join/r6edH1LCtlT3vPPiILRRVAEACgAAAcrRAjiV...
PlatformFolder
macOS~/Library/Application Support/jamstream/recordings
Linux~/.local/share/jamstream/recordings, or under $XDG_DATA_HOME
Windows%LOCALAPPDATA%\jamstream\recordings

jamstream end never removes a recording. Nothing but you deletes a take.

A take still being written ends in .part and is renamed when it finishes, so a laptop that dies mid-song leaves a file that does not look like a finished recording.

In a bucket

The take uploads while you play, so ending the session waits only for the last of it.

Let the UPLOADING lamp clear before you end the session. The machine holds on for ten minutes to finish an upload and then shuts down regardless, and a take still in flight at that point is lost.

Downloading is where recording costs money, because your cloud account bills egress on the way out and nothing on the way in.

Getting your takes

Takes, on the Recent sessions card on Home once a session is listed there, is every take this computer knows about, newest first.

A row is one take. The mix is already on this computer here, so it offers Reveal; the stems are still in the bucket and carry their price.

One row is one take, meaning one Record to Stop, so two takes of the same song are told apart by when they started and how big they are. The mix and the stems of a take are separate rows, because the stems are several times the bytes and pulling them is where the money is:

  • A take on this computer offers Reveal in Finder (Windows: Show in File Explorer, Linux: Show in Files), opening its folder with the file selected. A local session's takes are here from the moment you press Stop.
  • A take in a bucket has a button reading Download mix · 1.10 GB · about $0.01: the half, its size, and what your own account will bill for the egress. Clicking it starts the transfer; there is nothing further to confirm.
  • Downloaded takes land in a JamStream folder in your music folder, one folder per session. The row then offers Reveal instead.

A take can be gigabytes, so a download takes a while. The row shows how much of it has arrived, and the other takes wait until it finishes:

One download at a time.

Takes outlive the session that made them: one from a session that ended weeks ago is still here.

  • Where the bucket is really deleting them on a schedule, the session's card counts down to that, in red for the last three days.
  • Where the retention choice could not be applied, the card says so rather than counting down to nothing, and a line above the list counts those sessions and what a month of them costs.
  • A take that is still uploading is not in the bucket yet, so it appears here when it finishes and not before.
  • An expired take with no copy on this computer drops off the list once the window is full, and a line says how many.
  • Once the bucket has deleted a take you had already downloaded, the card says it is kept on this computer and the row still reveals the file in your music folder.

From the terminal

jamstream recordings lists and fetches the same takes, for scripts and machines with no screen. It reads the storage key from the environment and not from your keychain, so export the pair before you use it even if the app already has the key saved.

The mix, and stems

Without stems, a take is one stereo file. With stems it is one stereo file per musician as well, each carrying that player's own signal. The sheet reads back which of the two you launched with.

Stems are stereo rather than mono, so a stem is the same size as the mix, which is why mix-and-stems costs about five times mix-only for a four piece. Every file in a take starts at the same zero, so they line up when you import them.

What the room sees

The sheet readsThe bar showsWhat it means
idlenothingarmed, with no take running
recordingREC, a filled lampa take is running
uploadingUPLOADING, a hollow lampthe take ended and the last of it is still going to the bucket. Record is disabled until it clears
failedREC FAILED, a hollow lampthe take stopped, with the reason

A take on your own disk is finished the moment you press Stop, so it never reads uploading.

When it fails

The reason is in the sheet and on the lamp's hover, verbatim, for everyone in the room. A failure ends the take, not the session: press Record again for the next one.

Reason beginsWhat to do
recording is not configured for this sessionthe session was launched with recording off; host a new one with it on
cannot start the recorder, cannot open the mix filethe folder, or the first object in the bucket, could not be created. For a local session, check that the printed path exists and is writable
recording faileda write failed mid-take: a full disk, or a bucket that stopped accepting the upload. The take is abandoned rather than left half written
recording could not be finishedthe end of the take could not be written. Earlier takes in the session are unaffected

What it costs

A local take costs disk and nothing else. A take in a bucket costs a few cents of storage while it sits there, and egress when somebody downloads it. Understanding cost has the numbers.

Provider setup

JamStream hosts on three cloud providers. Each needs a one-time setup: an account, and a credential with the narrowest permissions that still work.

Checking the credential happens inside the app: selecting a provider that reads setup needed in the host wizard opens an inline pane that takes the credential, checks it with a real API call, and saves it to your system keychain, so the next session starts at ready.

One page per provider walks the account and credential creation from zero. Hosting on your own computer needs none of this; see Playing on the same network.

ProviderSetup effortMachine usedCredential
DigitalOceanone token, about 10 minutess-2vcpu-2gb dropletAPI token
AWSmore involved: IAM user, policy, access keyt4g.medium instanceaccess key id and secret
GCPproject, API enablement, service account keye2-medium instanceservice account JSON key

If you do not already live in one of these clouds, use DigitalOcean. Its setup is one token in one screen, and its included transfer means a session's audio traffic costs nothing extra.

AWS and GCP work well and are documented honestly: they take longer to set up because their permission systems are built for companies.

Least privilege, and why it matters here

Each page shows how to grant JamStream only what it uses: create, list, tag, and destroy one class of machine. Set up this way, the credential on your laptop can manage jam servers and nothing else. If it ever leaks, the blast radius is a few small VMs, not your storage, your DNS, or your bill.

One credential per provider covers everything except one feature. Recording a cloud session writes takes to a bucket, which needs a second key, scoped to that one bucket and nothing more.

The last section of each provider page creates it. Recording is off unless you turn it on, so the plain path stays one credential.

Verifying any provider

The app's Check credentials button makes a real authenticated call before saving anything: it fetches a price and lists anything JamStream-tagged, changing nothing. A failure is shown verbatim in the pane. The same check works from the terminal:

$ jamstream sweep --dry-run --provider digitalocean
No jamstream-tagged instances found.

That line means the credential works. An authentication error here names the environment variable it was missing.

For the CLI and automation: environment variables

The CLI reads credentials from the environment: DIGITALOCEAN_TOKEN, AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or GOOGLE_APPLICATION_CREDENTIALS, as each provider page shows.

The app reads the same variables as a silent fallback, so a machine configured for the CLI works in the app with no extra setup; a value saved from the app's pane takes precedence.

Credentials are never included in an invite. Scope a DigitalOcean token to droplets and nothing else: a session droplet is given that token so it can delete itself when the session ends.

DigitalOcean

The recommended provider: one token in one screen, and the droplet's included transfer covers a session's audio traffic. Total setup is about 10 minutes, most of it account signup.

JamStream launches an s-2vcpu-2gb Basic droplet: $0.02679 per hour as of July 2026, billed per second with a 60 second minimum, with 3,000 GB of transfer included (pricing).

Powered-off droplets still bill on DigitalOcean, which is why JamStream only ever destroys them outright.

1. Create the account

  1. Sign up at digitalocean.com.

  2. Add a payment method before creating anything; DigitalOcean requires one to verify the account.

    Cards, PayPal, Google Pay, and several others are accepted (payment methods). Card signups may show a temporary preauthorization hold; PayPal makes a $5 authorization charge.

  3. As of July 2026, new accounts get a $5 signup credit that expires 90 days after signup (signup credit). Promotions change; check the current terms. $5 covers roughly 60 session hours, so the credit alone funds a lot of rehearsal.

2. Create an API token

Follow DigitalOcean's token guide; the short form:

  1. Log in to the control panel at cloud.digitalocean.com. In the app's Connect DigitalOcean pane, Open the token page lands you in the right place.
  2. In the main menu, open Account, then API. You land on the Applications & API page, Tokens tab.
  3. Under Personal access tokens, click Generate New Token.
  4. Name it something you will recognize later, like jamstream.
  5. Pick an expiration. Shorter is safer; when it expires you generate a new one, which takes a minute.
  6. For scopes, choose Custom Scopes and grant exactly these:
ScopeWhy JamStream needs it
droplet:createlaunch the session server
droplet:readfind it and read its address
droplet:deletedestroy it when the session ends
tag:create, tag:read, tag:deleteevery JamStream droplet is tagged, and the sweeper finds strays by tag
firewall:create, firewall:read, firewall:deleteeach session gets its own firewall, created before the droplet so the server is never exposed unfiltered
regions:read, sizes:read, actions:read, image:read, ssh_key:readrequired companions of droplet:create in DigitalOcean's scope system, and sizes:read is how live pricing is fetched
snapshot:read, vpc:readfurther companions the droplet scopes pull in; JamStream never reads a snapshot or a VPC itself

The scope names are from DigitalOcean's scope reference. The droplet scopes list the read scopes as requirements, so the token cannot be created without them.

If the console offers to add a scope you did not pick, that is why: accept it. It shows the two in the last row as required once the others are selected, which brings the total to sixteen.

  1. Click generate and copy the token immediately; it is shown once.

Scoped this way, the token can manage droplets, tags, and firewalls, and nothing else in your account: no storage, no DNS, no billing.

A token missing the firewall scopes fails at launch with 403 Forbidden: You are not authorized to perform this operation, because creating the session firewall is the first thing a launch does. Add the three firewall scopes to the existing token; nothing needs to be recreated.

3. Connect the app

In the host wizard, select digitalocean; while no credential is saved the row reads setup needed and the Connect DigitalOcean pane opens:

The credential pane in the current build.

  1. Paste the token into the API token field (Show reveals it if you need to compare).
  2. Click Check credentials. The app authenticates against the API with the pasted token, fetching a price and listing anything JamStream-tagged.
  3. A passing check saves it: the pane says "Works. Saved to your keychain." and the row flips to ready.

A failure is shown verbatim, and nothing is stored.

The token lives in your system keychain from then on; the pane does not appear again. You are ready to host; continue with the quickstart.

4. Optional: a Space and a Spaces key, for recording

Recording a cloud session writes takes to a Spaces bucket in your own account. The Spaces key is not the API token from step 2.

That is the mistake everyone makes once: the dop_v1_... token cannot talk to Spaces at all, because Spaces is S3-compatible and signed with an access key pair rather than a bearer token.

  1. In the control panel, open Spaces Object Storage and create a bucket in the region you host droplets in. Spaces is a flat $5 per month including 250 GB of storage and 1 TB of transfer, so a recording adds nothing if you already have one. Give recordings a bucket that holds nothing else.

  2. Still under Spaces Object Storage, open the Access Keys tab, click Create Access Key, and name it jamstream-recording.

    Arming a session sets the bucket's expiry rule as well as writing to it, so the key needs full access to that bucket: a read-only or write-only key fails the check while you are configuring.

  3. Copy the secret immediately; it is shown once.

Keep it to the recording bucket. Launching a recorded session writes this key into the droplet's user data, so its worst case should be junk in a folder the retention rule empties anyway.

Paste both values into Settings, then Recording, in the app, and click Check.

From the terminal the pair goes in JAMSTREAM_RECORDING_ACCESS_KEY_ID and JAMSTREAM_RECORDING_SECRET_ACCESS_KEY, or in SPACES_ACCESS_KEY_ID and SPACES_SECRET_ACCESS_KEY; jamstream recordings covers every provider.

Spaces is not offered in every droplet region. If the check says so, it names the regions that have it.

For the CLI and automation

The CLI reads the token from the environment instead:

$ export DIGITALOCEAN_TOKEN=dop_v1_your_token_here
$ jamstream sweep --dry-run --provider digitalocean
No jamstream-tagged instances found.

That output means the token authenticates and can list droplets. In PowerShell the export line is $env:DIGITALOCEAN_TOKEN = 'dop_v1_your_token_here'.

Add the export to your shell profile if you host from the terminal regularly, or keep the token in a password manager and export it per session. The app reads DIGITALOCEAN_TOKEN as a silent fallback too, so a machine set up this way is ready in the wizard with nothing pasted.

AWS

AWS works well and is priced fine; the setup is more involved than DigitalOcean's because IAM is built for companies. Budget 30 minutes the first time. If you are not already an AWS user, DigitalOcean is less work.

JamStream launches a t4g.medium instance (arm64, Debian 12): about $0.034 per hour in us-east-1 as of July 2026 (on-demand pricing).

AWS includes 100 GB per month of free data transfer out across your whole account, which comfortably covers session audio.

1. Create the account

  1. Sign up at aws.amazon.com. You need a credit or debit card (AWS makes a temporary $1 authorization to verify it) and a phone number for an SMS or voice verification step (registration FAQ).
  2. As of July 2026, new accounts choose a free or paid plan and receive $100 in credits, with up to $100 more for completing onboarding activities; the free plan ends after 6 months or when credits run out (AWS Free Tier). Terms change; check the current ones.

2. Create an IAM user with a minimal policy

Do not use your root account's credentials. Create a user that can run the session VM, tear it down, and manage the firewall that lets your band reach it, and that cannot do anything else.

  1. Open the IAM console. In the navigation pane choose Users, then Create user (IAM user guide). Name it jamstream. It needs no console access.
  2. For permission options choose Attach policies directly, then Create policy, switch to the JSON editor, and paste:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "JamstreamInstances",
      "Effect": "Allow",
      "Action": [
        "ec2:RunInstances",
        "ec2:TerminateInstances",
        "ec2:DescribeInstances"
      ],
      "Resource": "*"
    },
    {
      "Sid": "JamstreamCreateSessionFirewall",
      "Effect": "Allow",
      "Action": "ec2:CreateSecurityGroup",
      "Resource": [
        "arn:aws:ec2:*:*:security-group/*",
        "arn:aws:ec2:*:*:vpc/*"
      ]
    },
    {
      "Sid": "JamstreamOwnSessionFirewalls",
      "Effect": "Allow",
      "Action": [
        "ec2:AuthorizeSecurityGroupIngress",
        "ec2:DeleteSecurityGroup"
      ],
      "Resource": "arn:aws:ec2:*:*:security-group/*",
      "Condition": {
        "StringLike": { "aws:ResourceTag/jamstream-session": "*" }
      }
    },
    {
      "Sid": "JamstreamFindSessionFirewalls",
      "Effect": "Allow",
      "Action": "ec2:DescribeSecurityGroups",
      "Resource": "*"
    },
    {
      "Sid": "JamstreamTagAtCreation",
      "Effect": "Allow",
      "Action": "ec2:CreateTags",
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "ec2:CreateAction": ["RunInstances", "CreateSecurityGroup"]
        }
      }
    },
    {
      "Sid": "JamstreamDebianAmiLookup",
      "Effect": "Allow",
      "Action": "ssm:GetParameter",
      "Resource": "arn:aws:ssm:*::parameter/aws/service/debian/*"
    }
  ]
}

What each Sid grants

One Sid per job. Action names are from the EC2 and SSM authorization references.

SidWhat it's forScope
JamstreamInstancesStart the session VM, stop it, read its address once AWS assigns one.
The sweeper reuses the same read to find VMs a crashed client left behind.
Resource: *
JamstreamCreateSessionFirewallCreate each session's own security group before the instance launches, so the VM is never briefly up on your VPC's defaults.Security groups and VPCs, Resource: *.
Can create a security group in any VPC in the account.
A group carries no tag before it exists, so there is nothing to condition this block on.
JamstreamOwnSessionFirewallsOpen the session's one UDP port, to 0.0.0.0/0 and ::/0.
Delete the group when the session ends or on the next sweep.
Groups tagged jamstream-session only; the rest of your account's groups carry no such tag and are out of reach.
JamstreamFindSessionFirewallsFind a group a half-finished launch left behind, and list the groups the sweeper should delete.Read-only, but across every security group in the region.
EC2's Describe actions take no resource or tag condition.
JamstreamTagAtCreationTag an instance or security group at the moment it's created (tagging at creation).Condition-scoped to RunInstances and CreateSecurityGroup; nothing else can be retagged.
JamstreamDebianAmiLookupLook up the current Debian 12 arm64 AMI for the region.The SSM parameter above, a public value published by AWS, not anything account-specific (details).
  1. Name the policy jamstream-host, create it, attach it to the user, and finish creating the user.

A key with this policy can run and terminate EC2 instances, manage its own session firewalls, read security groups and instances, and read one public parameter. It cannot reach S3, billing, or IAM.

If a launch fails with UnauthorizedOperation, the app names the action your policy is missing.

Compare it against the JSON above: an older jamstream-host policy predating the per-session firewall will be missing the security group blocks and the CreateSecurityGroup entry in the tagging condition.

3. Create an access key

  1. Open the jamstream user, go to the Security credentials tab, and under Access keys click Create access key (access key docs).
  2. When asked for a use case, choose Command Line Interface (CLI) and confirm.
  3. On the final page, copy both values or download the CSV; the secret is shown once.

4. Connect the app

In the host wizard, select aws. While no credentials are saved the row reads setup needed and the Connect AWS pane opens; Open the IAM console lands on the users page.

  1. Paste the access key id and the secret access key.
  2. Click Check credentials. The app authenticates against the API with the pasted keys.
  3. A passing check saves them: the pane says "Works. Saved to your keychain." and the row flips to ready.

A failure is shown verbatim, and nothing is stored.

The keys live in your system keychain from then on. You are ready to host; continue with the quickstart, picking aws in the wizard instead.

5. Optional: a bucket and a second key, for recording

Recording a cloud session writes takes to an S3 bucket in your own account. The key that writes them must not be the key from step 3.

Launching a recorded session writes this key into the session machine's user data, so it has to be a key whose worst case is junk in one bucket prefix, not a key that can start and destroy EC2 instances.

Leave recording off and everything above is all there is.

  1. In the S3 console, Create bucket, in the same region you host in, with the default settings. Give recordings a bucket that holds nothing else: the lifecycle permission below is bucket-wide.
  2. Create a second IAM user, jamstream-recording, the same way as step 2, with only this policy. Name your bucket in both places:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "JamstreamWriteRecordings",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject",
        "s3:AbortMultipartUpload"
      ],
      "Resource": "arn:aws:s3:::YOUR-BUCKET/jamstream/recordings/*"
    },
    {
      "Sid": "JamstreamRetentionRule",
      "Effect": "Allow",
      "Action": [
        "s3:GetLifecycleConfiguration",
        "s3:PutLifecycleConfiguration"
      ],
      "Resource": "arn:aws:s3:::YOUR-BUCKET"
    },
    {
      "Sid": "JamstreamFindTakes",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::YOUR-BUCKET",
      "Condition": {
        "StringLike": {
          "s3:prefix": "jamstream/recordings/*"
        }
      }
    }
  ]
}
  1. Give that user its own access key, exactly as in step 3.

The key can read, write and delete under one prefix of one bucket, list what is there, and read and set that bucket's expiry rules. It cannot see anything outside jamstream/recordings/, and it cannot touch EC2.

What each Sid grants

SidWhat it's forScope
JamstreamWriteRecordingsUpload each take (PutObject).
Download it again from the app or jamstream recordings (GetObject), the whole point of a bucket you own.
Clean up an interrupted upload (AbortMultipartUpload).
Remove the small probe object that arming a session writes and deletes, so a bucket that refuses the key fails while you're configuring rather than mid-song (DeleteObject).
One prefix of your bucket: jamstream/recordings/*
JamstreamFindTakesList what's under the prefix, so the app and CLI can find a take.arn:aws:s3:::YOUR-BUCKET, conditioned on prefix jamstream/recordings/*: keeps the rest of the bucket invisible.
JamstreamRetentionRuleRead and set the bucket's retention rule.Bucket-wide: the other reason recordings want a bucket of their own.
Both Get and Put are needed: setting a rule replaces the whole list, so the rules already there are read and written back with the new one.
Grant only the Put half and arming a session says retention could not be applied, and nothing will delete the takes for you.

The blast radius of this key

One thing to know rather than worry about: this key is written into each session machine so it can upload, so anything it can do, a compromised session machine could do to that prefix.

It could already delete your takes before it could read them, which is the worse of the two, so GetObject widens that less than it looks.

If you would rather the machine could only write, make a second key with PutObject and AbortMultipartUpload alone for recording and keep this one for the app; nothing in JamStream requires them to be the same key.

Paste both values into Settings, then Recording, in the app, and click Check. The app keeps this key in a keychain slot of its own, so the two AWS keys never stand in for each other.

From the terminal the pair goes in JAMSTREAM_RECORDING_ACCESS_KEY_ID and JAMSTREAM_RECORDING_SECRET_ACCESS_KEY.

AWS_ACCESS_KEY_ID is deliberately not read for recording, for the reason at the top of this section: if only that pair is set, the launch says so rather than handing the machine your launch key.

jamstream recordings covers every provider.

For the CLI and automation

The CLI reads the keys from the environment instead:

$ export AWS_ACCESS_KEY_ID=AKIA...
$ export AWS_SECRET_ACCESS_KEY=your_secret_here
$ jamstream sweep --dry-run --provider aws
No jamstream-tagged instances found.

That output means the key authenticates and can list instances. In PowerShell the export lines are $env:AWS_ACCESS_KEY_ID = 'AKIA...' and $env:AWS_SECRET_ACCESS_KEY = 'your_secret_here'.

JamStream reads exactly these two variables; it does not read ~/.aws/config profiles in the current build. The app reads the same variables as a silent fallback, so a machine set up this way is ready in the wizard with nothing pasted.

GCP

Google Cloud works well; the setup has the most moving parts of the three providers: a project, an API to enable, a service account, and a key file. Budget 30 minutes.

If you are starting from nothing, DigitalOcean is less work, and GCP is the only one of the three that bills the session's audio traffic on top of the machine.

JamStream launches an e2-medium instance: about $0.034 per hour in us-central1 as of July 2026 (VM pricing).

Internet egress on the default premium tier costs $0.12 per GiB after the first free GiB each month (network pricing), which adds roughly $0.19 to a three hour four musician session.

1. Create the account

  1. Sign in with a Google account at console.cloud.google.com and set up a billing account; a card or other valid payment method is required, with a small temporary authorization hold.
  2. As of July 2026, new customers get a $300 credit valid for 90 days (free trial terms). Terms change; check the current ones.

2. Create a project and enable Compute Engine

  1. Create a project at console.cloud.google.com/projectcreate; name it jamstream and note the generated project id, which may have a suffix (creating projects).
  2. Enable the Compute Engine API for the project: open APIs & Services, then API Library, search for "Compute Engine API", and click Enable. With the gcloud tool installed it is one command:
$ gcloud services enable compute.googleapis.com

3. Create a service account with one role

  1. In the console, open IAM & Admin, then Service Accounts, and click Create service account (docs). Name it jamstream.
  2. Grant it exactly one role: Compute Instance Admin (v1), roles/compute.instanceAdmin.v1. It covers creating, listing, labeling, and deleting instances (Compute IAM roles).
  3. Click Done. You do not need roles/iam.serviceAccountUser: that role is only required to create VMs that run as a service account, and JamStream's session VMs run with no service account attached at all, so the VM itself holds no Google credentials.

Scoped this way, the key can manage Compute Engine instances in this one project and nothing else: no storage, no other projects, no IAM changes.

4. Create a JSON key

  1. Open the jamstream service account, go to the Keys tab, click Add key, then Create new key, choose JSON, and click Create (key docs). The key file downloads once; store it like a password.

  2. If the create button is blocked with an organization policy error: organizations created since May 2024 disable service account key creation by default (secure by default).

    Personal accounts without an organization are unaffected. An organization admin can lift iam.disableServiceAccountKeyCreation for the project; otherwise use a short-lived token instead, below.

5. Connect the app

In the host wizard, select gcp. While no credential is saved the row reads setup needed and the Connect Google Cloud pane opens; Open the service accounts page lands in the right console section.

  1. Paste the downloaded key file's contents into the service account JSON field, or enter the file's path and click Load file.
  2. Click Check credentials. The app authenticates against the API with the pasted key.
  3. A passing check saves it: the pane says "Works. Saved to your keychain." and the row flips to ready.

A failure is shown verbatim, and nothing is stored.

The key lives in your system keychain from then on; the project id is read from the key itself. You are ready to host; continue with the quickstart, picking gcp in the wizard instead.

6. Optional: a bucket and an HMAC key, for recording

Recording a cloud session writes takes to a Cloud Storage bucket in your own account, through Cloud Storage's S3-compatible interoperability endpoint. The credential is an HMAC key pair, an access key id beginning GOOG and a secret, not the JSON key from step 4.

  1. Create a Standard bucket in the location you host in: Cloud Storage, Buckets, Create. Give recordings a bucket that holds nothing else.

  2. On that bucket's Permissions tab, Grant access, with the jamstream service account as the principal and one role: Storage Admin, roles/storage.admin, on this bucket alone.

    Arming a session writes and deletes a probe object, and reads and sets the bucket's expiry rules; the object-only roles cannot do the last two.

  3. Create the key: Cloud Storage, Settings, the Interoperability tab, Create a key for a service account, pick jamstream, then Create key. Copy both values; the secret is shown once.

Paste both values into Settings, then Recording, in the app, and click Check.

From the terminal the pair goes in JAMSTREAM_RECORDING_ACCESS_KEY_ID and JAMSTREAM_RECORDING_SECRET_ACCESS_KEY, or in GCS_ACCESS_KEY_ID and GCS_SECRET_ACCESS_KEY; jamstream recordings covers every provider.

Granted on one bucket, the key can do anything inside that bucket and nothing outside it, which is why recordings belong in a bucket of their own: launching a recorded session writes this key into the machine's user data, and the JSON key from step 4 must never go there.

For the CLI and automation

The CLI reads the key from the environment instead:

$ export GOOGLE_APPLICATION_CREDENTIALS=$HOME/keys/jamstream-gcp.json
$ jamstream sweep --dry-run --provider gcp
No jamstream-tagged instances found.

That output means the credential authenticates and can list instances. In PowerShell the export line is $env:GOOGLE_APPLICATION_CREDENTIALS = "$HOME\keys\jamstream-gcp.json". The project id is read from the key file; set GOOGLE_CLOUD_PROJECT only if you need to override it.

No key file, or key creation blocked? JamStream also accepts a pre-minted token, which expires after about an hour. This mode is environment-only, in the app and the CLI alike:

$ export GOOGLE_CLOUD_PROJECT=jamstream-123456
$ export GCP_ACCESS_TOKEN=$(gcloud auth print-access-token)

In PowerShell those are $env:GOOGLE_CLOUD_PROJECT = 'jamstream-123456' and $env:GCP_ACCESS_TOKEN = gcloud auth print-access-token. The app reads the same variables as a silent fallback, so a machine set up either way is ready in the wizard with nothing pasted.

Understanding cost

A session costs machine time plus network traffic, paid to your cloud provider. Both are small.

This page covers how the preview is computed, what egress is, and the guardrails that keep a mistake from costing more than a coffee.

Local sessions cost nothing: hosting on this computer (the wizard's local row, or --provider local in the CLI) rents no machine and meters no egress, and the preview says so instead of pricing anything.

The only guardrail a local session needs is the idle exit, since a forgotten process bills nobody. The rest of this page is about the cloud providers.

The preview

Before anything launches, the app's wizard and jamstream host show the same preview:

Cost preview for digitalocean nyc3 over 3.0 hours:
VM $0.02679/hr x 3.0 h                           $0.08037
Egress estimate 1.62 GB at $0.01/GB               $0.0162
Included egress credit (3000 GB free)            -$0.0162
Total (estimate)                                 $0.08037
LineWhat it means
VMHourly price times your expected length, set on the preview step (--hours in the CLI). Fetched live from the provider where possible (DigitalOcean's sizes API), otherwise from a bundled snapshot of public pricing, so the numbers here are approximations.
Egress estimatePredicted outbound traffic times the provider's per-GB rate.
Included egress creditFree transfer the provider bundles in, when it covers some or all of the estimate.
ProviderIncluded egress
DigitalOcean dropletsthousands of GB
AWS100 GB per month
GCPnone

Four musicians for three hours is about 1.6 GB; add a Twitch or YouTube destination and it is about 5.2 GB.

The expected length only shapes the estimate. The real bill is metered: elapsed time times the hourly rate, plus measured traffic. Play four hours after previewing three and you pay for four.

What egress is

Cloud providers charge for data leaving their network, per gigabyte, and call it egress. Inbound is free.

For JamStream that outbound data is the mixes the server sends to each member, so egress scales with people and hours, not with how loud you play.

ProviderA normal session's egress
DigitalOcean$0, inside the included allowance
AWS$0, inside the included allowance
GCPabout $0.05 per musician per three hours

Broadcasting is the exception: one Twitch or YouTube destination is about 1.2 GB per hour, more than four musicians' audio combined.

Two broadcast destinations, three hours (about 7 GB)Cost
DigitalOcean / AWS$0, inside the included allowance
GCPabout $0.80

See Streaming to Twitch and YouTube.

Recording

Recording a session on your own computer costs nothing but disk.

Local diskSize per hour
Mix onlyabout 0.4 GB
With stems (four piece)about 2 GB

A cloud session records to a bucket, which adds two charges: storage now, and download egress later.

A three hour takeSize
Mix onlyabout 1.2 GB
With stemsabout 6.2 GB

Storage is the small one:

StoragePrice30 days of stems, the default retention (~6.2 GB)
S3$0.023/GB-monthabout $0.14
Cloud Storageabout $0.02/GB-monthabout $0.12
DigitalOcean Spaces$5/month flat, includes 250 GBincluded

A Spaces subscription already covers a take at that price. Uploading itself costs nothing, since the machine and the bucket sit at the same provider.

Both lines are in the preview before you launch, in the wizard and in jamstream host --bucket, and they move when you switch between the mix and stems.

The egress lands on the download. That is the one cost in JamStream that arrives after a session has finished pricing itself, which is why jamstream recordings get prints the figure and waits for a yes before it moves a byte.

Downloading 6.2 GB of stemsCost
S3about $0.56
Cloud Storageabout $0.75
DigitalOcean Spacesabout $0.06

Those are list prices with no allowance taken off, which is what the app and the CLI quote too: a plan with included transfer draws on the same pool the session's own streaming does, so the figure is an upper bound rather than a guess.

The guardrails

Two timers set at launch, then a ticker and a sweeper.

GuardrailDefaultWhat it does
Idle exit (--idle-min)10 minutesShuts the server down once no musicians are connected.
Hard cap (--max-hours)12 hoursDestroys the server no matter what, enforced on the machine itself, not by your laptop.

GCP is the exception, and it costs money if you walk away. There the idle window stops the server but cannot delete the machine, so an abandoned session keeps billing until the hard cap.

Roughly $0.39 on an e2-medium if everyone leaves at twenty minutes and nobody comes back. End the session when you are done, or press Stop strays afterwards, and you pay for the time you played. Shortening --max-hours bounds it too.

The cost ticker. While a session runs, cost so far and elapsed time sit at the right-hand end of the app's status bar, beside Record and Leave, and jamstream status prints accrued and projected cost per session. You always know the meter's reading; nothing accrues silently.

The sweeper. Every machine JamStream launches is tagged. Stop strays, on the app's Recent sessions card, finds everything with the tag across every account this computer holds a key for and destroys it, then says what it could not account for.

jamstream sweep does the same from a terminal, with --dry-run to list without destroying. jamstream host also warns you at launch if tagged machines already exist. When in doubt, sweep.

12-hour walkaway, all guardrails ignoredCost
DigitalOceanabout $0.32
AWSabout $0.40
GCPabout $0.40

Plus cents of egress.

Checking the meter

$ jamstream status
SESSION    PROVIDER/REGION      STATUS      ELAPSED      ACCRUED      PROJECTED TAKES
3f2a9c01   digitalocean/nyc3    running  1 h 04 min    $0.028576 $0.08037 at 3.0 h our-jams +stems
b7e5c9b6   local/local          ended    2 h 13 min        $0.00              - -

Accrued is hourly rate times elapsed time; it stops when the session ends. TAKES is the bucket the session recorded to, if it recorded to one; a take on your own disk shows a dash and lives in the folder Recording a session names.

Troubleshooting

The app does not start

Every launch writes a log, and a failure before the window opens lands in it. The app prints the path itself under Settings, then You. Read the file directly at:

PlatformLog path
macOS~/Library/Application Support/jamstream/logs/app.log
Windows%LOCALAPPDATA%\jamstream\logs\app.log
Linux~/.local/share/jamstream/logs/app.log, or under $XDG_DATA_HOME if you set it

The file is truncated at every start, so it holds one run and no history. It opens with a version banner; everything after that line is a warning or a crash, and that is what to paste into the bug report. A file with the banner alone means the run was healthy.

Running .\jamstream-app.exe from PowerShell shows nothing: a release build on Windows has no console attached, by design, so the log is the only place the error appears. On macOS and Linux a terminal run also prints to stderr.

A local session server that dies at startup is a different case with its own log; see Playing on the same network.

Latency feels high

Start with the number, not the feel. The headline figure in the session status bar is mouth to ear: the milliseconds from sound entering an interface to the last buffer JamStream hands your sound card, the playout buffer included. What the card holds after that is the one part no figure here can see.

The network is in that figure twice, because your sound crosses to the server and out again to the player hearing it, and your own round trip is charged for both crossings. That is right when the band's connections are alike, and low by the difference when somebody's is worse than yours.

Two players in the same room stand about 3 ms apart per meter of air; a total under 30 ms feels like playing across a large stage, and most people stop noticing under 20 ms.

Check these in order of payoff:

  1. Bluetooth. Bluetooth headphones or earbuds add more delay than JamStream's entire network path. Use wired headphones, always. This is the most common cause of "it feels wrong" with a good-looking number.

  2. Wifi. Wifi adds jitter, which inflates the jitter buffers. Hover the latency number for the buffer readout, which reads "buffer 3/4 frames": the depth it is holding against the depth it is aiming for, in 2.5 ms frames. If it sits high or climbs, plug in ethernet.

    There are two of them and the number carries both. buffer is your own; server buffer under it is the one the session server holds on your uplink, which your link sets and only the server can see.

    Yours stands in for the buffer under whoever you are listening to, the same way your round trip stands in for theirs.

  3. Buffer size. On the Audio tab, under Buffer size, start at the smallest of 120, 240, or 480 frames (2.5, 5, or 10 ms).

    Each step costs three times its own size in the number: the buffer is paid once going in and twice coming out. The hover names both directions, as capture buffer and playout buffer.

    The playout side is not fixed. A machine whose audio keeps coming close to breaking up holds a little more than the size asks for, 2.5 ms at a time and never more than the size again, and gives it back once it is keeping up.

    A line under the choices says how much and why, so the latency you are getting is on screen even when it is not the size you picked. Nothing appears there while the size is all you are paying.

    You are asked to move the size only when that runs out of room. The same line then names the next size up, and says what taking it costs: more latency, and a reopen the band hears as a few hundred milliseconds missing of you. A crackling tag before that means the extra depth is still working on it.

    Add extra depth automatically, under the choices, is how you stop it. Untick it and you get the size you picked and nothing else, on this session and every later one on this computer.

    If the audio still comes close to breaking up with the box unticked, the line says so and points back at it: extra depth is the one fix that costs no reopen. The sizes stay pickable either way, because nothing in the app ever moves the size for you.

  4. Sample rate. A direction JamStream's own converter carries costs about 3 ms, and says so with a muted converting tag naming both rates.

    The headline number already includes it and the hover breaks it out per direction. Device problems has the whole ladder.

  5. WASAPI mode, on Windows. Exclusive adds about 10 ms, shared 20 to 30. The latency number's hover names which one this session got.

    JamStream asks for exclusive by default. The "Allow exclusive access" setting under Devices turns that off when another app needs the same device, at the shared-mode cost.

    Exclusive mode itself needs "Allow applications to take exclusive control of this device" ticked on the device's Advanced tab in the Sound dialog, for both input and output. Without it the session opens shared instead, and the hover is the only place that says so.

  6. Region. If the session's round trip (rtt, on the latency number's hover) is high for you specifically, the server is far from you. The host can pick a fairer region next time; see Hosting a session.

  7. Loss. The same hover carries a rate per direction over the last second, so a bad moment clears once it passes. uplink loss is what the band is missing of you; downlink loss is what you are missing of them.

    Both should sit near 0.0%. Above 1% sustained points at the local network: congested wifi, a saturated uplink, a bad cable.

    An uplink loss tag beside the latency number means the band is missing enough of you to hear it. Nothing you hear will tell you, because your monitoring, your meters and the room are all downstream of it. Same fix, from your end.

The band can't keep together

This is a different complaint from a high latency number: everyone sounds right alone, and only together does the tempo sag, worse the further apart the band is.

Each player hears their own instrument immediately and everyone else a full network path away, so the error each one feels is their own mouth to ear number, and no two players carry the same one.

Two fixes, and neither of them moves that number:

  • The host turns the click on, in the Metronome panel; each player chooses whether to hear it. The server mixes it into every personal mix, so it reaches everyone on one timeline and the band follows it instead of each other.
  • Turn on Hear yourself through the server, on the Audio tab. Your own sound joins the mix, so the gap you hear becomes the difference between two uplinks instead of the whole network path. Needs headphones: through speakers it loops your own signal back into the microphone.

When mouth to ear holds above about 30 ms with somebody else in the room, the Audio tab offers it above that control, once per session. Read the headphone line under it before ticking anything, and leave it off if you are on speakers.

Device problems

Input and output devices are picked on the Audio tab of Settings, under Devices: a Capture picker, a Playback picker, and Rescan beside the heading. The Input level meter above them should move when you play.

The lists come from the platform's audio backend (CoreAudio on macOS, WASAPI on Windows, PipeWire or ALSA on Linux). Each starts with a System default entry that follows the operating system when the default moves, with the concrete devices listed after it.

A change mid-session reopens the stream on the new device without leaving the session.

A stream that stops says so in three places: the device's own reason above the mixer strips, a no audio tag beside the latency number, and both the reason and what the reopen cadence is doing under the pickers.

SymptomCauseFix
Input meter stays stillWrong capture device selected, or the OS has not granted microphone accessPick the right device; on macOS grant microphone permission when prompted, and on Windows allow desktop apps to access your microphone, or capture is silence
New interface missing from a pickerDevices plugged in or enabled after launch are not listedPress Rescan, next to the Devices heading
"no devices found" in a pickerThe platform reported nothing for that directionCheck the interface is connected and visible to other apps, then Rescan
Selected device disappears mid-sessionA rescan found the device goneThe picker falls back to System default and says so under the pickers
A device pick does not takeThe device refused to openThe pick stays selected, the device's own reason sits under the pickers, and the app tries again six times on a widening wait; pick another device to get sound back sooner
Sound stops, and a no audio tag appears beside the latency numberThe stream stopped and is being reopened, or was reopened six times without staying openThe Audio tab says which; once it says the device did not stay open, nothing more is tried until you pick a device there
Short gaps the band hears, and a cutting out tag appears beside the latency numberThe device has stopped and been reopened three or more times in the last few minutes. Each one is a gap, and each is reopened too fast to show as no audioThe Audio tab counts them under the pickers. Check the cable and the port first; on Windows, ticking off "Allow exclusive access" also helps, because an exclusive endpoint drops the stream on any hiccup
Other apps go silent while you play (Windows)Exclusive mode holds the device alone for the lowest latencyUntick "Allow exclusive access" under Devices to share the device, at the 10 to 20 ms shared-mode cost

Sessions run at 48 kHz, and JamStream carries a device at any other rate automatically. A note under the pickers, per direction, names whichever of these it landed on:

What happenedThe noteCost
The device already runs at 48 kHzNothing is saidNone
macOS moved the device's own clock"moved the capture device to 48 kHz (was 44.1)"None
Windows or PipeWire converts inside the platform"the OS is converting capture to this device's 44.1 kHz"Not measured, and no tag
JamStream's own converter carries the direction"converting capture 44.1 kHz to 48 kHz (+2.7 ms)"About 3 ms, counted in mouth to ear, with a converting tag

On macOS a device another app holds at 44.1 kHz, BlackHole under GarageBand say, is moved to 48 kHz and GarageBand keeps playing, because macOS resamples app output to the device's rate. An app that takes the clock back mid-session gets it: JamStream stops asking and converts instead.

Only the last row costs milliseconds. Any of them goes away if you set the device to 48 kHz yourself:

PlatformWhere
macOSAudio MIDI Setup, Format
WindowsSound settings, then More sound settings (or run mmsys.cpl), the device's Properties, Advanced, Default Format; Recording tab for the input, Playback tab for the output
LinuxPipeWire carries any graph rate as it is; on bare ALSA the converter covers the card

The one device still refused is a Bluetooth or headset microphone in its hands-free mode: it has no 48 kHz setting anywhere and would sound like a phone call, so use another capture device.

Firewall and NAT

Members need only outbound UDP, so home routers and NAT need no configuration and no port forwarding.

  • If joining times out after about 10 seconds, something between you and the server is dropping UDP. Corporate and campus networks sometimes block outbound UDP on unusual ports; a phone hotspot is a quick way to confirm that is the cause.
  • Hosting on your own Windows machine is the exception: the first local host raises a Defender Firewall prompt for jamstreamd.exe, and it must be allowed on both Private and Public networks or bandmates on your network hit that same 10 second timeout.

Session ends unexpectedly

MessageMeaningAction
"removed from the session"The host revoked your invite or ended the sessionNone; ask the host for a new invite if the session is still running
"connection lost: no packets for 10 seconds"A network dropRejoin with the same invite; your seat is kept
(session vanished, nothing shown)Everyone left for longer than the idle window (default 10 minutes), and the server destroyed itself on purposeHost a new session; see the dead man's switch

Something is still running that should not be

In the app, Stop strays on the Recent sessions card destroys every JamStream-tagged machine in every account this computer holds a key for, and brings the session records back in line with what it found.

It reports three things apart: what it stopped, what it could not stop, and which providers it could not search. A provider with no credentials saved here, or one whose listing failed, was never looked at, which is not the same as finding nothing.

From a terminal:

$ jamstream sweep --dry-run

lists the same machines without destroying them. Run it without --dry-run to destroy them. Either way, a named instance that could not be stopped is still billing; destroy it from the provider's own console and report the bug.

CLI reference

Usage: jamstream <COMMAND>

The jamstream CLI is built for automation, scripting, and headless use.

It hosts, monitors, and ends sessions unattended, joins them without a display, and reads and writes the same session state files the app does, so either tool can watch or end what the other started.

People playing music want the app and the quickstart; the pages here document every command and flag.

CommandWhat it does
jamstream hostProvision a session server and mint invites.
jamstream statusList known sessions with elapsed time and accrued cost.
jamstream endDestroy a session's server and mark the session ended.
jamstream sweepFind and destroy orphaned jamstream instances.
jamstream joinJoin a session as a headless client.
jamstream recordingsList and fetch the takes a session recorded to a bucket.
jamstream completionsPrint shell completions for jamstream.

jamstream host

Provision a session server and mint invites.

Usage: jamstream host [OPTIONS]

Ranks the provider's regions by measured latency and price, shows a cost preview, asks for confirmation, launches the machine, verifies it answers a real encrypted handshake, prints one invite per seat, and records the session in a local state file.

With --provider local (the default) there is nothing to rank or bill: the server starts as a process on this computer, the cost line reads that local sessions cost nothing, and the handshake check still runs against the real server. See Playing on the same network.

Options

FlagDefaultMeaning
--provider <PROVIDER>localProvider to host on: local, digitalocean, aws, or gcp. Local runs the server on this computer, with no credentials and no cost.
--region <REGION>picked by rankingRegion id to use, skipping the latency ranking. Local has one region, local.
--musicians <MUSICIANS>4Musician seats in the session, counting you. 1 to 10, where 1 hosts alone and 10 is the server's capacity. --musicians 4 mints your host invite plus 3 musician invites.
--listeners <LISTENERS>0Listener seats in the session; one listener invite is minted per seat. 0 to 20.
--hours <HOURS>3Expected session length in hours, for the cost preview. Does not limit the session.
--destinations <DESTINATIONS>0Stream destination count, for the egress estimate.
--port <PORT>43210UDP port the session server listens on.
--idle-min <IDLE_MIN>10Minutes without musicians before the server shuts itself down.
--max-hours <MAX_HOURS>12Hard cap on session length in hours. Invites expire at the cap.
--recordoffLet this session record. A local session's takes land as FLAC files in a recordings folder on this computer and the launch output prints its path; a cloud session needs --bucket, because the machine deletes itself at the end and a take on its disk goes with it.
--record-stemsoffAlso capture a stereo stem per musician alongside the mix, named for them. Implies --record.
--bucket <BUCKET>noneBucket a cloud session records to, in its own region. Implies --record. Proves the key can write, applies the retention rule, and saves the bucket beside the session record for jamstream recordings. Needs a storage key; see that page for the variables.
--retention <RETENTION>30dHow long the bucket keeps this session's takes: 7d, 30d, 90d, or forever. Applies to --bucket, and is enforced by the bucket's own lifecycle rule, so it keeps working after the machine is gone.
--artifact-url <ARTIFACT_URL>pinned into release buildsOverride the URL of the jamstreamd artifact the VM downloads at boot. Release builds pin their own server build, so hosting on a cloud normally needs no flag; a source build has no pin and must pass both artifact flags. Local mode downloads nothing.
--artifact-sha256 <ARTIFACT_SHA256>pinned into release buildsOverride the expected sha256 of the jamstreamd artifact. Must be passed together with --artifact-url; the VM refuses to start on a mismatch.
--yesoffSkip the launch confirmation.
--jsonoffEmit one JSON object instead of human-readable output.

Examples

A session on this computer, for the people in the room:

$ jamstream host --provider local --yes

The same session able to record, with the folder takes land in printed at launch:

$ jamstream host --record --yes

A two hour duo session (you and one other player) with two spare listener seats, on DigitalOcean, unattended:

$ jamstream host --provider digitalocean --musicians 2 --listeners 2 \
    --hours 2 --yes

The same launch from a source build, which has no pinned server artifact, names one explicitly:

$ jamstream host --provider digitalocean --musicians 2 --listeners 2 \
    --hours 2 --yes \
    --artifact-url https://your-host.example/jamstreamd \
    --artifact-sha256 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08

The quickstart shows a full run end to end.

Notes

  • If JamStream-tagged machines already exist on the provider, they are listed with a warning before anything launches; run jamstream sweep if they are strays.
  • --record arms recording, it does not start it: the host presses Record in the session, and each Record to Stop is one take.
  • Finished takes are FLAC files named by date and time in the printed folder (record_dir in the --json output), and they stay there after jamstream end. See Recording a session.
  • --json prints the session id, address, invites, cost estimate, recording folder, and state file path as one object, for scripts.
  • The state file lands under your platform's data directory in jamstream/sessions/ and is what jamstream status and jamstream end read.

jamstream status

List known sessions with elapsed time and accrued cost.

Usage: jamstream status [OPTIONS]

Reads the local state files written by jamstream host and prints one row per session. Running sessions accrue by the second; ended ones show their final figures.

A row recorded as running is checked against its provider first: if the instance is gone the row prints stale instead, with a pointer at jamstream end to close the record.

Options

FlagDefaultMeaning
--hours <HOURS>3Hours to project the total cost over.
--jsonoffEmit a JSON array instead of a table.

Example

$ jamstream status --hours 4
SESSION    PROVIDER/REGION      STATUS      ELAPSED      ACCRUED      PROJECTED TAKES
3f2a9c01   digitalocean/nyc3    running  1 h 04 min    $0.028576 $0.10716 at 4.0 h our-jams +stems
b7e5c9b6   local/local          ended    2 h 13 min        $0.00              - -

Notes

  • ACCRUED is the machine's hourly rate times elapsed time; egress is not included, so the provider's bill can be cents higher.
  • PROJECTED extends the rate to the --hours horizon, for running sessions only.
  • A provider that cannot be checked (credentials not in this shell, network down, one of its regions unreachable) proves nothing, so the row keeps its recorded status and a line under the table says it could not be checked.
  • In --json, rows recorded as running carry "corroborated", and "status" is "stale" when the provider no longer lists the instance. Nothing is rewritten on disk; jamstream end and jamstream sweep do that.

jamstream end

Destroy a session's server and mark the session ended.

Usage: jamstream end [OPTIONS] [SESSION]

Destroys the machine, confirms with the provider that nothing tagged with the session is still listed, and rewrites the local state file as ended. Pass a session id prefix, or --last; one of the two is required.

Arguments and options

FlagMeaning
[SESSION]Session id prefix of the session to end. Any unambiguous prefix works; only running sessions match.
--lastEnd the most recently created running session. Conflicts with a prefix.

Example

$ jamstream end 3f2a9c01
Session 3f2a9c01 ended. Instance 512190713 is destroyed.

Or, when only one session is running:

$ jamstream end --last

Notes

  • If the machine is already gone (it idled out, hit its hard cap, or was swept), end says so and still marks the session ended:

    Instance 512190713 was already gone; marking the session ended.
    
  • If the app kept a copy of the session server's log, end names the file. The machine's own journal is destroyed along with the machine, so that copy is the only place a failed broadcast or take is explained:

    The session server's log is at ~/.local/share/jamstream/sessions/logs/3f2a9c01....log.
    
  • If the provider still lists instances for the session after the destroy call, end fails loudly and points you at jamstream sweep; it never silently leaves something billing.

  • An ambiguous prefix lists nothing and asks for more characters; run jamstream status to see the ids.

jamstream sweep

Find and destroy orphaned jamstream instances.

Usage: jamstream sweep [OPTIONS]

Every machine JamStream launches carries a jamstream tag with its session id. Sweep lists everything with that tag, across every provider whose credentials are in the environment, and destroys it.

Local sessions need no credentials and are always included, so a stray server process on this computer is found the same way.

This is the backstop for crashed sessions, lost laptops, and anything else that slipped past jamstream end.

Options

FlagMeaning
--dry-runReport what would be destroyed without destroying anything.
--provider <PROVIDER>Sweep one provider instead of every configured provider.

Example

$ jamstream sweep --dry-run
PROVIDER       REGION         INSTANCE         RESULT
digitalocean   nyc3           512190713        would destroy
1 found, 0 destroyed, 0 failed.

$ jamstream sweep
PROVIDER       REGION         INSTANCE         RESULT
digitalocean   nyc3           512190713        destroyed
1 found, 1 destroyed, 0 failed.

A clean account prints one line:

$ jamstream sweep --dry-run
No jamstream-tagged instances found.

Notes

  • sweep --dry-run --provider <name> doubles as the credential check for a newly configured provider; see Provider setup.
  • Sweep destroys by tag, so it also catches machines from other computers and old versions, and nothing untagged is ever touched.
  • Sweep also closes this machine's record of any session whose instance it destroyed or found already gone, so jamstream status stops reporting it as running.
  • Closing a record is only done on evidence: a provider that could not be searched, or one region of it that could not be listed, leaves its records alone.
  • If a destroy fails, or a provider or any of its regions could not be searched, sweep exits nonzero and says which, because anything it could not account for may still be billing.

jamstream join

Join a session as a headless client.

Usage: jamstream join [OPTIONS] --input <INPUT> --output <OUTPUT> --duration-secs <DURATION_SECS> [INVITE]

A real client without a screen: it joins with an invite, plays a WAV file as its capture signal, records the stereo mix it receives, and prints session events as plain lines. Built for test rigs and automation; people use the desktop app.

The invite is the seat. Pass it on stdin or in a file, never as an argument: process arguments are readable by every account on the machine, and the string stays in shell history.

Arguments and options

FlagMeaning
[INVITE]Invite string, with or without the jamstream://join/ prefix. Deprecated: readable by any local user in the process list. Prints a warning.
--invite-file <PATH>Read the invite from a file, one line, or - for stdin. With neither this nor the positional form, the invite is read from stdin.
--headlessRun without a UI. Required; the desktop app is the interactive client.
--input <INPUT>48 kHz mono or stereo WAV sent as the capture signal. Silence after the file ends.
--output <OUTPUT>Output WAV path for the received stereo mix.
--duration-secs <DURATION_SECS>Seconds to stay in the session after joining.
--chat <CHAT>Chat message to send once after joining.
--name <NAME>Display name to request. Not sent yet; names come from the invite.

Example

Hold a seat for two minutes, contribute a pre-recorded take, and keep what came back:

$ jamstream join --invite-file seat.txt \
    --headless --input take.wav --output mix.wav --duration-secs 120 \
    --chat "bot in the room"
joined
roster: 3 members
chat from 1: heard you
metronome: 112 bpm, 4 beats per bar, on
left after 120 s; wrote mix.wav

Or with nothing on disk at all:

$ pass show band/seat | jamstream join --headless \
    --input take.wav --output mix.wav --duration-secs 120

Notes

  • The invite file is read one line at a time and capped at 4 KiB; a trailing newline and surrounding blanks are ignored.
  • The input WAV must be 48 kHz; stereo files are downmixed to mono. Anything else is rejected with a message naming the problem.
  • The output WAV is written on the four session endings as well as a clean one: ejected, rejected, timed out, and session full. A socket or file error, or an --input file that fails validation, exits without writing it.
  • Chat lines, roster changes, metronome changes, and ejection reasons print one per line; latency samples are not printed.
  • The session's recorder prints as record: idle, record: recording (mix and stems), or record: failed: <reason> on every transition, so a rig can assert that a take ran. See Recording a session.
  • Whether the session can broadcast at all prints as broadcast: ready or broadcast: unavailable, <reason> when the server first answers and whenever the answer changes. See when a session cannot stream.
  • A version mismatch fails at the handshake with both versions named, never with silence.
  • A session with no free seat for the invite's role prints session full and exits nonzero, instead of waiting out a connection timeout. The desktop app keeps retrying instead, since a seat frees when somebody leaves.

jamstream recordings

List and fetch the takes a session recorded to a bucket.

Usage: jamstream recordings [OPTIONS] [COMMAND]
       jamstream recordings get [OPTIONS] <SESSION>

A cloud session records into your own bucket, because the machine deletes itself at the end and a take on its disk goes with it. This is how the takes come back out.

Local sessions need none of this: their takes are already on this computer, in the directory jamstream host printed.

Both forms read the bucket details jamstream host saved beside each session record when the session was launched with a bucket, and the storage key from the environment.

Options

FlagDefaultMeaning
--jsonoffEmit a JSON array instead of a table.

jamstream recordings get

FlagDefaultMeaning
<SESSION>requiredSession id prefix of the session whose takes to download. Any unambiguous prefix works.
--out <OUT>current directoryDirectory to write the takes into.
--yesoffSkip the download confirmation, which is where the egress cost is shown.

Listing

$ jamstream recordings
SESSION    TAKE                                           SIZE  MODIFIED
3f2a9c01   jamstream-2026-07-28-1930-mix.flac          1.38 GB  2026-07-28 19:30
3f2a9c01   jamstream-2026-07-28-1930-Ana.flac         691.2 MB  2026-07-28 19:30
3f2a9c01   jamstream-2026-07-28-1930-Bo.flac          691.2 MB  2026-07-28 19:31
5aed5593 has no takes in my-jams (aws/eu-west-1).
5aed5593 has 1 take on this computer, found in /Users/you/Music/JamStream/5aed5593.

Fetch a session's takes with: jamstream recordings get <session>

A take is named for the minute it started, in UTC: jamstream-YYYY-MM-DD-HHMM-mix.flac for the mix, and one jamstream-YYYY-MM-DD-HHMM-<name>.flac per musician when the session recorded stems. They all sit side by side under the session's own prefix; there is no stems folder.

A session that recorded nothing says so on its own line rather than showing an empty table.

The last line is the app's own download folder, a JamStream folder in your music directory with one folder per session, which this command reads as well as the bucket. So a take the retention rule has already deleted still shows as being here.

Nothing is left to check those files against, so no size stands beside them.

Fetching, and what it costs

Downloading is the one part of recording that costs money after the session has already been paid for: the bucket bills egress on every byte that leaves it. So the size and the price come first, and nothing moves until you say yes.

$ jamstream recordings get 3f2a9c01 --out ~/takes
Session 3f2a9c01 recorded 3 takes in my-jams (aws/eu-west-1), delete after 30 days.
Download 2.76 GB at $0.09/GB                    $0.248832
Egress is billed on the download, not on the recording.
Your plan includes 100 GB/month of free download, so this is an upper bound.
Billed to your own cloud account at list prices; JamStream never sees it.
Download these takes? [y/N] y
  jamstream-2026-07-28-1930-mix.flac       100%
  jamstream-2026-07-28-1930-Ana.flac       100%
  jamstream-2026-07-28-1930-Bo.flac        100%
3 takes in /Users/you/takes, 2.76 GB.
Egress for this download: $0.248832.

Pass --yes in a script. Progress is whole lines at fixed percentages, so a log reads the same as a terminal.

The storage key

The object stores want an access key pair, which is not the credential that launches machines. It goes in two variables of its own:

$ export JAMSTREAM_RECORDING_ACCESS_KEY_ID=...
$ export JAMSTREAM_RECORDING_SECRET_ACCESS_KEY=...

In PowerShell: $env:JAMSTREAM_RECORDING_ACCESS_KEY_ID = '...' and $env:JAMSTREAM_RECORDING_SECRET_ACCESS_KEY = '...'.

JAMSTREAM_RECORDING_ACCESS_KEY_ID and JAMSTREAM_RECORDING_SECRET_ACCESS_KEY are read on every provider. Some providers also read their own pair, since neither is a launch credential there:

ProviderAlso reads
AWSnothing extra. AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are deliberately not read here, because that pair launches instances and gets written into a machine's user data
DigitalOceanSPACES_ACCESS_KEY_ID, SPACES_SECRET_ACCESS_KEY
GCPGCS_ACCESS_KEY_ID, GCS_SECRET_ACCESS_KEY

Scope this key to writing the recordings prefix of one bucket; the last section of your provider's page creates it.

The app keeps the same key in your system keychain instead, and its Takes screen fetches from there. This command does not read that keychain, so export the pair here even when the app already has the key.

The key is never written to disk: only the bucket, region, and retention are kept beside the session record.

Notes

  • Takes outlive the session. A session ended weeks ago still lists, until the bucket's retention rule deletes the objects, and a copy already on this computer outlives those too.
  • get on a session the rule has emptied still fails, because there is nothing left to fetch, but it names the folder the copies it found are in.
  • What lands is checked against the size the bucket listed, and a file that arrives short is deleted rather than left looking like a recording.
  • A take already in the output directory at the right size is skipped and costs no egress. One at a different size stops the download instead of being overwritten.
  • If a bucket cannot be reached, that session's line says why and the other sessions still list, but the command exits nonzero.

jamstream completions

Print shell completions for jamstream.

Usage: jamstream completions <SHELL>

SHELL is one of bash, zsh, fish, powershell, or elvish.

Installed through Homebrew, completions are set up for you. Installed any other way, add one line:

# zsh (~/.zshrc)
$ source <(jamstream completions zsh)

# bash (~/.bashrc)
$ source <(jamstream completions bash)

# fish
$ jamstream completions fish > ~/.config/fish/completions/jamstream.fish

# PowerShell ($PROFILE)
jamstream completions powershell | Out-String | Invoke-Expression

PowerShell only runs $PROFILE scripts under an execution policy other than Restricted, the default; Set-ExecutionPolicy -Scope CurrentUser RemoteSigned is enough.

How it works

The machine

When you host, your computer asks a cloud provider for one small Linux VM, hands it a boot script, and completes an encrypted handshake with it before showing you any invite. A session is never announced that cannot be joined.

In local mode there is no VM: the same server runs as a process on your own machine.

Only the one UDP session port is reachable from outside. Nothing about a session persists in the cloud after it ends except a take you asked it to record, which lands in your own bucket.

The machine destroys itself three ways:

  • When you end the session.
  • When no musician has been connected for the idle window (default 10 minutes).
  • At the hard cap (default 12 hours), regardless.

The provider and the machine enforce those, not your laptop, so quitting the app does not leave anything running and billing. jamstream sweep destroys anything tagged that somehow survives.

GCP differs in one way worth knowing: an idle session stops serving but the machine is not deleted until you end the session, until your next jamstream sweep, or at the hard cap. The other two providers delete it on idle.

Invites

There are no accounts and no JamStream servers. Identity is the invite.

Hosting generates the session's keys on your computer and mints one invite per seat, each carrying one seat and an expiry. Invites are revocable mid-session and all of them expire at the hard cap. Every packet is encrypted and authenticated from the first handshake byte. There is no plaintext mode.

What that buys you: strangers cannot join, listen in, or disrupt a session, a leaked invite is one revocation away from useless, and no third party, JamStream included, sits between your band and your machine.

Latency

The target is under 30 ms mouth to ear. Measured with real Opus and real encryption over a simulated network:

Round trip to the serverMouth to ear
1 ms, one local network14.7 ms
12 ms, same region24.3 ms
45 ms, cross country over DSL69.8 ms

Each figure runs from sound entering the interface to the last buffer JamStream hands the sound card, the playout buffer included, which is as far as a measurement from inside the app can see: what the card holds after that is beyond its reach.

The two network legs are the only parts that grow with distance, which is why region choice is the decision that matters. Under about 30 ms it feels like standing on a stage together. At 70 ms it feels like a phone call.

About

License

JamStream is free software, dual-licensed under MIT and Apache 2.0; use it under either.

The bundled fonts, Public Sans and IBM Plex Mono, are under the SIL Open Font License, included alongside the font files in the repository and on this site.

Source

The source lives at github.com/sean-reid/jamstream. The desktop app, the CLI, and the session server are built from one repository, and this site is generated from the site/ directory in it.

Reporting problems

File issues at github.com/sean-reid/jamstream/issues. The reports that get fixed fastest include:

  • what you ran or clicked, and what happened instead of what you expected;
  • if the app never opens, the contents of logs/app.log in the app's data directory, which is where a crash before the window lands;
  • your operating system and, for audio problems, your interface and buffer size;
  • for session problems, the mouth to ear figure with every line from its hover, and the session id from the status bar;
  • for provider problems, the provider name and the exact CLI output, which never contains your credentials.

Documentation bugs are bugs. If this site disagrees with what the software does, file that too.