<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Antonis Psarras</title>
    <description>The latest articles on DEV Community by Antonis Psarras (@antonispsarras).</description>
    <link>https://dev.to/antonispsarras</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3992573%2F76c5bdfd-4bab-40ca-9281-4a5ee9375acf.jpg</url>
      <title>DEV Community: Antonis Psarras</title>
      <link>https://dev.to/antonispsarras</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/antonispsarras"/>
    <language>en</language>
    <item>
      <title>How I Built a Production-Grade Flutter &amp; Firebase App for $0 (and Open-Sourced It)</title>
      <dc:creator>Antonis Psarras</dc:creator>
      <pubDate>Fri, 19 Jun 2026 13:17:28 +0000</pubDate>
      <link>https://dev.to/antonispsarras/how-i-built-a-production-grade-flutter-firebase-app-for-0-and-open-sourced-it-298c</link>
      <guid>https://dev.to/antonispsarras/how-i-built-a-production-grade-flutter-firebase-app-for-0-and-open-sourced-it-298c</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3958m5cjh23h062xjxqt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3958m5cjh23h062xjxqt.png" alt=" " width="800" height="1772"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;.env&lt;/code&gt; files, hardcoded API keys, and Firebase configs that only work on the author’s machine.&lt;/p&gt;

&lt;p&gt;I wanted to build the exact opposite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ScholiLink&lt;/strong&gt; 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: &lt;strong&gt;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.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;⚠️ 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.&lt;/p&gt;

&lt;p&gt;⚠️ 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.&lt;/p&gt;

&lt;p&gt;⭐ &lt;strong&gt;&lt;a href="https://github.com/AntonisPsarras/Scholilink" rel="noopener noreferrer"&gt;View the Repo &amp;amp; Architecture on GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is how I solved the three biggest architectural challenges to make this happen, without spending a dime.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Client&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Flutter 3.38+, Dart 3.10+, Riverpod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Firebase Auth, Firestore, Cloud Functions (Node 22/TS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gemini via &lt;code&gt;@google/generative-ai&lt;/code&gt; (Server-side only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Local Dev&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Firebase Emulator Suite + deterministic AI mocks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faiiesy6w06spwqq1ua5r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faiiesy6w06spwqq1ua5r.png" alt=" " width="800" height="1772"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge 1: Zero Cloud Costs via Emulators
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt; A dual-mode architecture flipped by a single flag.&lt;/p&gt;

&lt;p&gt;ScholiLink runs in two modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Local Demo (&lt;code&gt;USE_LOCAL_EMULATORS=true&lt;/code&gt;):&lt;/strong&gt; All SDKs point at local emulators.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Live Cloud (&lt;code&gt;USE_LOCAL_EMULATORS=false&lt;/code&gt;):&lt;/strong&gt; Hits a real Firebase project.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When emulators are enabled, a core helper function wires every SDK to the right local port, handling Android's &lt;code&gt;10.0.2.2&lt;/code&gt; quirk automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/core/config.dart&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;emulatorHost&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kIsWeb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;'localhost'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defaultTargetPlatform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;TargetPlatform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;android&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;'10.0.2.2'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Android emulator routing&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;'127.0.0.1'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Challenge 2: Secure AI (Server-Side Gemini)&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;The Solution: The Flutter app never imports the generative AI SDK. All traffic routes through a single Firebase HTTPS Callable function.&lt;/p&gt;

&lt;p&gt;On the backend, a strict TypeScript Cloud Function acts as a gatekeeper:&lt;/p&gt;

&lt;p&gt;Authenticates the user via Firebase Auth.&lt;/p&gt;

&lt;p&gt;Deducts quota (a custom "Sparks" currency) via a Firestore transaction to prevent abuse.&lt;/p&gt;

&lt;p&gt;Resolves the API Key securely from Firebase Secret Manager.&lt;/p&gt;

&lt;p&gt;Streams output directly to a Firestore document, which the Flutter UI listens to via a .snapshots() stream.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// functions/src/index.ts (Teaser)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chatWithAi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;onCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enforceAppCheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;googleAiApiKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HttpsError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unauthenticated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Must be signed in.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ... Quota deduction, Secret Manager resolution, and Gemini streaming logic ...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Curious how the streaming logic works without WebSockets? Check out the full Cloud Function in the repo.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fth7ntdqva83wz6dc3oaw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fth7ntdqva83wz6dc3oaw.png" alt=" " width="800" height="1772"&gt;&lt;/a&gt;&lt;br&gt;
Challenge 3: Clean Architecture at Scale&lt;br&gt;
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.&lt;/p&gt;

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

&lt;p&gt;Auth as the Spine: Every feature watches the root auth stream. If the user logs out, dependent providers rebuild automatically.&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/AntonisPsarras/Scholilink
&lt;span class="nb"&gt;cd &lt;/span&gt;Student-Dashboard
&lt;span class="c"&gt;# Windows&lt;/span&gt;
.&lt;span class="se"&gt;\S&lt;/span&gt;tart-Demo.ps1
&lt;span class="c"&gt;# macOS / Linux&lt;/span&gt;
bash start-demo.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Demo login: &lt;a href="mailto:student@example.com"&gt;student@example.com&lt;/a&gt; / Passw0rd!)&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4pbfhpox4h66wq9p1a9k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4pbfhpox4h66wq9p1a9k.png" alt=" " width="800" height="1772"&gt;&lt;/a&gt;&lt;br&gt;
⭐ If this architecture helps you, consider starring the repo!&lt;/p&gt;

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

</description>
      <category>flutter</category>
      <category>firebase</category>
      <category>opensource</category>
      <category>gemini</category>
    </item>
  </channel>
</rss>
