# FullStory

- Tool: FlutterFlow
- Difficulty: Intermediate
- Time required: 45 minutes
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to FullStory using two separate paths: a Custom Action wrapping the `fullstory_flutter` pub.dev SDK to capture sessions inside your app (using a public org ID, safe on the client), and a FlutterFlow API Call through a Firebase or Supabase proxy to read session data and replay links back via FullStory's REST API. The account-level REST API key must never ship in the app bundle.

## Add Session Replay to Your Flutter App — Capture and Read Back with FullStory

FullStory does something no simple event tracker can: it records an exact replay of what every user saw and did in your app. When a user hits a bug or drops off mid-flow, you can watch the session rather than guess. For FlutterFlow apps this means two things: instrumenting the app so FullStory records sessions (capture), and pulling those recordings into an in-app panel so your support team can look up a user's recent sessions without leaving the app (read back).

Capture is the primary use case and the simpler one. FullStory publishes an official `fullstory_flutter` package on pub.dev. You initialize it once on app start with your FullStory org ID — a public identifier that scopes session data to your organization and is safe to include in the compiled Flutter build. Once initialized, FullStory automatically captures interactions. You identify users by calling the SDK's identify equivalent with your own user ID, which becomes the join key between FullStory sessions and your own database records.

Read-back requires more setup because FullStory's REST API (`https://api.fullstory.com`) uses an account-level API key that has broad access to all session and user data. Placing that key inside a FlutterFlow API Call header would embed it in the compiled app binary — anyone who downloads the app can extract it. The safe path is a thin Firebase Cloud Function or Supabase Edge Function that holds the key server-side and forwards authenticated requests to FullStory. Your FlutterFlow API Call group targets the proxy, not FullStory directly. FullStory's Business plan or higher is required for REST API access.

Session replay links (`fsUrl`) returned by the API open in the FullStory player and require a FullStory viewer license — you surface the link in your app, you don't embed the player. This is the right architecture: the FlutterFlow app becomes a quick-lookup tool your CS team uses to find the right session, then watches it in FullStory's own UI.

## Before you start

- A FullStory account — capture SDK works on any plan; REST API access requires Business plan or higher
- Your FullStory org ID (visible in FullStory app settings or your account URL)
- A FullStory API key from Admin → Account Settings → Integrations → API (for the read-back proxy only)
- A FlutterFlow project with Custom Code enabled
- A Firebase project with Cloud Functions or a Supabase project with Edge Functions (for the read-back proxy)

## Step-by-step guide

### 1. Add the fullstory_flutter Custom Action and initialize on app start

In FlutterFlow, open the left nav and click Custom Code, then click + Add and select Action. Name the action InitFullStory. In the Dependencies field, add the pub.dev package: `fullstory_flutter` (search pub.dev for the current stable version and paste the full package name; FlutterFlow will resolve the version from pub.dev). This tells FlutterFlow to include the package in the generated Flutter project without you running any terminal commands.

In the Dart code editor, write the initialization call. Import the package at the top with `import 'package:fullstory_flutter/fullstory_flutter.dart';`. In the function body, call `FS.init(OrgIdOptions(orgId: 'YOUR_ORG_ID'))`. Replace YOUR_ORG_ID with the alphanumeric org ID from your FullStory account settings — this is a public identifier, not a secret, so it is safe to include directly in the Dart code.

Add a `kIsWeb` guard: `if (kIsWeb) return;` at the top of the function body, because the `fullstory_flutter` package is designed for native iOS and Android. Without this guard the app will fail to compile for web targets. Import `foundation.dart` for the `kIsWeb` constant: `import 'package:flutter/foundation.dart';`.

Once the Custom Action is saved, open your app's main entry point — the widget that FlutterFlow designates as the initial page or your app's root. Open its Actions panel, click + Add Action, navigate to Custom Actions, and select InitFullStory. Set the action to run On Page Load so FullStory initializes as soon as the app starts. Click Save.

