Docs

Quickstart: Flutter

Build the notes app in Flutter: initialise Sovaria with a secure session store, switch screens on sign-in, and list and add the signed-in user's notes.

Updated Oct 24, 2026

A Flutter app for iOS and Android where people sign up, sign in, add notes and see only their own.

#Before you start

Do steps 1 to 3 of the Quickstart first: create a project, copy its project URL and anon key (Quickstart), and create the notes table with its row rules (Quickstart). Every signed-in user then sees and changes only their own notes, whatever the app code does.

#1. Create the app and install

flutter create notes
cd notes
flutter pub add sovaria flutter_secure_storage

#2. Initialise Sovaria with a secure session store

import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:sovaria/sovaria.dart';

import 'notes_app.dart';

/// Keeps the session in the Keychain (iOS) or Keystore (Android).
class SecureStore implements SessionStorage {
  final _s = const FlutterSecureStorage();
  @override
  Future<String?> read(String key) => _s.read(key: key);
  @override
  Future<void> write(String key, String value) => _s.write(key: key, value: value);
  @override
  Future<void> delete(String key) => _s.delete(key: key);
}

late final Sovaria sovaria;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // Restores the saved session (refreshing it if it's about to expire).
  sovaria = await Sovaria.initialize(
    url: 'https://<project-ref>.sovarias.in',
    anonKey: '<anon-key>',
    storage: SecureStore(),
  );
  runApp(const NotesApp());
}

#3. Sign in and work with notes

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

import 'main.dart';

class NotesApp extends StatelessWidget {
  const NotesApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
        home: StreamBuilder<AuthState>(
          // The first event is initialSession with the restored session (or null).
          stream: sovaria.auth.onAuthStateChange,
          builder: (context, snap) => snap.data?.session == null ? const SignInPage() : const NotesPage(),
        ),
      );
}

class SignInPage extends StatefulWidget {
  const SignInPage({super.key});
  @override
  State<SignInPage> createState() => _SignInPageState();
}

class _SignInPageState extends State<SignInPage> {
  final email = TextEditingController(), password = TextEditingController();
  String error = '';

  Future<void> run(Future<Object?> Function() action) async {
    try {
      await action();
    } on SovariaException catch (e) {
      setState(() => error = e.message);
    }
  }

  @override
  Widget build(BuildContext context) => Scaffold(
        body: Padding(
          padding: const EdgeInsets.all(24),
          child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
            TextField(controller: email, decoration: const InputDecoration(labelText: 'Email')),
            TextField(controller: password, obscureText: true, decoration: const InputDecoration(labelText: 'Password')),
            FilledButton(
              onPressed: () => run(() => sovaria.auth.signInWithPassword(email: email.text, password: password.text)),
              child: const Text('Sign in'),
            ),
            TextButton(
              onPressed: () => run(() => sovaria.auth.signUp(email: email.text, password: password.text)),
              child: const Text('Create account'),
            ),
            if (error.isNotEmpty) Text(error),
          ]),
        ),
      );
}

class NotesPage extends StatefulWidget {
  const NotesPage({super.key});
  @override
  State<NotesPage> createState() => _NotesPageState();
}

class _NotesPageState extends State<NotesPage> {
  final body = TextEditingController();
  List<Map<String, Object?>> notes = [];

  @override
  void initState() {
    super.initState();
    load();
  }

  // Rows come back as maps keyed by column name. The row rules add "where owner_id = auth.uid()".
  Future<void> load() async {
    final result = await sovaria.db.sql('select id, body from notes order by id desc');
    setState(() => notes = result.rows);
  }

  Future<void> add() async {
    await sovaria.db.sql(r'insert into notes (body) values ($1)', [body.text]);
    body.clear();
    await load();
  }

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: const Text('Notes'), actions: [
          IconButton(icon: const Icon(Icons.logout), onPressed: () => sovaria.auth.signOut()),
        ]),
        body: Column(children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Row(children: [
              Expanded(child: TextField(controller: body, decoration: const InputDecoration(hintText: 'Write a note'))),
              IconButton(icon: const Icon(Icons.add), onPressed: add),
            ]),
          ),
          Expanded(
            child: ListView(children: [for (final n in notes) ListTile(title: Text('${n['body']}'))]),
          ),
        ]),
      );
}
  • Use a raw string (r'…') for SQL with $1 so Dart doesn't read it as interpolation.
  • signUp creates the user and signs them in, unless the project asks people to confirm their email first (Email and password).

#4. Run it

flutter run

#Next steps

#Coming from Firebase or Supabase?

PlatformWhat this is called there
FirebaseFlutterFire
SupabaseSupabase Flutter quickstart
SovariaQuickstart: Flutter

Moving an existing app? Migrate from Supabase or Firebase imports users (with their passwords), data, files and rules.