Skip to main content
Version: Next

Flutter web apps

Serverpod can serve your Flutter web application directly, allowing you to host both your API and web frontend from the same server. FlutterRoute handles the specifics of serving Flutter web apps, including WASM multi-threading headers and SPA-style routing.

Basic setup

Use FlutterRoute to serve your Flutter web build:

pod.webServer.addRoute(
FlutterRoute(Directory('web/app')),
);
info

The route path defaults to '/' (root). See Serving from a sub-path to mount the app at a different location.

This configuration:

  • Serves all static files from the Flutter build
  • Falls back to index.html for client-side routing
  • Adds WASM multi-threading headers automatically
  • Applies smart caching: critical files are never cached, other files are cached for 1 day

Building Flutter for web

Build your Flutter app for web with WASM support for improved performance and multi-threading:

cd my_project_flutter
flutter build web --wasm
info

WASM builds automatically fall back to JavaScript in browsers that don't support WebAssembly Garbage Collection (WasmGC). Your app works everywhere while taking advantage of WASM performance where available.

Project structure

Copy your Flutter build output to the server's web directory:

my_project/
├── my_project_server/
│ ├── lib/
│ │ └── server.dart
│ └── web/
│ └── app/ # Flutter web build output
│ ├── index.html
│ ├── main.dart.js
│ ├── flutter.js
│ └── ...
├── my_project_flutter/
│ └── build/
│ └── web/ # Flutter build output (source)
└── my_project_client/

WASM multi-threading

Flutter WASM builds can use multi-threaded rendering for improved performance. This requires SharedArrayBuffer, which browsers only enable with specific security headers.

FlutterRoute automatically adds these headers:

  • Cross-Origin-Opener-Policy: same-origin
  • Cross-Origin-Embedder-Policy: require-corp

Using WasmHeadersMiddleware directly

If you're using SpaRoute or custom routes instead of FlutterRoute, add the headers manually with WasmHeadersMiddleware:

pod.webServer.addMiddleware(const WasmHeadersMiddleware());

pod.webServer.addRoute(
SpaRoute(
Directory('web/app'),
fallback: File('web/app/index.html'),
),
'/',
);

Cache control

FlutterRoute automatically applies smart caching to prevent issues with stale app versions:

Default caching behavior

By default, FlutterRoute uses different cache strategies for different file types:

Critical files (never cached):

  • index.html
  • flutter_service_worker.js
  • flutter_bootstrap.js
  • manifest.json
  • version.json

These files are served with Cache-Control: private, no-cache, no-store headers to ensure users always get the latest version after deployments.

All other files (cached for 1 day):

Static assets like JavaScript, WASM, images, and fonts are served with Cache-Control: public, max-age=86400 headers, allowing browsers to cache them for better performance.

Invalidating cached assets

When you deploy a new version of your Flutter app, cached assets need to be invalidated to ensure users get the latest version. To do this:

  1. Update the version in your Flutter app's pubspec.yaml:

    version: 1.0.1+2  # Increment from previous version
  2. Rebuild your Flutter web app:

    flutter build web --wasm
  3. Deploy the new build to your server

Flutter's build process includes the version in asset paths and metadata, which causes browsers to fetch the new assets instead of using cached versions.

Custom cache control

Override the default behavior using cacheControlFactory:

pod.webServer.addRoute(
FlutterRoute(
Directory('web/app'),
cacheControlFactory: StaticRoute.publicImmutable(
maxAge: const Duration(hours: 1),
),
),
);
warning

Custom cache control applies to all files served from the directory, except for the fallback index.html which is always served with no-cache headers. Make sure your strategy prevents caching of service workers and manifests to avoid serving stale app versions.

See Static Files for more on cache control.

Cache busting

Enable cache-busted URLs:

final webDir = Directory('web/app');

final cacheBustingConfig = CacheBustingConfig(
mountPrefix: '/',
fileSystemRoot: webDir,
);

pod.webServer.addRoute(
FlutterRoute(
webDir,
cacheBustingConfig: cacheBustingConfig,
cacheControlFactory: StaticRoute.publicImmutable(
maxAge: const Duration(minutes: 5),
),
),
);

See Static Files for more on cache busting.

Complete example

Here's a complete server.dart serving a Flutter web app:

import 'dart:io';

import 'package:serverpod/serverpod.dart';

import 'src/generated/protocol.dart';
import 'src/generated/endpoints.dart';

void run(List<String> args) async {
final pod = Serverpod(args, Protocol(), Endpoints());

final flutterAppDir = Directory('web/app');

if (!flutterAppDir.existsSync()) {
print('Warning: Flutter web app not found at ${flutterAppDir.path}');
print('Build your Flutter app and copy it to web/app/');
} else {
pod.webServer.addRoute(FlutterRoute(flutterAppDir));
}

await pod.start();
}

With this configuration, your Flutter web app is served at the root URL of your web server (typically http://localhost:8082 in development).