Bloc + Clean Architecture + Appwrite — The Enterprise Flutter Stack

Guidestate management#bloc#clean-architecture#appwrite#flutter#enterprise

Bloc + Clean Architecture + Appwrite delivers predictable events/states, Clean layers, and a backend you can self-host. The enterprise Flutter stack, explained.

Arjun Mahar
Arjun Mahar@arjun_mahar1
4 min read

Stack Configuration — what FlutterInit generates for this guide

Architecture
Clean Architecture
State Management
Bloc
Backend
Appwrite
Navigation
go_router

When to choose this stack

Choose this stack when you need event/state predictability for large teams, Clean Architecture boundaries that survive feature growth, and a backend you can self-host. Bloc's explicit events make auth and domain flows auditable; Appwrite gives you Auth/DB/Storage with an open-source, self-host option when Firebase's cloud-only model is a non-starter.

Bloc + Clean Architecture + Appwrite is the enterprise Flutter stack: explicit events and states, Clean data / domain / presentation folders, and an Appwrite backend you can run on cloud or self-host. Predictable state transitions plus vendor-flexible infra. FlutterInit generates the blocs, repository boundaries, and Appwrite client (endpoint + project id) so the team starts from a shared shape.

What This Stack Generates

Select Bloc + Clean Architecture + Appwrite and you get:

  • Clean feature folderslib/src/features/auth/{data,domain,presentation}/
  • BlocAuthBloc / SessionBloc with LoginRequested-style events and loading states
  • Appwriteappwrite package, Client + Account, email-password sessions via createEmailPasswordSession
  • go_router — auth redirects driven by session state

Compared to Firebase stacks: no Google Services files. You configure APPWRITE_ENDPOINT and APPWRITE_PROJECT_ID instead — and you can point that endpoint at Appwrite Cloud or your own server.

Project Structure

Why Bloc Fits Enterprise Teams

Bloc forces every state change through an event. That sounds ceremonial until you're debugging a login race in a six-person team.

  • Events — what happened (LoginRequested, SignUpRequested)
  • States — what the UI shows (isLoading, success/error handled via side effects)
  • Handlers — pure-ish async methods that emit new states

No silent notifyListeners() from a random widget. You can log every event in CI and replay flows in tests.

presentation/providers/auth_bloc.dart
class AuthBloc extends Bloc<AuthEvent, AuthState> {
  final AuthRepository _repository;

  AuthBloc({required AuthRepository repository})
      : _repository = repository,
        super(const AuthState.initial()) {
    on<LoginRequested>(_onLoginRequested);
    on<SignUpRequested>(_onSignUpRequested);
    on<ForgotPasswordRequested>(_onForgotPasswordRequested);
  }

  Future<void> _onLoginRequested(
    LoginRequested event,
    Emitter<AuthState> emit,
  ) async {
    emit(state.copyWith(isLoading: true));

    final result = await _repository.login(
      email: event.email,
      password: event.password,
    );

    result.fold(
      (failure) {
        emit(state.copyWith(isLoading: false));
        if (event.context.mounted) {
          showToast(event.context, message: failure.message, status: 'error');
        }
      },
      (_) {
        emit(state.copyWith(isLoading: false));
        if (event.context.mounted) {
          event.context.go(AppRoutes.home);
        }
      },
    );
  }
}

abstract class AuthEvent extends Equatable {
  const AuthEvent();
  @override
  List<Object> get props => [];
}

class LoginRequested extends AuthEvent {
  final BuildContext context;
  final String email;
  final String password;
  const LoginRequested({
    required this.context,
    required this.email,
    required this.password,
  });
}

class AuthState extends Equatable {
  final bool isLoading;
  const AuthState({required this.isLoading});
  const AuthState.initial() : isLoading = false;

  AuthState copyWith({bool? isLoading}) {
    return AuthState(isLoading: isLoading ?? this.isLoading);
  }

  @override
  List<Object?> get props => [isLoading];
}

UI dispatches events. It doesn't call repositories. That's the boundary enterprise code reviews care about.

Clean Layers Keep Appwrite Swappable

Domain never imports package:appwrite. The repository interface is the contract; data owns the SDK.

domain/repositories/auth_repository.dart
abstract class AuthRepository {
  Stream<AppUser?> get onAuthStateChanged;

  FutureEither<AppUser> login({
    required String email,
    required String password,
  });

  FutureEither<AppUser> signUp({
    required String name,
    required String email,
    required String password,
  });

  FutureEither<void> logout();
}

AuthRepositoryImpl maps Appwrite responses into AppUser. Swap the service later — domain and blocs stay put.

Appwrite — Endpoint + Project ID (Self-Host Friendly)

FlutterInit initializes the Appwrite client from env:

config/app_config.dart
appwriteClient = Client()
  ..setEndpoint(
    dotenv.get('APPWRITE_ENDPOINT', fallback: 'https://cloud.appwrite.io/v1'),
  )
  ..setProject(
    dotenv.get('APPWRITE_PROJECT_ID', fallback: 'your-project-id'),
  )
  ..setSelfSigned(status: true);

appwriteAccount = Account(appwriteClient);

Auth uses Account sessions:

services/auth_service.dart
FutureEither<Map<String, dynamic>?> login({
  required String email,
  required String password,
}) async {
  return runTask(() async {
    await _account.createEmailPasswordSession(
      email: email,
      password: password,
    );
    final user = await _account.get();
    return {
      'id': user.$id,
      'email': user.email,
      'name': user.name,
    };
  }, requiresNetwork: true);
}

Why enterprises care: point APPWRITE_ENDPOINT at Appwrite Cloud today, or at a self-hosted instance behind your VPC tomorrow. Same Flutter code. Firebase doesn't give you that lever.

For a fuller backend comparison, see Appwrite vs Firebase vs Supabase.

When to Choose This vs Alternatives

Pick Bloc + Clean + Appwrite when:

  • Multiple developers touch auth and domain flows — events make intent reviewable
  • Compliance or ops wants self-hosting (or the option to move there)
  • You're already standardized on Bloc elsewhere in the org

Skip it when:

  • The team is small and wants less ceremony — Provider + MVVM + Firebase is faster to teach
  • You specifically want Supabase's Postgres + RLS story — see Bloc + Feature-First + Supabase
  • You don't need Clean's folder tax yet — Feature-First + Bloc is enough structure for many mid-size apps

Ready to Generate?

Open the dashboard, choose Clean + Bloc + Appwrite + go_router, set APPWRITE_ENDPOINT and APPWRITE_PROJECT_ID, and ship from a stack your team can audit and self-host.

Ready to build?

Generate this project in seconds

FlutterInit scaffolds the entire structure described in this guide — wired up, typed, and ready for flutter run.

Start Generating →