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 processThe 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 togetherThen 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 onesDependencies 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. Preferunknownat 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.
- A transaction is atomic only inside one storage authority. A tenant Durable Object and D1 never commit together. Cross-authority work is a saga.
- 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.
- Validate at every boundary, including storage — a row read back from SQLite is untrusted input like any other.
- A released migration is immutable. Add a version; never edit one.
- Index ordering has one definition.
compareIndexValuesand the byte encoding an index is keyed by must agree exactly, and there is an exhaustive test that says so. - 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 deploymentThe 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 restThe 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.tsdeclarations, and source mapssrc/— TypeScript sources, which Bun consumers read directlyLICENSEandREADME.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 tarballExact 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 lockfileThe 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.