koolbase_flutter 10.4.0
koolbase_flutter: ^10.4.0 copied to clipboard
Flutter SDK for Koolbase — feature flags, remote config, version enforcement, authentication, storage, database, realtime, OTA updates, and code push for mobile apps.
10.4.0 #
Fixed — streams now behave the way they read #
Two gaps that both surfaced as "the UI just doesn't update". Nothing errored, nothing logged, the data was simply absent — the worst kind of bug to hand a developer, and both were found by building an app against the SDK rather than by reading it.
-
.streamnow fetches on first listen. It 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. A stream that stays silent until an unrelated call happens to populate the cache is indistinguishable from a broken one, and the caller has no way to tell the difference. The initial fetch is cache-first, exactly asget()is. -
Writes now refresh open streams.
insert,upsert,deleteWhere,batch, and conflict resolution all invalidated the collection cache and stopped — which only affects the NEXT query. A listener already watching sat unchanged until something happened to re-fetch, so a message sent into a chat thread did not appear in that thread.Each open query re-runs ITSELF and pushes its own result, so a stream only ever receives records matching its own filters. The refresh is a registered closure per stream rather than something rebuilt from the stream key: a key carries collection, filters, and user — not ordering, limit, or populated fields — so a reconstructed query would run differently and push the wrong records into a stream that never asked for them. Mutation-verified: dropping the collection filter refreshes every query in the process.
Neither is breaking. Code that called get() before listening still
works; it simply no longer has to.
10.3.0 #
Added — behavioral widgets: the SDK now ships correctness, not just calls #
KoolbaseAuthGate: branches the tree on auth state correctly. CallsrestoreSession()once at mount behind arestoringslot, so a returning user never sees a login-screen flash; seeds synchronously fromcurrentUser(the auth stream is a broadcast with no replay — a listen-only gate would hang forever); mapsRestoreResult.offlineto signed-IN per its optimistic semantics, exposingrestoredOffline; a restore that throws lands on signed-out, never a crash. Headless with slots:signedInandsignedOutrequired,restoringoptional.KoolbaseAuthScope(InheritedWidget): descendants readuser/status/restoredOfflinewithout statics, rebuilding on identity change.KoolbaseCollectionList: an opinionated list over a collection —ListView.separatedinside aRefreshIndicator— with the stale-while-revalidate contract handled: the cached first arrival renders (isFromCacheexposed), the background network arrival replaces it, a failed refresh keeps the records (stale beats blank; only a first load with nothing to show is an error state, with a retry slot). Per-item appearance is the caller'sitemBuilder;empty/error/loadingare slotted, and the empty state stays pull-refreshable.KoolbaseCollectionController: the list's data half, widget-free — fetch, stream subscription, and refresh lifecycle for custom scroll layouts (grids, slivers) without re-implementing the SWR discipline.
Fixed — query streams are per-query, matching their documented contract #
- Query refresh streams were keyed by collection name alone, so two
DIFFERENT queries on one collection shared a controller: each background
refresh emitted its records to both listeners — silently wrong data on
any screen with two lists over the same collection. Streams are now keyed
by query identity (collection + filters + user), the same construction as
the cache key.
KoolbaseQuery.streamKeyexposes the identity for consumers managing subscriptions across rebuilt query instances. - Behavior note: a stream on a never-fetched query no longer receives
refreshes triggered by OTHER queries on the same collection — that was
the contamination, not a feature. The common pattern (build a query,
get(), listen to the same query'sstream) is unchanged. - Reminder:
KoolbaseQuery's builder methods mutate their instance — construct a fresh query per fetch.KoolbaseCollectionList'squerycallback does this for you and must be deterministic.
10.2.0 #
Added — insert-conflicts are real, and resolvable #
ConflictOperation.insert: a queued insert refused as a duplicate (unique constraint) is held like any other terminal refusal — previously its operation was coerced toupdate, and resolving it issued a PATCH against a record id that exists nowhere.- Resolving a rejected insert IS the insert, retried.
resolveWithMergecarries amended data (the "fix the colliding title" path). Unconditional — no record, no revision to be conditional against. The conflict's id rides as the idempotency key, so a resolution whose response is lost returns the original on retry rather than duplicating. resolveWithServeron an insert-conflict means the colliding row stands: the conflict clears without a request.
10.1.2 #
Fixed — a refused resolution now teaches the stored conflict #
- Resolving a conflict is conditional on the server revision the refusal reported. When the record moved AGAIN while someone was deciding, the resolution was correctly refused — but the stored conflict never absorbed the 409's current revision, so every retry replayed the stale condition and a conflict whose resolution failed once was permanently unresolvable except by abandon. Device-proven on the React Native side; identical defect here.
- The refusal now updates the stored conflict (revision and fresh server
snapshot — divergence recomputes automatically) and rethrows
KoolbaseRevisionMismatchExceptionwith "review and retry": the next attempt is conditional against reality, and succeeds if the user re-decides. - Internal: the conflict-resolution HTTP path is now injectable
(
httpClientconstructor parameter) — an unmockable resolution path is why this bug had no test to catch it.
10.1.1 #
Changed — signed out is a refusal, not an empty list #
pendingWrites()andwatchPendingWrites()now throwKoolbaseUnauthenticatedExceptionwhen no user is signed in (the stream emits it as an error event, per emission — a sign-out under a live badge becomes an error, not a fake zero). Previously the null identity filtered to a silent empty — indistinguishable from "all synced" — and could even match legacy null-owner writes. Found on device: a signed-out display showed 0 pending while a user's queued writes sat unsent.- Check pending writes BEFORE signing out (as the README example always showed); after sign-out the question has no answer and now says so.
10.1.0 #
Added — the queue is observable #
Koolbase.db.pendingWrites()— changes made offline, waiting to be sent, oldest first. For sync indicators and for warning a user about to log out with unsynced edits: queues are per-user and survive logout by design, so those edits sync whenever that user next signs in on this device — possibly never.conflicts()got this treatment; the queue, the same durable state one step earlier, now has it too.Koolbase.db.watchPendingWrites()— the live version, for a sync badge, mirroringwatchConflicts().- Both are per-user, reading identity through the sync engine — the same object replay consults, so the filter and the replay skip can never disagree.
- The returned
PendingWritedeliberately excludes replay internals (baselines, revisions, raw payloads). A delete carries nodata: there is nothing the user "changed", only a removal.
10.0.0 #
Breaking — one exception hierarchy #
The three exception families were unrelated roots, each implementing Exception
directly. They now share one:
KoolbaseException
├── KoolbaseAuthException
├── KoolbaseDataException
├── KoolbaseStorageException
└── KoolbaseUnauthenticatedException
A rejected credential is no longer a data error. It was
KoolbaseSessionExpiredException extends KoolbaseDataException, so code
catching KoolbaseDataException around a query caught it too. It is now
KoolbaseUnauthenticatedException, a sibling. Catch it explicitly:
try {
await Koolbase.db.collection('posts').get();
} on KoolbaseUnauthenticatedException {
await goToLogin();
} on KoolbaseDataException catch (e) {
showError(e.message);
}
KoolbaseSessionExpiredException remains as a deprecated subclass, so existing
catches still match. It will be removed in 11.0.0.
Renamed because the old name over claimed. A 401 covers an expired session, a revoked key, and malformed or missing credentials, and the server does not distinguish them. Signing a user out on a revoked API key would be acting on a precision that never existed.
Added — Function failures are told apart #
Every failed invocation raised one type carrying a status number, so a missing Function, a caller without permission, a Function that threw, and an exhausted plan limit all arrived identically. An application had to read the number to tell them apart — and they call for entirely different responses.
FunctionNotFoundException— nothing deployed under that nameFunctionPermissionException— the caller may not invoke itFunctionValidationException— the Function rejected its argumentsFunctionExecutionException— the Function ran and threwFunctionQuotaExceededException— the plan's invocations are used up
FunctionInvokeException remains the base, so existing catches still match, and
it now sits under KoolbaseException with the other families.
A 401 raises KoolbaseUnauthenticatedException rather than a Function type: a
rejected credential is not a Function failure. A 403 deliberately does not —
signing a user out for calling something they are not allowed to call would be
wrong.
Fixed — every surface clears the session #
Only database calls cleared a rejected session. Storage, Functions, and background sync did not, so an app whose failing call happened to be an upload or a Function invoke kept believing it was signed in — the loop 9.7.0 fixed, on three other surfaces.
All four now clear it and raise KoolbaseUnauthenticatedException, so one
handler covers the SDK rather than one per subsystem.
9.9.0 #
Added — conditional writes #
update and delete take an optional expectedRevision, applying the write
only if the record still carries it and throwing
KoolbaseRevisionMismatchException — with the record as it now stands —
otherwise.
final record = await Koolbase.db.doc(id).get();
await Koolbase.db.doc(id).update(
{'title': 'Corrected'},
expectedRevision: record.revision,
);
Opt-in: a write without it behaves exactly as before. The check and the write are one operation on the server, so nothing can land between them — which reading a record, comparing it, then writing cannot promise.
Worth reaching for where two things can write the same record. Without it, the second write replaces the first and neither writer learns that it happened.
9.8.0 #
Added — offline update and delete #
Insert, update, and delete now all queue when the network is unreachable. Previously only inserts did: an update or delete threw, so a user without connectivity could create data but not correct or remove it.
await Koolbase.db.doc(id).update({'kg': 68.6}); // queued, applied on reconnect
await Koolbase.db.doc(id).delete(); // same
An offline update or delete requires having read the record. Replaying a
change without knowing what it was composed against means applying it blindly,
overwriting whatever happened in the meantime with nobody able to tell. The SDK
has that state if the record was read on this device — through a query, a
doc().get(), or because it was created here. If not, the write throws
KoolbaseOfflineBaselineUnavailableException rather than queueing. Deliberate:
most offline updates being conflict-safe and some quietly not is a worse
guarantee than a clear refusal.
Conflicts. A queued write replays only if the record still carries the revision it was based on. If something changed it meanwhile, the write is refused and becomes a conflict — held, not applied, not lost, and surviving restarts.
final conflicts = await Koolbase.db.conflicts();
Koolbase.db.watchConflicts().listen(...);
await conflict.resolveWithLocal();
await conflict.resolveWithServer();
await conflict.resolveWithMerge({...});
await conflict.abandon();
Conflicts do not expire. An app that never reads them accumulates them invisibly, with the changes they hold never applied. If you support offline editing, surface them somewhere.
Changed #
- Records now carry
revision, which the server advances on every write. - Query results cache each record individually, so anything the user has seen can be edited offline. Previously a query cached only its result set.
- Direct online writes are unchanged and remain unconditional. The concurrency guarantee added here applies to queued replay.
Note for existing code #
An offline update or delete that previously threw now succeeds and returns
optimistically. Code written to treat that failure as an error will find it no
longer occurs — which is the point, but it is a change to paths you may already
have written around.
Local database #
Schema 3 → 5. Queued writes gain the state they were composed against; cached records gain their revision; conflicts get their own table.
Records cached before this version have no revision and cannot be edited offline until they are read again. Nothing is lost, and skipped writes are logged.
9.7.0 #
Changed — read this before upgrading #
-
A rejected session now signs the user out. When the server refuses a session token (401), the SDK clears the stored session and throws
KoolbaseSessionExpiredException. Previously a 401 produced a genericKoolbaseDataExceptionand the session stayed on disk, so an app kept believing it was authenticated and looped: call, reject, retry, forever, with no path back to login.If your app catches exceptions broadly around data calls, note that by the time you catch this one the user is already signed out. Route to login rather than retrying.
Verified on device: a session invalidated server-side (password reset) left a stale token on the device; the next call returned 401, the SDK cleared the session, and the app routed to login with no app-side handling.
This only fires when the server actually rejects the call. A Function deployed without
requires_authruns with an unauthenticated context and returns whatever error it decides — a 200 carrying a failure, which no SDK can classify. Deploy Functions that assume a signed-in caller with--requires-authso the platform rejects the invoke before your code runs.A 403 is unchanged and still means a permission failure — the session is valid, the caller may not touch that resource. Only 401 clears.
Added #
auth.clearStoredSession()— discards the persisted session without contacting the server. For when the session is already known to be unusable: the token was refused, or a build was pointed at a different project and the stored session belongs to the old one. Unlikelogout()there is no server call, and it is safe in any state including with no session at all.
Fixed #
-
Queued offline writes are no longer replayed under the wrong user. The write queue is per-device and outlives sessions, so a write made offline could be sent after someone else had signed in on the same device — attributing one user's record to another. Writes now record who made them and are only replayed under a session for that same user.
Local database schema 2 → 3. Existing queued writes are preserved but have no recorded owner, so they are not replayed; they are logged when skipped.
-
The SDK now tracks the signed-in user itself for offline attribution rather than depending on the app calling
setUserId.
9.6.0 #
Added #
auth.resendVerificationEmail()— re-send the email-verification link to the current authenticated-but-unverified user. ReturnsResendVerificationResultwithexpiresAtandcooldownUntil(for a "resend in N seconds" countdown), oralreadyVerified: truewhen the account is already verified (nothing is sent). Server-throttled: a 60s cooldown between resends and a daily cap. ThrowsResendCooldownException(recoverable — retry after cooldown) orResendDailyCapException. The automatic verification email sent at sign-up does not count against the cooldown, so the first manual resend is always allowed.
9.5.0 #
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.
9.4.1 #
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.
9.4.0 #
- Code Push (VM-level, iOS): flash-free boot apply.
Koolbase.initializenow 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. - Code Push (VM-level): device outcome events. The client reports
patch_downloaded(on successful stage),patch_activated(on the boot that promotes a new patch, withpatch_number), andpatch_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. - Code Push (VM-level, iOS): correct patch bookkeeping after rejections. The boot apply now records what actually happened (new patch applied / durable re-applied / clean base boot) and reconciliation consumes that record. Previously, a rejected download that fell back to the existing patch could mark the rejected patch as current — the device then reported a patch it wasn't running, suppressing future update offers. A clean base boot (e.g. after an app-store update invalidates a persisted patch) now also resets the reported patch to 0.
- Code Push (VM-level, iOS): rejected patches are quarantined, not retried. A persisted patch that fails verification (for example, stale after an app update) is moved aside on first rejection instead of being re-read and re-rejected on every subsequent launch. Quarantined artifacts are removed once a healthy boot completes.
9.3.0 #
- Code Push (VM-level): the client now reports
flutter_versionon patch-check so the resolver can refuse a patch built on a different Flutter engine version.- Reads the CLI-stamped
assets/koolbase_flutter_versionasset (written bykoolbase build/koolbase release) and sends it alongsidebuild_id/release_version. - Pairs with the server-side resolver guard that constrains matching on
flutter_version, closing two cross-engine mis-serve cases (collidingbuild_idacross engine versions;release_versionmatching on app version alone). - Fully backward-compatible: an app built without the asset sends no
flutter_versionand the server falls back to legacy matching — no change for apps already in the field.
- Reads the CLI-stamped
9.2.1 #
- Widen
package_info_plusto>=8.0.0 <10.0.0andflutter_secure_storageto>=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.
9.2.0 #
- Code Push (bundle): recall/rollback now actually reverts a recalled bundle on device.
- The runtime resolver persists a pending-revert marker when the server issues a rollback and consumes it at the start of the next cold launch, before re-applying any stored bundle. Previously the rollback was logged but never persisted, so a recalled bundle kept re-applying on every launch.
- Pairs with the server-side resolver fix returning rollback/revert_to:0 when a device runs a bundle that has been recalled with no published replacement.
- Affects iOS and Android bundle recall.
9.1.0 #
- Code Push (VM-level):
KoolbaseVmPatchClient— over-the-air Dart code updates for Android.- Self-contained: no MainActivity or platform-channel wiring required.
- Checks in with the Koolbase resolver, downloads and stages patches; the Koolbase engine applies them on next launch with signature + build_id verification and automatic crash-revert.
- Per-ABI build_id resolution for multi-ABI app bundles (arm64-v8a / armeabi-v7a).
- Reports the running build's build_id and current patch number on check-in.
Changelog #
9.0.0 #
Breaking changes #
- None.
modeandminSimilarityare both optional; existingsearchSemanticcallers continue to work unchanged. Major bump reflects the conceptual expansion of the search contract (three retrieval modes instead of one), not API-breaking removals.
Added #
KoolbaseQuery.searchSemanticaccepts a newmodeparameter of typeKoolbaseSearchMode. Three retrieval strategies are supported:KoolbaseSearchMode.semantic(default) — pure vector search via HNSW on cosine distance. Best for fuzzy / conceptual queries.KoolbaseSearchMode.lexical— pure BM25 over the field's source text via Postgrests_rank_cd. Best for exact terms, codes, names.KoolbaseSearchMode.hybrid— vector + lexical fused with reciprocal rank fusion (k=60). Generally the strongest default for production search.
KoolbaseQuery.searchSemanticaccepts a newminSimilarityparameter (0..100, optional). Server-side filter that drops results below the given similarity percentage before they cross the wire. Saves bandwidth on weak matches. Only valid for semantic and hybrid modes; the server rejects it on lexical mode (BM25 ranks aren't comparable to cosine similarity).- New
KoolbaseSearchModeenum exported frompackage:koolbase_flutter/koolbase_flutter.dart.
Server requirements #
- Requires Koolbase API release with hybrid search shipped (June 8 2026 or later).
- Lexical and hybrid modes require the vector field to have a
source_fieldconfigured. The lexical sidecar table populates automatically on record write via the same hook that drives auto-embed.
8.0.0 #
Breaking changes #
KoolbaseQuery.searchSemantic: thequeryVectorparameter is now optional. Existing callers continue to work unchanged — the breaking aspect is that the SDK now validates that exactly one ofqueryVector/queryTextis supplied, and throwsArgumentErrorotherwise.
Added #
KoolbaseQuery.searchSemanticaccepts a newqueryTextparameter. When supplied, the server embeds it inline using the vector field's configured provider (Gemini or OpenAI) before running HNSW lookup. No client-side embedding model required for typical search use cases.KoolbaseQuery.embedTextqueues an embedding job for a specific record's vector field. Used for backfilling vectors on records that pre-date the auto-embed hook, or for embedding text other than the record's configured source field.
Server requirements #
- Requires Koolbase API release
771728dor later (Phase 2 Stage A3a). - Auto-embed on record write is automatic once a vector field has its
embedding_provider,embedding_model, andsource_fieldconfigured (see docs).
7.0.0 #
Added #
- Semantic search via vector similarity — query records by meaning, not just by field equality. New methods on the database namespace mirror the server-side vector primitive shipped in Koolbase Phase 1 AI:
Koolbase.db.doc(id).setVector(field, vector)— store a vector for a recordKoolbase.db.doc(id).getVector(field)— read a stored vectorKoolbase.db.doc(id).deleteVector(field)— remove a vectorKoolbase.db.collection(name).searchSemantic(field:, queryVector:, limit:, where:)— HNSW similarity search with optional filter scoping, returning ranked hits with cosine distance
- New types:
KoolbaseVector,KoolbaseSemanticHit,KoolbaseSemanticSearchResult - New typed exception:
KoolbaseVectorDimensionMismatchExceptionfor length-mismatch errors
Notes #
- Vector fields must be declared on a collection ahead of time via the Koolbase dashboard or CLI; the mobile SDK does not declare schema.
- Supported dimensions in this release: 384, 768, 1024, 1536. Higher dimensions (e.g. OpenAI text-embedding-3-large at 3072) will be supported in a future release once pgvector is upgraded — in the meantime, use the model's
dimensions=1536parameter (Matryoshka truncation). - Vector operations are online-only — they're not cached locally or queued offline because HNSW search has no useful offline semantics.
- Semantic search respects the collection's read rule the same way
.get()does: owner/scoped/conditional rules are applied after the HNSW lookup, so strict rules may return fewer thanlimitresults in this version.
Migration #
Purely additive — no existing methods or types changed. Upgrading from 6.x requires only flutter pub upgrade koolbase_flutter and a re-run of flutter pub get.
6.5.0 #
Object versioning support — full read + write surface against versioned buckets. None of this changes existing behavior on non-versioned buckets.
New #
KoolbaseObjectVersionmodel — one entry in a path's version timeline. CarriesversionId,size,metadata,isDeleteMarker,isCurrent,createdAtand 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).
Extended #
KoolbaseStorageClient.getDownloadUrl(...)accepts an optionalversionId— when present, the returned URL points to that specific version's bytes from.versions/.KoolbaseStorageClient.delete(...)accepts an optionalforcePurge—truewipes the entire timeline for the path (all history rows, all.versions/R2 keys, canonical, and the current row).
6.4.0 #
- feat(storage): edge image transforms (Gap #8).
- New
KoolbaseImageTransformvalue class — width, height, format, quality, fit, dpr, gravity. Pair with the newKoolbaseImageFormat,KoolbaseImageFit, andKoolbaseImageGravityenums 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})andKoolbaseObject.publicUrl(bucket, {transform})accept an optional transform; the resulting URL hits Cloudflare's image pipeline atcdn.koolbase.com/cdn-cgi/image/<opts>/...and serves a resized, re-encoded copy of the source. Original URL behavior unchanged whentransformis omitted.KoolbaseStorageClient.publicUrlWithPreset({projectId, presetName, bucket, path})andKoolbaseObject.publicUrlWithPreset(bucket, presetName)resolve a named preset stored server-side (managed via the dashboard or REST API) atcdn.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.
- New
- Cloudflare bills unique transformations per calendar month; every Koolbase account includes 5,000 free. Transformed responses are edge-cached for 4 hours.
- No breaking changes. All new APIs are additive; existing
publicUrlcalls withouttransformproduce the exact same URL they did in 6.3.0.
6.3.0 #
- feat(storage): public bucket CDN URLs (Gap #2 SDK polish).
KoolbaseObjectgains anr2Bucket: Stringfield 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 viagetDownloadUrl.KoolbaseObject.publicUrl(String bucketName)returns the stablehttps://cdn.koolbase.com/...URL for the object when it lives in the public R2 bucket,nullotherwise. Use this when you have an object instance and want a safe URL — returnsnullrather 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.
- No breaking changes.
getDownloadUrlalready 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.
6.2.0 #
- feat(storage): custom object metadata. Attach arbitrary key/value
pairs to stored objects at upload time, mutate via merge semantics
post-upload, read alongside any
KoolbaseObject.KoolbaseStorageClient.upload()gains an optionalmetadata: Map<String, String>named param. Set at confirm time; REPLACES prior metadata on theoverwrite: truepath (matches GCS semantics — a new upload at a path produces a new object, not a patch of the old).- New
KoolbaseStorageClient.updateMetadata()method with merge semantics: keys with a non-null value are set/updated, keys withnullare deleted, keys absent from the payload are untouched. One call handles add, update, and delete atomically. KoolbaseObjectgains ametadata: Map<String, String>field. Always non-null —{}when empty, nevernull— so callers can treat it as a guaranteed map without nil checks.- New
KoolbaseStorageMetadataInvalidException(extendsKoolbaseStorageException) thrown for server-side validation failures. Itsdetailfield 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.
- Validation rules (server-side, recreated for SDK doc convenience):
≤50 keys, ≤8KB total, keys 1–64 chars
[a-z0-9_]+, values ≤1024 chars, leading underscore reserved for system keys.
6.1.1 #
Fixed #
- Storage error mapper switches on lowercase wire codes (
path_conflict,quota_exceeded,file_too_large,mime_not_allowed) after the server normalized storage codes to lowercase snake_case. Without this patch, v6.1.0 customers see genericKoolbaseStorageExceptioninstead of the typed subclass for storage limit errors. No semantic changes beyond the case match.
6.1.0 #
- feat(storage): three new typed exceptions for bucket-limit failures
introduced server-side in Storage #2. All extend
KoolbaseStorageExceptionso existing catch-all blocks continue to work; catch the specifics to branch on the kind of limit hit.KoolbaseStorageQuotaExceededException— 409 +QUOTA_EXCEEDED, thrown when an upload would push the bucket past itsmax_size_bytescap.KoolbaseStorageFileTooLargeException— 413 +FILE_TOO_LARGE, thrown when a single file exceeds the bucket'smax_file_size_bytescap.KoolbaseStorageMimeTypeException— 415 +MIME_NOT_ALLOWED, thrown when an upload's content-type isn't in the bucket'sallowed_mime_typesallowlist (supportstype/*wildcards).
- Mapper (
koolbaseStorageError/koolbaseStorageErrorFromResponse) recognizes the new codes and the new HTTP statuses (413, 415). - Backwards-compatible: existing callers using
on KoolbaseStorageExceptionkeep working; the new types let callers surface clearer messages or prompt the user to delete files / pick a smaller file / pick a different file type.
6.0.0 #
Breaking — realtime #
Koolbase.realtime.on/onRecordCreated/onRecordUpdated/onRecordDeletedno longer take aprojectId— the project is derived from your session token, matching the React Native SDK. Migrateon(projectId: ..., collection: 'x')toon(collection: 'x').
Breaking — storage #
KoolbaseStorageClient.upload()is now safe-by-default. Uploads to a path where an object already exists are rejected with a newKoolbaseStorageConflictExceptioninstead of silently overwriting the existing object. Passoverwrite: trueto opt into the previous replacing behavior.- Storage operations now throw typed
KoolbaseStorageExceptionsubtypes instead of genericException— catchingExceptionstill works but catching the specific subtypes (or theKoolbaseStorageExceptionbase) gives you cleaner branching.
Added #
KoolbaseStorageException— base class for all storage failures, mirroring theKoolbaseDataExceptionpattern from the database layer.KoolbaseStorageConflictException(code: PATH_CONFLICT) — thrown when an upload would replace an existing object andoverwrite: false. Exposes the collidingpathfrom the server response.KoolbaseStorageNotFoundException,KoolbaseStorageValidationException,KoolbaseStoragePermissionException— typed exceptions for the other storage error classes (404, 400, 403). Storage operations now throw these instead of a genericException.koolbaseStorageError(statusCode, body)andkoolbaseStorageErrorFromResponse(res)— code-first response-to-exception mappers, matching the database layer's pattern.
Migration — storage uploads #
If your app uploads to deterministic paths (e.g. avatars/{user_id}.png)
and relied on the upload silently replacing the previous file:
// Before — silent overwrite
await Koolbase.storage.upload(
bucket: 'avatars',
path: 'me.png',
file: file,
);
// After — explicit overwrite
await Koolbase.storage.upload(
bucket: 'avatars',
path: 'me.png',
file: file,
overwrite: true,
);
If you want a conflict prompt (recommended for user-supplied filenames):
try {
await Koolbase.storage.upload(
bucket: 'documents',
path: filename,
file: file,
);
} on KoolbaseStorageConflictException catch (e) {
final ok = await showConfirm('${e.path} already exists. Overwrite?');
if (ok) {
await Koolbase.storage.upload(
bucket: 'documents',
path: filename,
file: file,
overwrite: true,
);
}
}
If you catch generic exceptions from storage operations, consider
catching KoolbaseStorageException (or specific subtypes) for cleaner
error handling:
try {
await Koolbase.storage.upload(...);
} on KoolbaseStorageConflictException {
// Path already exists — prompt user
} on KoolbaseStorageNotFoundException {
// Bucket missing or deleted
} on KoolbaseStoragePermissionException {
// Caller not authorized
} on KoolbaseStorageException catch (e) {
// Any other storage error
showError(e.message);
}
Server requirements #
- Requires a Koolbase server build with
PATH_CONFLICT409 support (shipped alongside this release).
5.1.0 #
Fixed #
- Realtime now connects. The client was protocol-correct but was never handed an access token (the push-based
setTokenwas never wired), so it never connected. Switched to the same token-provider model as the other clients; it now authenticates with the user session and streamscreated/updated/deletedevents.
Removed #
KoolbaseRealtimeClient.setToken— dead push-model plumbing that was never wired. The token now flows from the SDK automatically.
5.0.0 #
BREAKING — security #
- Data-plane requests (database, storage, functions, offline sync) authenticate with the signed-in user's access token (Authorization: Bearer) instead of the x-user-id header. The header is no longer sent or trusted. Requires the matching Koolbase server build.
- KoolbaseStorageClient.upload() no longer accepts a
userIdparameter — identity comes from the active session automatically. - End-user identity now flows automatically from Koolbase.auth; manual Koolbase.db.setUserId(...) is no longer needed for auth (it remains only for tagging offline-cached records).
Added #
- KoolbaseAuthClient.validAccessToken() — returns a currently-valid token, refreshing (single-flight) near expiry; data-plane clients pull from it per request so identity follows the live session.
4.1.0 #
- Code Push — mandatory bundles. The SDK now honors a bundle's
mandatoryflag (set from the dashboard or viaPATCH /mandatory). When a mandatory bundle is staged for the next launch:Koolbase.codePush.hasMandatoryUpdatereturnstrue— poll it on resume to gate your UI.- The optional
onMandatoryUpdatecallback onKoolbaseConfigfires immediately withMandatoryUpdateInfo(version, bundleId), so you can prompt the user to restart and apply the required update.
- No breaking changes.
4.0.0 #
Breaking #
- Removed the
Koolbase.otaclient (KoolbaseOtaClient) and its models (OtaCheckResult,OtaProgress,OtaDownloadState). UseKoolbase.codePushfor config/flag/directive overrides, or Koolbase Storage for shipping and reading raw files. This consolidates onto a single bundle client — matching the React Native SDK and the server, which already use code push exclusively. - Dropped the
sign_in_with_appledependency — it was only used by the removedKoolbaseAppleAuth. The currentsignInWithApple({identityToken})is library-agnostic, so the SDK no longer pulls it. If your app uses Apple Sign-In, declaresign_in_with_applein your ownpubspec.yaml.
3.3.0 #
- Auth exceptions are now selected from the server's stable error
code(with status/message fallback for older servers), retiring brittle message string-matching. - New typed data-layer exceptions —
KoolbaseNotFoundException,KoolbaseValidationException,KoolbasePermissionException,KoolbaseRateLimitException— plus a sharedKoolbaseDataExceptionbase. Database operations now throw these (code-first) instead of a genericException. KoolbaseConflictExceptionnow exposes the collidedfieldwhen the server reports it.- Fix:
insertno longer queues a server-rejected write (e.g. a unique conflict) as an offline write — 4xx rejections surface immediately; only genuine network failures are queued.
3.2.0 #
- Added
KoolbaseConflictException, thrown byinsert,update, andupsertwhen a write violates a collection's unique constraint (HTTP 409). Catch it to handle duplicates.
3.1.1 #
- Docs: document
upsertanddeleteWherein the README (no code changes).
3.1.0 #
- Added
Koolbase.db.upsert(collection:, match:, data:)— insert-or-update by a match filter; returnsKoolbaseUpsertResult { record, created }. Online-only. - Added
Koolbase.db.deleteWhere(collection:, filters:)— bulk delete by filter; returns the number of records deleted. Online-only.
3.0.0 #
Breaking #
- Flat record shape.
KoolbaseRecordno longer wraps your fields under adataenvelope. Your fields are now top-level, with system metadata in a reserved$-prefixed namespace:$id,$createdAt,$updatedAt,$collection, and$createdBy(when set). - Removed
KoolbaseRecord.projectIdandKoolbaseRecord.collectionId— internal identifiers are no longer exposed on records. - Requires a Koolbase server on the flat record contract (shipped alongside this release). Older servers return the legacy envelope and will not parse.
Added #
record['field']— direct field access, shorthand forrecord.data['field'].record.collection— the record's collection name.
Changed #
- Populated/related records (via
populate()) and realtime payloads now use the same flat$-shape as direct reads. - Offline cache (Drift) bumped to schema v2: stale read caches are cleared on upgrade so they refetch in the new shape; pending offline writes are preserved.
Migration #
- Remove any
record.projectId/record.collectionIdusage — those fields are gone. record.data['field']still works;record['field']is the new shorthand.
2.11.0 #
Added #
- Sign in with Google — production-ready end-user OAuth via
Koolbase.auth.signInWithGoogle(idToken: ..., nonce: ...). Routes to the server endpoint at/v1/sdk/auth/oauth/googlewith RS256-only JWKS verification against Google's certs endpoint, multi-audience support (iOS / Android / web client IDs configured per environment), 15-minute replay defense, and optional nonce check. - Three new typed exceptions in
auth_exceptions.dart:GoogleSignInNotConfiguredException,InvalidGoogleTokenException,GoogleEmailRequiredException. Reuses existingOAuthEmailConflictExceptionandUserDisabledException.
Example with the google_sign_in package
import 'package:google_sign_in/google_sign_in.dart';
final googleUser = await GoogleSignIn().signIn();
final googleAuth = await googleUser?.authentication;
final user = await Koolbase.auth.signInWithGoogle(
idToken: googleAuth!.idToken!,
);
Auto-link policy #
Same as Apple Sign-In (v2.10.0). A new Google identity attaches to an
existing user only when BOTH the Google email AND the existing user's
email are verified, AND emails match (case-insensitive). Otherwise
sign-in either creates a new user (no email collision) or surfaces
OAuthEmailConflictException.
Configuration required #
Before users can sign in with Google, configure the provider for your
environment. Run this against your Koolbase project's project_oauth_configs
(dashboard UI for OAuth config lands in a later release):
UPDATE project_oauth_configs
SET google_client_ids = ARRAY[
'<your-ios-client-id>.apps.googleusercontent.com',
'<your-android-client-id>.apps.googleusercontent.com',
'<your-web-client-id>.apps.googleusercontent.com'
],
enabled = true
WHERE environment_id = '<your-env-id>'
AND provider = 'google';
Get the client IDs from Google Cloud Console under Credentials → OAuth 2.0 Client IDs. You'll need one per platform (iOS, Android, web).
Coming next #
- React Native SDK v1.11.0 — same surface
- Dashboard UI for OAuth config — replaces the SQL workflow
Documentation #
- README rewritten to accurately reflect the v2.10.0 SDK surface. No SDK code changes; this release exists to refresh the README rendered on the pub.dev package page.
- Removed fictional
Koolbase.auth.signInWithGooglereference. Google Sign-In is planned for v2.11.0 — noted explicitly in the OAuth section. - Replaced the deprecated
KoolbaseAppleAuth.signIn()example with the newKoolbase.auth.signInWithApple(identityToken: ..., nonce: ..., fullName: ...)v2.10.0 API using thesign_in_with_applepackage. - Added
Koolbase.auth.authStateChanges.listen()example. - Replaced the Firebase/Supabase comparison table with a Koolbase-only feature inventory.
- Bumped install snippet from
^2.8.0to^2.10.0.
2.10.0 #
✨ New features #
Sign in with Apple — production-ready end-user OAuth.
After being deprecated in v2.5.0 through v2.9.x (the old implementation
routed to the dashboard OAuth endpoint at /v1/auth/oauth and never
created project-scoped end-user sessions), Apple Sign-In is now properly
supported via a dedicated server endpoint at
/v1/sdk/auth/oauth/apple.
// Get the Apple credential using any native Apple Sign-In library
// (sign_in_with_apple, etc.) — the SDK is library-agnostic.
final credential = await SignInWithApple.getAppleIDCredential(
scopes: [AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName],
);
// Pass the credential to Koolbase.
final user = await koolbase.auth.signInWithApple(
identityToken: credential.identityToken!,
nonce: credential.nonce, // optional but recommended (replay defense)
fullName: credential.givenName != null
? AppleFullName(
givenName: credential.givenName,
familyName: credential.familyName,
)
: null,
);
Server-side verification runs against the project's configured Bundle ID via Apple's JWKS — RS256-only, audience-bound to your project's iOS app, with replay defense (iat max-age of 15min) and optional nonce check.
Auto-link policy: A new Apple identity attaches to an existing user
only when BOTH the provider email AND the existing user's email are
verified, AND emails match (case-insensitive). Otherwise sign-in either
creates a new user (no email collision) or surfaces
OAuthEmailConflictException (collision but auto-link rule blocked — user
can sign in with existing method and link from settings).
Four new typed exceptions for granular error handling:
| Exception | When |
|---|---|
AppleSignInNotConfiguredException |
Apple not enabled for this environment in dashboard OAuth config |
InvalidAppleTokenException |
Token signature, audience, expiry, replay, or nonce check failed |
AppleEmailRequiredException |
Apple didn't return email AND no existing identity. Recovery: revoke this app's Apple ID access in iOS Settings → Apple ID → Sign-In & Security → Apps Using Apple ID |
OAuthEmailConflictException |
Email matches existing user but auto-link rule blocked |
Configuration required #
Before users can sign in with Apple, configure the provider for your
environment via direct DB insert against project_oauth_configs (the
dashboard UI is on its way — landing in v2.10.1 or v2.11.0):
INSERT INTO project_oauth_configs (environment_id, provider, bundle_id, enabled)
VALUES ('<your-environment-id>', 'apple', 'com.yourapp.bundle', true);
You'll need your iOS app's Bundle ID — it's the audience claim in identity tokens from native Sign in with Apple, and must match exactly.
Still deprecated — KoolbaseAppleAuth.signIn and oauthLogin #
These remain deprecated and continue to throw UnimplementedError. The
v2.10.0 surface is koolbase.auth.signInWithApple(...) on the auth client
— same place as all other auth methods. The class-level
KoolbaseAppleAuth.signIn(callback) API from v1.5.0 was a wrong-shape
design (callback-based, locked consumers to a specific native library) and
won't be revived.
Coming next #
- Dashboard UI for OAuth config (v2.10.1) — minimal Bundle-ID input, enable/disable toggle, validation. Removes the SQL-direct workflow.
- Google Sign-In (v2.11.0) — same endpoint pattern at
/v1/sdk/auth/oauth/google - GitHub OAuth (v2.12.0) — code-exchange flow
2.9.1 #
Polish release: configurable HTTP timeout, injectable HTTP client, and a logout() that lets you know whether the server-side call succeeded. Also fixes a wiring oversight from v2.9.0: the device metadata headers introduced in that release were not actually being attached to requests — the SDK had the code but no construction site. v2.9.1 wires it through properly.
Fixed #
- Device metadata headers (from v2.9.0) are now actually attached to authentication requests. v2.9.0 introduced the
DeviceMetadataclass and the supportingkoolbaseSdkVersionconstant, andAuthApiwas updated to accept aDeviceMetadatainstance — but theKoolbase.initialize()flow was not updated to construct one and pass it in. As a result, nox-koolbase-*headers or structuredUser-Agentwere sent on the wire from v2.9.0. v2.9.1 restores the intended behavior. Upgrade from v2.9.0 to get the metadata features described in the v2.9.0 release notes.
Added (2.9.1) #
KoolbaseConfig.authTimeout(Duration, default 10s): timeout applied to every authentication HTTP request. Tune up for high-latency networks; tune down for fast-fail UX on first-byte latency.KoolbaseConfig.httpClient(http.Client?, default null): inject your own HTTP client for logging interception, retry middleware, proxy configuration, or sharing connection pools. The SDK will NOT close a caller-supplied client; the caller owns its lifecycle. Currently scoped to auth requests; other SDK modules (storage, database, realtime, etc.) will adopt this in a future release.AuthApi.dispose()closes the underlying HTTP client iff the SDK owns it. Called automatically byKoolbaseAuthClient.dispose()for clean shutdown.
Changed #
KoolbaseAuthClient.logout()now returnsFuture<bool>instead ofFuture<void>. The local session is always cleared regardless of whether the server-side logout succeeded (intentional best-effort behavior to avoid leaving stale tokens client-side after a network error). The return value indicates whether the server-side call succeeded —trueif it did (or if there was no access token to invalidate),falseif the server call failed. Source-compatible for callers ignoring the return value.KoolbaseAuthClient.dispose()now also cascades toAuthApi.dispose()so the HTTP client gets closed on shutdown.
Usage #
// Default: 10s auth timeout, SDK-owned http.Client
await Koolbase.initialize(KoolbaseConfig(
publicKey: 'pk_live_...',
baseUrl: 'https://api.koolbase.com',
));
// Custom timeout for slow networks
await Koolbase.initialize(KoolbaseConfig(
publicKey: 'pk_live_...',
baseUrl: 'https://api.koolbase.com',
authTimeout: const Duration(seconds: 30),
));
// Inject your own HTTP client (logging, retries, proxy, etc.)
final myClient = MyLoggingClient();
await Koolbase.initialize(KoolbaseConfig(
publicKey: 'pk_live_...',
baseUrl: 'https://api.koolbase.com',
httpClient: myClient,
));
// Check whether server logout succeeded
final ok = await Koolbase.auth.logout();
if (!ok) {
// Local session was cleared; server may not be fully aware
}
2.9.0 #
A comprehensive overhaul of the authentication module. This release closes seven independent gaps identified by a focused security and reliability audit, adds proper device-attributed session tracking, fixes a refresh-token race that could invalidate concurrent in-flight requests, and honestly deprecates OAuth methods that were never fully wired up on the server side.
Highlights #
- Pluggable storage: a new
KoolbaseAuthStorageabstract interface lets you plug in custom storage backends (compliant encryption layers, web targets, in-memory test mocks). The defaultSecureAuthStoragenow persists the full session — access token, refresh token, expiry, and user — not just the refresh token. - Offline-aware session restoration:
restoreSession()returns aRestoreResultenum (noSession/restored/expired/offline) so your app can render the correct UI immediately. App launches no longer require a network round-trip to show authenticated state. - Single-flight refresh: concurrent API calls that find an expired token now share one underlying refresh call, fixing a race where parallel refreshes could invalidate each other's tokens.
- Expanded typed exceptions:
AccountLockedException,RateLimitException,UnlockTokenInvalidException,TokenRevokedException— covering brute-force lockouts (HTTP 429), general rate limits, the unlock-email flow, and centrally revoked sessions. - Device metadata on every auth request: a structured
User-Agentplusx-koolbase-*headers (SDK version, platform, app version, stable per-install device label) so the server's sessions infrastructure can attribute activity for the sessions UI, future security alerts, and analytics.
Added #
KoolbaseAuthStorageabstract interface — implement your own to plug in custom auth storage backends.SecureAuthStoragedefault implementation backed byflutter_secure_storagewith explicit iOS Keychain accessibility (first_unlock_this_device) and Android EncryptedSharedPreferences.PersistedSessionvalue class for fully-typed session persistence.RestoreResultenum returned byKoolbaseAuthClient.restoreSession().KoolbaseAuthClient.unlock(String token)— consume an unlock token from a brute-force unlock email.DeviceMetadataclass built automatically atKoolbase.initialize(); persists a stable per-install device label.koolbaseSdkVersionconstant exported for consumers who need to assert SDK version at runtime.- New typed exceptions:
AccountLockedException— brute-force lockout (HTTP 429 + lockout marker). Includes a forward-compatible nullablelockedUntilfield.RateLimitException— general HTTP 429 without the lockout marker.UnlockTokenInvalidException— invalid or expired unlock email token (one-shot).TokenRevokedException— session has been revoked centrally (distinct fromSessionExpiredException).
Changed #
KoolbaseAuthClient.restoreSession()signature changed fromFuture<void>toFuture<RestoreResult>. Source-compatible for callers ignoring the return value; callers wanting offline-aware UI should branch on the enum.AuthApiconstructor is no longerconst— it now accepts optionalDeviceMetadata. Source-compatible for code that doesn't use theconstkeyword (the default in most apps).KoolbaseAuthClient.refreshSession()and the internal_ensureValidToken()go through a single-flight refresh path; concurrent callers share one underlying refresh.- Every authenticated request now carries
User-Agent,x-koolbase-sdk,x-koolbase-sdk-version,x-koolbase-platform,x-koolbase-platform-version,x-koolbase-app-version, andx-koolbase-device-labelheaders.
Fixed #
- Refresh-token race: parallel API calls hitting an expired token no longer trigger competing refresh calls. Server-side refresh-token rotation no longer invalidates peer in-flight tokens.
- Offline launch:
restoreSession()previously cleared all auth state on any error including network failures, silently logging users out. It now distinguishes auth rejection from network errors and keeps optimistic state in the offline case. - 401-on-refresh: refresh failures returning HTTP 401 previously surfaced as
InvalidCredentialsException("wrong password"). They now correctly throwSessionExpiredException. - Profile updates not persisting:
updateProfile(),getCurrentUser(), andlinkPhone()updated in-memory state but didn't re-persist the user. Changes were lost on app restart. Now persisted via a new internal helper. linkPhonelistener not firing: profile updates after phone linking now correctly emit onauthStateChanges.forgotPasswordsilently swallowed errors: now properly checks the response status and surfaces errors as typed exceptions.
Deprecated #
- OAuth methods:
KoolbaseAuthClient.oauthLogin(),AuthApi.oauthLogin(), andKoolbaseAppleAuth.signIn(). The previous implementations targeted/v1/auth/oauth— the dashboard's developer OAuth handler — which never created project-scoped sessions for end-users. All three methods now throwUnimplementedError. Proper end-user OAuth endpoints (/v1/sdk/auth/oauth/apple,/google,/github) are tracked for v2.10.x. UseKoolbaseAuthClient.login()with email/password until then. AuthStorageclass: replaced bySecureAuthStorage. The old class remains as a@Deprecatedsubclass for source compatibility and will be removed in v3.0.0.
Migration #
If you construct AuthStorage directly:
// Before
final client = KoolbaseAuthClient(api: api, storage: AuthStorage());
// After (recommended — uses the default)
final client = KoolbaseAuthClient(api: api);
// Or explicit
final client = KoolbaseAuthClient(api: api, storage: SecureAuthStorage());
If you handle restoreSession():
// Before
await Koolbase.auth.restoreSession();
if (Koolbase.auth.isAuthenticated) {
// Show app
} else {
// Show login
}
// After (recommended — branch on outcome)
final result = await Koolbase.auth.restoreSession();
switch (result) {
case RestoreResult.noSession:
// Show login
case RestoreResult.restored:
// Show app
case RestoreResult.expired:
// Show login with "session expired" message
case RestoreResult.offline:
// Show app optimistically; retry refresh when network returns
}
If you call oauthLogin() or KoolbaseAppleAuth.signIn():
These now throw UnimplementedError. End-user OAuth is blocked on a server-side endpoint that ships in v2.10.x. Use email/password authentication via KoolbaseAuthClient.login() for now.
If you catch generic KoolbaseAuthException for lockout or rate-limit cases:
Consider catching the more specific types now:
try {
await Koolbase.auth.login(email: email, password: password);
} on AccountLockedException {
// Show "account temporarily locked" UI; offer "unlock via email" path
} on RateLimitException {
// Show "too many attempts, please wait" UI
} on InvalidCredentialsException {
// Show "wrong email or password"
}
Internal #
KoolbaseAuthClientno longer importspackage:flutter/material.dart(was only needed fordebugPrintin OAuth error paths, which are now deprecated stubs).- New
lib/src/auth/device_metadata.dartmodule.
2.8.0 #
- Functions: Authenticated invocations now forward the signed-in user's session automatically.
- When a user is signed in via
Koolbase.auth, calls toKoolbase.functions.invoke()include their access token in the request. - Functions receive caller identity via
ctx.auth— a map withuser_id(string or null) andis_authenticated(bool). - Unauthenticated invokes continue to work; Functions decide whether they require auth and respond with
AUTH_REQUIREDif needed. - Token refresh is handled transparently — the next invoke after a refresh uses the fresh token without any client-side wiring.
- When a user is signed in via
- Backwards compatible: no breaking changes. Existing code paths continue to work.
2.7.0 #
Phone + OTP authentication #
Sign users in with their phone number — for emerging markets and apps where email isn't the primary identifier.
New methods on Koolbase.auth:
sendOtp({required String phoneNumber})— sends a 6-digit OTP to an E.164 phone number, returns the expiry timestamp.verifyOtp({required String phoneNumber, required String code})— verifies the code and signs the user in (creates the account if new). ReturnsPhoneVerifyResultwith anisNewUserflag for routing first-time users to onboarding.linkPhone({required String phoneNumber, required String code})— links a phone number to an already-authenticated user.
New types: OtpSendResult, PhoneVerifyResult.
KoolbaseUser now exposes phoneNumber and phoneVerified fields.
New exceptions: InvalidPhoneNumberException, OtpExpiredException, OtpInvalidException, OtpMaxAttemptsException, OtpRateLimitException, PhoneAlreadyLinkedException, SmsConfigMissingException.
Phone numbers must be in E.164 format (e.g. +233244000000). Configure your SMS provider (Twilio, Africa's Talking, or Hubtel) in the Koolbase dashboard before using.
2.6.4 #
- README update — full feature documentation
2.6.3 #
- Updated drift to ^2.31.0
- Updated drift_flutter to ^0.2.8
2.6.2 #
- Updated dependencies to latest versions
- Fixed static analysis warnings
- Removed deprecated encryptedSharedPreferences parameter
2.6.1 #
- README update — Logic Engine v2 operators
2.6.0 #
Logic Engine v2 — Richer conditions #
New operators:
gte— greater than or equalslte— less than or equalscontains— string or list contains valuestarts_with— string starts withends_with— string ends within_list— value is in a listnot_in_list— value is not in a listbetween— numeric value in range [min, max]is_true— value is boolean trueis_false— value is boolean falsenot_exists— value is null or missing
All operators work with AND/OR condition groups.
Example #
{
"op": "and",
"conditions": [
{ "op": "gte", "left": { "from": "context.usage" }, "right": 5 },
{ "op": "in_list", "left": { "from": "context.plan" }, "right": ["free", "trial"] }
]
}
2.5.1 #
- README update — added Sign in with Apple section
2.5.0 #
Sign in with Apple #
- Added
KoolbaseAppleAuth.signIn()— Sign in with Apple for Flutter - Added
KoolbaseAuthClient.oauthLogin()— unified OAuth login method - Added
AuthApi.oauthLogin()— server-side Apple identity token verification - Apple identity token verified server-side using Apple's JWKS endpoint
- Supports email relay addresses from Apple private email relay
Usage #
import 'package:koolbase_flutter/koolbase_flutter.dart';
final session = await KoolbaseAppleAuth.signIn();
if (session != null) {
print('Signed in: \${session['user']['email']}');
}
Setup required #
Add sign_in_with_apple to your pubspec.yaml and configure your App ID in the Apple Developer portal.
2.4.0 #
Koolbase Cloud Messaging #
- Added
KoolbaseMessaging— push notification delivery via FCM - Added
Koolbase.messaging.registerToken(token, platform)— register FCM device token with Koolbase - Added
Koolbase.messaging.send(to, title, body, data)— send push notification to a specific device KoolbaseConfigextended withmessagingEnabledparameter (default: true)- Device ID automatically attached to token registration
Usage #
// After obtaining FCM token from firebase_messaging
final fcmToken = await FirebaseMessaging.instance.getToken();
await Koolbase.messaging.registerToken(
token: fcmToken!,
platform: 'android', // or 'ios'
);
// Send to a specific device
await Koolbase.messaging.send(
to: deviceToken,
title: 'Your order is ready',
body: 'Pick up at counter 3',
data: {'order_id': '123'},
);
Setup required #
Add your FCM server key as a project secret named FCM_SERVER_KEY in the Koolbase dashboard.
2.3.1 #
- Updated README — added Code Push, Analytics, Logic Engine sections, comparison table, clearer get started guide
2.3.0 #
Koolbase Analytics #
- Added
KoolbaseAnalyticsClient— event tracking with batched flush - Added
Koolbase.analytics— top-level static accessor - Added
Koolbase.analytics.track(eventName, properties)— custom event tracking - Added
Koolbase.analytics.screenView(screenName)— screen view tracking - Added
Koolbase.analytics.setUserProperty(key, value)— user property management - Added
Koolbase.analytics.identify(userId)— attach authenticated user to events - Added
Koolbase.analytics.reset()— clear user identity on logout - Added
KoolbaseNavigatorObserver— auto screen tracking via Flutter navigator - Auto events:
app_open,screen_view,session_end - Batch flush: every 30 seconds, on background, on close, or when 20 events queued
- Events retry on network failure — re-queued up to batch size limit
KoolbaseConfigextended withanalyticsEnabledparameter (default: true)
Usage #
// Auto screen tracking
MaterialApp(
navigatorObservers: [
KoolbaseNavigatorObserver(client: Koolbase.analytics),
],
)
// Manual tracking
Koolbase.analytics.track('purchase', properties: {
'value': 1200,
'currency': 'GHS',
});
// User identity
Koolbase.analytics.identify(user.id);
Koolbase.analytics.setUserProperty('plan', 'pro');
// Flush on app background
Koolbase.analytics.flush();
2.2.0 #
Logic Engine v1 — Event-Driven Flows #
- Added
FlowExecutor— safe, deterministic runtime for evaluating flow node trees - Added
FlowContext— resolves data from context, config, and flags with dot-notation support - Added
FlowResult— typed result with event name, args, and error state - Supported node types:
if,sequence,event(terminal),set - Supported operators:
eq,neq,gt,lt,and,or,exists - Supported data sources:
context(app-provided),config(bundle),flags(bundle) BundlePayloadextended withflowsfield —Map<String, dynamic>defaulting to{}KoolbaseDynamicScreennow auto-executes flows on rfw events — if a flow emits a new event, that event is passed toonEventinsteadKoolbase.executeFlow()— top-level static accessorKoolbaseCodePushClient.executeFlow()— direct client accessKoolbaseScreenClientabstract interface extended withexecuteFlow()
Usage #
// In your bundle's flows.json
{
"on_checkout_tap": {
"type": "if",
"condition": {
"op": "eq",
"left": { "from": "context.plan" },
"right": "free"
},
"then": { "type": "event", "name": "show_upgrade" },
"else": { "type": "event", "name": "go_checkout" }
}
}
// In your app — flows execute automatically from KoolbaseDynamicScreen events
// Or call directly:
final result = Koolbase.executeFlow(
flowId: 'on_checkout_tap',
context: { 'plan': user.plan },
);
if (result.hasEvent) {
Navigator.pushNamed(context, result.eventName!);
}
2.1.0 #
Layer 2 — Server-Driven UI via rfw #
- Added
KoolbaseDynamicScreen— drop-in widget that renders server-defined UI from the active bundle - Added
KoolbaseCodePushScope— InheritedWidget that wires the code push client into the widget tree - Added
KoolbaseRfwWidget— registration type for custom widgets in the rfw runtime - Added default widget library: Column, Row, Stack, Container, Padding, SizedBox, Expanded, Center, Text, ElevatedButton, TextButton, OutlinedButton, Card, Divider, CircularProgressIndicator, KoolbaseText, KoolbaseButton, KoolbaseSpacer, KoolbaseBadge
- Added
ScreenResolver— extracts and caches rfw binaries from the active bundle zip - Bundle payload now supports
screensfield — map of screenId to .rfw filename KoolbaseDynamicScreenguarantees: never crash, never block, never surprise — all failures fall back to the local widget- Fixed:
KoolbaseCodePushScope.of(context)moved todidChangeDependenciesto avoid initState context restrictions
Usage #
// Wrap your app with KoolbaseCodePushScope
KoolbaseCodePushScope(
client: Koolbase.codePush,
child: MyApp(),
)
// Drop KoolbaseDynamicScreen anywhere
KoolbaseDynamicScreen(
screenId: 'onboarding',
data: {'username': user.name},
onEvent: (name, args) {
if (name == 'get_started') Navigator.pushNamed(context, '/home');
},
fallback: const OnboardingScreen(),
)
2.0.0 #
Code Push — Runtime Bundle Delivery #
- Added
KoolbaseCodePushClient— full bundle lifecycle management (check, download, verify, cache, activate) - Added
BundleCache— four-slot cache system (pending, ready, active, archive) - Added
BundleVerifier— sha256 checksum verification on every download - Added
KoolbaseUpdater— background check and download on cold launch - Added
BundleLoader— promotes ready bundles to active, handles rollback - Added
RuntimeOverrideEngine— merges bundle config and flags with merge precedence: app defaults → Remote Config → Runtime Bundle Koolbase.configInt(),configString(),configDouble(),configBool()— now transparently return bundle values when a bundle is activeKoolbase.isEnabled()— now checks bundle flag overrides firstKoolbaseConfig— newcodePushChannelparameter (default:'stable')Koolbase.codePush— new static accessor for the code push client
Migration from 1.x #
Add codePushChannel to your KoolbaseConfig if you want to subscribe to a specific channel:
await Koolbase.initialize(KoolbaseConfig(
publicKey: 'pk_live_xxx',
baseUrl: 'https://api.koolbase.com',
codePushChannel: 'stable', // new — defaults to 'stable'
));
No other breaking changes.
1.9.0 #
- Functions: Added Dart runtime support
- New
FunctionRuntimeenum —FunctionRuntime.denoandFunctionRuntime.dart - New
deploy()method — deploy functions directly from Flutter - Fixed
invoke()request body format
- New
1.8.0 #
- Database: Offline-first support powered by Drift
- Cache-first reads — instant UI, background network refresh
- Optimistic writes — insert locally, sync when online
- Auto-sync on network reconnect via connectivity_plus
- Manual
Koolbase.db.syncPendingWrites() QueryResult.isFromCacheflag- Write queue with max 3 retries before dropping
- User-scoped cache — no cross-user data leakage
1.7.0 #
- Database: Added
.populate()support on query builder for relational data- Fetch related records from other collections in a single query
- Usage:
.populate(['author_id:users', 'category_id:categories']) - Populated records are injected into
datawith the_idsuffix removed (e.g.author_id→author)
1.6.0 #
- Full BaaS feature set — auth, database, storage, realtime, functions, feature flags, remote config, version enforcement, OTA updates
1.5.0 #
- OTA Updates: Added
Koolbase.ota— over-the-air bundle updates for Flutter apps
1.4.0 #
- Realtime: Added
Koolbase.realtime— WebSocket realtime SDKKoolbase.realtime.on(projectId, collection)— stream of all eventsKoolbase.realtime.onRecordCreated(projectId, collection)— stream of new recordsKoolbase.realtime.onRecordUpdated(projectId, collection)— stream of updated recordsKoolbase.realtime.onRecordDeleted(projectId, collection)— stream of deleted record IDsKoolbase.realtime.connectionState— stream of connection status (true/false)Koolbase.realtime.setToken(token)— set auth token for subscriptions- Auto-reconnect with 3 second backoff
- Reference-counted subscriptions — safe for multiple listeners
1.3.0 #
- Database: Added
Koolbase.db— database SDKKoolbase.db.collection('name').get()— query records with fluent builderKoolbase.db.collection('name').where('field', isEqualTo: value).limit(20).get()Koolbase.db.insert(collection: 'name', data: {...})— insert recordsKoolbase.db.doc(id).get()— fetch single recordKoolbase.db.doc(id).update({...})— patch record fieldsKoolbase.db.doc(id).delete()— soft delete recordKoolbaseRecord,KoolbaseCollection,QueryResultmodels- Collection-level permission enforcement (public, authenticated, owner)
1.2.0 #
- Storage: Added
Koolbase.storage— file storage SDKupload()— upload files directly to Cloudflare R2 via presigned URLsgetDownloadUrl()— get signed download URLs for private filesdelete()— delete files from storageKoolbaseObject,KoolbaseBucket,UploadResultmodels- Three-step upload flow: get URL → upload → confirm
1.1.0 #
- Auth: Added
Koolbase.auth— full authentication SDKsignUp,login,logout,forgotPassword,resetPassword,verifyEmailcurrentUser,isAuthenticated,authStateChangesstream- Automatic session restoration on app start
- Secure token storage via
flutter_secure_storage - JWT access tokens with automatic refresh
KoolbaseUser,AuthSessionmodelsKoolbaseAuthExceptionand typed exceptions
1.0.0 #
- Initial release
- Feature flags with rollout percentages and kill switches
- Remote config (string, int, double, bool, map)
- Version enforcement with force/soft update policies
- Offline support with local cache
- Background polling