form_concierge
Form Concierge is a self-hosted survey platform for Flutter, backed by Cloudflare Workers and D1. Embed surveys in your app, collect anonymous responses, and optionally run AI-powered adaptive follow-up interviews. You can also send replies directly to respondents.
The package exports both the Flutter widget and the Dart client API.
Features
- Native Flutter survey UI
- Web survey forms powered by Jaspr
- Anonymous responses without email or login
- Single-choice, multiple-choice, text, and image-upload questions
- Multilingual survey content
- Administrator replies for anonymous respondents
- Optional AI-powered adaptive follow-up interviews
- Self-hosted Cloudflare backend and admin dashboard
Quick Start
Deploy the Backend
Install the setup CLI and authenticate with Cloudflare:
dart pub global activate form_concierge_cli
npx wrangler login
Check the local environment, then deploy the Worker, D1 database, R2 storage, and admin dashboard:
form_concierge doctor
form_concierge setup cloudflare
During setup, the CLI asks for a Cloudflare API token used by the deployed Worker to manage Secrets Store values. Create a custom token from Cloudflare API Tokens with this permission:
Account→Secrets Store→Edit
Paste the API token when prompted. The CLI stores it as the Worker secret CF_API_TOKEN; do not paste a Secrets Store ID.
To upgrade an existing deployment, run:
form_concierge update cloudflare
Create a Survey
Create a project and survey in the deployed admin dashboard, then pass their slugs to the widget:
FormConciergeSurvey(
projectSlug: 'demo-project',
surveySlug: 'customer-feedback',
// ...
)
Provider credentials and other deployment settings are also managed from the admin dashboard.
Add the Flutter Package
flutter pub add form_concierge
Create a client for the deployed Worker:
import 'package:form_concierge/form_concierge.dart';
final client = Client('https://your-worker.example.com');
Code that only needs the Dart client can use:
import 'package:form_concierge/client.dart';
Embed the Survey
FormConciergeSurvey(
client: client,
projectSlug: 'demo-project',
surveySlug: 'customer-feedback',
anonymousToken: savedAnonymousToken,
locale: 'ja_JP',
onAnonymousSession: (session) async {
// Write the token persistence process here.
await secureStorage.write(key, session.token);
},
onDone: () {
Navigator.pop(context);
},
)
Surveys with adaptive follow-up enabled generate and render additional questions after the initial response.
Widget Configuration
Anonymous Sessions
The widget creates an anonymous account before the first submission. Persist the token and pass it back on later app launches:
FormConciergeSurvey(
anonymousToken: savedAnonymousToken,
onAnonymousSession: (session) async {
// Write the token persistence process here.
await secureStorage.write(key, session.token);
},
// ...
)
Reusing the token lets a respondent receive administrator replies without a conventional account.
Submission Callbacks
| Callback | Called when | Typical use |
|---|---|---|
onAnonymousSession |
A session is created or restored | Persist the anonymous token |
onResponseSubmitted |
The main response is saved | Store the response and submitted answers |
onFollowUpSubmitted |
Follow-up answers are saved | Update the stored receipt |
onDone |
The respondent taps Done | Close the survey route |
onSubmitError |
Submission fails | Log or display error details |
Do not close the route from onResponseSubmitted when adaptive follow-up is enabled. Additional questions may appear after it runs.
For security, anonymous respondents cannot retrieve their submitted answers through the API. To show a response history, persist the response and answers received by onResponseSubmitted in the host application.
Administrator Replies
Use the stored anonymous token to retrieve replies for a response:
client.anonymous.useToken(savedAnonymousToken);
final replies = await client.anonymous.getReplies(
responseId: responseId,
);
Store a local submission receipt if the app needs respondent-facing submission history.
To check for unread replies without downloading the full list, use FormConciergeReplyChecker. The host app provides storage for the last-seen timestamp:
final checker = FormConciergeReplyChecker(
client: client,
anonymousToken: savedAnonymousToken,
responseId: responseId,
store: FormConciergeReplySeenStore(
read: prefs.getString,
write: (key, value) async => prefs.setString(key, value),
remove: (key) async => prefs.remove(key),
),
);
final result = await checker.check();
if (result.hasNewReplies) {
// Show an unread indicator.
}
await checker.markLatestSeen();
Device Information and Metadata
Use deviceInfo for structured device and app information, and metadata for application-specific context:
FormConciergeSurvey(
deviceInfo: DeviceInfo(
deviceId: savedLocalDeviceId,
label: 'Pixel 9',
platform: 'flutter',
os: 'android',
osVersion: '16',
appVersion: '1.4.2',
),
metadata: {
'uid': currentUser.uid,
'tenant': currentTenant.id,
'plan': currentUser.plan,
},
// ...
)
The widget also attaches basic environment information when available, including screen details, locale, time zone, and Flutter platform. Avoid sensitive personal data unless required and disclosed to the respondent.
Image Processing
Image questions open the image picker and upload the selected image. Use processImage to resize, compress, redact, remove metadata, or convert it first:
processImage: (image) async {
// Write the image conversion process here.
return image;
},
Localization
Pass locale to control survey content and widget messages. Locale tags such as ja, ja_JP, and ja-JP are normalized automatically.
Supported locales:
| Language | Locale |
|---|---|
| English | en |
| Japanese | ja |
| Simplified Chinese | zh-Hans |
| Traditional Chinese | zh-Hant |
| Korean | ko |
| German | de |
| Spanish | es |
| French | fr |
| Italian | it |
| Thai | th |
| Turkish | tr |
Allow respondents to change language inside the widget with:
showLocalePicker: true,
Question Types
- Single choice
- Multiple choice
- Single-line text
- Multi-line text
- Image upload
Question definitions and localized content are managed in the admin dashboard.
Adaptive Follow-Up Interviews
Adaptive follow-up interviews are optional and configured per survey in the admin dashboard.
When enabled, the respondent submits the main survey, the backend generates relevant follow-up questions with the configured AI provider, and the widget renders and submits them. A custom prompt controls the interview's purpose, tone, and focus.
When email notifications are enabled, the main response and follow-up response each send a notification, resulting in two emails. This is because some respondents may close the survey without providing a follow-up response.
Configure the AI provider and its credentials in the admin dashboard before enabling this feature.
CAPTCHA with Turnstile
Turnstile runs JavaScript in a browser environment, so it cannot be implemented using only the Flutter UI. Supporting it directly would require an additional browser integration, such as a WebView package or native platform code. To avoid imposing that dependency and implementation choice on every application, this package does not include CAPTCHA UI.
When the API reports captchaRequired: true, the widget requests a verification token from the host application through captchaTokenProvider. The older captchaEnabled model property is deprecated for submission decisions because it represents the saved survey setting, not whether Turnstile is currently configured.
FormConciergeSurvey(
client: client,
projectSlug: 'my-project',
surveySlug: 'contact',
captchaTokenProvider: () => resolveCaptchaToken(context),
);
Future<String?> resolveCaptchaToken(BuildContext context) async {
final config = await client.config.getPublicConfig();
final siteKey = config.turnstileSiteKey;
if (siteKey == null || siteKey.isEmpty || !context.mounted) return null;
final completer = Completer<String?>();
await showModalBottomSheet<void>(
context: context,
builder: (sheetContext) => CloudflareTurnstile(
siteKey: siteKey,
baseUrl: 'https://your-form-domain.example.com',
onTokenReceived: (token) {
completer.complete(token);
Navigator.of(sheetContext).pop();
},
),
);
if (!completer.isCompleted) completer.complete(null);
return completer.future;
}
This example uses cloudflare_turnstile and dart:async. The provider runs only when CAPTCHA is required; return null to cancel submission. Turnstile tokens are single-use, and baseUrl must use a hostname allowed by the Turnstile widget configuration.
See examples/flutter_mobile_full for complete error and timeout handling.
Privacy and Data Ownership
- Respondents need no email address or password.
- Survey answers remain server-side and are visible to administrators.
- Anonymous respondents can retrieve administrator replies, but not their answer history.
- The host app may store a local receipt when respondent-facing history is needed.
- The application owner controls the Cloudflare account, Worker, database, storage, and deployment configuration.
Application owners remain responsible for privacy notices, consent, retention rules, and uploaded-image handling.
Libraries
- client
- form_concierge
- Flutter survey widgets and client APIs for Form Concierge.