Serve users from more places, give every contested record one winner, and review every change against real data before it ships.
Distributing an application has always been a trade. Put nodes near your users and reads get fast. But now your data has to live in more than one place, and you pay for that one of two ways: every node carries a full copy, or you run a system that tracks which data lives on which node. Either way, once data lives in several places, two nodes can each make a decision the other doesn't know about. So teams keep the cluster small, or they add a second system whose only job is to keep the first one honest.
Harper 5.4 takes on both halves of that trade. Schema-defined sharding lets a cluster spread across many small nodes in many places, each holding what it owns plus what its region reads. Cluster-wide record locks give you one winner when two nodes want to change the same record. Both live in the same process that already runs your database, cache, and application code.
This release also changes how you ship: every pull request can get a preview of the whole application, with its own data.
What You Can Build Now

Cluster-wide record locks open Harper to applications where two requests can race for the same thing, and only one should win. These used to need a coordinator bolted on the side.
- Booking and reservations. Hold a seat, a room, or an appointment slot while a customer checks out, and release it automatically if they walk away.
- Limited inventory and flash sales. Sell the last unit to one buyer, even when the orders arrive in different regions at the same moment.
- Auctions, dispatch, and matchmaking. Accept one bid, assign one driver, fill one slot, with no queue in the middle.
- Payment authorization and spending limits. Approve or decline against a limit close to the user, before the request reaches your ledger.
- Promo codes and rewards. Let each code or reward be claimed once.
The rest of this post covers how record locks work, along with the two other changes in this release: sharding and pull request previews.
Closer to Every User: A Home for Every Record
Once a cached request costs about a millisecond, distance is most of the latency your users feel. The way to cut distance is more nodes in more places. The obstacle has been the cost of spreading data out: either every node holds all of it, or you write and operate the logic that decides what lives where.
5.4 adds schema-defined sharding. Assign your nodes to shards in the cluster configuration, then add one directive to a table:
type Product @table @sharded {
id: ID @primaryKey
category: String @indexed
body: String
}
Harper hashes each primary key to a slot, and a slot map assigns slots to shards. The nodes in a shard own the full record. Every other node keeps the index entries, fetches the body from an owner on first read, and caches it locally so the next read is served on the spot. Any node still accepts a write and delivers it to the owners.
The result is a cluster of many small nodes close to users, where each node stores what it owns plus what its region reads. Need more read capacity for a peak? Add replica-only nodes. They own no data, so they join without copying the dataset and fill their cache from live requests. cluster_status shows which shard owns what.
What that buys you: lower latency for more of your users without multiplying your storage bill, room for a dataset to grow past what one node can hold, and a way to absorb a seasonal peak by adding nodes for a few weeks instead of sizing for it all year.
Correct on Every Write: Record Locks Across the Cluster
Two customers in two regions tap "buy" on the last seat at the same instant. Each request lands on a different node, and both nodes read the seat as open.
Most teams solve this outside their data layer: a Redis lock in front of the database, a queue with a single consumer, or a dedicated service that decides who goes first. It works, and it puts a network hop and a failure mode between the fast path and the careful path. In 5.4 the careful path moves inside.
table.lock(id) is exclusive across every node. While one node holds a record's lock, no other node's lock() on that record succeeds. The others wait their turn or time out.
import { tables, transaction } from 'harper';
const { Seat } = tables;
await transaction(async () => {
const seat = await Seat.lock(seatId); // one holder, cluster-wide
if (seat.getProperty('status') !== 'open') throw new Error('Seat taken');
seat.set('status', 'sold');
seat.set('buyer', userId);
await seat.save();
}); // released at commit
That is the whole pattern. No lock service to run, no second system to keep in sync.
A few details for production:
- Leases. A lock expires on its own if its holder never releases it (30 seconds by default, up to five minutes). Pass
{ hold: true }to keep a lock beyond one transaction, which is how you hold a seat while a customer checks out. - Plain HTTP works too. A
PUT,PATCHorDELETEwithIf-MatchorIf-None-Matchtakes the same lock by default. One conditional request wins cluster-wide, with no application code. - Restarts. A restarted node recovers its locks within the caller's timeout, instead of making callers wait out the old grants.
- Opt in, per record. Everything you do not lock keeps Harper's default: fast, eventually consistent replication. You pay for coordination only where you ask for it.
Cluster locks are opt-in. To turn them on, set replication.recordLocks: true and apply a home map once per database.
We want to be exact about the guarantee. A lock controls who may write. It does not change how Harper resolves conflicting writes, and it is not a consensus protocol. Use it to coordinate inventory, reservations, claims, and limits. Keep your ledger of record where it is.
This is also a primitive AI coding agents kept asking us for. Read, decide, write is the shape of most business logic, and an agent can now generate it correctly for a distributed system with one call.
What that buys you: the oversell, the double booking, and the coupon redeemed twice stop being problems you reconcile after the fact. The decision is made once, next to the data, in a few lines of application code, with one less system between your users and an answer.
A Preview of Every Pull Request, With Its Data
Preview environments usually stop at the front end. In 5.4 a preview is the whole application: code, routes, and its own branch of the database.
harper preview setup app=my-app target=https://my-cluster.example.com:9925
Run that once. From then on, each pull request deploys to its own URL on your cluster from GitHub Actions; the URL is posted on the PR, and the preview is removed when the PR closes. Authentication uses GitHub's OIDC identity, so no deploy secret is stored anywhere.
Production deploys got the same treatment. harper deploy setup=true provider=github-actions configures secretless deploys in one command. Each release is now loaded in a canary worker and checked before it takes traffic, and a rolling deploy is followed to every node so you know where it landed.
What that buys you: reviewers open a link and use the change with real data before it merges, schema changes are tried against a branch of the data instead of production, and a release that fails to load is caught by the canary instead of by your users.
Also in 5.4
- Agent budgets. The built-in agent enforces session-wide token and dollar caps. Declare model pricing once and every
generate()call on that model can report its cost. - MCP subscriptions. MCP clients can subscribe to custom resources and receive updates as they change.
- Restores you can trust. A restore now proves the database before publishing it: the log replays, every record decodes, every blob is present. A crash mid-restore leaves the database as it was.
- Smaller footprint. An idle thread uses about 40% less JavaScript heap, down from roughly 58 MB to 35 MB.
- Predictable queries. Results for nulls, to-many relationships and SQL
LIKEno longer depend on which index the planner picks. - Dedicated replication threads. A peer catching up no longer competes with the threads serving your requests.
The full list is in the 5.4 release notes. Three items need attention before you upgrade: full-text indexes rebuild after the upgrade, conflicting OIDC trust policies are now refused, and a node that has run 5.4 should not be downgraded below 5.3.5.
Where This Leaves Harper
Harper started 5.0 as a single runtime for your database, cache, application logic and messaging. Since then it has gained vector and full-text search, a built-in agent, backups and staged releases. 5.4 adds two things a distributed system needs before you hand it a decision: coordination, and a safe way to change it.
Try It
npm create harper@latest
Build locally, or point your coding agent at it. Then deploy free on Harper Fabric.
Questions are welcome on Discord.






.webp)



