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.

If you're new to Primate, we recommend reading the quickstart page to get started.

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.