flutterflow_cloud 0.4.0-dev.1
flutterflow_cloud: ^0.4.0-dev.1 copied to clipboard
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. 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. Late responses cannot overwrite a newer session. These lifecycle and storage races are covered by test/account_deletion_test.dart.
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 an app's own purchases
getter (a purchase model's); an app with both writes
FFXCloudPurchases(cloud).purchases:
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. 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.
FlutterFlowCloud.insidePreview is true in drip's App Preview, 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 the account that bought it |
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.
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.