# Use SupaHost with Flutter

> Connect a Flutter app to a SupaHost project with supabase_flutter. Pass keys with --dart-define, initialize the client, query a table, react to auth changes and sign in with an emailed code.

Source: https://supahost.app/docs/getting-started/flutter · Updated 2026-11-11

`supabase_flutter` is the official Flutter package. It wraps the Dart client, stores the session on the device and refreshes tokens for you. It works with SupaHost unchanged: you give it your project URL and `anon` key.

The examples use the `recipes` table from the [quickstart](/docs/getting-started/quickstart).

## Install

```bash
flutter create recipe_box
cd recipe_box
flutter pub add supabase_flutter
```

## Pass your project keys

Flutter has no runtime environment variables, so pass the values at build time with `--dart-define`. Use the Project URL and `anon` key from the **Connect** tab (subtab **App**):

```bash
flutter run \
  --dart-define=SUPABASE_URL=https://app-one.acme.supahost.space \
  --dart-define=SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIs...
```

You can also keep them in a JSON file and use `--dart-define-from-file=env.json`. Leave that file out of version control. The `anon` key is safe inside an app; the `service_role` key never is.

## Initialize the client

```dart
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Supabase.initialize(
    url: const String.fromEnvironment('SUPABASE_URL'),
    anonKey: const String.fromEnvironment('SUPABASE_ANON_KEY'),
  );

  runApp(const RecipeApp());
}

final supabase = Supabase.instance.client;
```

## Query a table

```dart
class RecipeList extends StatefulWidget {
  const RecipeList({super.key});

  @override
  State<RecipeList> createState() => _RecipeListState();
}

class _RecipeListState extends State<RecipeList> {
  final _recipes = supabase
      .from('recipes')
      .select('id, title, minutes')
      .order('created_at', ascending: false);

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<Map<String, dynamic>>>(
      future: _recipes,
      builder: (context, snapshot) {
        if (snapshot.hasError) return Text('Error: ${snapshot.error}');
        if (!snapshot.hasData) return const CircularProgressIndicator();
        final rows = snapshot.data!;
        return ListView.builder(
          itemCount: rows.length,
          itemBuilder: (context, i) => ListTile(
            title: Text(rows[i]['title'] as String),
            subtitle: Text('${rows[i]['minutes'] ?? '?'} min'),
          ),
        );
      },
    );
  }
}
```

Errors from the database, such as a Row Level Security violation, are thrown as `PostgrestException`. Wrap writes in `try`/`catch`:

```dart
try {
  await supabase.from('recipes').insert({'title': 'Lemon rice', 'minutes': 30});
} on PostgrestException catch (e) {
  debugPrint('Insert failed: ${e.message}');
}
```

The insert only succeeds for a signed-in user, because the quickstart's insert policy is for `authenticated`.

## React to sign-in and sign-out

```dart
class AuthGate extends StatelessWidget {
  const AuthGate({super.key});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<AuthState>(
      stream: supabase.auth.onAuthStateChange,
      builder: (context, _) {
        final session = supabase.auth.currentSession;
        return session == null ? const SignInPage() : const RecipeList();
      },
    );
  }
}
```

## Sign in with an emailed code

The emailed one-time code avoids redirects entirely: the magic link email includes a code the user types into the app. If you prefer tapping the link, add your app's scheme (for example `recipebox://login`) to **Redirect URLs** on the Auth tab and pass it as `emailRedirectTo`.

```dart
Future<void> sendCode(String email) async {
  await supabase.auth.signInWithOtp(email: email);
}

Future<void> verifyCode(String email, String code) async {
  await supabase.auth.verifyOTP(
    type: OtpType.email,
    email: email,
    token: code,
  );
}
```

Password sign-in works as well:

```dart
await supabase.auth.signInWithPassword(email: email, password: password);
await supabase.auth.signUp(email: email, password: password);
await supabase.auth.signOut();
```

Both throw `AuthException` on failure.

## Social login

For Google and Apple, use the native sign-in packages (`google_sign_in`, `sign_in_with_apple`) to get an ID token, then call `supabase.auth.signInWithIdToken(provider: OAuthProvider.google, idToken: idToken)`. Configure the provider on the **Auth** tab first. See [Sign in with Google](/docs/auth/google) and [Sign in with Apple](/docs/auth/apple).

Browser-based OAuth with `signInWithOAuth` needs a return address on the Redirect URLs list, which means an `https://` link your app opens (Universal Links on iOS, App Links on Android).

## Flutter web

On Flutter web the same code works. Add your web origin (for example `http://localhost:3000/**` and your production URL) under **Redirect URLs** on the Auth tab, then click **Save Auth settings**.

## Next steps

- [Magic links and email OTP](/docs/auth/magic-links-otp)
- [Postgres Changes](/docs/realtime/postgres-changes) with `supabase.channel()`
- [Uploading files](/docs/storage/uploads)
- [Can I use SupaHost for a mobile app?](/kb/mobile-app-database)
