All work

JamScript

A live-coding music workbench where the knobs edit the code.

Kind
Product. Live, still developed.
Timeline
Started 17 October 2026 at Hacktoberfest Georgia
Team
Hackathon team of four. Two of us commit to the codebase.
Built with
TypeScript, TanStack Start, CodeMirror and Lezer, Strudel, Web Audio, Tauri, Cloudflare Workers, PostgreSQL, Liveblocks

01Context

Strudel lets you write music as code. A pattern like s("bd ~ sd ~") is a bar of drums, and .lpf(1200) is a filter on it. It is fast and expressive, and it is all text. Changing that filter while the loop plays means selecting four digits and typing.

JamScript began as our entry at Hacktoberfest Georgia 2026, a 36 hour hackathon. The idea was to keep the code and add hands: a knob under the line that has a number in it, a row of steps under the line that has a rhythm in it.

It reached the top 10 of the main track. I kept working on it afterwards, and it now runs at jamscript.dev and ships as a desktop app for Windows, macOS and Linux.

02My role

The workbench foundation, the control model, playback, audio export and the release pipeline.

The source repository is private. Every release publishes installers, signatures and a source archive.

03Two views of one song

The moment a knob exists, there are two descriptions of the music: what the code says and what the knob shows. Any editor that tries this has to answer what happens when they disagree. You type 800 while the knob still shows 1200. You paste a pattern the grid cannot draw. You press undo.

The usual answer is a sync engine that reconciles the two. I did not want to write one, because a sync engine has edge cases for as long as it exists.

Try it: the code is the only copy

A reconstruction of the idea, not JamScript itself. The real product parses the full syntax tree and plays Strudel. The controls here read one row of steps and two numbers. The player is a little wider: it also runs groups, repeats, alternation, layers, tempo and a swept filter, with sounds synthesised in your browser.

Move a control to see the patch it sends.

Rhythm
1200 Hz
0.8

04Decisions

  1. Where does a control keep its value?

    Options
    • Keep a separate model of the song and generate code from it
    • Keep code and controls as two states and reconcile them
    • Keep only the code and derive every control from it
    Chose
    Only the code. A control is a lens: a kind, a value read from the syntax tree, and the range of characters it came from. Moving it produces one text patch on that range.
    Because
    With one copy there is nothing to reconcile. Typing in the editor moves the knob, because the knob is read again from the text. Undo works, because a control edit is an ordinary editor change in the same history.
    It costs
    A control can only exist where the parser can prove what a piece of text means. Anything computed, like a filter driven by a sine wave, has no knob.
  2. What should a control do with a pattern it only half understands?

    Options
    • Draw what it can and rewrite the rest as best it can
    • Hide the control
    • Show the control locked, with the reason
    Chose
    Lock it and say "Edit this pattern in code". And after every patch, derive the whole document again and throw the patch away if any other control changed.
    Because
    A wrong rewrite of someone's song costs more trust than a missing control. The second check catches a patch that is valid text but shifts the meaning of something next to it.
    It costs
    Some patterns a cleverer rewriter could handle stay locked. I would rather extend the list of proven cases one at a time.
  3. How do you name a version of a song?

    Options
    • By timestamp
    • Ask the player to name each one
    • Describe what changed
    Chose
    Every Play saves a version, labelled from the same lens model: "bass lpf 1200 → 430", or "drums rhythm edit". When the change is not something a lens can identify, the label is a plain "Source edit".
    Because
    A label you can read is what makes history usable while music is playing. Reusing the lens model means the labels cannot disagree with the controls.
    It costs
    History is stored on the device. It is not shared between collaborators.
  4. How do three platforms ship from one repository?

    Options
    • Build the installers by hand for each release
    • One pipeline that verifies, deploys the site, then publishes installers
    Chose
    CI checks types, lint, tests and the web build, deploys the site to Cloudflare Workers, checks the live pages, and only then publishes installers for Windows, macOS and Linux to a public repository with an update feed. A version bump on main is the release.
    Because
    A release that depends on a person following a checklist will one day ship a desktop app that talks to a website nobody deployed.
    It costs
    The pipeline is long. Windows installers are still not code signed, so SmartScreen warns on first launch.

Going deeper

What a patch looks like

The product parses the document with Lezer, the parser behind CodeMirror. It walks call chains such as s(...).lpf(...).gain(...) and creates a lens for each literal argument it recognises. Each parameter has a range and a scale: a filter runs from 50 to 20,000 Hz on a logarithmic scale, gain from 0 to 1 on a linear one.

Turning the filter knob below from 1200 to 430 is this patch and nothing else. The demo on this page does the same thing with a small tokenizer in place of the full parser.

One control edit, as the editor receives it
// s("bd ~ sd ~").lpf(1200)
//                    ^^^^  characters 19 to 23
{ from: 19, to: 23, insert: "430" }
A database eleven time zones away

Shared workspaces keep their metadata in Postgres. The first database was in Sydney. From Tbilisi, opening a connection took 2.6 to 3.6 seconds and each round trip about half a second, while the query itself ran in under 3 milliseconds. More indexes could not fix that.

I moved the data to Ireland, verified every row by checksum, and put Cloudflare Hyperdrive in front for pooling. Query caching is off on purpose: when someone is removed from a workspace, the very next request has to see it.

One mistake on the way. I pointed Hyperdrive at the transaction pooler, which rejects prepared statements, and every shared query returned a 500 until the port was corrected. The timings are from my machine on 20 and 21 October 2026. They are not production guarantees.

The assistant never edits your song

Ask explains code. Edit proposes a change as a diff. Before the diff is shown, JamScript checks that the file is one the assistant may touch, that the source has not changed underneath it, and that the result still parses. Nothing changes until you accept.

If a model fails after its answer has started to stream, JamScript reports the interruption. It does not continue the sentence with a second model's text.

05The result

What works

  • Live at jamscript.dev. Browser workspaces need no account.
  • A desktop app for Windows, macOS and Linux with in-app updates. The macOS build is signed and notarized.
  • Shared workspaces with live collaboration, and view-only links that play inside a sandboxed frame.

What is not there

  • Windows installers are not code signed yet.
  • Version history lives on one device.
  • Controls cover literal values and simple patterns. Computed expressions stay in code, by design.

06Reflection

The decision I would defend hardest is the dull one: refusing to guess. In a demo, a locked control looks like a missing feature. In use, it is the reason you can trust the controls that are not locked.

The database work left me with a habit. Measure the whole path before touching the query. The SQL took a tenth of a millisecond. The connection took three seconds.