Note: Custom Actions do not run in FlutterFlow's web-based Test Mode or the preview canvas. You will see no FullStory recording activity until you test in Run Mode on a physical device or a real iOS/Android build. This is expected and is a known FlutterFlow limitation for any pub.dev SDK-based Custom Action.

```
import 'package:flutter/foundation.dart';
import 'package:fullstory_flutter/fullstory_flutter.dart';

Future initFullStory() async {
  // FullStory Flutter SDK does not support web
  if (kIsWeb) return;

  // orgId is a public capture identifier — safe to embed
  await FS.init(OrgIdOptions(
    orgId: 'YOUR_ORG_ID',
    // Optional: set logLevel to debug during development
    // logLevel: FSLogLevel.debug,
  ));
}
```

**Expected result:** The Custom Action appears under Custom Code in the left nav. When you build and run the app on a device, a new session appears in your FullStory dashboard within a few minutes of app launch.

### 2. Identify users so sessions link to your own user records

After the user signs in to your FlutterFlow app, you need to call FullStory's identify function to tag the session with your own user ID. This is the join key: when you later query FullStory's REST API for a user's sessions using `/v1/users/{uid}`, the uid you search with must exactly match the uid you passed to FS.identify in the app. Without this step, sessions are anonymous and you cannot look them up per user.

Create a second Custom Action named IdentifyFullStoryUser. Add the same `fullstory_flutter` package dependency. The function should accept two String arguments: userId (your internal user ID — a stable, opaque identifier, never an email alone) and userEmail (optional, for display in FullStory).

In the function body, add the `kIsWeb` guard first, then call `FS.identify(userId, {'email': userEmail, 'displayName': userDisplayName})`. FullStory stores whatever custom properties you pass as user variables, so include attributes that will be useful for filtering sessions in the FullStory dashboard: subscription plan, user role, account creation date.

Wire this Custom Action to fire after a successful sign-in. In FlutterFlow, open your sign-in page or the page users land on after authentication. In the Actions panel for the navigation event or the success outcome of your auth action, add IdentifyFullStoryUser. Pass the signed-in user's ID from your Supabase or Firestore user record as the userId argument. This ensures every session from a logged-in user is tagged with your internal ID.

Store the userId in an App State variable as well, because you will need it in Step 5 when you query the proxy for this user's sessions. Make the App State variable a String type and set it during the same sign-in action flow.

```
import 'package:flutter/foundation.dart';
import 'package:fullstory_flutter/fullstory_flutter.dart';

// Arguments: userId (String), userEmail (String)
Future identifyFullStoryUser(
  String userId,
  String userEmail,
) async {
  if (kIsWeb) return;

  // Identify the user so sessions are searchable by your internal ID
  await FS.identify(userId, {
    'email': userEmail,
    // Add any attributes useful for FullStory filtering
    // 'plan': userPlan,
    // 'createdAt': userCreatedAt,
  });
}
```

**Expected result:** After signing in, the user's FullStory sessions in the FullStory dashboard show their display name and email instead of 'Anonymous'. The Sessions panel in FullStory shows sessions associated with the user ID you set.

### 3. Deploy a proxy Cloud Function to hold the FullStory REST API key

FullStory's REST API at `https://api.fullstory.com` requires an account-level API key sent as a Bearer token. This key has read access to all sessions and user data in your FullStory organization. If you place it in a FlutterFlow API Call header, it is compiled into the app binary and any user who downloads your app can extract it using standard tools.

The solution is a thin proxy function. If you are using Firebase, open the Firebase Console, enable Cloud Functions, and write a Node.js function (Firebase Functions v2 is recommended). The function receives a request from your FlutterFlow app, adds the FullStory Bearer token from environment variables, forwards the request to `https://api.fullstory.com`, and returns the response. This way the token never touches the client.

