# Grafana

- Tool: FlutterFlow
- Difficulty: Advanced
- Time required: 60 minutes
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to Grafana by embedding a Grafana dashboard or panel inside a FlutterFlow Custom Widget that wraps the flutter_inappwebview package. Enable allow_embedding in your Grafana configuration, generate a panel share or kiosk URL, and load it in the WebView. For token-gated dashboards, a Firebase Cloud Function mints short-lived embed URLs so the Bearer token never appears in Dart code.

## Embedding Grafana Dashboards in a FlutterFlow App

Grafana is a complete observability platform — you do not rebuild its dashboards inside Flutter widgets. The right integration pattern is embedding: you generate a URL pointing at a specific Grafana panel or dashboard, and a FlutterFlow Custom Widget loads that URL in a WebView. Your users see the full, interactive Grafana panel (with time-range pickers, tooltips, and zoom) inside your app, without leaving it.

FlutterFlow has a built-in WebView widget, but it is limited to simple URL loading. Grafana embeds often require authenticated sessions (via cookies or Bearer tokens in headers), JavaScript execution for interactivity, and precise control over viewport sizing. For these reasons, the recommended approach is a Custom Widget wrapping the `flutter_inappwebview` pub.dev package, which provides all of these capabilities.

The security consideration: Grafana HTTP API calls require a service account token as a Bearer header. This token grants access to your Grafana dashboards and datasources. Placing it in a FlutterFlow API Call header or in Dart code would ship it inside your compiled app binary — any user could extract it from the APK or web bundle. For token-gated Grafana instances, a Firebase Cloud Function holds the service account token, generates a short-lived authenticated embed URL (or proxies the API request), and hands only that temporary URL to your FlutterFlow widget. Grafana Cloud and self-hosted Grafana both support anonymous or token-based embedding — the setup differs slightly between them.

## Before you start

- A FlutterFlow project on a paid plan with Custom Code access (Basic plan or above)
- A Grafana instance — either self-hosted (Grafana OSS or Enterprise) or Grafana Cloud — with at least one dashboard configured
- Admin access to your Grafana instance to enable embedding settings and create service account tokens
- Firebase project connected to your FlutterFlow app (for the Cloud Function proxy on token-gated instances)
- Basic familiarity with FlutterFlow's Custom Code panel and Action Flow Editor

## Step-by-step guide

### 1. Enable Grafana embedding and generate a panel URL

For self-hosted Grafana, embedding is disabled by default for security. You must enable it in your Grafana configuration file (`grafana.ini` or environment variables). Find the `[security]` section and set `allow_embedding = true`. Also set `cookie_samesite = none` and ensure `cookie_secure = true` (HTTPS required). Restart Grafana after changing these settings. Without `allow_embedding = true`, your WebView will show a blank panel or a browser error because Grafana sends an `X-Frame-Options: DENY` header that blocks iframe/WebView rendering.

For Grafana Cloud, embedding is controlled per dashboard. Go to Dashboard → Share → Embed tab — Grafana Cloud may restrict embedding based on your plan tier; check your plan.

Once embedding is enabled, open the dashboard you want to embed in Grafana. For a full dashboard in kiosk mode (no sidebar, no top bar), append `?kiosk` to the URL: `https://your-grafana/d/DASHBOARD_ID/dashboard-name?kiosk`. For a single panel, click the panel title → Share → Embed tab — copy the generated iframe src URL. For anonymous access, set 'auth.anonymous.enabled = true' and 'auth.anonymous.org_role = Viewer' in grafana.ini — this lets the WebView load the panel without authentication. Copy the final panel or dashboard URL — you will use it in the Custom Widget in the next step.

```
# grafana.ini settings for embedding
[security]
allow_embedding = true
cookie_samesite = none
cookie_secure = true

[auth.anonymous]
enabled = true
org_role = Viewer

# If using reverse proxy, also ensure:
# [server]
# root_url = https://your-grafana-domain.com
```

**Expected result:** Opening your Grafana panel URL in a browser shows the panel without the login redirect, confirming anonymous or public access is working.

### 2. Create the Custom Widget wrapping flutter_inappwebview

