Skip to main content

flutterflow_cloud 0.5.0 copy "flutterflow_cloud: ^0.5.0" to clipboard
flutterflow_cloud: ^0.5.0 copied to clipboard

unlisted

The FlutterFlow Cloud SDK for Flutter — typed database records and queries, end-user auth with self-contained persistent sessions, file storage with progress and URL caching, and a fast built-in proje [...]

flutterflow_cloud #

The FlutterFlow Cloud SDK for Flutter: sign-in and accounts, database records and queries, and file storage for apps built on FlutterFlow Cloud.

The runtime contract is 14 — Dart SDK.

Auth events and listener ownership #

app.auth.authStateChanges() delivers the initial user immediately. While signed in, the first listener owns one GET /v1/auth/events fetch stream; additional listeners share it. A newer matching revision refreshes the token and emits. Cancel the final subscription or call app.close() to release the request and reader. Sign-out, account switches and deletion do the same. Transient failures reconnect without requiring a new listener, backing off to 30 s (a Retry-After can lengthen the wait, never shorten it). A refusal the backend repeats, such as an expired App Preview ticket or a stopped thread, waits for a new session or a renewed base URL instead of asking again. The transport uses http 1.5 or later AbortableRequest; see 06 §7.1 and 14 §6.

Terminal auth.sessionRevoked notices bind to the required FFX-Auth-Session-Id response header. The SDK validates its strict session ID, keeps refresh tokens opaque, and clears only the original matching auth epoch. A stale stream cannot sign out a newer account; an ordinary transport close does not imply sign-out. Disable/update/revoke/delete reach active listeners.

final subscription = app.auth.authStateChanges().listen((user) {
  print(user?.id);
});
// When the owning UI is disposed:
await subscription.cancel();
app.close();

FFXPersistedSession and auth responses require a valid claimsRevision. Use matching server/SDK builds and reauthenticate dev sessions after a format change; there is no upgrade-on-read path. Exact refresh replay retains its original token/expiry only while the session remains live, then a still-newer claims notice causes a fresh successor rotation. Record/channel watch and general profile-change events remain deferred.

Mutation recovery #

Mutations keep the same idempotency key across network failures and ordinary retries. Stored error statuses are terminal, including malformed 5xx bodies. An unreadable success replays the same key to recover its receipt; the SDK never replaces its identity automatically. An in-flight 409 idempotencyKeyInUse permits two extra same-key attempts. A lost response may hide a committed mutation, so inspect its original outcome before starting a new logical call. 14 §11 and 18 §8 define the contract.

HTML/empty proxy errors and body resets use status-aware retries. The total FFXRetryPolicy.requestTimeoutMs defaults to 60 seconds and covers credentials, body consumption and waits; preview cold starts retain the documented five-minute default allowance. Error bodies are bounded to 64 KiB, JSON successes to 32 MiB. Retry-After accepts seconds or HTTP-dates. Timeout/cancellation releases body consumption without claiming a dispatched mutation did not commit.

The transport regression suite in test/idempotency_test.dart covers recovery, mixed failure budgets, caller-supplied initial keys, and auth refresh. Run bun run dart:analyze and bun run dart:test from the repository root with the pinned Flutter SDK installed.

Account deletion #

await app.auth.deleteAccount() deletes the signed-in full or guest identity across all environments in its project. The future completes after server cleanup confirms, clears local auth state and identity caches, attempts token-storage cleanup, and emits signed-out. Customer records and files retain their existing ownership. 06 §5.10 defines the exact removal, retained snapshots/backups, and existing JWT read window.