Deploy the function to Firebase using the Firebase Console's inline editor or the Firebase CLI from your development machine. Once deployed, note the function's HTTPS URL — this is the base URL you will use in your FlutterFlow API Group.

If you are using Supabase instead, the same pattern applies: create a Supabase Edge Function (Deno runtime) that reads the API key from `Deno.env.get('FULLSTORY_API_KEY')`, forwards the request to FullStory, and returns the response. Store the API key in Supabase Dashboard → Settings → Edge Functions → Secrets.

This proxy step is also where RapidDev can help if you'd rather have experts handle the backend plumbing — the team builds FlutterFlow integrations like this every week and offers a free scoping call at rapidevelopers.com/contact.

```
// Firebase Cloud Function (Node.js) — FullStory API proxy
const { onRequest } = require('firebase-functions/v2/https');
const { defineSecret } = require('firebase-functions/params');
const axios = require('axios');

const FULLSTORY_API_KEY = defineSecret('FULLSTORY_API_KEY');

exports.fullstoryProxy = onRequest(
  { secrets: [FULLSTORY_API_KEY] },
  async (req, res) => {
    // CORS for web builds
    res.set('Access-Control-Allow-Origin', '*');
    if (req.method === 'OPTIONS') {
      res.set('Access-Control-Allow-Headers', 'Content-Type');
      return res.status(204).send('');
    }

    const apiKey = FULLSTORY_API_KEY.value();
    const path = req.query.path || '/v2/users';

    try {
      const response = await axios({
        method: req.method,
        url: `https://api.fullstory.com${path}`,
        headers: {
          Authorization: `Basic ${Buffer.from(apiKey + ':').toString('base64')}`,
          'Content-Type': 'application/json',
        },
        data: req.body,
        params: req.query.params ? JSON.parse(req.query.params) : {},
      });
      return res.json(response.data);
    } catch (err) {
      return res.status(err.response?.status || 500).json({
        error: err.message,
      });
    }
  }
);
```

**Expected result:** The Cloud Function deploys successfully and appears in the Firebase Console → Functions list with a green status. Visiting the function URL in a browser returns a FullStory API response (or a 400 if no path is passed — which confirms the function itself is running and the API key is working).

### 4. Create a FlutterFlow API Group targeting the proxy

Now that the proxy is deployed, wire it up in FlutterFlow. In the left nav, click API Calls, then click + Add and select Create API Group. Name the group FullStoryProxy. In the Base URL field, paste your deployed Firebase Cloud Function URL (or Supabase Edge Function URL) from the previous step. Do not add any Authorization headers here — authentication is handled by the proxy function, not by FlutterFlow.

With the API Group created, add an API Call inside it: click + Add Call on the group. Name it GetUserSessions. Set the method to GET. In the endpoint path field, leave it empty or add a path variable if your proxy forwards the FullStory path as a query parameter — for example `/` with a query variable named `path` set to `/v2/users/{{userId}}/sessions`. Add a Variable named userId in the Variables tab and set its type to String.

Click the Response & Test tab. Paste a sample FullStory sessions API response (you can get this by calling FullStory's API directly from a REST client like Postman using your API key). Click Generate JSON Paths to extract the fields you want to bind in FlutterFlow: session ID, creation time, duration, and `fsUrl` (the replay link). FlutterFlow will generate JSON Path expressions like `$.sessions[*].fsUrl` for each field.

Create a custom Data Type in FlutterFlow named FullStorySession with fields: sessionId (String), createdTime (String), durationMs (Integer), fsUrl (String). Map the JSON Paths to this Data Type so FlutterFlow knows how to deserialize the response into typed objects you can bind to list widgets.

Save the API Group. Test it by entering a real userId in the Test tab and verifying sessions are returned.

```
{
  "group_name": "FullStoryProxy",
  "base_url": "https://YOUR_REGION-YOUR_PROJECT.cloudfunctions.net/fullstoryProxy",
  "calls": [
    {
      "name": "GetUserSessions",
      "method": "GET",
      "endpoint": "/",
      "query_params": {
        "path": "/v2/users/{{userId}}/sessions"
      },
      "variables": [
        { "name": "userId", "type": "String" }
      ],
      "json_paths": {
        "sessionId": "$.sessions[*].id",
        "createdTime": "$.sessions[*].createdTime",
        "durationMs": "$.sessions[*].durationMs",
        "fsUrl": "$.sessions[*].fsUrl"
      }
    }
  ]
}
```

**Expected result:** The API Group appears in the left nav under API Calls. The Test tab returns a JSON response containing session records for the user ID you entered. JSON Paths are generated and mappable to the FullStorySession Data Type.

### 5. Build the support panel: session list with replay links

Create a new screen in FlutterFlow named SupportPanel (or add it to an existing user-profile or account screen). This screen will show the signed-in user's recent FullStory sessions and let them open replay links, or let a CS agent look up any user by ID.

Add a ListView widget to the screen. Set its data source to an API Call backend query — select the FullStoryProxy → GetUserSessions API Call. Pass the userId from your App State variable as the query argument. FlutterFlow will execute the call when the page loads and populate the list with the returned FullStorySession items.

Inside the ListView item template, add: a Text widget bound to `createdTime` (format it as a readable date), another Text widget bound to `durationMs` (you can use a Dart string expression to convert milliseconds to minutes), and a Button labeled 'Watch Replay' whose On Tap action is Launch URL → bound to `fsUrl` from the list item. This opens the FullStory session player in the device browser.

Add a gate to prevent the panel from showing if the user is not signed in or if the API call is still loading. Use a Conditional Widget wrapping the ListView: show a CircularProgressIndicator while the API call is in the Loading state, and show an error Text when the API call returns an error state.

Role-gate the entire screen: only accounts with a support or admin role should be able to navigate to SupportPanel. Configure this in FlutterFlow's navigation action by checking the user's role from your Firestore or Supabase user document before navigating. Add a note in the UI that session replay links require a FullStory viewer license to open — users without a FullStory account will see a login page when they tap the link.

**Expected result:** The SupportPanel screen loads and displays the signed-in user's recent sessions. Each list item shows the session date, approximate duration, and a button. Tapping 'Watch Replay' opens the session in the device browser, prompting the user to log in to FullStory if they are not already authenticated.

### 6. Add consent gating and test on a real device

FullStory records user behavior which is personally identifiable information — session recordings capture what users tap, type, and see. You need a consent flow before initializing FullStory, both for GDPR (EU users) and for good product practice. In FlutterFlow, the simplest approach is to store a consent flag in your Supabase or Firestore user document and only call the InitFullStory Custom Action if consent is granted.

Open your app's consent or privacy screen. Add a toggle or checkbox labeled 'Allow session recording to help us improve the app.' When the user taps Accept, set a `sessionRecordingConsent` boolean field to true in their user document, then fire the InitFullStory Custom Action. When the user declines or revokes consent, call FS.shutdown() (add a ShutdownFullStory Custom Action) and update the field to false.

Note that iOS 14.5+ App Tracking Transparency (ATT) applies when you track users for cross-app attribution. For first-party product analytics within your own app, ATT is generally not required — FullStory's session recording is considered first-party analytics. However, consult your legal team for your specific jurisdiction and use case.

For testing: Custom Actions do not execute in FlutterFlow's browser-based Test Mode. To verify FullStory is capturing sessions, use Run Mode which builds and runs the app on a real device or emulator. Check the FullStory dashboard → Live Sessions to confirm sessions appear. If you see no sessions after running on a device, verify that the org ID in the Custom Action matches your FullStory account and that the function is not returning an error in the device logs.

```
import 'package:flutter/foundation.dart';
import 'package:fullstory_flutter/fullstory_flutter.dart';