In FlutterFlow, click 'Custom Code' in the left nav → '+ Add' → 'Widget'. Name it `GrafanaEmbedWidget`. In the 'Pubspec Dependencies' field, add `flutter_inappwebview: ^6.1.5` (check pub.dev/packages/flutter_inappwebview for the latest stable version). Set the widget parameters: add a `String` parameter named `panelUrl` (the Grafana panel or dashboard URL to load) and an `int` parameter named `heightPx` (the height of the widget in pixels, e.g., 400). Paste the Dart code below into the widget body. The code creates an InAppWebView that loads the provided URL, enables JavaScript, and disables the browser's pull-to-refresh to prevent conflicts with FlutterFlow's page scroll.

Click 'Save and Compile'. After compiling, the widget will appear in your FlutterFlow component library under 'Custom Widgets'. Important: this widget will NOT render correctly in FlutterFlow's browser-based preview or Run mode. Custom widgets with platform-specific packages like flutter_inappwebview only work when running on a real device or an emulator via FlutterFlow's Local Run feature. This is a platform constraint, not a bug in your code.

```
// FlutterFlow Custom Widget: GrafanaEmbedWidget
// Dependency: flutter_inappwebview: ^6.1.5
// Parameters: panelUrl (String), heightPx (int)

import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';

class GrafanaEmbedWidget extends StatefulWidget {
  final String panelUrl;
  final int heightPx;
  const GrafanaEmbedWidget({
    Key? key,
    required this.panelUrl,
    required this.heightPx,
  }) : super(key: key);

  @override
  State<GrafanaEmbedWidget> createState() => _GrafanaEmbedWidgetState();
}

class _GrafanaEmbedWidgetState extends State<GrafanaEmbedWidget> {
  InAppWebViewController? _controller;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: widget.heightPx.toDouble(),
      child: InAppWebView(
        initialUrlRequest: URLRequest(
          url: WebUri(widget.panelUrl),
        ),
        initialSettings: InAppWebViewSettings(
          javaScriptEnabled: true,
          allowsInlineMediaPlayback: true,
          disallowOverScroll: true,
          scrollsToTop: false,
        ),
        onWebViewCreated: (controller) {
          _controller = controller;
        },
        onLoadError: (controller, url, code, message) {
          debugPrint('Grafana WebView error: $message (code: $code)');
        },
      ),
    );
  }
}
```

**Expected result:** The GrafanaEmbedWidget appears in the Custom Widgets section of your FlutterFlow component library with panelUrl and heightPx parameters.

### 3. Add the Custom Widget to a FlutterFlow page

Open the FlutterFlow page where you want to display the Grafana panel. In the widget library on the left, scroll to the bottom and click 'Custom Widgets' to expand the section. Drag the GrafanaEmbedWidget onto your page canvas. In the widget's property panel on the right, set the `panelUrl` parameter to your Grafana panel URL (either a hardcoded value for now, or a variable from App State if you want the URL to change based on user selection). Set `heightPx` to an appropriate value — 400 pixels works for a standard panel, 600-800 for a dashboard in kiosk mode.

For anonymous public panels, the URL is all you need. For dashboards that require authentication but you want to handle via a Cloud Function token (Step 4), set `panelUrl` to a page state variable that gets populated by the Cloud Function response.

Position the widget inside a SingleChildScrollView or Column to allow the rest of the page to scroll while the Grafana panel remains a fixed-height block. If you want multiple panels stacked, add multiple GrafanaEmbedWidget instances, each with a different panel URL. You can also wrap the widget in a Container with a border radius and subtle shadow to integrate it visually with your FlutterFlow app's design.

**Expected result:** The GrafanaEmbedWidget appears on your page canvas with the configured URL and height parameters. On a real device or Local Run, it loads the Grafana panel.

### 4. Secure token-gated embeds with a Cloud Function proxy

If your Grafana instance requires authentication (no anonymous access allowed), you need to generate authenticated embed URLs server-side. Grafana service account tokens are Bearer credentials that grant access to dashboards and datasources — they must never appear in Dart code. The pattern: a Firebase Cloud Function accepts a request from your FlutterFlow app, uses the service account token (stored as a Firebase environment variable) to call Grafana's HTTP API, and returns either a signed embed URL or the raw metric data.

For embed URLs: Grafana does not natively generate signed short-lived URLs the way AWS S3 does, but you can use Grafana's 'Snapshot' feature — the API endpoint `/api/snapshots` creates a public, time-limited snapshot of a dashboard panel. The Cloud Function creates the snapshot using the service account token, and FlutterFlow's WebView loads the public snapshot URL (which requires no auth). Set the snapshot expiry time to your session length (e.g., 3600 seconds for one hour).

