NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
pub.dev · #5723 most downloaded on pub.dev
Flutter SDK for Koolbase — feature flags, remote config, version enforcement, authentication, storage, database, realtime, OTA updates, and code push for mobile apps.
Last release 3 days ago
05 Oct 2026
Ships on a steady schedule
a new release about every 2 weeks
Most releases are documented
notes for 50 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
6 months old
104 releases · first in 2026
One column per month.
Offline observability: what an app needs to show offline state honestly. Same contract as @koolbase/* 12.14.0.
Offline observability: what an app needs to show offline state honestly. Same contract as @koolbase/* 12.14.0.
Koolbase.connectivity -- KoolbaseConnectivityState.unknown | online | offline, with state and changes. Whether the device reports a connection: a hint, not a promise the server is reachable. unknown until the platform first answers, so an app neither flashes "offline" nor runs online-only logic on a guess.watchPendingWrites() follows sign-in and sign-out. It re-reads for the new user (or reports signed out) instead of waiting for the queue to change. db.sessionChanged(), wired to auth by Koolbase.initialize.db.doc(id).getSaved() returns the device's saved copy (with changes queued on this device applied) without the network. KoolbaseRecordController shows it at once with isSaved true while the server is asked: success replaces it (isSaved false), failure keeps it, not found is notFound. Opening a record offline now shows the saved copy instead of an error.isFromCache true) instead of staying as it was.An offline add keeps one id. The id the app is given is now the id the record is created under when it syncs. Before, the server created the record un
Offline correctness fixes.
Realtime for signed-out visitors. With nobody signed in, live lists and live record views on a collection anyone can read (read rule public ) now upda
public) now update live: the SDK connects with the project's public key. Any other collection behaves as a normal list or view until a user signs in. Needs the Koolbase API from 2026-10-04 or later. Same contract as @koolbase/* 12.13.0.KoolbaseRealtimeClient.sessionChanged() does this; Koolbase.initialize wires it to authStateChanges.Live record views: KoolbaseRecordView(..., live: true) (also KoolbaseRecordController(live: true) ). When Koolbase realtime reports a change to THIS r
KoolbaseRecordView(..., live: true) (also KoolbaseRecordController(live: true)). When Koolbase realtime reports a change to THIS record, it is read again silently -- no refreshing state -- and a burst of changes within 250 ms is one read. When it is deleted, the view shows not-found at once, and a read still in flight is dropped. Changes to other records in the collection are ignored: no extra reads. Same contract as @koolbase/* 12.12.0.liveEvents): a silent re-read on a change, other records ignored, one read for a burst, a delete at once and without a read, a delete dropping a pending re-read, no subscription unless live or without an id, and dispose unsubscribing and cancelling.Realtime subscriptions are counted exactly. Two listeners on one collection -- a live list and a live grid on the same screen -- were counted twice bu
idleGrace, 1 s). A listener back within it -- navigating back to a screen -- keeps the same connection; one after it opens a fresh one. Nothing reconnects without a subscription. Same contract as @koolbase/* 12.11.1.connector), without a server: one connection for many listeners, unsubscribing only when the last listener leaves, the close after the moment, reuse within it, a fresh connection after it, nothing opened for nobody, and recovery from a failed token.Live lists: KoolbaseCollectionList(..., live: true) and KoolbaseCollectionGrid(..., live: true) (also KoolbaseCollectionController(live: true) ). When
KoolbaseCollectionList(..., live: true) and KoolbaseCollectionGrid(..., live: true)KoolbaseCollectionController(live: true)). When Koolbase realtime reports a recordrefreshing state -- and a burst of changes within 250 ms is one read. The list's@koolbase/* 12.11.0.Live lists refresh after doc(id).update and doc(id).delete . Every open query on the record's collection re-runs in the background, as it already did
doc(id).update and doc(id).delete. Every open query on theinsert, upsert,batch and deleteWhere; before, an update or delete through a document reference left everyrefreshAllCollectionStreams).update or delete withexpectedRevision is refused because the record changed (KoolbaseRevisionMismatchException),##, which the release workflow# every GitHub release since the workflow began had empty notes.Merge pull request #9 from koolbase/release/12.13.0
Merge pull request #9 from koolbase/release/12.13.0
fix: KoolbaseCollectionGrid inside a scrolling page
KoolbaseCollectionGrid works inside a scrolling page: its empty state fills a height
only when there is one (it threw "BoxConstraints forces an infinite height"), and
scrollsWithPage: true lays the cells out at their natural height with Load more below
them, as the list's page mode does. The grid now renders from KoolbaseTestData too.Merge pull request #8 from koolbase/release/12.12.0
Merge pull request #8 from koolbase/release/12.12.0
feat: KoolbaseTestData, populated widget tests for lists and record views (12.12.0)
KoolbaseTestData (package:koolbase_flutter/testing.dart): records supplied to
KoolbaseCollectionList and KoolbaseRecordView in widget tests, so a screen can be
pumped populated. It replaces only the data source; controllers, paging, states and
rendering are the production code. Lists page like the server (limit, offset, exact
total); an unknown record id is not found, as the API answers. Testing and
certification infrastructure, not a local-data API.Merge pull request #7 from koolbase/release/12.11.0
Merge pull request #7 from koolbase/release/12.11.0
feat: KoolbaseCollectionList(scrollsWithPage) for a list inside a scrolling page (12.11.0)
KoolbaseCollectionList(scrollsWithPage: true), for a list inside a scrolling page.
The rows lay out at their natural height and the page does the scrolling; Load more
stays a row; pull-to-refresh belongs to the page, so the list adds none. Without it,
a list with records inside a scrolling column cannot size itself ("Vertical viewport
was given unbounded height"). The default is unchanged.Merge pull request #6 from koolbase/release/12.10.0
Merge pull request #6 from koolbase/release/12.10.0
feat: KoolbaseRecordView, one record by id as a scope (12.10.0)
KoolbaseRecordView: one record by id, as a scope. Hands the record to
builder, which returns the caller's own widget: it adds no scrolling or
layout, the way a list row's children read their record. loading,
notFound and error are slotted, with defaults. notFound covers a missing
id, a record that does not exist, one this user may not read (the API does not
tell them apart) and a record from a different collection. A changed id
starts a new load, so the same detail route opened for another record never
shows the previous one.KoolbaseRecordController: the same without the widget. A failed refresh
keeps the record shown.Merge pull request #4 from koolbase/release/12.9.0
Merge pull request #4 from koolbase/release/12.9.0
feat: analytics is opt-in, network errors carry a userMessage (12.9.0)
analyticsEnabled: true in
KoolbaseConfig to send events. While it is off, Koolbase.analytics calls do
nothing (the first logs how to turn it on), so apps that track events, and
KoolbaseNavigatorObserver(client: Koolbase.analytics), keep working.KoolbaseError.userMessage: a short message that is safe to show to people for
network errors ("We can't connect right now. Check your connection and try
again."), alongside the detailed message for developers. On the web, the
network error's message also names the page's address and Trusted Origins.Koolbase.db.invalidate(collection) forgets cached query results for a
collection, so the next query waits for the server.Two-step sign-in (MFA). Every sign-in method now throws MfaRequiredException carrying a challengeToken when the person's account has an authenticator
Two-step sign-in (MFA). Every sign-in method now throws
MfaRequiredException carrying a challengeToken when the person's account
has an authenticator on — including phone, Google and Apple, whose error
parsers all map it (a source test now enforces this for any parser added
later). verifyMfa(email: …, code: …) and verifyRecoveryCode(challengeToken: …, code: …) finish sign-in and store the session.
For managing MFA: enrollMfa(), confirmMfaEnrollment(code), mfaStatus(),
stepUpMfa(…), disableMfa(), and regenerateRecoveryCodes(). Six new
exceptions — MfaRequiredException, RecentAuthRequiredException,
RecentMfaRequiredException, MfaAlreadyEnabledException,
MfaEnrollmentNotFoundException and MfaNotEnabledException — each
registered in the error-code fidelity suite.
Sign in with an emailed code. requestEmailCode(email) sends a six-digit code; signInWithEmailCode(email: ..., code: ...) signs in and stores the sessi
Sign in with an emailed code. requestEmailCode(email) sends a
six-digit code; signInWithEmailCode(email: ..., code: ...) signs in and
stores the session exactly as login does. No password needed.
An unknown address becomes an account only while the project accepts
sign-ups, checked at the moment of sign-in. Each code works once and allows
three attempts. requestEmailCode resolves the same way whether or not the
address has an account, so it cannot be used to check who is registered.
A project can switch this off; requests then throw
EmailCodeDisabledException. The OTP exceptions phone sign-in already uses
— OtpInvalidException, OtpExpiredException, OtpMaxAttemptsException —
now come from the shared mapping, so every path that returns them throws the
class an app catches.
Reference errors are catchable. KoolbaseReferenceInvalidException,
KoolbaseReferenceInUseException, KoolbaseDanglingReferencesException
(carrying the offending records) and KoolbaseCollectionReferencedException,
for collections with declared references.
12.6.0: UploadResult exposes path and publicUrl, so a persisted uploa…
12.6.0: UploadResult exposes path and publicUrl, so a persisted uploa…
…d is never a signed URL that expires
UploadResult exposes path and publicUrl, not only a signed
downloadUrl.
downloadUrl is signed and expires, so persisting it produces a broken
image later. What to persist instead depends on the bucket: path for a
private bucket (display it through the storage client), publicUrl for a
public one — a stable CDN URL, null for a private bucket.
UploadResult also carries the bucket the object went to, which
publicUrl needs. upload() always sets it; the field is nullable only so
existing constructors keep compiling.
Found through the Designer: its exporter reads upload-result fields by
name, so a binding to an upload's path emitted code that did not compile.
12.5.0: session management, audit log, and resend verification withou…
12.5.0: session management, audit log, and resend verification withou…
…t a session
auth.listSessions(), auth.revokeSession(id), auth.revokeAllOtherSessions().
Three endpoints had been live on the server the whole time and no SDK
exposed any of them — so "sign out my other devices" existed and was
unreachable.
listSessions returns where the user is signed in: device label, IP, user
agent, timestamps, and isCurrent marking this device. Token hashes are
never included; the server excludes them.
revokeAllOtherSessions keeps this session and returns how many ended,
which is what the feature means after a lost phone.
Revoking the current session through revokeSession ends this one too. The
local session is not cleared there — the SDK cannot tell from the response
which session it was, and the next request's 401 handles it. A UI listing
sessions knows which row is current from isCurrent and should call
logout() for that one.
auth.auditLog({limit, offset}) — the account's own security history,
for a "recent activity" screen: sign-ins, failures, lockouts, password
changes, verification. Each event carries what its type is allowed to say
and nothing more; the server sanitizes them against a per-type field
allowlist. Returns a page with the total across all pages.
auth.resendVerificationEmailToAddress(email) — ask for a new
verification email with no session. Built for the TypeScript SDKs on
20 September; this SDK had the same dead end.
A project requiring verified contact issues no session until the account verifies and refuses login until then, so a user whose email went to spam or who waited past the link expiry could not sign in to ask for another. Returns nothing and reveals nothing: the server answers identically whether the address has an account, has none, or is already verified. Show the same "check your email" either way, and never say "we sent it".
The six x-koolbase-* identity headers this SDK sends are now pinned by a
test against the set the API allows, so they cannot drift without failing
here.
Fourteen exceptions reported codes the API has never emitted. EmailAlreadyInUseException said email_taken for a server that says email_in_use; UserDis
Fourteen exceptions reported codes the API has never emitted.
EmailAlreadyInUseException said email_taken for a server that says
email_in_use; UserDisabledException said user_disabled for
account_disabled; the Apple and Google exceptions each reported a
provider-split code where the server sends one unified code. code is a
public field, and anyone reading it instead of catching by type was
comparing against a string that would never arrive. Catching by type has
always worked and is unaffected.
invalid_refresh_token was unmapped in the database path — the code the
server actually sends when a session is over. It fell through to the
generic fallback, so koolbaseDataErrorNotifying never fired for it and
the session was not cleared. An app could keep a token the server refuses,
which is the failure that notifier exists to prevent. It now produces
KoolbaseSessionExpiredException, which four doc comments told
applications to catch and nothing had ever thrown.
A function that timed out was indistinguishable from one that threw. A
504 fell into the generic 5xx branch and arrived as
FunctionExecutionException. Those need different answers — a timeout
means retry or raise the limit; an exception means fix the code. Now
FunctionTimeoutException, and 429 gets FunctionRateLimitException.
Forty-four error codes the API emits were not mapped, arriving as bare
exceptions so an app's on clause silently never ran. Thirteen in auth,
including token_expired, token_used, account_exists,
oauth_only_account and insufficient_scope; the rest across database,
storage and functions, including revision_mismatch's siblings,
plan_limit_reached (shared across all three families, carrying resource,
limit and plan), ambiguous_match and upload_expired.
KoolbaseNotFoundException and KoolbaseValidationException report the
code they were built from, not their category. Each stood for several
codes while reporting one, so a collection_not_found response produced an
exception saying not_found.
Why these were missing: The API wrote error codes through four different helpers, so no single search found them all. It now declares every code as a constant in one file, with a test that fails the build on a literal — which made the comparison against this SDK exact for the first time. That is how these were found.
Both halves are now guarded here: one test asserts every code maps to its own exception, another that every exception reports a code the server sends. The auth error mapping had never had a test before this release.
12.3.0: auth.changePassword for the signed-in user
12.3.0: auth.changePassword for the signed-in user
Until now an app on Koolbase could not offer a change-password screen. Verifies the current password, signs out every other session, keeps this device in. Two new typed exceptions so a wrong current password and a rejected new password are distinguishable without string matching.
auth.changePassword(currentPassword:, newPassword:). An app could not
offer a change-password screen until now; the only route to a new
password was the reset email. The current password is verified first.
On success every other session for the user is signed out and this
device stays in -- a password is usually changed because someone else
may have it.InvalidPasswordException when the current password is wrong, or the
account signed up through a provider and has no password to change;
the server returns the same code for both so a caller cannot probe an
account's sign-in methods.WeakPasswordException is now actually thrown. It was defined and
mapped to KoolbaseErrorCode.validation but nothing raised it: the
server sent a too-short password back as an uncoded 400 on register,
and as invalid_token on reset. All three password paths now return
weak_password, so the same catch covers signup, reset and change.Pagination. KoolbaseCollectionList and KoolbaseCollectionGrid show a "Load more" past the last loaded record while the collection has more -- exact, f
KoolbaseCollectionList and KoolbaseCollectionGrid show
a "Load more" past the last loaded record while the collection has
more -- exact, from the query's total, not a guess from a short page.
KoolbaseCollectionController gains loadMore(), hasMore and
loadingMore. A list that showed twenty and stopped was quietly
claiming to be the whole collection.loadMore does not resubscribe: the realtime subscription stays on
the first page, so a live insert updates that page in place and the
pages loaded after it stay put. refresh() resets to one page.Koolbase.db.aggregate(...): count, sum, avg, min, max over the whole authorized set, grouped by a field or a calendar bucket. Every result carries its
Koolbase.db.aggregate(...): count, sum, avg, min, max over
the whole authorized set, grouped by a field or a calendar bucket.
Every result carries its accounting -- how many records contributed
and how many were skipped as missing or non-numeric -- so a total is
never a bare number. A calendar bucket requires a timezone; the
KoolbaseGroupBy.month(...) constructors make it impossible to omit.
Online-only. Read rules apply inside the query.initialize no longer refuses the browser. Code push is skipped (no binary, no bundle cache — path_provider has no web implementation) and codePush thr
12.0.0: Flutter Web
initialize no longer refuses the browser. Code push is skipped (no
binary, no bundle cache — path_provider has no web implementation)
and codePush throws with isCodePushAvailable false; offline sync is
skipped (drift on web needs wasm shipped beside the app). Auth,
database, storage, realtime and functions run. Proven on the example
app in Chrome against api.koolbase.com from a trusted origin.
Koolbase.initialize no longer refuses the browser: auth,
database, storage, realtime and functions work. Two capabilities are
absent on web and say so — code push (no binary to patch;
Koolbase.codePush throws, Koolbase.isCodePushAvailable is false) and
offline sync (reads are live, writes go straight out). A web app's
origin must be added under Configuration → Trusted Origins in the
dashboard; that list now governs API CORS as well as storage.Koolbase.codePush throws UnsupportedError on web instead of
initialize throwing. Guard with kIsWeb or isCodePushAvailable.README documents KoolbaseCollectionList.visible; install snippet at ^11.6.1.
KoolbaseCollectionList.visible; install snippet at ^11.6.1.11.6.0: KoolbaseCollectionList.visible
11.6.0: KoolbaseCollectionList.visible
Transform the loaded records before both the empty decision and the
rows. Runs on every build over what is already loaded; it does not
query. The Designer's search-on-list is built on it.
KoolbaseCollectionList.visible: transform the loaded records before both
the empty decision and the rows. Runs on every build over what is already
loaded; it does not query. A filter that leaves nothing shows the empty slot.Widget tests can now build Koolbase screens. Koolbase.initializeForTesting() sets up enough SDK for widgets to render — db and auth exist, every HTTP
Koolbase.initializeForTesting()
sets up enough SDK for widgets to render — db and auth exist, every HTTP
request answers with an empty success, auth storage is in memory — and none
of the platform channels a widget test cannot answer. resetForTesting()
pairs with it. InMemoryAuthStorage is public for your own tests.httpClient passed to the database client now reaches every query and
document reference. It was accepted and never used: queries called the
package-level http.post, so a test client was cosmetic and the database
could not be exercised without a network.Device outcomes are now attributable. Every patch event the SDK sent omitted patch_id, and the server only increments a patch's counters when that fie
patch_id, and the server only increments a patch's counters when that field
is present — so every download and activation ever reported landed in the
events table and moved no number. The staged patch id is now persisted
alongside the staged patch number and promoted on activation, so the id
survives the process boundary between downloading a patch and applying it.patch_activated. The reconcile branch that promotes a
staged patch on Android logged and returned without sending anything, so the
platform carrying almost all real traffic had never recorded a single
activation.patch_failed with a
reason. Previously a device that could not install a patch was
indistinguishable from one that never tried.app_version is populated on events rather than sent as an empty string.A patch, not a minor: refusing on a platform that was never supported adds no capability, and the version should say 'safe to take' rather than 'somet
11.4.1
A patch, not a minor: refusing on a platform that was never supported
adds no capability, and the version should say 'safe to take' rather
than 'something new arrived'.
Koolbase.initialize rather than starting a
client whose most important features silently do nothing. Code push, OTA
updates, offline-first sync and version enforcement have no browser
equivalent, so this SDK targets Android and iOS; the message points at the
JS SDK. The package began compiling for web in 11.4.0, which is precisely
why an explicit refusal became necessary — a working-looking client is
worse than a clear no.platforms: is declared in the pubspec (android, ios) rather than inferred,
so pub.dev states the boundary instead of guessing at it.patch_client is split by conditional export, and local_database passes
drift's web options. Both are what make the package compile off-device at
all; neither makes web a supported target.11.4.0: runtime storage public URLs via bootstrap-learned project ide…
11.4.0: runtime storage public URLs via bootstrap-learned project ide…
…ntity
publicUrlFor(bucket:, path:, transform:) needs no projectId — identity
arrives with the bootstrap payload the SDK already fetches, caches and
refreshes. Typed KoolbaseStorageProjectIdentityException while identity
is unavailable (distinct from null = private bucket); the throw nudges a
refresh so the session heals without restart. Update gate compares
project_id explicitly since it lives outside the payload hash. README,
CHANGELOG updated.
Koolbase.storage.publicUrlFor(bucket:, path:, transform:) — the runtime
form of publicUrl that needs no projectId argument. The SDK learns its
project identity from the bootstrap payload it already fetches and caches;
apps never carry or compose project identity themselves.KoolbaseStorageProjectIdentityException (code
project_identity_unavailable) — thrown while identity has not yet
arrived, i.e. a fresh install that has never completed a bootstrap.
Deliberately distinct from a null public URL, which means the project is
known and the object simply has no public URL (private bucket). The throw
also nudges a background bootstrap refresh, so the session heals once
connectivity returns — no restart needed.project_id (server-side from this release) and
the update gate compares it explicitly: identity is outside the payload
hash (it can never change), so version comparison alone would have
discarded the first identity-bearing payload on apps with unchanged
flags/config.publicUrl(projectId:, ...) is unchanged — still the right
tool for build-time URL generation where the caller holds the id.KoolbaseError: one canonical error type across the whole SDK
KoolbaseError: one canonical error type across the whole SDK
Catch anything, call KoolbaseError.from(e), branch on code. The 44 typed
exceptions are unchanged; this sits above them so an app can branch on what a
failure MEANS without knowing the SDK's internal taxonomy.
Found by measuring: across 14,732 lines, four places in the entire SDK catch a
transport failure. A database read that loses connection has been throwing a
raw SocketException straight through to the caller — on a phone with patchy
signal, the most common failure there is, and the one with no typed home.
Normalization is total: an unmapped server code, a bare string, an integer all
become a KoolbaseError rather than escaping. This runs in error paths, where a
second failure has nowhere to go.
Eleven codes. The bar for adding one is that an application could reasonably do
something DIFFERENT because of it — rateLimited because waiting is not
retrying, contactNotVerified because it routes to "resend verification" not
"check your password". retryable is derived from code, never stored, so the two
cannot disagree. details carries structured context through rather than
flattening it, including the server's current record on a revision mismatch.
message is developer diagnostic data and must never reach an end user. Written
into the doc comment, not just the changelog.
Also fixes the README's first auth example, which called a method that does not
exist (Koolbase.auth.register). signUp appeared nowhere in 1382 lines, so the
front page had no correct path to creating a user — including no mention of
SignUpResult.verificationRequired, where the account exists but no session was
issued.
Additive only. Nothing throws differently, no signature moved.
KoolbaseError — one canonical, platform-wide error type. Catch anything,
call KoolbaseError.from(e), branch on code. The 44 typed exceptions stay
exactly as they are; this sits above them, so an app that wants to branch on
what a failure MEANS no longer has to know the SDK's internal taxonomy.SocketException straight through to the
caller. On a phone with patchy signal that is the most common failure there
is, and it was the one failure with no typed home. KoolbaseError.from maps
SocketException, TimeoutException and ClientException to
KoolbaseErrorCode.network, which makes it actionable for the first time.KoolbaseError rather than escaping. This
runs in error paths, where a second failure has nowhere to go.KoolbaseErrorCode has eleven members, and the bar for adding one is that
an application could reasonably do something DIFFERENT because of it.
rateLimited exists because waiting is not retrying. contactNotVerified
exists because it routes to "resend verification", not "check your
password". Diagnostic distinctions that change nothing an app does stay in
message and rawCode, where they belong.retryable is derived from code, not stored, so the two can never
disagree.details carries structured context through normalization rather than
flattening it: the field that collided, the path that was taken, and — for a
revision mismatch — the server's current record, which is returned WITH the
refusal precisely so that deciding what to do next needs no second fetch.message is developer diagnostic data and must never be shown to an end
user. It is English-only, it is not stable API, and it names formats and
infrastructure: "Phone number must be in E.164 format", "SMS provider not
configured for this project". Map code to your own product copy. That is
also the localisation boundary — your copy table translates, the SDK does
not.catch clauses keep working unchanged.Analytics events now carry the signed-in user automatically. identify() existed, but nothing errored when an app never called it — so every event land
identify()
existed, but nothing errored when an app never called it — so every event
landed anonymous, and retention, funnels and any per-user analysis were
quietly worthless. Found by looking at a real project: 53 events, 8
registered users, zero events carrying a user id. The SDK already knows who
is signed in; it should say so.identify() still wins where an app has its own identity system, and the
existing reset() releases that override — without it, events keep carrying
the previous user after they sign out.Koolbase.fiscal — authority-grade sales recording. submit() records a transaction and drives fiscalization; status() follows it and returns the certif
Koolbase.fiscal — authority-grade sales recording. submit() records a
transaction and drives fiscalization; status() follows it and returns the
certification the tax authority granted (in Ghana: the GRA signature,
receipt number and verification QR a compliant receipt must carry). Live
for Ghana's GRA E-VAT; the reference adapter runs the same machine —
gapless numbering, sealed immutable documents, full evidence — anywhere an
authority integration doesn't exist yet.clientRef you pass is idempotent: resubmitting the same reference
always returns the same intent, never a second fiscal number. A timed-out
submit may still have fiscalized, so retrying or polling both converge on
the truth — the exception message says so rather than leaving you guessing.clientRef makes
that replay a single call.FiscalIntentResult exposes status, numbers, blockedReason,
certification, isFiscalized and isPending. A blocked result means
nothing was consumed — no fiscal number was spent — so fixing the cause and
resubmitting the same reference resumes that exact transaction. A
fiscalized result with a null certification is valid for document kinds
the authority acknowledges without certifying (purchase records); it is
never an error.fix: offline writes refuse signed-out — never enqueue under a null owner
fix: offline writes refuse signed-out — never enqueue under a null owner
A ticket parked on-device while the SDK session was dead was enqueued
with userId null. Every per-user surface then did its job correctly —
pendingWrites() throws signed-out, reads and replay filter by owner — so
the null-owner row matched nothing, forever: physically present,
invisible, unreplayable. The write neither failed nor succeeded; it
vanished into a bucket nobody owns.
All three offline enqueue sites (insert fallback, queued update, queued
delete) now throw KoolbaseUnauthenticatedException signed-out. Signed-in
behavior byte-for-byte unchanged. Mutation-pinned: removing the insert
guard fails 'writes to NO bucket'. Mirrors the RN SDK's Aug 4 fix of the
same bug family. v11.1.1
KoolbaseUnauthenticatedException when no
user is signed in, instead of enqueueing under a null owner. A null-owner
queue row is invisible to every per-user read and replay — the write
neither fails nor succeeds; it vanishes. Found on device as a parked
ticket that never reached the server. Signed-in offline behavior is
unchanged. Mirrors the React Native SDK's fix of the same bug.feat: get(fresh: true) — cache-skipping network read
feat: get(fresh: true) — cache-skipping network read
SWR cannot express read-after-write: a cached answer is the state before
your write, and awaiting the background-refresh stream proved racy
on-device (a local projection tracked the server exactly one sale
behind). get(fresh: true) skips the cache and returns the network's
answer; the read still updates the cache, so SWR callers benefit.
Plain get() unchanged. Fake queries in tests updated to the new
signature. v11.1.0
KoolbaseQuery.get() gains fresh: true — skips the cache and returns
the network's answer directly. Required for read-after-write (verifying
the effect of a write you just made), reconciliation, and local
projections that must only ingest server-provenance data. A fresh read
still updates the cache, so SWR callers benefit from it. Plain get()
behavior is unchanged.feat(auth)!: SignUpResult + ContactNotVerifiedException (11.0.0)
feat(auth)!: SignUpResult + ContactNotVerifiedException (11.0.0)
BREAKING: signUp returns SignUpResult, not KoolbaseUser.
The server can now withhold a session at registration when a project requires
a verified contact channel. The old return type could not express 'account
created, not signed in' — the session-less 201 reached AuthSession.fromJson
and threw 'type Null is not a subtype of type String' on the absent
access_token. Observed on device before this fix.
SignUpResult carries the user in both cases and a verificationRequired flag,
so consumers must branch. Deliberately not a nullable user or nullable
session on the public surface: that would push the same null-check onto every
caller and reproduce the crash one layer up.
The session is persisted only when one was issued. Calling _setSession with
null would leave the SDK half-authenticated — reporting a signed-in user with
no token.
signUp no longer routes through _parseSession. That parser assumes tokens
exist, which is correct for login and refresh and wrong for registration
under this policy; loosening it would have weakened login's contract for no
reason.
ContactNotVerifiedException maps the server's contact_not_verified code on
login. Credentials were correct and the policy refused, so it is distinct
from InvalidCredentialsException — apps route to resend-verification rather
than telling the user to re-check a password that was right.
Added only to _checkError, not to the Apple/Google parsers: OAuth satisfies
the requirement by definition (a provider attestation IS a verified contact),
so the server never emits this code on those paths.
Enforcement is off for every project that existed before the server-side
release, so verificationRequired is false and behaviour is unchanged unless a
customer opts in.
feat: KoolbaseCollectionGrid (10.5.0)
feat: KoolbaseCollectionGrid (10.5.0)
The same collection laid out as a grid. Deliberately thin: it shares
KoolbaseCollectionController with KoolbaseCollectionList, so
stale-while-revalidate, pull-to-refresh and the loading/empty/error
slots behave identically — the two differ only in layout, and sharing
the controller is what keeps them from drifting on the hard parts.
Fixed crossAxisCount rather than responsive reflow: a caller who needs
reflow can vary it from a LayoutBuilder, and building it in would have
meant guessing at tile sizing for everyone else.
Tests are narrow on purpose — the controller's properties are already
covered by the list's tests, so re-testing them here would test the
same code twice. The query fake moves to test/support so two copies
can't drift.
README's 'for grids, drive the controller directly' is no longer true
and now documents the widget instead.
10.4.0: streams fetch on listen, and writes refresh them
10.4.0: streams fetch on listen, and writes refresh them
Two gaps that both surfaced as "the UI just does not update" — nothing
errored, nothing logged, the data was simply absent. Both were found by
building an app against the SDK rather than by reading it.
.stream was a bare relay off a broadcast controller: get() performed the
fetch and pushed refreshes into it, so a stream-only listener waited
forever on a collection nothing else had read. It now fetches on first
listen, cache-first exactly as get() is.
Every write invalidated the collection cache and stopped, which only
affects the NEXT query. A listener already watching sat unchanged until
something happened to re-fetch — a message sent into a chat thread did
not appear in that thread. insert, upsert, deleteWhere, batch, and
conflict resolution now refresh open queries on the collection.
Each query re-runs ITSELF, so a stream only receives records matching its
own filters. The refresh is a registered closure per stream rather than
one rebuilt from the stream key: a key carries collection, filters, and
user but not ordering, limit, or populated fields, so a reconstructed
query would push the wrong records into a stream that never asked.
Mutation-verified: dropping the collection filter refreshes every query
in the process, failing three tests.
README gains a Live queries section — .stream was undocumented — and its
where() example is corrected to the real named-argument signature.
KoolbaseAuthGate, KoolbaseAuthScope, KoolbaseCollectionList, and KoolbaseCollectionController ship as the first behavioral widgets: the SDK now encode
KoolbaseAuthGate, KoolbaseAuthScope, KoolbaseCollectionList, and
KoolbaseCollectionController ship as the first behavioral widgets: the
SDK now encodes auth branching and the stale-while-revalidate list
contract as composable components rather than patterns every app
hand-writes. Query refresh streams are now keyed by query identity
rather than collection name, matching their documented contract — see
the changelog behavior note. README gains a Widgets section and catches
up two versions of version-string drift (was still ^10.1.2).
database: insert-conflicts are real, and resolvable (10.2.0)
database: insert-conflicts are real, and resolvable (10.2.0)
The Flutter twin of RN 94d8a21, closing the cross-SDK batch. Unique
constraints made insert-conflicts a genuine third kind: a queued insert
refused as a duplicate is held like any terminal refusal — but the client
coerced its operation to update (ConflictOperation admitted only two members),
and resolving one issued a PATCH against a record id that exists nowhere.
Storage never lied; only the mapper did.
ConflictOperation gains insert. Resolving a rejected insert IS the insert,
retried: resolveWithMerge carries amended data (the fix-the-colliding-title
path), unconditional — no record, no revision to be conditional against — with
the conflict's id as the idempotency key, so a resolution whose response is
lost returns the original on retry rather than duplicating. Wire-proven on the
exact route. resolveWithServer means the colliding row stands: clears with
zero requests, asserted by request count.
Seeded through the production path (enqueue → moveToRejected). Mutation-
verified: deleting the insert branch resurfaces the pre-fix wrong-verb PATCH,
caught by name by a scripted client that refuses unknown routes loudly.
README documents the insert-conflict resolution semantics in the same change.
database: a refused resolution teaches the stored conflict (10.1.2)
database: a refused resolution teaches the stored conflict (10.1.2)
The Flutter twin of RN fb4480d, live in a published SDK until now. Resolution
was conditional on the revision the ORIGINAL refusal reported; when the record
moved again mid-decision, the 409 — carrying current_revision and the record —
was correctly refused and entirely discarded by _resolveWrite. Every retry
replayed the stale condition; abandon was the only exit. Device-proven on RN
(three identical refusals against an unchanged server).
Flutter's factory already parsed the 409 fully into
KoolbaseRevisionMismatchException — the information died one method later. The
fix is one catch: refreshConflict absorbs currentRevision and currentRecord
into the Drift row (honoring serverState's own documented contract — 'as
returned with the refusal, so resolving does not need a fetch' — which the
CREATING refusal honored and the resolution refusal violated), then rethrows
with review-and-retry.
Proven by the revision sequence [8, 9]: the second attempt is conditional
against the LEARNED revision and succeeds without the server moving again.
Seeded through the production path (enqueue → moveToConflict). Mutation-
verified: removing the refreshConflict call fails the storage assertion.
The resolution HTTP path gained an injectable client (httpClient param) —
an unmockable resolution path is why no test ever caught this.
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Breaking: `Koolbase.messaging.send()` removed. Sending push notifications is server-initiated only — it requires a secret kb_live_ key and must run on
Breaking: Koolbase.messaging.send() removed. Sending push notifications is server-initiated only — it requires a secret kb_live_ key and must run on
your backend or in a Koolbase Function, never in the app. The publishable key the SDK holds ships inside your app binary; allowing it to send would let
anyone who extracts it push to your users. The API already rejects publishable-key sends with 401. Device registration
(Koolbase.messaging.registerToken) is unchanged. See docs: /sdk/messaging.
Fixed: in-app functions.deploy() now uses the auto-refreshing session token. It previously read a manually-set static token that expired after 15
minutes while invoke() (which already auto-refreshed) kept working — so a long-lived app could see deploys fail while invocations succeeded. deploy()
now shares invoke()'s token path. The removed setAuthToken() method is no longer needed; the SDK manages the session token itself.
Fix: package failed to compile on stock Flutter (all platforms). 9.4.0 unconditionally imported dart:_internal (VM code-push bindings that resolve onl
Fix: package failed to compile on stock Flutter (all platforms). 9.4.0 unconditionally imported dart:_internal (VM code-push bindings that
resolve only against the Koolbase-patched engine). The stock Dart frontend rejects platform-private imports at kernel compile, so every build failed with
"Can't access platform private library" — analyzer-clean, caught only at build time. The bindings are now a stock-safe stub: on a stock engine, code push
reports "engine not present" through existing failure paths (koolbaseBuildId() returns '', applyKoolbasePatch returns sentinel -990). No API changes; upgrading from 9.4.0 requires no code changes.
Code Push (VM-level, iOS): flash-free boot apply. Koolbase.initialize now completes the iOS boot-time patch apply (local disk only, ~5ms measured on d
Koolbase.initialize now completes the iOS boot-time patch apply (local disk only, ~5ms measured on device) before returning, so the app's first frame already runs the patched code — the brief v1→v2 flash on cold launch is gone, with zero configuration. The network check/download remains fully asynchronous and can never block startup.patch_downloaded (on successful stage), patch_activated (on the boot that promotes a new patch, with patch_number), and patch_failed (on rejection, with the rejection code and whether the artifact was newly staged or the durable copy) to /v1/code-push/patch-events. Dashboards and rollout decisions can now count real device activations instead of inferring installs from patch-check serves. Fire-and-forget: event reporting never delays boot and failures are silent.Nothing published for this version
Code Push (VM-level): the client now reports flutter_version on patch-check so the resolver can refuse a patch built on a different Flutter engine ver
flutter_version on patch-check so the resolver can refuse a patch built on a different Flutter engine version.
assets/koolbase_flutter_version asset (written by koolbase build / koolbase release) and sends it alongside build_id / release_version.flutter_version, closing two cross-engine mis-serve cases (colliding build_id across engine versions; release_version matching on app version alone).flutter_version and the server falls back to legacy matching — no change for apps already in the field.Widen package_info_plus to >=8.0.0 <10.0.0 and flutter_secure_storage to >=9.0.0 <11.0.0. The SDK only uses the stable surface of both (PackageInfo ve
package_info_plus to >=8.0.0 <10.0.0 and flutter_secure_storage to >=9.0.0 <11.0.0. The SDK only uses the stable surface of both (PackageInfo version/buildNumber; SecureStorage read/write/delete with default AndroidOptions and standard KeychainAccessibility), so the previous latest-major pins needlessly blocked — and for secure_storage risked force-migrating — host apps on the prior major.Code Push (bundle): recall/rollback now actually reverts a recalled bundle on device.
Code Push (VM-level): KoolbaseVmPatchClient — over-the-air Dart code updates for Android.
KoolbaseVmPatchClient — over-the-air Dart code updates for Android.
None. mode and minSimilarity are both optional; existing searchSemantic callers continue to work unchanged. Major bump reflects the conceptual expansi
mode and minSimilarity are both optional; existing
searchSemantic callers continue to work unchanged. Major bump reflects
the conceptual expansion of the search contract (three retrieval modes
instead of one), not API-breaking removals.Nothing published for this version
Nothing published for this version
Object versioning support — full read + write surface against versioned buckets. None of this changes existing behavior on non-versioned buckets.
Object versioning support — full read + write surface against versioned buckets. None of this changes existing behavior on non-versioned buckets.
KoolbaseObjectVersion model — one entry in a path's version timeline.
Carries versionId, size, metadata, isDeleteMarker, isCurrent,
createdAt and the rest of the version-row shape.KoolbaseStorageClient.listVersions({bucket, path}) — returns the full
timeline newest-first, current + history mixed.KoolbaseStorageClient.getVersion({bucket, path, versionId}) — metadata
for one specific version.KoolbaseStorageClient.restoreVersion({bucket, path, versionId}) — brings
a history version back to current; the previously-current row is
snapshotted to history first, so the restore is itself a versioned event.KoolbaseStorageClient.purgeVersion({bucket, path, versionId}) — hard
removes a single history version (row + R2 bytes).KoolbaseStorageClient.getDownloadUrl(...) accepts an optional
versionId — when present, the returned URL points to that specific
version's bytes from .versions/.KoolbaseStorageClient.delete(...) accepts an optional forcePurge
— true wipes the entire timeline for the path (all history rows,
all .versions/ R2 keys, canonical, and the current row).No breaking changes. All new APIs are additive; existing publicUrl calls without transform produce the exact same URL they did in 6.3.0.
KoolbaseImageTransform value class — width, height,
format, quality, fit, dpr, gravity. Pair with the new
KoolbaseImageFormat, KoolbaseImageFit, and
KoolbaseImageGravity enums for type-safe option construction.
Out-of-range numeric values clamp silently to Cloudflare's
valid ranges (width/height 1–2000, quality 1–100, dpr 1–3).KoolbaseStorageClient.publicUrl({transform}) and
KoolbaseObject.publicUrl(bucket, {transform}) accept an
optional transform; the resulting URL hits Cloudflare's image
pipeline at cdn.koolbase.com/cdn-cgi/image/<opts>/... and
serves a resized, re-encoded copy of the source. Original URL
behavior unchanged when transform is omitted.KoolbaseStorageClient.publicUrlWithPreset({projectId, presetName, bucket, path}) and
KoolbaseObject.publicUrlWithPreset(bucket, presetName)
resolve a named preset stored server-side (managed via the
dashboard or REST API) at cdn.koolbase.com/p/{project_id}/ {preset_name}/{bucket}/{path}. Edit the preset once on the
server and every URL using it updates as the edge cache rolls
over.publicUrl calls without transform produce the exact same
URL they did in 6.3.0.No breaking changes. getDownloadUrl already returns the CDN URL for objects in public buckets since the server-side Gap #2 deploy on Jun 2 2026 — this…
KoolbaseObject gains an r2Bucket: String field identifying
which physical R2 bucket holds the object's bytes. Always
populated. 'koolbase-storage-public' means the object has a
stable CDN URL; anything else (typically 'koolbase-storage')
means it's in private storage and reads go through a presigned
URL via getDownloadUrl.KoolbaseObject.publicUrl(String bucketName) returns the stable
https://cdn.koolbase.com/... URL for the object when it lives
in the public R2 bucket, null otherwise. Use this when you
have an object instance and want a safe URL — returns null
rather than a URL that 404s for private or legacy public-bucket
files.KoolbaseStorageClient.publicUrl({projectId, bucket, path}) —
static helper that builds the CDN URL pattern unconditionally.
Use for build-time URL generation where you have the inputs but
don't need (or want) a check that the file is actually in a
public bucket.getDownloadUrl already returns the CDN URL
for objects in public buckets since the server-side Gap #2 deploy
on Jun 2 2026 — this release just makes that URL constructible
without a network round-trip.feat(storage): custom object metadata. Attach arbitrary key/value pairs to stored objects at upload time, mutate via merge semantics post-upload, read
KoolbaseObject.
KoolbaseStorageClient.upload() gains an optional
metadata: Map<String, String> named param. Set at confirm time;
REPLACES prior metadata on the overwrite: true path (matches
GCS semantics — a new upload at a path produces a new object,
not a patch of the old).KoolbaseStorageClient.updateMetadata() method with merge
semantics: keys with a non-null value are set/updated, keys with
null are deleted, keys absent from the payload are untouched.
One call handles add, update, and delete atomically.KoolbaseObject gains a metadata: Map<String, String> field.
Always non-null — {} when empty, never null — so callers can
treat it as a guaranteed map without nil checks.KoolbaseStorageMetadataInvalidException (extends
KoolbaseStorageException) thrown for server-side validation
failures. Its detail field names the failing key and rule
(e.g. key "bad key": must match [a-z0-9_]+,
exceeds 50 keys (got 53)) so callers can surface actionable
errors without guessing what shape rule was violated.[a-z0-9_]+, values ≤1024
chars, leading underscore reserved for system keys.Your coding agent can read these notes before it upgrades. Set up the MCP server →