DEV Community

Antonis Psarras
Antonis Psarras

Posted on

How I Built a Production-Grade Flutter & Firebase App for $0 (and Open-Sourced It)

I’ve seen a lot of open-source Flutter portfolio projects. Most look polished in screenshots but fall apart the moment you try to run them: missing .env files, hardcoded API keys, and Firebase configs that only work on the author’s machine.

I wanted to build the exact opposite.

ScholiLink started as a feature-rich student dashboard (optimized for Greek students). But this article isn’t about the product—it’s about the engineering bet underneath: Build a production-grade Flutter + Firebase app that any developer can clone and run locally with zero cloud spend, zero API keys, and zero friction.

⚠️ Disclaimer 1: ScholiLink is an unofficial, independent open-source portfolio project. It is provided "as is" for educational and architectural demonstration purposes only, without warranty, support, or maintenance. The author assumes no legal responsibility or liability for any direct or indirect consequences arising from cloning, deploying, or using this software.

⚠️ Disclaimer 2: ScholiLink's Screenshots are on the greek version of the app, but both English and Greek are perfectly supported through the in-app settings. Keep in mind though that the app is more oriented to the greek education system, with accurate grading and subjects, but could also be custom.

View the Repo & Architecture on GitHub

Here is how I solved the three biggest architectural challenges to make this happen, without spending a dime.

The Stack

Layer Technology
Client Flutter 3.38+, Dart 3.10+, Riverpod
Backend Firebase Auth, Firestore, Cloud Functions (Node 22/TS)
AI Gemini via @google/generative-ai (Server-side only)
Local Dev Firebase Emulator Suite + deterministic AI mocks

Challenge 1: Zero Cloud Costs via Emulators

Firebase is fantastic for shipping fast, but terrible for open-source portfolios where you want strangers to clone and run your code without creating their own cloud projects.

The Solution: A dual-mode architecture flipped by a single flag.

ScholiLink runs in two modes:

  1. Local Demo (USE_LOCAL_EMULATORS=true): All SDKs point at local emulators.
  2. Live Cloud (USE_LOCAL_EMULATORS=false): Hits a real Firebase project.

When emulators are enabled, a core helper function wires every SDK to the right local port, handling Android's 10.0.2.2 quirk automatically:

// lib/core/config.dart
static String get emulatorHost {
  if (kIsWeb) return 'localhost';
  switch (defaultTargetPlatform) {
    case TargetPlatform.android:
      return '10.0.2.2'; // Android emulator routing
    default:
      return '127.0.0.1';
  }
}
Enter fullscreen mode Exit fullscreen mode

To make onboarding magical, I wrote a single setup script (Start-Demo.ps1 / start-demo.sh). It boots the Firebase emulators, seeds the local database with dummy users, and launches Flutter. Zero cloud costs, zero setup.

Challenge 2: Secure AI (Server-Side Gemini)
For a student app featuring AI study help and OCR, Gemini is core. But the moment you drop a Gemini API key into a Flutter client, you’ve lost. Keys get extracted, and quotas get drained.

The Solution: The Flutter app never imports the generative AI SDK. All traffic routes through a single Firebase HTTPS Callable function.

On the backend, a strict TypeScript Cloud Function acts as a gatekeeper:

Authenticates the user via Firebase Auth.

Deducts quota (a custom "Sparks" currency) via a Firestore transaction to prevent abuse.

Resolves the API Key securely from Firebase Secret Manager.

Streams output directly to a Firestore document, which the Flutter UI listens to via a .snapshots() stream.

// functions/src/index.ts (Teaser)
export const chatWithAi = onCall(
  { enforceAppCheck: true, secrets: [googleAiApiKey] },
  async (request) => {
    if (!request.auth) throw new HttpsError('unauthenticated', 'Must be signed in.');
    // ... Quota deduction, Secret Manager resolution, and Gemini streaming logic ...
  }
);
Enter fullscreen mode Exit fullscreen mode

(Curious how the streaming logic works without WebSockets? Check out the full Cloud Function in the repo.)


Challenge 3: Clean Architecture at Scale
A real app with auth, messaging, AI, and profile settings needs structure, or it becomes a spaghetti import graph. I organized the app using Feature Folders (lib/features/auth, lib/features/ai_tutor) and leaned heavily on Riverpod for state management.

Instead of heavy code-generation or a global service locator, Riverpod handles everything cleanly. A few patterns I rely on:

Auth as the Spine: Every feature watches the root auth stream. If the user logs out, dependent providers rebuild automatically.

Auto-Disposing Listeners: Classroom and chat providers use StreamProvider.autoDispose so active Firestore listeners detach instantly when a screen pops, saving memory and reads.

Optimistic UI: For the AI chat, StateNotifier handles appending user messages instantly while waiting for the Cloud Function stream to start returning the AI's response.

Try It Yourself (Takes 60 Seconds)
It’s an open-source portfolio piece, so the code is meant to be read, cloned, and critiqued.

git clone https://github.com/AntonisPsarras/Scholilink
cd Student-Dashboard
# Windows
.\Start-Demo.ps1
# macOS / Linux
bash start-demo.sh
Enter fullscreen mode Exit fullscreen mode

(Demo login: student@example.com / Passw0rd!)

The hardest part of a Firebase portfolio project isn’t the UI. It’s making the repo runnable by strangers without handing them your credit card. If you’re building a Flutter + Firebase app and wrestling with emulator setup, secure API keys, or Riverpod, I hope this repository saves you a few evenings.


⭐ If this architecture helps you, consider starring the repo!

Questions? Drop a comment below—I’m happy to go deeper on emulator setup, Cloud Function security, or Riverpod patterns.

Top comments (0)