For HTTP API access to raw metrics (chart numbers rather than visual panels): add a Firebase Cloud Function that calls Grafana's `/api/datasources/proxy/{datasourceId}/...` or `/api/dashboards/uid/{uid}` endpoints with the Bearer token and returns the JSON. Your FlutterFlow API Call targets this function. See the Cloud Function proxy code below.

```
// Firebase Cloud Function: getGrafanaSnapshot
// Creates a Grafana dashboard snapshot and returns the public URL
const functions = require('firebase-functions');
const axios = require('axios');

exports.getGrafanaSnapshot = functions.https.onCall(async (data, context) => {
  if (!context.auth) {
    throw new functions.https.HttpsError('unauthenticated', 'Must be signed in.');
  }

  const grafanaUrl = process.env.GRAFANA_URL; // e.g. https://your-grafana.com
  const serviceToken = process.env.GRAFANA_SERVICE_TOKEN; // Bearer token from env var
  const { dashboardUid, panelId } = data;

  try {
    // First, get the dashboard JSON
    const dashResponse = await axios.get(
      `${grafanaUrl}/api/dashboards/uid/${dashboardUid}`,
      { headers: { Authorization: `Bearer ${serviceToken}` } }
    );

    // Create a snapshot with 1-hour expiry
    const snapshotResponse = await axios.post(
      `${grafanaUrl}/api/snapshots`,
      {
        dashboard: dashResponse.data.dashboard,
        name: `App embed - ${dashboardUid}`,
        expires: 3600,
      },
      { headers: { Authorization: `Bearer ${serviceToken}` } }
    );

    return {
      snapshotUrl: snapshotResponse.data.url,
      expiresIn: 3600,
    };
  } catch (error) {
    throw new functions.https.HttpsError('internal', error.message);
  }
});
```

**Expected result:** The Cloud Function deploys and returns a public Grafana snapshot URL when called. FlutterFlow's GrafanaEmbedWidget loads the snapshot URL without requiring the service account token in Dart.

### 5. (Optional) Query Grafana HTTP API for native chart data

Embedding a Grafana panel covers most use cases, but sometimes you want to display a single metric value or a simple chart using FlutterFlow's native widget (faster to load, more control over styling, no WebView). For this, use Grafana's HTTP API via a FlutterFlow API Call routed through your Cloud Function. The two most useful endpoints are: `/api/dashboards/uid/{dashboardUid}` (returns the dashboard JSON including all panel configurations and data queries) and the datasource proxy `/api/datasources/proxy/{datasourceId}/...` which allows you to query your underlying data source (Prometheus, InfluxDB, Loki, etc.) through Grafana.

In FlutterFlow, create an API Group named 'GrafanaAPI' with the base URL pointing to your Cloud Function. Add an API Call 'Get Dashboard' (POST) that sends the dashboardUid as a body parameter. The Cloud Function fetches the data from Grafana and returns it. In the Response & Test tab, use the JSON Path `$.data.panels[*].title` to extract panel titles and `$.data.panels[*].targets[*]` to extract data query targets.

For a simpler metric display (e.g., current value of a gauge panel), the Cloud Function can query the Grafana API and extract just the current value number, returning it as a single JSON field. Bind that field to a Text widget in FlutterFlow for a lightweight native display. This hybrid approach — native text for KPI numbers, embedded WebView for complex time-series panels — gives you the best of both worlds: fast rendering for simple values and full Grafana interactivity for complex visualizations.

```
// Firebase Cloud Function: getGrafanaMetricValue
// Returns a single metric value from a Grafana panel
const functions = require('firebase-functions');
const axios = require('axios');

exports.getGrafanaMetricValue = functions.https.onCall(async (data, context) => {
  if (!context.auth) {
    throw new functions.https.HttpsError('unauthenticated', 'Must be signed in.');
  }

  const grafanaUrl = process.env.GRAFANA_URL;
  const serviceToken = process.env.GRAFANA_SERVICE_TOKEN;
  const { datasourceId, promQuery } = data;

  try {
    // Query Prometheus through Grafana's datasource proxy
    const response = await axios.get(
      `${grafanaUrl}/api/datasources/proxy/${datasourceId}/api/v1/query`,
      {
        headers: { Authorization: `Bearer ${serviceToken}` },
        params: { query: promQuery },
      }
    );

    const result = response.data.data.result;
    const currentValue = result.length > 0 ? result[0].value[1] : null;

    return { value: currentValue, unit: 'auto' };
  } catch (error) {
    throw new functions.https.HttpsError('internal', error.message);
  }
});
```

