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.
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
);
}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 acode(for exampleinvalid_credentials) and amessageyou can show. - Realtime isn't in this package yet: use the REST API for now.
- The same package works in server-side Dart.
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;
exchangeCodeForSessionfinishes the sign-in. Only this app can finish it: the one-time secret stays on the phone.