# Flutter web apps

https://docs.serverpod.dev/3.3.0/concepts/webserver/flutter-web

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:

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

:::info
The route path defaults to `'/'` (root). See [Serving from a sub-path](https://docs.serverpod.dev/3.3.0/concepts/webserver/single-page-apps.md#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:

```bash
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:

```text
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`:

```dart
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`:

   ```yaml
   version: 1.0.1+2  # Increment from previous version
   ```

2. Rebuild your Flutter web app:

   ```bash
   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`:

```dart
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](https://docs.serverpod.dev/3.3.0/concepts/webserver/static-files.md#cache-control) for more on cache control.

## Cache busting

Enable cache-busted URLs:

```dart
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](https://docs.serverpod.dev/3.3.0/concepts/webserver/static-files.md#static-file-cache-busting) for more on cache busting.

## Complete example

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

```dart
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).
