Dox 3.2 is out
Dox 3.2 adds dox add to set up a package with one command, read replicas, streaming queries, model dirty tracking, job batches and chains, a database cache store, broadcasting for Socket.IO, sub-minute schedules, layouts in views, and many more features and fixes.
Every Dox package is released as 3.2.0. This includes dox_http_native, dox_postgres_native, dox_mysql_native and ioredis.
Two packages are new: dox_cache_database and dox_queue_database.
How to update
-
Update the
doxCLI: -
In your
pubspec.yaml, set every Dox package to^3.2.0, then get them. Do not mix versions. -
Make the Breaking changes that apply to your app.
-
Make the generated code again:
Update with an AI agent
You can also let your AI coding agent do the update. Copy this prompt into it:
Update this Dox app to Dox 3.2.
1. Read the release post: https://dartondox.dev/blog/dox-3-2/
Follow the "How to update" steps and every item in "Breaking changes"
that applies to this app. Also check the "Rare cases" list.
2. Set every Dox package in pubspec.yaml to ^3.2.0 and run
`dart pub upgrade`. Make the breaking changes, then run
`dox build_runner:build`.
3. Run `dart run skills@ get --all` to get the Dox 3.2 agent skills, and use
them for the code changes.
4. Run `dart analyze` and `dart test`. Fix every error.
5. Do not change app behaviour that is not part of the update.
At the end, list what you changed and anything I must check by hand,
like log dashboards that read the old JSON field names.
New
Add a package with one command
dox add adds a Dox package and sets it up: config files, provider, bin/ file, migrations and .env keys. It never overwrites a file, so you can run it again.
It works for orm, queue, scheduler, mailer, cache, auth, storage, socketio, view, inertia and mcp. Each package ships its own setup command, so a package you write can have one too.
Docs: Add a package
Opt-in flags
Behaviors that change old results are opt-in. Each package has a flags class. Every flag is off by default, so nothing changes until you turn one on. .all() turns on all of them.
CoreFlags(inAppConfig(flags: ...)):envExpand,onlyPresent,cookieDefaults,hasKeyOnly,htmlStrings,replaceHeaders,decryptServerError,cacheCopiesand a flag for the shape of a422.OrmFlags(inDatabaseConfig(flags: ...)):skipCleanUpdateEvents,belongsToKeyFromRelation,pluralTableNames,createWritesNull,sumReturnsZero,selectRawAdds,groupByAdds,lastPageAtLeastOne, a Postgres column type flag,toJsonLoadedRelations,preloadUnknownThrows,updateSetsUpdatedAtandincrementFiresEvents.AuthFlags(inAuthConfig(flags: ...)):forbidGuest,jsonNoRedirectandendSessionsOnPasswordChange.ValidationFlags(Validator.flags = ...):emptyStringsToNull,unicodeAlpha,shortInMessage, a message key flag,dtoAllErrors,dtoRejectEmptyListanddtoMaybeRejectsEmpty.CacheFlags(inCacheConfig(flags: ...)):lockThrows.
Docs: Core flags, ORM flags, Validation flags, Auth flags
ORM and database
- Read replicas:
ConnectionConfig(read: [...], write: {...}, sticky: ...). A plainSELECTgoes to a read host. Writes, locks and transactions go to the write host.query.onWriteConnection()forces the write host. - Streams:
query.cursor()reads rows from one query as you are ready for them, so a big result does not fill the memory.query.lazy()andlazyById()read in chunks.DBDriver.stream()is the driver call under them. - Cancel a running query with
QueryCancelTokenandquery.cancelWith(token)(Postgres and MySQL). - Pool settings in
extra:requestTimeout,maxConnectionAge,idleTimeoutandserverKill. - Relations:
@HasManyThroughand@HasOneThrough.preload('posts.comments')with a dot path, andpreload(name, where)with conditions.createRelated(),saveRelated()andsaveManyRelated()set the foreign key for you. - Dirty tracking:
isDirty(),isClean(),getDirty(),getOriginal(),getChanges(),wasChanged()andwasRecentlyCreated. refresh()reads the row again in place.fresh()gives a new model.visibleFields(the opposite ofhidden),makeHidden()andmakeVisible().trashed()and a newtrashedmodel event, withonTrashed()andTrashedObserver.toJson()of a many to many model has apivotkey.- Transactions:
transaction(callback, attempts: n)runs again on a deadlock.beginTransaction()gives a transaction you commit or roll back yourself.QueryBuilder.afterCommit(callback)runs work after the commit. - Logging:
SqlQueryBuilder().listen(listener)runs after each query.slowQueryMslogs slow queries.PrettyQueryPrinterprints each query on one line, with the values in the SQL. - SQLite:
extra: {'background': true}runs queries in a helper isolate, so one slow query does not stop the other requests. - Query builder: JSON paths like
where('meta->lang', 'en'), join callbacks (JoinClause),crossJoin(),orderByDesc(),reorder(),skip(),forPage(),sole(),updateOrInsert(),whereAny(),whereAll()andselectRaw(sql, params). - Schema:
.collation(),hasColumns(),hasIndex(),disableForeignKeyConstraints(),enableForeignKeyConstraints()andwithoutForeignKeyConstraints(). - Migrations:
rollback --batch=3.Migrator(flags: ...). Migration queries reach the query listeners and the slow query log. - Postgres and MySQL native:
PgCancelTokenandMysqlCancelToken,stream()in chunks, and the host and port in connect errors.
// rows one by one, with a small memory use
await for (Blog blog in Blog().orderBy('id').cursor()) {
print(blog.title);
}
// posts, and the comments of those posts
List<User> users = await User().preload('posts.comments').get();
Docs: Replicas, Transactions, Query builder, Model, Has many through, Schema, Migration
Queue
- New package
dox_queue_database: thedatabasedriver, batches,afterCommit,QueueTablesanddox queue:table.DatabaseQueueProvidersets it up for you. - The generated
jobs.g.dartnow also lists the mail jobs ofdox_mailer, so a worker knows them without extra setup. QueueConfig(inProcess: true)runs a worker inside the web server. It is the same name asScheduleConfig(inProcess:). The old nameworkInProcessstill works, but is deprecated.- Job batches:
Queue.batch(jobs)withthen,catchError,whenComplete,progress,allowFailures()andcancel(). A batch can hold chains. They needdox_queue_database. - Job chains:
Queue.chain(jobs). The next job is pushed when the one before is done. If one fails for good, the rest is dropped and a catch job runs. - Job middleware:
HasMiddleware,RateLimited,WithoutOverlappingandThrottlesExceptions. - Unique jobs:
ShouldBeUniqueandShouldBeUniqueUntilProcessing. InteractsWithQueue:attempts(),release(),fail()anddelete()insidehandle().- Queue events:
Queue.queued(),before(),after(),released(),exceptionOccurred(),failing()and the batch events. dispatch(afterCommit: true)andQueueConfig(afterCommit: true). They needdox_queue_database.dispatchAfterResponse()runs a job after the response is sent.- Named connections:
QueueConfig(connections: {...}),dispatch(connection: ...)and--connection=NAME.failed_jobsgets a nullableconnectioncolumn. A table without it keeps working. QueueConfig(failedAtTimestamp: true),QueueConfig(serializeSync: true)andQueue.extend(name, factory).Queue.fake()keepsdispatchedSyncanddispatchedAfterResponse.
Docs: Queue, Background work
Cache
- New package
dox_cache_database: a cache store on SQLite, Postgres, MySQL and MariaDB. It keeps items, locks and sessions in a table. Register it withDox().cache.extend('database', DatabaseCacheDriverFactory(encrypter)).dox cache:tablewrites the migration. A write that hits a deadlock runs again after a short wait. 'encrypt': trueon a store encrypts its values with theAPP_KEY. It works forfile,redisand the database store.CacheLock.throwOnTimeoutand theCacheFlags.lockThrowsflag:lock.block(timeout)throwsLockTimeoutException.
Docs: Cache, Cache in the app
Core
RateLimit(sliding: true)uses a sliding window..envfiles:${OTHER}values (withCoreFlags.envExpand) andexport KEY=valuelines.AppConfig(htmlErrors: true)sends a small HTML error page to a client that does not want JSON.AppConfig(errorPage: ...)replaces the page.Route.resource(..., parameter: 'photo')names the param of one item.route()andRoute.url()put a param that is not in the path into the query string.absolute: truegives the full URL.Dox().addGlobalMiddleware()lets a provider add global middleware.Dox().loadRoutes()gives the routes with no server.RouteData.isWebSocketmarks WebSocket routes.res.header(k, v, replace: true), andsession.regenerate(keepToken: true).ServerTimingmiddleware,ServerTiming.add()andServerTiming.measure().AfterResponse.run(work)runs work after the response is sent.- The
JSONhelper:JSON.stringify,JSON.bytesandJSON.parse. - OpenAPI answers: a route with
Can,GuestMiddleware,ValidateSignature,req.authorize(),Gate.authorize()orreq.validateSchema()gets the401,403or422answer in the docs. A@Responsewith the same status wins.
Docs: Routing, Rate limiting, Environment
Validation and DTOs
- New rules, each as a fluent rule and a DTO annotation:
requiredUnless,requiredWithout,requiredWithoutAll,filled,present,prohibited,prohibitedIf,accepted,declined,different,ulid,afterOrEqualandbeforeOrEqual, and the field compare rules. @Afterand@Beforetake'today','tomorrow','yesterday'and the key of anotherDateTimefield.@ExcludeIfand@ExcludeUnless, and.excludeIf()and.excludeUnless()for fluent objects..bail()stops the other rules of a schema at the first failure.@Dto(trim: true)trims text.@Dto(coerce: true)lets JSON send"18"or"true".@Dto(attributes: {...})names the fields in messages.- DTOs that have no
@RequiredIfor@RequiredWithfield are faster.
Docs: Validation, Validation rules
Auth
Auth().logoutOtherDevices(req, password)ends the other sessions of the user. It needs theendSessionsOnPasswordChangeflag.Auth().updateSessionPasswordHash(req, user)keeps the current session after your own password change.Auth().rotateAccessToken()makes a new access token in place of an old one, with an optionalgracetime. It needs a nullablerotated_atcolumn.dox auth:tokens-tableadds it to new tables. For an old table, add it in a new migration.Auth().setAccessTokenAbilities()changes the abilities of a token.AccessTokenAuthDriver(throwOnError: true)sends a token check error to yourExceptionHandler, so the app can answer503.
Docs: Authentication
Scheduler
- Sub-minute tasks:
everySecond(),everyFiveSeconds(),everyThirtySeconds()and more. schedule.exec(executable, args: [...])runs a process with no shell.schedule.command('queue:work')runs adoxcommand.- Output hooks for these tasks:
sendOutputTo(),appendOutputTo()andonOutput(). - Ping hooks:
pingBefore(),thenPing(),pingOnSuccess()andpingOnFailure(), and theIfforms. dox schedule:clear-cacheremoves the overlap locks that a crash left behind.
Docs: Scheduler
Socket.IO
- Broadcasting, off by default. Turn it on with
SocketIoConfig(broadcasting: true).Broadcast.channel()adds rules for private and presence channels.ShouldBroadcastandbroadcast(event)send events.Broadcast.routes()addsPOST /broadcasting/auth.Broadcast.fake()is for tests. - Queued broadcasting with
ShouldBroadcastQueuedandBroadcast.useQueue(). It does not depend ondox_queue. - Whisper: client to client events on private and presence channels, with
whisper: true. ServerOptions(allowRequest:)andServerOptions(maxSessions:).
Docs: Broadcasting, Socket.IO
LogTransport(bothBodies: true)shows the text body and the HTML body.- Queued mail lives in
dox_mailernow.Mail.queue()andMail.later()usedox_queueby default.Mail.configure()registers the mail jobs for the worker.
Docs: Mail
Storage
Storage().put(path, contents, exact: true)writes to that exact path.putFile()andputFileAs().copy,move,files,allFiles,directories,deleteDirectory,missing,getVisibilityandsetVisibilityonStorage().Storage().download(path, name: ...)sets the name that the browser saves.- Local disk
permissions: truemakes visibility a file mode. It is off by default and does nothing on Windows.
Docs: File storage
Views
- Layouts:
@extends,@sectionand@yield. @stack,@pushand@prepend.@csrfand@method('PUT').- Your own tags:
View.directive('datetime', DateTimeDirective()). @include('card', {...})with variables,@includeWhen,@jsonand@error. Every template has ajson()function. A variable namedjsonin your data still wins.- More
$loopfields:iteration,count,remaining,even,odd,depthandparent. $attributes.except([...]).- Options in
ViewConfig:strictVariables,stringZeroIsFalseandbladeOutput. All are off by default. - A warning for tags that Dox does not have, like
@foreach.bladeWarnings: falseturns it off.
Docs: Views
Inertia
Inertia.withViewData(req, data)gives values to the root view.{{ csrfToken }}is filled for you.Inertia.setRootView()andRootViewMiddlewareuse another root view.- Server side rendering:
ssr: InertiaSsr()inInertiaConfig. If the SSR server is down, the normal page is sent. - History encryption with
encryptHistory: true, andInertia.clearHistory(req).
Docs: Inertia
OpenAPI, MCP and CLI
- OpenAPI:
ApiDoc.route()takesparamResponses. A modelvisibleFieldslist is read for the schema. - MCP:
McpOAuth(now:)takes a fake clock for tests. - CLI:
dox route:list(and--json),make:provider,make:command,make:mail,make:viewandmake:test. - CLI:
make:modeltakes-m,-c,-r,--api,-f,-sand-a.make:controllertakes--resource,--apiand--model.
Docs: CLI, Package commands
AI skills
Most packages now ship an AI agent skill, like dox-core-guide and dox-orm-guide. skills get finds the packages in your pubspec.yaml, so list dox_cache, dox_validation and dox_openapi there too to get their skills. Install them in your app:
A 3.1 app can delete the old .agents/skills/dox folder and the .claude/skills/dox link.
Breaking changes
These are the changes that most apps need to act on.
- Upgrade the Dox packages together. The new
dox_builderfinds providers indox_annotation, so an olddox_builderdoes not find them. The native drivers changed too, so upgradedox_postgres_nativeanddox_mysql_nativetogether withdox_orm. - New field names in the JSON logs. In
Loggerlines,levelis nowstatusandnameis nowlogger.name. The values are lower case now (info,warn,danger). InLogMiddlewarelines,source_ipis nownetwork.client.ipandrequest_idis nowhttp.request_id. Check your log parsers and dashboards.LogMiddlewarenow also followsLoggerConfig: with the logger off, or itslevelaboveinfo, no request lines are written. -
The database queue moved to its own package. Only apps that use the
databasequeue driver need these steps:-
Add the package:
-
Make
AppQueueProviderextendDatabaseQueueProviderin place ofQueueProvider:import 'package:dox_queue_database/dox_queue_database.dart'; class AppQueueProvider extends DatabaseQueueProvider { AppQueueProvider() : super(queueConfig); }Remove the old
package:dox_queue/dox_queue.dartimport if nothing else uses it. If you skip this step, the app still builds and starts. The firstdispatch()then fails, so check it by hand. -
In your
create_queue_tablesmigration, importpackage:dox_queue_database/dox_queue_database.dart. - Only if you want job batches: add a new migration that calls
QueueTables.upBatches(schema).
Mail jobs that are already in a queue keep their names, so they still run.
-
-
The scheduler process no longer runs queue jobs. In 3.1,
bin/schedule.dartalso started a queue worker when the in-process worker was on. Now only the web server (QueueConfig(inProcess: true)) orbin/queue.dartruns jobs. If your jobs only ran in the scheduler process, start a worker. maxConnectionAgeis a real age now. On MySQL, a connection older than this closes when it is next free, even if it was busy. Before, it was the idle time. UseidleTimeoutfor the idle time. Postgres now uses this option too.
Rare cases
Most apps are not affected by these. Check them if your app does one of these things.
-
A model field with the name of a new query method (
- MySQL behind a proxy with its own connection ids, like ProxySQL: a timed out query now gets askip,sole,reorder,forPage,orderByDesc,updateOrInsert,whereAny,orWhereAny,whereAll,orWhereAll,crossJoin) does not compile. Rename the Dart field and keep the column name with@Column(name: ...). The table does not change:KILL, so setserverKill: false. Each new connection also runs one small query to learn the server name. - The job code generator moved todox_builder. Upgradedox_builderanddox_queuetogether. If yourbuild.yamlnamesdox_queue:jobs, change it todox_builder:jobs. A plain Dart app needsdart pub add dev:dox_builder. - Nested DTOs deeper in lists and maps are now checked, likeList<List<A>>. Such a DTO can now give validation errors. - View text like@json(...)or@error(...)is now a tag. To print it as text, write@@json(...). -RedactedMailErroris now imported frompackage:dox_mailer/dox_mailer.dart. -Queue.fake()no longer catches a custom queue set withMail.useQueue(). - Custom queue drivers: callQueue.retriedPayload()in place ofJobPayload.retried(). -Loggerwrites in groups, every 20 ms or 64 KB. Warnings and errors are still written at once. - The native HTTP server refuses broken requests that used to pass, likeTransfer-Encodingin an HTTP/1.0 request, or a method likeGOTthat came in asGET. - MCP: the token route answers401forinvalid_client, not400. - OpenAPI: operation ids that clashed for optional routes now both use the long form.
Fixes
Security
- Native HTTP:
Transfer-Encodingin an HTTP/1.0 request or in two headers is refused. A proxy in front could read the body another way (request smuggling).
Routing and core
Route.group,Route.domain,Route.middlewareand the CORS helpers restore their state when the callback throws. Before, the next routes got the wrong path or middleware.Dox().stop()closes theIsolateBusagain, so the process can end.- Pretty
Loggerlines fordebug,warninganderrorhave a color.
ORM and database
orWhereinsidewhereHas,withCountandpreloadcallbacks no longer matches rows of other parents.- When the UPDATE of
save()fails, the model stays dirty, so the nextsave()sends the changes again. chunk()with anorderBy()on a column that is not unique no longer reads a row twice or skips one.transaction(connection: 'main')with the name of the default connection now joins the transaction.insertGetId()drops keys thatfillableandguardeddo not allow, before it looks for the id.- SQLite: a plain query no longer runs inside a transaction that started in the same event-loop turn.
- Postgres native: a query that times out now gets a CancelRequest. A wrong DataRow is refused. A server error that is not UTF-8 no longer hides the real error. A query in a hand-opened transaction is not sent again after a "cached plan" error.
- MySQL native: a schema change inside a multi-statement query is seen.
--with no space is not a comment. A backslash is read both ways withNO_BACKSLASH_ESCAPES. A bad auth switch, a huge column name, a result over 4 GiB, a text that is not UTF-8 and a bad JSON value no longer stop the process or hang the request. - Native drivers: each download try of the native library has a 60 second limit.
Packages
- Builder:
@OrDefault()on aMaybe<T>field no longer stops the build. - Queue: the worker now stops on SIGTERM when Redis is down. A job that a late pop returns is released back to the queue.
- Auth: after
once()orattempt()logs in another user, the access token of the request is dropped. SorevokeToken()andtokenCan()use the right user. - Mail: an error in an
onSenthook no longer failssend()after the email was sent. In a queue worker, it no longer sends the email again. - Scheduler:
schedule listshowsbetween(),unlessBetween()andenvironments(). - Socket.IO: a closed
IsolateAdapterno longer piles up listeners, and no longer makes other isolates wait 5 seconds in a shutdown. - Storage:
copy()andmove()of a missing file throwFileNotFoundExceptionand make no folders.size()andlastModified()of a missing file answer 404, not 500.directoryExists('')is true on every disk.storeAs()andputRequestFileAs()return the stored key. - Views: text like
@empty (none)or@class (see docs)no longer throws. Many nested tags with a space before the(no longer parse very slowly. Spaces after the last tag are removed. - Inertia: an empty
200that goes back to the page keeps the headers and cookies of the controller. - MCP: the token route gives
invalid_grant, not500, when a lock of a code times out. A code now ends exactly aftercodeTtl. - Redis client: one socket failure calls
onErroronce.setSocket()on a connected client uses the new socket.
Full list
Every change is in the changelogs:
- The Changelog page of the docs (
dox_core,dox_builder,dox_annotation,dox_cache,dox_cache_database). - GitHub releases: dox, dox-orm, dox-auth, dox-validation, dox-mcp, dox-cli, dox-queue, dox-mailer, dox-scheduler, dox-storage, dox-socketio, dox-view, dox-inertia, dox-openapi, dox-http-native, dox-postgres-native and dox-mysql-native.