Primate 0.45: SQL transactions, in-process tests and API references
Primate 0.45 brings transactions to SQL stores. Work across multiple stores can commit together or roll back together, with locking reads for changes that depend on current data. This draft also introduces in-process request testing and a broader API reference.
SQL transactions
PostgreSQL, MySQL, Oracle, and SQLite stores can share a transaction when they use the same database instance. There is no need to change imported store modules or pass a transaction to every call. For example, lock an account before checking its balance, then write the transfer and new balance together:
await using tx = await db.transaction();
const account = await Account.lock(id);
if (account.balance < amount) {
throw new Error("Insufficient funds");
}
await Transfer.insert({ accountId: id, amount });
await Account.update(id, {
set: { balance: account.balance - amount },
});
await tx.commit();db.transaction() starts the transaction; Primate does not start one around
each request or store call. await tx.commit() saves all its changes. If the
scope ends without a commit, await using waits for rollback before the
surrounding error handler runs. Nested transactions on the same database are
rejected, and work that outlives the scope fails rather than silently running
outside the transaction.
PostgreSQL, MySQL, and Oracle provide held row locks with their native
lock strengths. SQLite uses a database-wide BEGIN IMMEDIATE fence, so its
Store.lock() returns the requested row under that fence rather than claiming
a row-level lock. MemoryDB, JSONDB, and MongoDB do not support this transaction
API. See the Store transaction reference for lifecycle, errors, and backend
details.
Backend SDKs move into the Primate monorepo
The Go backend SDK is now maintained alongside Primate under backends/go.
Its canonical module path changes from github.com/primate-run/go to
primate.run/go, keeping framework and backend changes in one reviewed source
tree. Existing Go applications should update their module requirement and
imports:
go get primate.run/go@latest
go mod tidy
// Before
import "github.com/primate-run/go/route"
// After
import "primate.run/go/route"Primate's Go fixture uses the in-tree module directly during development and
tests. Released versions are delivered as immutable Go module proxy artifacts
from primate.run, with each release tied to its exact monorepo revision.
The Go SDK and application fixture now require Go 1.27 or later.
The Ruby SDK is now maintained in the same source tree under backends/ruby.
The public gem remains primate-run, and existing require "primate/..."
paths do not change. Monorepo development and backend tests use the in-tree gem
directly, keeping Ruby bridge changes aligned with the framework release.
The Python SDK has likewise moved under backends/python. The published
package remains primate-run, and applications continue to import primate.
Primate's Python fixture builds and installs the in-tree package for development
and tests, while releases remain normal immutable Python package artifacts.
The Go, Python, and Ruby SDKs no longer bundle their separate Pema
implementations. Their request bags retain direct value access, but remove the
Pema-specific Parse or parse methods. Before upgrading, remove imports of
the backend SDK Pema modules and replace request-bag parsing with explicit value
access plus the validation library appropriate for your application.
Session reads and writes are now asynchronous Store operations. Await
session.create(), get(), try(), set() and destroy() in TypeScript and
JavaScript routes; Python session methods are coroutines, Go routes use
s := session.From(request) and handle returned errors, and Ruby methods wait
for their JS promises. The Go capability stays bound to its request even when
the native scheduler resumes a goroutine after a timer. Operations join an active
transaction on the same database instance. Rolled-back creation or destruction
does not send a cookie; a committed change can still be attached to an error
response. Outside a transaction, an awaited write commits independently, and
HTTP delivery cannot be atomic with a SQL commit.
Route paths now have one lowercase identity across backends. Source files keep
their exact names: Route.js maps to /route, ExtendedRoute.kt maps to
/extendedroute (not /extended-route), and extended-route/Index.kt maps to
/extended-route. Static matching is case-insensitive, while dynamic values
retain their spelling. A build now rejects files that resolve to the same
lowercase route, even across extensions or HTTP methods. Before upgrading,
rename or remove case-only duplicates and route/index conflicts; filenames in
collision diagnostics preserve their original case.
The Python SDK now requires Python 3.14 or later, and the Ruby SDK requires Ruby 4.0 or later. Upgrade those runtimes before installing the corresponding 0.45 backend SDK releases.
In-process tests
The new primate/test harness builds and initializes the real application,
then sends requests through its normal request-to-response pipeline without
binding a network port:
import test from "primate/test";
await using app = await test(import.meta.dirname);
const response = await app.request("https://example.test/api/items");Use it to test routing, hooks, request data and schemas, static assets,
response conversion, and error handling. Startup hooks still run, so configure
test dependencies and dispose each harness. For WebSockets, TLS, timeouts, and
other transport behavior, use a served application or browser fixture instead.
See Testing and the primate/test API reference for details.
API references
The API overview now leads to references for Stores, routes, requests,
and responses, as well as configuration, module lifecycle, and the
application facade. References for sessions, i18n, navigation, and
events make the rest of the framework easier to explore. The API area also
includes the primate changelog, starting with the verified 0.44.0 package
release history.
The website also has a refreshed design across the homepage, documentation, guides, and blog, with clearer navigation, more readable examples, and a more consistent light and dark appearance. On documentation pages, the sidebar now stops before the footer.
Builds now reject route.hook() exports outside +hook files instead of
silently omitting them. The application guide also uses app.views.get() in
place of obsolete app.view() examples. Route exports now expose only supported
operations; typed HTTP methods and route behavior are unchanged.