// Custom Action: ShutdownFullStory
// Call this when user revokes consent
Future shutdownFullStory() async {
  if (kIsWeb) return;
  await FS.shutdown();
}
```

**Expected result:** The consent flow correctly gates FullStory initialization. Sessions appear in the FullStory dashboard only after the user has granted consent. Revoking consent stops new sessions from being recorded. In the FullStory Live Sessions panel, you can see your device's session in real time.

## Best practices

- Never place the FullStory REST API key in a FlutterFlow API Call header — always route read-back requests through a Firebase Cloud Function or Supabase Edge Function proxy.
- Gate FullStory initialization behind user consent and store the consent flag in your user database so it persists across sessions and app reinstalls.
- Use a stable, opaque internal user ID (not email) as the FS.identify uid so sessions remain searchable even if the user changes their email.
- Add a kIsWeb guard in every FullStory Custom Action — the fullstory_flutter SDK is native-only and will cause compilation errors on web targets without it.
- Test FullStory session capture exclusively in Run Mode on a real device — Custom Actions do not execute in FlutterFlow's browser-based Test Mode preview.
- Surface replay links as Launch URL buttons rather than attempting to embed the FullStory player in a WebView — FullStory does not support iframe embedding without enterprise configuration.
- Cache the proxy response in Firestore or Supabase so the support panel does not re-query FullStory's API every time it opens — session lists change slowly.
- Role-gate the session replay panel so only CS or support roles can access it — FullStory recordings contain sensitive PII and should not be visible to all users of the app.

## Use cases

### SaaS app that surfaces session replays for the in-app support panel

Build a FlutterFlow app where users can tap a 'Contact Support' button that also shows the support agent (or an in-app CS portal) the user's three most recent FullStory sessions and their replay links. The proxy fetches `/v1/users/{uid}/sessions` and returns replay URLs the agent can open in FullStory. This eliminates the 'what were you doing when it happened?' back-and-forth.

Prompt example:

```
Add a CS panel screen that looks up the signed-in user's FullStory sessions via a proxy API call and shows a list with session date, duration, and a button that opens the replay link in a WebView or external browser.
```

### Onboarding flow with session capture to diagnose drop-off

Instrument a multi-step onboarding flow in FlutterFlow with FullStory session capture. Initialize FullStory on app launch and identify each user so sessions are tagged to their account. The product team reviews replays in the FullStory dashboard to see exactly where users get confused or abandon the flow, enabling targeted UI fixes without relying on user surveys.

Prompt example:

```
Initialize the fullstory_flutter SDK on app launch, call identify with the user's ID and email after sign-in, and make sure the onboarding steps are navigated as separate screens so FullStory captures each step as a distinct page view in the session replay.
```

### Frustration signal monitoring for a mobile e-commerce checkout

Add FullStory session capture to a FlutterFlow e-commerce checkout flow so rage clicks and dead clicks during checkout are captured and visible in the FullStory dashboard. When the conversion rate drops, the team pulls up sessions from the affected day and immediately sees which button or form field triggered frustration — no guesswork.

Prompt example:

```
Integrate the fullstory_flutter Custom Action so sessions are recorded during checkout. Add a custom event call after a successful purchase and after an abandoned cart so FullStory can segment sessions by outcome.
```

## Troubleshooting

### No sessions appear in FullStory dashboard after running on a device

Cause: The Custom Action may not have initialized correctly, or the org ID is wrong. Custom Actions do not run in FlutterFlow web Test Mode — only on real builds or Run Mode on a device.

Solution: Verify you are testing in Run Mode or a real device build, not the browser preview. Double-check that the org ID in InitFullStory matches the one in your FullStory account settings exactly. Check device logs for SDK errors. Confirm the InitFullStory action is wired to On Page Load of your root widget.

### 401 Unauthorized on all proxy requests to FullStory's REST API

Cause: You are likely using the FullStory org ID (the capture identifier) as the REST API Bearer token. These are two completely different credentials. The org ID is a public capture token; the REST API key is a separate account-level secret found under Admin → Account Settings → Integrations → API.

Solution: Get a separate API key from FullStory Admin → Account Settings → Integrations → API. Store this key in your Firebase Cloud Function using `defineSecret` or in Supabase Edge Function secrets. Update the proxy function to use this key, not the org ID. Also verify that your FullStory plan includes REST API access — this requires Business plan or higher.

### XMLHttpRequest error or CORS error when calling the proxy from FlutterFlow web build

Cause: The Firebase Cloud Function or Supabase Edge Function is not returning the correct CORS headers, so browser-based web builds are blocked. Native iOS and Android builds do not enforce CORS, so this only appears on web.

Solution: Add CORS headers to your proxy function response: `Access-Control-Allow-Origin: *` and handle OPTIONS preflight requests with a 204 response. The code example in Step 3 includes this pattern. Redeploy the function after adding the CORS headers.

### fsUrl field is null or empty in the session list from the API

Cause: The replay link may not be available yet for very recent sessions that FullStory is still processing, or the JSON Path mapping may not match the actual field name in the API response version you are using.

Solution: Check which API version your proxy is calling (v1 vs v2) — the field name for the replay URL differs between versions. In v2 the field may be `replayUrl` or `url` rather than `fsUrl`. Open the Response & Test tab on your FlutterFlow API Call, paste a fresh real response, and regenerate JSON Paths. For sessions recorded in the last few minutes, wait and retry — FullStory processes recordings asynchronously.

## Frequently asked questions

### Can I watch FullStory session recordings inside my FlutterFlow app?

No — you cannot embed the FullStory player inside a FlutterFlow WebView. FullStory's player requires authentication to FullStory's own domain and does not support iframe embedding without a special enterprise configuration. The right pattern is to surface the session replay link (fsUrl) as a button in your app. Tapping it opens the recording in the device browser, where the viewer logs in to FullStory and watches the session. Your CS team needs FullStory viewer licenses to watch recordings.

### Does the fullstory_flutter SDK work in FlutterFlow web builds?

No — the fullstory_flutter pub.dev package is native-only (iOS and Android). For web targets you would need to inject FullStory's JavaScript snippet into your web app's index.html, which FlutterFlow allows for web builds. Add a kIsWeb guard in your Custom Action so the native SDK initialization is skipped on web. Session recording on web via the JS snippet and on mobile via the SDK operates independently — you will see two separate session types in your FullStory dashboard.

### What plan do I need for FullStory REST API access?

The capture SDK (`fullstory_flutter`) works on all FullStory plans, including free trials. The REST API — which you need to read sessions and replay links back into your app — requires a Business plan or higher. If you do not see an API Keys section in your FullStory Admin → Account Settings, you will need to upgrade. Contact FullStory support to enable API access for evaluation if you need it before upgrading.

### Why does the FullStory Custom Action not run in FlutterFlow's test mode?

FlutterFlow's browser-based Test Mode (the preview canvas and Run Mode in-browser) does not execute Custom Actions — Custom Actions are Dart code that only runs in a compiled Flutter build on a real device or emulator. This is a FlutterFlow platform limitation, not a FullStory issue. Use Run Mode connected to a real iOS or Android device to test FullStory session capture, and check the FullStory dashboard's Live Sessions panel to confirm sessions are being recorded.

### Is the FullStory org ID safe to include in the compiled Flutter app?

Yes — the org ID is a public capture identifier by design. It scopes session recordings to your FullStory organization, but having the org ID does not grant read access to your session data. Someone who extracts your org ID from the app binary could theoretically send synthetic sessions to your FullStory account, which is why proxying is the RapidDev-recommended default for production. However, the org ID is architecturally different from the REST API key, which must never appear in the client bundle.

---

Source: https://www.rapidevelopers.com/flutterflow-integrations/fullstory
© RapidDev — https://www.rapidevelopers.com/flutterflow-integrations/fullstory
