GUIDE

Your first app with Flutter

A to-do list for iPhone and Android where people sign up and see only their own todos. It uses the Espobase package for Dart; the database and keys come from the dashboard.

← All guides

1. Create a project and a table

In the dashboard, create a project. Open SQL Editor and run:

create table todos (
  id bigint generated by default as identity primary key,
  owner_id uuid not null default espobase.user_id(),
  title text not null,
  done boolean not null default false,
  created_at timestamptz not null default now()
);

-- Row level security is already on. Each signed-in user sees and changes only their own todos:
create policy "Users manage their own todos" on todos
  for all
  using (owner_id = espobase.user_id())
  with check (owner_id = espobase.user_id());

2. Settings and keys

  • API: create a publishable key and copy it now; keys are only shown when created. Never put a secret key (eb_sk_…) in an app: anyone can unpack an app and read it.
  • Authentication › Settings: add your app's link under Allowed redirect URLs, for example myapp://auth/callback. Confirmation emails and social sign-in return to it. Register the same scheme in your Android and iOS projects.

3. Install

The Espobase package works in every Flutter platform. The other packages keep the session safe and open sign-in pages:

flutter pub add espobase flutter_secure_storage flutter_web_auth_2 sign_in_with_apple

4. The client

By default the session lives in memory. Give the client secure storage, and the user stays signed in when the app restarts. The session is refreshed for you.

// lib/espobase.dart
import 'package:espobase/espobase.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class SecureSessionStorage implements SessionStorage {
  final _store = const FlutterSecureStorage();
  @override Future<String?> get(String key) => _store.read(key: key);
  @override Future<void> set(String key, String value) => _store.write(key: key, value: value);
  @override Future<void> remove(String key) => _store.delete(key: key);
}

final espobase = EspobaseClient(
  const String.fromEnvironment('ESPOBASE_URL'), // https://api.espobase.com/project/<your project ref>
  const String.fromEnvironment('ESPOBASE_KEY'), // the PUBLISHABLE key (eb_pk_…): safe in an app
  options: ClientOptions(sessionStorage: SecureSessionStorage()),
);

5. Sign-up and sign-in

// myapp://auth/callback (the scheme is in your Android and iOS projects). Add it under Authentication › Settings.
const redirectTo = 'myapp://auth/callback';

await espobase.auth.signUp(email: email, password: password, redirectTo: redirectTo);
await espobase.auth.signInWithPassword(email: email, password: password);

// The confirmation email opens the app at redirectTo#type=signup&token=…
await espobase.auth.verifyEmail(token: token);

// Re-render when the user signs in or out.
espobase.auth.onAuthStateChange.listen((change) => setState(() => session = change.session));

With email confirmation on, the user gets a link that opens your app at the address you passed as redirectTo, with the token after a #. Confirm it with espobase.auth.verifyEmail(token: …).

6. Sign in with Apple

Turn it on under Authentication › Providers › Apple with your own Apple Developer account, and add your app's bundle ID there. Give Apple the hashed nonce and Espobase the raw one:

import 'package:sign_in_with_apple/sign_in_with_apple.dart';

Future<void> signInWithApple() async {
  final nonce = AppleNonce();
  final credential = await SignInWithApple.getAppleIDCredential(
    scopes: [AppleIDAuthorizationScopes.email],
    nonce: nonce.hashed, // Apple gets the hash …
  );
  await espobase.auth.signInWithIdToken(
    token: credential.identityToken!,
    nonce: nonce.raw, // … Espobase gets the raw value
  );
}

7. Sign in with Google or GitHub

import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';

Future<void> signInWithGoogle() async {
  final url = await espobase.auth.signInWithOAuth(
    provider: OAuthProvider.google,
    redirectTo: 'myapp://auth/callback',
  );
  final result = await FlutterWebAuth2.authenticate(url: url.toString(), callbackUrlScheme: 'myapp');
  final code = Uri.parse(result).queryParameters['code'];
  if (code != null) await espobase.auth.exchangeCodeForSession(code);
}

Turn the provider on under Authentication › Providers first. The app opens the provider in the system browser and gets the user back through your app link; exchangeCodeForSession finishes the sign-in. Only this app can finish it: the one-time secret stays on the phone.

8. Read and write your data

final todos = await espobase.from('todos').select().order('created_at'); // List<Map<String, dynamic>>

// owner_id fills itself in (its default is espobase.user_id()).
await espobase.from('todos').insert({'title': 'Buy milk'});
await espobase.from('todos').update({'done': true}).eq('id', todo['id']);
await espobase.from('todos').delete().eq('id', todo['id']);

Every request carries the signed-in user, and the database applies your rule. A user who isn't signed in gets an empty list, and nobody can read or change someone else's todos, even by calling the API directly.

9. Upload a photo

Create a bucket under Storage and add a policy so each user can write to their own folder (see the file uploads guide). Pass the file's bytes to upload: File uploads

final bytes = await pickedFile.readAsBytes();
final userId = (await espobase.auth.currentUser())!.id;
await espobase.storage.from('photos').upload('$userId/photo.jpg', bytes, contentType: 'image/jpeg');

Good to know

  • Errors are EspobaseExceptions with a code (for example invalid_credentials) and a message you can show.
  • Realtime isn't in this package yet: use the REST API for now.
  • The same package works in server-side Dart.