HighX

Contributing

How the repository is laid out, what the checks enforce, and the rules that are not negotiable.

Getting set up

bun install
bun run check-types    # strict TypeScript across every package
bun run test           # unit, plus workerd via @cloudflare/vitest-pool-workers
bun run bench          # the reactive engine, in process

The reference application in example/note is not a demo bolted on afterwards — it is where the framework is exercised end to end, and a change that does not work there is not finished.

cd example/note
bun run check-types    # the client, and the Worker's own project
bun run dev            # the Worker and Vite together

Then open it in two browser profiles and edit the same note in both. Most of what this framework claims is only observable that way: convergence, presence, what happens to an edit made while disconnected, and whether a reconnect resumes or resubscribes.

Layout

packages/core         descriptors, schemas, permissions — no platform dependencies
packages/runtime      execution, auth, migrations, retention, reactivity
packages/protocol     versioned wire frames and codecs
packages/client       connection, cache, subscriptions
packages/react        bindings over client
packages/compiler     graph analysis, exposure checks, codegen
packages/cloudflare   Durable Objects, D1, R2, KV, Vectorize
packages/cli          the highx command
packages/testing      in-memory adapters whose contracts match the real ones

Dependencies run one way: core knows about no platform, runtime never imports client code, client never imports a server adapter. A cycle is a build failure, not a style note.

What the checks enforce

  • No any, @ts-ignore, or @ts-nocheck. Prefer unknown at untrusted boundaries and narrow it.
  • Every exposed function declares auth. Omitting it fails the build. See authorization.
  • Server code cannot reach a client bundle. The compiler rejects the crossing and names the import chain.
  • Both success and failure paths are tested. A feature whose error path is untested is not covered.

Rules that do not bend

These are architectural commitments, not preferences. A change that violates one needs to argue with the design rather than work around it.

  1. A transaction is atomic only inside one storage authority. A tenant Durable Object and D1 never commit together. Cross-authority work is a saga.
  2. Correctness beats invalidation precision. Over-invalidating is a performance bug; under-invalidating is a correctness bug, and they are not the same kind of mistake.
  3. Validate at every boundary, including storage — a row read back from SQLite is untrusted input like any other.
  4. A released migration is immutable. Add a version; never edit one.
  5. Index ordering has one definition. compareIndexValues and the byte encoding an index is keyed by must agree exactly, and there is an exhaustive test that says so.
  6. Never claim a capability the platform does not have. If Cloudflare cannot do it, HighX does not pretend to.

Changing the wire protocol

Frames are versioned in @highx/protocol. A deployed server and a browser that loaded an hour ago will not always agree on which release they are running, so additive changes need a default and removals need a version bump. There is no "everyone will refresh" assumption available.

Performance changes

Anything touching the reactive engine, storage, or the client connection should come with numbers:

bun run bench                                    # in-process
bun run load --target <url>                      # a deployment

The load gate compares against a committed baseline and fails on regression — see load testing. If a change is meant to make something faster, update the baseline in the same pull request so the improvement is visible in the diff.

Releasing

Releases are cut from a tag by the Release workflow. Publishing by hand is possible but is not how a release should normally happen.

bun run set-version 0.2.0
bun install
git commit -am "Release 0.2.0" && git tag v0.2.0
git push origin main --tags        # the workflow does the rest

The workflow re-runs every gate that CI runs, builds, checks the compiled output loads under Node, and then publishes. workflow_dispatch offers a dry run that verifies and packs without publishing anything.

bun run release is what it calls, and it can be run locally with --dry-run. Three things it does that a list of commands would not:

  • Order. Packages publish in dependency order, computed from the workspace rather than from a list that will drift.
  • Resumability. Publishing is not atomic. A version already on the registry is skipped, so a re-run after a partial failure finishes the job instead of failing on what already succeeded.
  • Refusing the wrong version. Versions must agree with each other and with the tag. A version number, once published, cannot be reused.

Bun publishes the workspace because it rewrites workspace:* ranges to the concrete release version before creating each tarball. This is required for consumers to install the published packages.

A dry run reports exactly what ships. What should be in it:

  • dist/ — compiled JavaScript, .d.ts declarations, and source maps
  • src/ — TypeScript sources, which Bun consumers read directly
  • LICENSE and README.md

What should not: tests, tsconfig*.json, or anything under node_modules. prepublishOnly rebuilds from clean, so a stale dist cannot be published.

Versions

Packages depend on each other through the workspace:* protocol, which the publish step rewrites to the exact version being released:

"dependencies": { "@highx/core": "workspace:*" }   // in the repository
"dependencies": { "@highx/core": "0.1.0" }         // in the tarball

Exact rather than a range, because these packages share internal contracts and are released together. A consumer that resolved two different versions of them would be holding a bug that is very hard to see.

All nine move on one version, so there is a script rather than nine edits:

bun run set-version 0.2.0
bun install                 # refresh the lockfile

The protocol version in @highx/protocol is separate and moves only when a frame changes.

Documentation

Docs live in apps/docs/content/docs. A claim in them should be traceable to a test or a measurement; docs/phase-0-contracts.md maps contracts to the tests that cover them, and gaps are listed there as deferred rather than left implied.

Writing down what does not work is as valuable as documenting what does.

On this page