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.

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.

Install

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):

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

// 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

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:

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

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.

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:

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 and Sign in with 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

Updated 11 November 2026 · View as Markdown · Something wrong or missing? Tell us