flutter_instantdb 2.2.0
flutter_instantdb: ^2.2.0 copied to clipboard
A real-time, offline-first database client for Flutter with reactive bindings.
2.2.0 - 2026-06-15 #
CI / release #
- Main package now publishes through the official
dart-lang/setup-dartreusable workflow (proper GitHub→pub.dev OIDC token exchange, fails fast instead of hanging on interactive auth). Generator publish keeps a 180s timeout guard.
2.1.0 - 2026-06-15 #
Aggregations #
db.count(entityType, {where})anddb.aggregate(entityType, {aggregates, where, groupBy})convenience APIs (count/sum/avg/min/max, optionalgroupBy). Fixed the cache/sync query path so$aggregate/$groupByare honored over cached data instead of returning raw rows.
Storage #
db.storage.list({where, order, limit, offset})lists files via the$filesnamespace (requires sync enabled).
Auth / OAuth #
db.auth.createAuthorizationUrl({clientName, redirectUri, usePKCE, scopes})builds the OAuth redirect-flow URL with PKCE (S256), returningOAuthFlow{url, codeVerifier, state}.- Provider helpers:
signInWithGoogle/signInWithApple/signInWithClerk/signInWithFirebase(wrapsignInWithIdToken).
Reactive widgets #
- New collaboration widgets (Flutter equivalents of React hooks):
PresenceBuilder(usePresence),TopicListener(useTopicEffect),TypingIndicatorBuilder,ReactionsBuilder,CursorOverlay(<Cursors>), andOAuthButton(provider sign-in).
Docs #
- Documentation site now serves
/llms.txt(index) and/llms-full.txt(full docs) generated from the docs content. Rewrote the README. Documented the typed link API without code generation.
2.0.0 - 2026-06-14 #
Internal: store/sync refactor (no behavior change) #
- Split
triple_store.dartandsync_engine.dartinto focused files (pure restructure, no behavior change). Extracted and added unit tests for the pure query/aggregate helpers (triple_query_eval.dart) and the datalog-conversion helpers (datalog_convert.dart); moved large private clusters intopart ofextensions.
Schema converter (schema-io) #
bin/schema.dartnow convertsinstant.schema.ts⇆@InstantModelDart with a pure-Dart converter (no analyzer, no new dependencies).pullrunsinstant-cli pullthen converts TS → Dart to--schema-file;pushconverts the Dart schema →instant.schema.tsthen runsinstant-cli push. New offline subcommandsto-dart <input.ts>andto-tsconvert without touching the cloud, anddiffnow does a best-effort normalized line diff.@InstantFieldgainsunique/indexedflags (additive named params) so Dart → TS preserves constraints. The code generator ignores them (no codegen/golden impact).- Type mapping:
i.string()↔String,i.number()↔num(int/double/num all collapse toi.number()on the way back — documented),i.boolean()↔bool,i.json()↔Map<String, dynamic>?,i.date()↔DateTime?. json/date are always nullable + optional ctor params so the generatedfromRow(which skips them) still compiles. Every entity gets a requiredfinal String id.$-prefixed system entities are not emitted as Dart classes (only resolved as link targets). - Links: TS
linksforward/reverse ⇆ paired@InstantLinkfields (has:'one'→T?,has:'many'→List<T>). The side landing on a system entity is skipped; Dart → TS dedupes reciprocal links and synthesizes ahas:'many'reverse when only one side is declared (hand-tuned link names may change — documented).
mergeModel + Table().tx(db) sugar (6e) #
mergeModel(id, Model): deep-merges a whole model's scalar attributes (mirrorsupdateModelbut usesmerge). Backed by the new runtime primitiveTypedTx.mergeFromMap(id, map, {opts}), which copies the map and delegates toEntityInstanceBuilder.merge.Table().tx(db)sugar: each generated table now emitsTypedTx<${Model}Table> tx(InstantDB db) => db.txFor(this), sotable.tx(db).createModel(...)is shorthand fordb.txFor(table).createModel(...). A model field literally namedtxwould collide with this method (documented edge, extremely rare).- Generator: emits the
txmethod on each table (aftertoMap) andmergeModelin each${Model}TxXextension (afterupdateModel). No public API removed.
Whole-model writes + typed relation link (6d) #
- Whole-model writes:
db.txFor(table).createModel(Model)andupdateModel(id, Model)write a model's scalar fields in one call via a generatedtoMap. The generator emitsMap<String, dynamic> toMap(Model m)on the table plus a${Model}TxXextension onTypedTx<${Model}Table>. toMapis scalar-only: every scalar field is included (idtoo); relation fields are excluded. A model's relations are therefore not persisted bycreateModel— write them withlinkRel/unlinkRel. ForcreateModel,data['id']from the model is used as the entity id; forupdateModel, theidattribute in the update map is harmless (it equals the entity id and reconstruction skips it).- Typed relation link: the generator emits a
static const ${field}Rel = RelationRef<${Target}Table>('${attr}')per relation.db.txFor(table).linkRel(id, Table.relRel, ids)/unlinkRel(id, Table.relRel, ids)write typed links (idsis a single id or aList). - Runtime primitives: new
RelationRef<R>handle intyped_query.dart; newTypedTxmethodscreateFromMap(map, {id}),updateFromMap(id, map, {opts})(copies the map),linkRel/unlinkRel. All delegate to the existing untypedEntityBuilder/EntityInstanceBuilder. No base-class orTypedTxgeneric change; no public API removed. - Deferred to 6e:
mergeModel, theTable().tx(db)sugar, and richer typed relation handles that also carry the target table factory.
Typed transactions (6c) #
- Typed write builder:
db.txFor(table)returns aTypedTx<E>whose fluentset<T>(Col<T>, T)binds each value's type to its column — wrong-typed writes (set(t.priority, 'x')) no longer compile. Cascade-friendly (..set(..)..set(..)). - Ops:
create({id}),update(id),merge(id),delete(id),link(id, relation, target),unlink(id, relation, target), plus typedlookup(Col, value)upsert-by-unique-attribute.opts(TxOpts(...))controls upsert/strict on update/merge. All delegate to the existing untypedEntityBuilder/EntityInstanceBuilder— no op-construction is reimplemented. - Core seam: new
abstract interface class ToTransaction { TransactionChunk toTransactionChunk(); }in core;TransactionChunk implements ToTransaction, anddb.transactnow accepts anyToTransaction(sodb.transact(db.txFor(t).create()..set(...))works) whileList<Operation>and existingTransactionChunkcallers are unchanged. No core→typed import. - Landed in 6d: whole-model writes (
createModel(Todo(...))via a generatedtoMap) and typed relationlink/unlink(RelationRefconsts). The generatedTable().tx(db)convenience is deferred to 6e.
Per-relation pageInfo (nested-4) #
- Engine: cursor-paginated nested
includerelations now surfacepageInfoatQueryResult.pageInfo['<parentType>.<relation>'](e.g.result.pageInfo['goals.todos']). Deeper nesting produces dotted keys ('goals.todos.tags'). The existing read API is unchanged — composite keys (containing.) coexist with top-level namespace keys in the samepageInfomap. - Mechanism: a pageInfo sink (the fresh per-query
pageInfomap) and a dottedpathPrefixare threaded through_processIncludeson the forward-triple path. Writes go directly into the map — no_lastPageInfoshared state, no race for nested relations. - Typed exposure:
queryOnceTyped/queryTypedreturnQueryResultunchanged, soresult.pageInfo?['goals.todos']is readable from the typed path with no code change. - Non-paginated includes add no composite pageInfo key (sink only written when pagination parameters are present).
- Limitations: pageInfo is per relation path, not per parent entity (with multiple parents the key reflects the last parent's window). FK-convention path not threaded (forward-triple / InstantLink only). A fully-typed
RelationPage<T>accessor is deferred to a later generator follow-up.
Relation pagination (nested-3) #
- Engine: nested
includemaps now support cursor pagination (first/last/after/before/afterInclusive/beforeInclusive) andfieldsprojection on the related set (forward-triple path). The cursor window is computed viapaginate()—limit/offsetare stripped from the_applyQueryFilterspass when any cursor/fields key is present, preventing double-windowing. - Typed DSL:
TypedQuery._includeOptions()now serializesfirst/last/after/before/afterInclusive/beforeInclusiveandlimit/offset.fieldsis intentionally not serialized on the typed path. - Guard:
TypedQuery.include()now throwsArgumentErrorif the relation sub-query carries.select()(fields projection). The generatedfromRowhard-casts every field, so a projected map would cause aTypeError. Use the untyped map API for projected relations. - Deferred: per-relation
pageInfo(needs a typed relation-page wrapper — targeted at nested-4). - Known interaction (untyped only):
fieldsprojection on a relation strips all attributes from nested maps before deeperincluderecursion runs. If a projected relation also has a deeperinclude, the nested relation attribute is dropped unless it is among the projected fields. Rare combination; typed path forbids relationfields.
Typed relations (nested-2) #
- Added
@InstantLinkannotation for marking relation fields on@InstantModelclasses. Cardinality is inferred from the field type (List<T>→ to-many,T→ to-one); the target must itself be an@InstantModel. - Generator now emits a typed accessor getter per
@InstantLinkfield (e.g.TypedQuery<TodoTable> get todos => ...) and a recursively-typedfromRowarm that maps included relation maps toList<T>(to-many) orT?(to-one). Un-included relations safely yield[]/nullviawhereType<Map>guard. - Added
TypedQuery<E>.include((t) => t.relation.where(...).limit(n))— serializes to the nested-1 engine'sincludemap inside the$options, supporting nestedwhere/order/limit/offsetand recursive includes. Immutable: the source query is never mutated. - Implemented in nested-3: typed cursor pagination on relation sub-queries; typed
fieldsprojection on relations is intentionally forbidden (see nested-3 section above).
Relational reads (nested-1) #
- Fixed
includeto resolvelink()-created relations: an entity's relation triples are read directly and the targets fetched by id (with nestedwhere/order/limitand recursive includes). The previous foreign-key-convention heuristic remains as a fallback. - To-many links now reconstruct as a list of related entities/ids.
Typed model codegen (Phase 6b) #
- Added
@InstantModel/@InstantFieldannotations and theInstantModelTable<Self, Row>base. - Added a
flutter_instantdb_generatorpackage (build_runner) that emits a typed table +getAll/watchAllextension from an annotated model class, returning typedList<Model>. - Flat models (primitive fields) for now; relation/nested fields are deferred (non-nullable relations are rejected with guidance).
Typed query DSL (Phase 6a) #
- Added a type-safe query builder:
Col<T>,Filter(combine with&/|),Order,InstantTable<Self>,TypedQuery<E>. - Added
db.queryTyped(...)(reactive) anddb.queryOnceTyped(...)(one-shot), compiling to the existing InstaQL maps. - Compile-time safety:
$like/$ilikeonly onCol<String>, comparisons only onCol<Comparable>, value types checked against the column type.
Query operators (InstaQL parity) #
- Added
$like(case-sensitive) and$ilike(case-insensitive) string match operators with SQL%/_wildcards. - Added
$notoperator (alias of$ne). - Added
and/orlogical combinators inwhereclauses. - Added dot-notation nested-field matching (e.g.
where: { 'todos.title': 'Run' }). - Existing
$nin/$exists/$eqextensions remain supported.
Query pagination & fields #
- Added cursor pagination:
first/after/last/before(+afterInclusive/beforeInclusive) under a namespace's$options. - Added
pageInfoonQueryResult(startCursor/endCursor/hasNextPage/hasPreviousPageper namespace). - Added
fieldsprojection:$: { fields: ['title', 'status'] }(id always included). - Added
db.infiniteQuery(...)accumulator +InstantInfiniteBuilderwidget.
Transactions (InstaML parity) #
- Added chainable
lookuptarget:db.tx.profiles.lookup('email', 'a@b.com').update({...})— upsert by unique attribute (also works withmerge,delete,link,unlink). - Added
{upsert: false}strict mode:db.tx.goals[id].update({...}, opts: TxOpts(upsert: false))does not create the entity if it does not exist. - Added
ruleParams:db.tx.docs[id].update({...}).ruleParams({...}), forwarded to the server for permission rules.
Connection status & local id #
- Added
ConnectionStatusenum (connecting/opened/authenticated/closed/errored) exposed viadb.connectionStatusand the newConnectionStateBuilderwidget. - Added persistent
db.getLocalId(name)— a stable id per name that survives restarts (matchesuseLocalId). - Deprecated
db.isOnline(useconnectionStatus; online ==authenticated) anddb.getAnonymousUserId()(usegetLocalId). Both still work.
Files & storage #
- Added
db.storage(InstantStorage):uploadFile(path, bytes, {contentType, contentDisposition}),getDownloadUrl(path),delete(path). - Added the
InstantFilemodel. $filesis queryable like any namespace (db.query({r'$files': {}})); local file refs can be removed withdb.tx[r'$files'][id].delete().