class GetDrawUseCase {
  final DrawRepository repo;
  Future<Either<Failure, Draw>> call(String id) =>
      repo.getDrawById(id);
}
abstract class DrawRepository {
  Future<Either<Failure, Draw>> getDrawById(String id);
  Future<Either<Failure, void>> saveDraw(Draw draw);
  Stream<List<Draw>> watchAllDraws();
}
export const generateInterpretation =
  onCall(async (request) => {
    const { uid } = request.auth;
    const { cards, intention } = request.data;

    await validateQuota(uid);
    const result = await geminiAI.generate({
      cards, intention, lang: "en"
    });
    await incrementQuota(uid);
    return { interpretation: result };
});
MultiBlocProvider(
  providers: [
    BlocProvider(create: (_) => AuthCubit(
      signIn: sl(), signOut: sl(), watchAuth: sl(),
    )),
    BlocProvider(create: (_) => DrawCubit(
      getDraw: sl(), createDraw: sl(),
    )),
    BlocProvider(create: (_) => JournalCubit(
      getEntries: sl(), deleteEntry: sl(),
    )),
  ],
  child: const App(),
)
class Draw extends Equatable {
  final String id;
  final List<TarotCard> cards;
  final DateTime createdAt;
  final Interpretation? interpretation;
}
class DrawRemoteDataSource {
  final FirebaseFirestore _firestore;

  Future<DrawModel> getDraw(String id) async {
    final doc = await _firestore
        .collection('users')
        .doc(userId)
        .collection('draws')
        .doc(id)
        .get();
    return DrawModel.fromFirestore(doc);
  }
}
GoRouter router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomePage(),
      routes: [
        GoRoute(
          path: 'draw/:id',
          builder: (context, state) =>
            DrawPage(id: state.pathParameters['id']!),
        ),
      ],
    ),
  ],
);
match /users/{userId}/draws/{drawId} {
  allow read: if request.auth.uid == userId;
  allow create: if request.auth.uid == userId
    && request.resource.data.keys().hasAll(
      ['cards', 'intention', 'createdAt']
    );
  allow delete: if request.auth.uid == userId;
}
class DrawCubit extends Cubit<DrawState> {
  final GetDrawUseCase _getDraw;
  final CreateDrawUseCase _createDraw;

  Future<void> loadDraw(String id) async {
    emit(DrawLoading());
    final result = await _getDraw(id);
    result.fold(
      (failure) => emit(DrawError(failure)),
      (draw) => emit(DrawLoaded(draw)),
    );
  }
}
sealed class DrawState extends Equatable {}
class DrawInitial extends DrawState {}
class DrawLoading extends DrawState {}
class DrawLoaded extends DrawState {
  final Draw draw;
}
class DrawError extends DrawState {
  final Failure failure;
}
class DrawRepositoryImpl implements DrawRepository {
  final DrawRemoteDataSource remote;
  final NetworkInfo networkInfo;

  @override
  Future<Either<Failure, Draw>> getDrawById(String id) async {
    try {
      final model = await remote.getDraw(id);
      return Right(model.toEntity());
    } on FirebaseException catch (e) {
      return Left(ServerFailure(e.message));
    }
  }
}
abstract class DrawRepository {
  Future<Either<Failure, Draw>> getDrawById(String id);
  Future<Either<Failure, void>> saveDraw(Draw draw);
  Stream<List<Draw>> watchAllDraws();
}
export const generateInterpretation =
  onCall(async (request) => {
    const { uid } = request.auth;
    const { cards, intention } = request.data;

    await validateQuota(uid);
    const result = await geminiAI.generate({
      cards, intention, lang: "en"
    });
    await incrementQuota(uid);
    return { interpretation: result };
});
MultiBlocProvider(
  providers: [
    BlocProvider(create: (_) => AuthCubit(
      signIn: sl(), signOut: sl(), watchAuth: sl(),
    )),
    BlocProvider(create: (_) => DrawCubit(
      getDraw: sl(), createDraw: sl(),
    )),
    BlocProvider(create: (_) => JournalCubit(
      getEntries: sl(), deleteEntry: sl(),
    )),
  ],
  child: const App(),
)
class GetDrawUseCase {
  final DrawRepository repo;
  Future<Either<Failure, Draw>> call(String id) =>
      repo.getDrawById(id);
}
abstract class DrawRepository {
  Future<Either<Failure, Draw>> getDrawById(String id);
  Future<Either<Failure, void>> saveDraw(Draw draw);
  Stream<List<Draw>> watchAllDraws();
}
class DrawRemoteDataSource {
  final FirebaseFirestore _firestore;

  Future<DrawModel> getDraw(String id) async {
    final doc = await _firestore
        .collection('users')
        .doc(userId)
        .collection('draws')
        .doc(id)
        .get();
    return DrawModel.fromFirestore(doc);
  }
}
GoRouter router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomePage(),
      routes: [
        GoRoute(
          path: 'draw/:id',
          builder: (context, state) =>
            DrawPage(id: state.pathParameters['id']!),
        ),
      ],
    ),
  ],
);
match /users/{userId}/draws/{drawId} {
  allow read: if request.auth.uid == userId;
  allow create: if request.auth.uid == userId
    && request.resource.data.keys().hasAll(
      ['cards', 'intention', 'createdAt']
    );
  allow delete: if request.auth.uid == userId;
}
class Draw extends Equatable {
  final String id;
  final List<TarotCard> cards;
  final DateTime createdAt;
  final Interpretation? interpretation;
}
sealed class DrawState extends Equatable {}
class DrawInitial extends DrawState {}
class DrawLoading extends DrawState {}
class DrawLoaded extends DrawState {
  final Draw draw;
}
class DrawError extends DrawState {
  final Failure failure;
}
class DrawRepositoryImpl implements DrawRepository {
  final DrawRemoteDataSource remote;
  final NetworkInfo networkInfo;