**Expected result:** The Grafana HTTP API Cloud Function returns metric values that can be bound to native FlutterFlow Text or chart widgets, complementing the embedded panel WebView.

## Best practices

- Never store a Grafana service account token in FlutterFlow API Call headers, App Constants, or Dart code — it grants access to your dashboards and datasources and must live only in Firebase Cloud Function environment variables.
- Enable allow_embedding in Grafana only if your instance is HTTPS-secured. Embedding over plain HTTP risks man-in-the-middle attacks and also causes the cookie_samesite = none requirement to fail (SameSite=None requires Secure).
- Test your GrafanaEmbedWidget on a real iOS and Android device via FlutterFlow Local Run before publishing. The browser preview is always blank for flutter_inappwebview components — only device testing confirms the widget works.
- Use Grafana's snapshot API for time-limited authenticated embeds rather than passing Bearer tokens through the widget. Snapshots expire automatically, limiting the impact of a URL being shared or intercepted.
- Prefer single-panel embed URLs over full-dashboard kiosk URLs for mobile. Full dashboards pack many panels that become too small to read on a phone — embed specific panels in a scrollable list for a better mobile experience.
- For simple KPI displays (a single number, a percentage), use a Grafana HTTP API Cloud Function to fetch the raw value and bind it to a native FlutterFlow Text widget rather than embedding a full panel. This loads faster and integrates visually with your app's design.
- Gate your Cloud Function with Firebase Authentication (check context.auth) so that only signed-in FlutterFlow users can trigger Grafana API requests through your function.
- For self-hosted Grafana, consider running it behind a subdomain with a CDN (Cloudflare) to improve panel load times in mobile WebViews. Grafana panels load multiple JS chunks that can be slow on mobile networks without caching headers.

## Use cases

### Operations dashboard embedded in a team mobile app

A DevOps team builds a FlutterFlow mobile app that shows their Grafana server health panels on iOS and Android. On-call engineers get push notifications for alerts and tap through to see the embedded Grafana CPU/memory panels without switching to a browser. The panels load in a flutter_inappwebview Custom Widget in kiosk mode.

Prompt example:

```
I want to show our Grafana server health dashboard inside our on-call mobile app. Embed the CPU and memory usage panels as a scrollable list of embedded panels so engineers can see the current state at a glance.
```

### IoT sensor data viewer with real-time charts

An IoT startup builds a FlutterFlow customer-facing app that shows each customer their device's sensor data. A Cloud Function authenticates with Grafana using a service account, generates a time-limited embed URL scoped to that customer's dashboard, and the flutter_inappwebview Custom Widget renders the live chart. Customers see their data without needing a Grafana account.

Prompt example:

```
Show each customer their own IoT device's sensor readings as a live Grafana chart embedded in our app — the chart should update every 30 seconds and cover the last 24 hours of data.
```

### Business metrics screen with native chart fallback

A SaaS product embeds key business metrics from Grafana into an admin app. For high-level KPIs (total revenue, active users) the app uses FlutterFlow's native chart widgets bound to a Grafana HTTP API Call via a Cloud Function — these render as fast native UI. For complex time-series panels with multiple data series, the app falls back to an embedded Grafana panel URL in a Custom Widget WebView.

Prompt example:

```
Pull our monthly active users metric from Grafana's HTTP API and display it as a native FlutterFlow number widget, then embed the full retention cohort Grafana panel below it in a scrollable WebView.
```

## Troubleshooting

### GrafanaEmbedWidget shows a blank white screen or 'Connection refused' in the WebView

Cause: The most common cause is that allow_embedding is disabled in Grafana's configuration. Without it, Grafana sends an X-Frame-Options: DENY header which blocks WebView rendering. The second cause is an unreachable Grafana URL (e.g., a local IP like 192.168.x.x that is not accessible from a real device on a different network).