Concurrent calls share one operation. A retry after an ambiguous failure in the same client reuses its original key and bearer; the server continues cleanup independently, and receipt replay still requires an unexpired bearer. A refusal the server will repeat (a 4xx problem the SDK does not retry, such as the 403 permissionDenied for drip's test account) ends the deletion: the session carries on as before, and the next call is a new deletion with a new key. A deletion started during a refresh waits for it to land, so a refused one hands back the refreshed session. Late responses cannot overwrite a newer session. These lifecycle and storage races are covered by test/account_deletion_test.dart.

Password reset #

// Forgot-password screen:
await app.auth.sendPasswordResetEmail(email: email);
// The page the emailed link opens, with the link's oobCode parameter:
await app.auth.completePasswordReset(oobCode: oobCode, newPassword: password);
await app.auth.signInWithEmail(email: email, password: password);

Both are API-key-only mutations: they send no bearer, carry one Idempotency-Key across their retries, complete with void, and leave the session, currentUser and session storage untouched. sendPasswordResetEmail completes the same way whether or not an account uses the email, so the app never learns which emails are registered. completePasswordReset signs no one in; the server revokes every session of the reset account, which a local session of that account learns through its auth event or next refresh. A used or unknown code throws FFXValidationError with code oobCodeInvalid, an expired one oobCodeExpired, a short password weakPassword (the code stays usable). 06 §5.8 defines the flow and 14 §6 the surface; test/password_reset_test.dart covers the request shapes, keys and errors. Against the emulator, the reset email lands in its outbox instead of a mailbox (emulator README).

Connectors #

await app.connectors.call('slack', 'postMessage', params: {...}) calls a third-party service through the platform, which holds the credential. The maker connects the service in Project Settings › Connections and declares the operations the app uses under connectors in .ffx/project.json; app code names only the connector and the operation (lowerCamelCase identifiers, as declared) and receives the operation's result object. On the wire this is POST /v1/connectors/{connector}/{operation} with {"params": {...}}, a mutation with the usual idempotency key and retry budget. Problems map as everywhere else:

Code Class userMessage says
connectorParamsInvalid (400) FFXValidationError the request's details aren't valid, so it wasn't sent
connectorUnknown, connectorNotEnabled, connectorOperationUnknown, connectionNotLinked (404) FFXNotFoundError the feature isn't set up in this app yet
connectionNeedsReconnect (409) FFXConflictError the feature isn't available right now
connectorUpstreamFailed (502) FFXServiceUnavailableError; the provider's text is in problem['upstream'] the service couldn't complete the request; try again
connectorOutcomeUnknown (504, retry: never) FFXTimeoutError it may have gone through; check before trying again

A connector problem's detail and fix are written for the maker ("Link one in Project Settings › Connections."), so userMessage has copy of its own for each code. None of these is retried by the SDK. connectorOutcomeUnknown means an operation that acts sent its request and either no answer came back or the provider failed partway: it may have happened, and a new call would do it again, so the app checks the provider before calling again. Against the emulator the call is forwarded to FFX_CONNECTORS_URL, where the preview's door holds it to the served config's declaration and rule with the data plane's own check — see the emulator README. test/connectors_test.dart covers the surface.

Purchases #

cloud.purchases is the app's own App Store and Google Play sales (payments-ux §11). The app buys through the store itself (in_app_purchase); the SDK only reads and checks. It is an extension in a library of its own, which the main library doesn't export, so it never shadows the purchases getter of an app that sells nothing and has a purchase model. When the config sells (it has a payments section), the generator names that model's getter purchaseCollection instead, so cloud.purchases is always the store's in a file that imports both:

import 'package:flutterflow_cloud/purchases.dart';

final state = await cloud.purchases.read();
// Pass state.purchaseId to the store as applicationUserName, buy one of
// state.products by its appStoreId or googlePlayId, then:
final after = await cloud.purchases.checkAppStore(signedTransaction);
if (after.upgraded) …;            // or after.owns('firstAidGuide')
final credits = after.credits;    // a pack's credits are in credits.balance

Finish the store's transaction only after the check answers. Check a purchase the store gave back on Restore purchases with restored: true: an upgrade or unlock another account of the app holds then moves to the signed-in one. A check that changes the upgrade refreshes the session, so its claim reaches the next request's rules. current is the signed-in user's last answer and changes streams them. credits (FFXCredits) is the balance the backend keeps: bought plus this month's free, which are spent first and refill on the 1st; the app shows it and never counts it itself.

cloud.usesEmulator is true when the instance talks to an emulator, App Preview's backend or a local one (FlutterFlowCloud.insidePreview is true in App Preview alone), where the kit offers its pretend store: checkPretend(item: 'plus', plan: 'yearly'), or checkPretend(item: 'vetQuestions', amount: 10) for a pack, records its purchase as a test purchase (FFXStore.pretend). Only the preview's backend, the emulator, records one; the platform refuses it. The emulator refuses App Store and Google Play checks.

recordEvents sends what the payments kit saw, for Payments' Reports: the paywall or the shop shown and what opened it, a plan or pack picked, a purchase started or cancelled (FFXPurchaseEvent, no content, up to 50 at a time). Each event's id makes a repeat record nothing. Only the production environment's are kept; the emulator keeps none.

Code Class userMessage says
purchaseNotVerified (422) FFXValidationError the store couldn't confirm this purchase
purchaseNotSold (422) FFXValidationError this purchase isn't available in this app
paymentsNotConfigured (409) FFXConflictError purchases aren't set up in this app yet
purchaseHeldByAnother (409) FFXConflictError it belongs to another account; sign in with that account, or, for a subscription or unlock, restore purchases to move it here
insufficientCredits (409) FFXInsufficientCreditsError you don't have enough credits for this

The backend spends credits, never the app: a write whose rule calls spend("vetQuestions", 1) takes them with the write, and a short balance fails it with FFXInsufficientCreditsError (credits, needed, balance; nothing was written or spent). Offer a pack, then send the same write again.

test/purchases_test.dart covers the surface.

Emulator URLs #

FlutterFlowCloud.initialize(emulatorUrl: 'https://host/path/backend') selects the emulator through an absolute HTTP(S) URL, including an optional path prefix. Standalone Flutter builds can supply the same value with --dart-define=FFX_EMULATOR_URL=https://host/path/backend. A nonempty URL selects emulator mode even in release builds. Without it, useEmulator, emulatorHost, and emulatorPort retain their local-development behavior.

The preview runtime's configurePreviewBackend(url, scope) bridge takes precedence over those settings and can rotate the URL on existing clients. Browser wasm builds read globalThis.__ffxPreviewBackend = {url, scope}. Sessions and caches are scoped to the emulator endpoint or stable preview thread identity, separately from deployed environments. Preview tickets are temporary capabilities: use the preview launcher's renewal flow rather than committing them to app code. See emulator spec §10.

The host renews a ticket in the base URL before it runs out, and the SDK reads the base URL for every request. When the preview proxy still answers 401 previewTicketExpired (a ticket that ran out first, say in a background tab), the SDK waits up to two seconds for the renewed base URL and sends the request once more. A second refusal surfaces as FFXServiceUnavailableError (code previewTicketExpired, status 401): a transient error that never clears the session, from a request, a refresh, the background reconcile or the auth events stream alike.

Sessions shared by several tabs #

Every tab of a web app holds the session stored under one scope (localStorage ffx.<scope>.session) and keeps its tokens in memory. Each refresh rotates the refresh token, and presenting a spent one revokes the session for every holder (06 AUTH-R2), so tabs refresh one at a time: under the Web Lock ffx.<scope>.refresh (navigator.locks) where the browser has it, otherwise (an insecure context, a frame the API refuses, every other platform) under a lock local to the isolate with the same name. Under the lock a refresh reads the stored session first. When another tab has rotated it since (another refresh token for the same end user, with a later access-token expiry), the refresh takes the stored tokens and goes to the network only when they are due, with the stored refresh token. A tab that saves its session for another reason (a fetched profile) keeps the tokens another tab stored since rather than writing back the spent ones. test/shared_session_test.dart covers both this and the preview ticket.

Screen previews #

A screen preview never runs main(), so nothing initializes the SDK there. Inside App Preview (the host called configurePreviewBackend, or the page carries __ffxPreviewBackend), the first read of FlutterFlowCloud.instance before any initialize connects an instance itself: emulator mode with pk_emulator_local, the key drip apps pass in previews, on the injected backend or else the emulator defaults. It restores the session the whole-app preview persisted (the same options keep the same session namespace), or else signs in as a guest, and every request that carries a session waits for that; a refused guest sign-in leaves the screen signed out. The whole-app preview and the other screens share that session, so this instance borrows it: it never presents the refresh token (a second instance rotating it would trip the reuse tripwire and revoke the session for all of them), takes the session another instance stored since when its access token runs out or else signs in as a guest, stores only sessions it signed in itself, and clears the stored session only while it is still the one it holds. A later initialize (the app's own bootstrap) replaces that instance as the default instead of failing as already initialized. Outside App Preview, reading instance before initialize throws. test/preview_instance_test.dart covers it.

A retired published pin raises FFXConfigVersionRetiredError once and keeps the original pin. Its problem retains requestedConfigVersion and minimumVersion; the SDK neither retries a terminal 410 nor silently upgrades the requested config.

test/http_resilience_test.dart verifies real loopback HTTP malformed errors, response resets, deadline/cancellation and same-key replay.

The opt-in deployed acceptance gate is flutter test test/deployed_resilience_test.dart from this package directory with FFX_AV108_DEPLOYED=1 and FFX_AV108_DATA_URL, FFX_AV108_API_KEY, FFX_AV108_DATA_PATH (a readable /v1/records/... fixture), and optional FFX_AV108_DATA_TOKEN. Use the deployed HTTPS endpoint and a dedicated test tenant. The gate performs only reads: baseline, a real upstream response whose local body is deliberately reset, and successful recovery after injected HTML 502, empty 503 and 429. It never prints credentials or creates/deletes customer resources. CI skips this gate without explicit credentials; loopback regression tests run normally.

For the mutation acceptance gate, additionally set FFX_AV108_MUTATIONS=1, FFX_AV108_MODEL, and FFX_AV108_FIELDS_JSON to fields valid for that dedicated test model, containing the literal __AV108_MARKER__ (for example a title). The test creates a uniquely marked record, drops a real successful PATCH body, then verifies replay returns the same record and exactly one version increment. Readback verifies the changed fields and cleanup deletes only the created ID. This gate exercises successfully stored receipts. It does not establish crash/receipt-persistence atomicity, which remains AV-002's separate gate; local fixtures alone do not establish remote exactly-once behavior.

Live data #

Watch one record when the screen shows one known record — a game, a profile, a document. It needs the model's get and subscribe rules, and only that record's writes reach it. Generated bindings give every collection a typed watch(id):

StreamBuilder<GameState?>(
  stream: cloud.gameStates.watch(gameStateId),
  builder: (context, snapshot) {
    if (snapshot.hasError) return ErrorView(snapshot.error!);
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const CircularProgressIndicator();
    }
    final state = snapshot.data; // null while missing or hidden
    return state == null ? const GameGone() : Board(state: state);
  },
);

It emits the record, then each change, and null while the record is missing or outside either rule; a deletion is null, not an error. Untyped, the same watch is app.database.collection('gameState').watch(id) (Stream<FFXRecord?>).

Watch a subscribable named list query when the screen shows a list:

final listener = app.database.query('tasksInList',
  parameters: {'listId': 'list_1'}, pageSize: 25,
).watch().listen((snapshot) => render(snapshot.records));
// Later: await listener.cancel(); app.close();

Query watches maintain the authorized first page, including sorting, refill after deletions and scope changes. Identical query name, canonical JSON parameters and page size, or model and record ID, share one subscription and read within the app. Ordered, server-proven updates can replace an already-visible row without a database read; changed membership/order, missing proofs, gaps and pokes trigger a coalesced strong query. Reconnect subscribes and bootstraps current state. Canceling one observer leaves other observers running; the last observer releases the subscription. .withConverter(...) also supports .watch(). Page-token and aggregate watches are rejected. Snapshot changes are relative to the listener's previous delivery; slow consumers keep one latest pending snapshot. Auth changes clear old state before fresh admission.

One lazy socket is shared by the app. app.realtime.connectionState and watchConnectionState() expose its lifecycle. The socket uses the existing platform base URL, including emulator/preview URLs; realtimeUrl is optional. Cancel the listener or close the app to release resources. See 08 for limits, quota and recovery details.

Platform builds include the committed runtime/ dependency lock alongside the pinned SDK. Its standalone closure supplies SDK imports missing from older app lockfiles; native and preview private package configs preserve existing customer package resolutions. Resolve it with dart pub get --enforce-lockfile in that directory when using a local APP_PREVIEW_CLOUD_SDK override.

Atomic numeric updates #

FFXFieldValue.increment(delta) is exported by both SDK barrels for numeric values in collection update and batch-update patches: {'count': FFXFieldValue.increment(1)}. Generated collections expose increment(id, count: 1) with typed optional deltas for writable top-level numeric fields. The ordinary retry identity, permissions and field bounds apply; see the contract.

App claim delegation #

await app.auth.setCustomClaim(endUserId: id, claim: "role", value: "seller") sets one explicitly delegated claim; value: null revokes it. Other claims remain unchanged. The result is FFXAppClaimResult with id, claim, value, revision; optional expectedRevision rejects stale changes and idempotencyKey preserves a deliberate operation across calls. The current published app policy decides access; the key is publishable and no admin credential belongs in an app. Success confirms source assignment; auth listeners refresh after outbox delivery. See auth delegation.

1
likes
0
points
11.2k
downloads

Publisher

verified publisherflutterflow.io

Weekly Downloads

The FlutterFlow Cloud SDK for Flutter — typed database records and queries, end-user auth with self-contained persistent sessions, file storage with progress and URL caching, and a fast built-in project code generator.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, http, web_socket

More

Packages that depend on flutterflow_cloud