  @override
  Future<Either<Failure, Draw>> getDrawById(String id) async {
    try {
      final model = await remote.getDraw(id);
      return Right(model.toEntity());
    } on FirebaseException catch (e) {
      return Left(ServerFailure(e.message));
    }
  }
}
factory DrawModel.fromFirestore(DocumentSnapshot doc) {
  final data = doc.data() as Map<String, dynamic>;
  return DrawModel(
    id: doc.id,
    cards: (data['cards'] as List)
        .map((c) => TarotCard.fromMap(c))
        .toList(),
    intention: data['intention'] ?? '',
    createdAt: (data['createdAt'] as Timestamp).toDate(),
  );
}
class DrawCubit extends Cubit<DrawState> {
  final GetDrawUseCase _getDraw;
  final CreateDrawUseCase _createDraw;

  Future<void> loadDraw(String id) async {
    emit(DrawLoading());
    final result = await _getDraw(id);
    result.fold(
      (failure) => emit(DrawError(failure)),
      (draw) => emit(DrawLoaded(draw)),
    );
  }
}

I build apps from idea to deployment.

FlutterFlutter · FirebaseFirebase · TypeScriptTypeScript

Idea

You have an app idea

Discussion

We align on scope & goals

Build

I turn it into a real app

Live

Published on the stores

[ what I use ]

MindTarot

Built from scratch to store, live in production.

Clean Architecture

Each feature is independent. Easy to maintain, easy to test, easy to scale.

lib/features/draw/
├── data/
│   ├── datasources/
│   └── repositories/
├── domain/
│   ├── entities/
│   └── usecases/
└── presentation/
    ├── cubit/
    └── pages/

AI Integration

Smart AI readings. Built with Gemini, secured server-side.

FlutterClient FirebaseCF GeminiGemini FlutterReading

Security

Read-only client. All business logic runs server-side.

App Check Auth Server logic Firestore Rules Rate Limiting

Authentication

Quick sign-in with email, Google, or Apple.

Email Google Apple

Payments

Freemium model. Users pay, upgrade, cancel — everything handled automatically.

RevenueCat 3 CFs 7 Webhooks Server-side

Multilingual

3 languages. UI, AI responses, card data — everything adapts to the user's language.

EN FR RU

State Management

Fast, responsive UI. Every action gives instant feedback.

emit(DrawLoading());

final result = await
  drawUseCase(params);

emit(DrawComplete(result));

MindTarot

Free to download. Free trial available.

What I've built

[ live ]
MindTarot Tarot app with AI interpretations — live on stores website play ios
[ practice ]
study_flutter Hands-on Flutter practice — everything coded by hand, no AI github
dart_algos Algorithmic logic practice in Dart github
chat_app Tutorial project — real-time chat with Firebase Auth & Firestore github
la_tarot_academie Interactive tarot draw tool used in live webinars demo github
leadmagnet_audio Audio lead-magnet landing page for a tarot masterclass live github
[ client work ]
la-tarot-academy-website Full website for a tarot academy in progress

Phuvatat

Flutter developer · Self-taught

I started building the website for my cleaning business with no-code tools. They didn't let me do what I really wanted. All the no-code and low-code tools, all the courses — they were bad investments that disappointed me. So I decided to learn to code myself, so I could build my own projects.

It was hard at the beginning. The determination I built from eight years of jiu-jitsu got me through it. I know that learning means producing and moving forward through the obstacles and breakthroughs, layer by layer.

Over a year later, I have solid experience behind me and an app running in production. Ready for the next project.

Based in Thailand · Working internationally

What are we building next?

Available for freelance projects and full-time roles.

email pvtdev.app@gmail.com github github.com/PhuvatatDev

[ what I use ]

MindTarot

Built from scratch to store, live in production.

Clean Architecture

Each feature is independent. Easy to maintain, easy to test, easy to scale.

lib/features/draw/
├── data/
│   ├── datasources/
│   └── repositories/
├── domain/
│   ├── entities/
│   └── usecases/
└── presentation/
    ├── cubit/
    └── pages/

AI Integration

Smart AI readings. Built with Gemini, secured server-side.

FlutterClient FirebaseCF GeminiGemini FlutterReading

Security

Read-only client. All business logic runs server-side.

App Check Auth Server logic Firestore Rules Rate Limiting

Authentication

Quick sign-in with email, Google, or Apple.

Email Google Apple

Payments

Freemium model. Users pay, upgrade, cancel — everything handled automatically.

RevenueCat 3 CFs 7 Webhooks Server-side

Multilingual

3 languages. UI, AI responses, card data — everything adapts to the user's language.

EN FR RU

State Management

Fast, responsive UI. Every action gives instant feedback.

emit(DrawLoading());

final result = await
  drawUseCase(params);

emit(DrawComplete(result));

Free to download. Free trial available.