Solution: Confirm allow_embedding = true in grafana.ini and restart Grafana. To verify: open the Grafana panel URL in your phone's mobile browser (not FlutterFlow) — if it loads there, the URL is reachable. If it does not load, the network/URL is the issue (ensure Grafana has a public domain or the device is on the same VPN). For Grafana Cloud, check your plan's embedding permissions.

### Custom Widget shows a blank area in FlutterFlow's browser preview and Run mode

Cause: This is expected behavior. The flutter_inappwebview package uses platform-specific native WebView implementations (WKWebView on iOS, Android WebView) that cannot run in FlutterFlow's browser-based code preview or Run mode.

Solution: Test the GrafanaEmbedWidget using FlutterFlow's Local Run feature (connect your physical phone via USB) or download the project as a ZIP and run it in Android Studio / Xcode. The widget works correctly on real devices even though the preview is blank.

### Grafana panel loads but the interface is too small or text is unreadable inside the WebView

Cause: Grafana's panels are designed for desktop viewport sizes. When loaded in a small mobile WebView, they do not automatically reflow or scale to mobile proportions. The panel may be rendered at desktop resolution and scaled down, making it hard to read.

Solution: Append Grafana URL parameters to optimize for mobile: `&width=400&height=350` sets the panel's render dimensions. Also append `&theme=dark` or `&theme=light` to match your app's theme. Consider increasing the `heightPx` parameter of the Custom Widget to give the panel more vertical space. For single-stat or gauge panels, these look better at smaller sizes; time-series charts typically need at least 350px height.

### 403 Unauthorized error when the Cloud Function calls the Grafana HTTP API

Cause: The Grafana service account token stored in the Firebase environment variable does not have permission to access the requested dashboard or datasource, or the token has been revoked.

Solution: In Grafana, go to Administration → Service Accounts, find the service account, and verify the token is still active and has at least Viewer role. If the token was revoked, create a new one and update the Firebase environment variable (GRAFANA_SERVICE_TOKEN) via the Firebase CLI, then redeploy the Cloud Function.

## Frequently asked questions

### Can I use FlutterFlow's built-in WebView widget instead of a Custom Widget for Grafana?

FlutterFlow's basic WebView widget works for simple public Grafana panels with anonymous access. However, for dashboards that require authentication, need JavaScript execution for interactivity, or require cookie-based session handling, you need the flutter_inappwebview Custom Widget. The built-in WebView also cannot pass custom headers for Bearer token auth. Start with the built-in WebView for quick testing and upgrade to the Custom Widget if you need any of these additional capabilities.

### Why is the Grafana panel blank in FlutterFlow's Run mode preview?

Custom Widgets using the flutter_inappwebview package use platform-native WebView implementations (WKWebView on iOS, Android WebView on Android) that do not run in FlutterFlow's browser-based code preview environment. This is expected — the widget works correctly on real devices. Test using FlutterFlow's Local Run feature with a physical device connected via USB, or by deploying a test APK.

### Is Grafana free to use for this integration?

Grafana OSS (open-source) is free to download and self-host — your only costs are server infrastructure. Grafana Cloud offers a free tier with limited data retention, active series limits, and features; check grafana.com/pricing for current free tier specs. The embedding and HTTP API features used in this integration are available on both self-hosted and cloud plans, though some advanced embedding features may require paid Grafana Cloud tiers.

### Can I embed Grafana panels that show real-time live data?

Yes. Grafana panels embedded via WebView refresh on their own configured interval (set in the panel's time range settings). You can append `&refresh=30s` to the panel URL to force a 30-second auto-refresh. The flutter_inappwebview WebView keeps the JavaScript running while the widget is visible, so Grafana's own auto-refresh mechanism works as expected. For the HTTP API pull approach, add a Timer widget in FlutterFlow that fires the API Call every N seconds.

### What is the difference between embedding a Grafana panel and using the Grafana HTTP API?

Embedding loads the full Grafana panel as an HTML/JavaScript application inside a WebView — users see exactly what they would see in a Grafana browser tab, including interactivity (hover tooltips, time range selection, panel zooming). The HTTP API approach queries Grafana for raw data (numbers, series, labels) and returns JSON that you bind to native FlutterFlow widgets (charts, text, containers). Embedding is faster to set up and visually rich; HTTP API gives you full design control and faster loading but requires more implementation work.

---

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