Velocity v0.66: rollback-isolated tests and two new packages
Velocity v0.66 adds transaction-rollback test isolation, plus two new first-party Go packages: velocity-mcp and velocity-arrow. An honest look at what shipped.
Velocity is the full-stack Go framework I've been building. We're at v0.66.2. This is a "what's worth knowing right now" post, not a launch.
I'll be straight about what's new versus what's been here a while, because a release post that blurs the two is just marketing. v0.66 itself is a small release. Its one real feature is faster, isolated tests. Around it, two new first-party packages landed in the ecosystem, and there's an older capability (using any subsystem standalone) that I don't think enough people know about. All three are below.
What v0.66 actually changed: transaction-rollback test isolation
Velocity already had a test-database story: drop-and-migrate per test (RefreshDatabase), or migrate-once-then-truncate (LazyRefreshDatabase). Both are correct. Both are slow when you have hundreds of tests, because every test pays for schema or table resets.
v0.66 adds a third strategy: each test runs inside a transaction that is rolled back when the test ends. Migrate once for the whole suite, then every test gets a clean slate for the cost of a ROLLBACK. Nested transactions in your handler code work too, because they map to savepoints on the test transaction.
func TestOrders(t *testing.T) {
tc := ormtesting.NewTestCase(t, app.DB)
ctx := tc.BeginTransaction() // savepoint; rolled back when the test ends
orders, err := models.Order{}.Factory(app.DB).CreateMany(ctx, 3, nil)
if err != nil {
t.Fatal(err)
}
ormtesting.AssertDatabaseCountCtx(t, ctx, app.DB, "orders", 3)
}The one rule: thread ctx through your ORM calls and use the *Ctx assertion variants (AssertDatabaseCountCtx, AssertDatabaseHasCtx, AssertDatabaseMissingCtx). The non-ctx assertions read the connection pool and will not see the transaction's own uncommitted writes.
Alongside it, v0.66 fixed a couple of test-related rough edges: the queue and mail subsystems are now in the fake list (so you can assert against them like the others), and CSRF validation is bypassed under APP_ENV=testing, so a mutating request from an authenticated test client no longer needs a token round-trip.
That's the honest extent of v0.66. The rest of this post is the context that makes that one feature land: the testing surface it slots into, and what else is new in the ecosystem.
The testing surface it rounds out
The rollback strategy is the newest piece of a test toolkit that has been building for many releases. None of the following is new in v0.66, but it is the reason testing in Velocity feels like a framework feature rather than glue you assemble.
You get an in-memory app harness (velocitytest.NewApp) that boots a real app with in-memory defaults and no port binding, a fluent HTTP client (velocity/testing/http) that drives your router through httptest and asserts against the response, fakes that record queued jobs and dispatched events and commands, and typed model factories.
Here is what an endpoint test looks like with the pieces together. Build data with a factory, drive the real router, assert the database write, and assert the side effect that got queued instead of sent:
import (
"github.com/velocitykode/velocity"
"github.com/velocitykode/velocity/contract"
"github.com/velocitykode/velocity/queue/queuetest"
ormtesting "github.com/velocitykode/velocity/orm/testing"
velhttp "github.com/velocitykode/velocity/testing/http"
"github.com/velocitykode/velocity/velocitytest"
)
// Boot once with a fake queue you can assert against.
fakeQueue := queuetest.NewFakeQueue()
app, _ := velocitytest.NewApp(velocity.WithFakeQueue(fakeQueue))
// register your providers, middleware, and routes on app, then:
app.Router.Freeze()
client := velhttp.NewTestClient(t, app.Router)
func TestCreateOrder(t *testing.T) {
ctx := context.Background()
// A real user row, built through the ORM by the model factory.
user, _ := models.User{}.Factory(app.DB).CreateOne(ctx, nil)
// Authenticate the client as that user.
client.ActingAsID(guard, user.ID)
resp := client.PostJSON("/orders", map[string]any{
"item": "Keyboard",
"qty": 2,
})
resp.AssertStatus(http.StatusCreated)
// The row landed through the real handler, service, and ORM path.
ormtesting.AssertDatabaseHas(t, app.DB, "orders", map[string]any{
"item": "Keyboard",
"qty": 2,
})
// The confirmation email was queued, not sent to a real broker.
fakeQueue.AssertPushedOn(t, "emails", func(j contract.QueueJob) bool {
job, ok := j.(*OrderConfirmation)
return ok && job.UserID == user.ID
})
}The factory (models.Order{}.Factory(db)) is reused by your database seeders too, so test fixtures and dev seed data come from one definition. ActingAsID seeds the session cookie so the request hits authenticated routes. AssertPushedOn checks the fake queue without a real broker. Wire transaction-rollback isolation underneath, from the section above, and the whole suite stays fast and isolated.
Full surface: the testing docs.
Two new first-party packages
Both are real and available now. Both are separate modules with their own version numbers, so they are not "v0.66" in the framework's sense. They are new arrivals in the ecosystem around it.
velocity-mcp
A native Go SDK for building Model Context Protocol servers on Velocity: tools, resources, and prompts, served over stdio or HTTP. The MCP and JSON-RPC layers are first-party (there is no third-party MCP SDK in the dependency graph), and you define primitives with the same router, validation, and event patterns you already use.
func WeatherTool() server.Tool {
return server.NewTool("current-weather", "Get weather for a city.").
WithSchema(func(s *schema.Object) {
s.String("city").Required()
}).
HandleFunc(func(ctx context.Context, req *server.Request) (*server.Response, error) {
city := req.String("city")
return server.Text("It is sunny in " + city + "."), nil
})
}Module: github.com/velocitykode/velocity-mcp (currently v0.10.x). Docs: the velocity-mcp guide.
velocity-arrow
The other side of MCP. A standalone binary that runs alongside your project and gives an AI coding agent live, contextual facts about your app instead of guesses from stale training data. The agent asks Arrow what is actually there.
go install github.com/velocitykode/velocity-arrow/cmd/arrow@latest
arrow mcpIt exposes eight tools, all prefixed velocity_:
velocity_app_info: module path, Go version, Velocity version, dependenciesvelocity_routes: registered endpoints with methods, paths, handlers, middlewarevelocity_db_schema: schema introspection with filteringvelocity_db_query: SQL queries, read-only by default (SELECT, SHOW, EXPLAIN, DESCRIBE, and WITH-SELECT)velocity_config: config from.envwith secrets redactedvelocity_search_docs: TF-IDF ranked docs searchvelocity_last_error: most recent error log entryvelocity_log_entries: historical log entries, up to 100
Point your editor's MCP client at it:
{
"mcpServers": {
"velocity-arrow": {
"command": "/path/to/arrow",
"args": ["mcp"]
}
}
}velocity_db_query being read-only by default and velocity_config redacting secrets are deliberate. An agent with a live line into your app is useful right up until it runs a DELETE or leaks an API key. Writes are off unless you explicitly opt in with --allow-writes (or ARROW_ALLOW_WRITES=1), and secret-shaped keys are always redacted.
Module: github.com/velocitykode/velocity-arrow (currently v0.2.x). Docs: the velocity-arrow guide.
In case you missed it: use any subsystem without booting the app
This shipped a few releases back (around v0.50), not in v0.66, but it keeps surprising people, so it earns a spot here.
You do not have to call velocity.New() to use Velocity's parts. Import a single subsystem, construct its manager, and control the lifecycle yourself. No app, no .env scan, no config/ directory. Good for a CLI, a script, a library, or a test that wants nothing but the cache.
mgr := cache.NewManager(&cache.Config{
Default: "memory",
Stores: map[string]cache.StoreConfig{
"memory": {Driver: cache.DriverMemory},
},
})
mgr.Put("greeting", "hello", 5*time.Minute)The subsystems with a standalone entry point: cache, crypto, validation, httpclient, str, log, pipeline, collect, async. Pure-function packages like str and collect need no construction at all. Import and call.
Drivers live in separate leaf packages, so importing cache for a script does not drag go-redis into your binary. The Go toolchain compiles only what you import. When you want everything wired at once, blank-import a bundle:
import _ "github.com/velocitykode/velocity/cache/standard" // registers memory, file, redisYou still reach for velocity.New() when you want the framework to read config and hand you a fully wired *velocity.App. (It returns (*velocity.App, error), so handle the error.) But for one subsystem in an existing project, that is now a choice, not a tax.
Full surface: the standalone docs.
What pre-1.0 still means
We're at v0.66, and the cadence is exactly what this post shows: most releases are small and focused, and the framework grows by accretion across them. The driver-swap promise is the load-bearing contract and I won't break it. Below that, internal interfaces still move between minor versions.
New here? The fastest path is a starter template. Clone velocity-template-react or velocity-template-vue, then ./vel serve to start the dev server.
Repo: github.com/velocitykode/velocity. Site: vel.build.