diff --git a/sites/www/content/blog/converted-genlatte-fullstack-dart/images/DartFullStack_GenLatteArt.webp b/sites/www/content/blog/converted-genlatte-fullstack-dart/images/DartFullStack_GenLatteArt.webp
new file mode 100644
index 0000000000..589a2b40be
Binary files /dev/null and b/sites/www/content/blog/converted-genlatte-fullstack-dart/images/DartFullStack_GenLatteArt.webp differ
diff --git a/sites/www/content/blog/converted-genlatte-fullstack-dart/index.md b/sites/www/content/blog/converted-genlatte-fullstack-dart/index.md
new file mode 100644
index 0000000000..f9017e514e
--- /dev/null
+++ b/sites/www/content/blog/converted-genlatte-fullstack-dart/index.md
@@ -0,0 +1,267 @@
+---
+title: "How I converted GenLatte to fullstack Dart"
+description: >-
+ And reduced the app's server bill
+publishDate: 2026-09-22
+author: craiglabenz
+image: images/DartFullStack_GenLatteArt.webp
+socialImage: images/DartFullStack_GenLatteArt.webp
+category: deep-dive
+layout: blog
+---
+
+
+
+If you've followed the
+[winding saga of the Flutter team's venture into running pop-up coffee stands](/blog/how-we-built-a-flutter-powered-ai-coffee-shop),
+you know that we combined Flutter, Firebase, and Gemini to serve whimsical coffees.
+You also know that, by not charging anything, we failed to make a profit.
+
+Further, if you looked at
+[the code](https://github.com/flutter/demos/blob/main/genlatte),
+you might also have noticed that our Flutter front-end was complemented
+by Firebase functions written in Node.js.
+This is a somewhat surprising historical artifact,
+given that Firebase's support for Dart functions entered public preview
+at almost the exact same time as GenLatte's first appearance at Google Cloud Next.
+
+
+
+But that's just the thing—with Firebase's Dart support coming in hot
+at the very last second, we simply couldn't commit to using it for a
+production app with the-show-must-go-on time pressure.
+If Firebase's support for Dart had been delayed for any reason,
+we might not have been able to deploy in time for Cloud Next.
+And so, when GenLatte appeared in the months of April, May, or June;
+it did so with a Node.js backend.
+
+*And I took that personally.*
+
+## Fully leaning in to fullstack Dart
+
+Dart and JavaScript are different languages with different strengths.
+In some sense, this is a bit of a *No duh* statement,
+but it also has deeper implications for a server-side migration,
+so a 1-to-1 rewrite is probably not worth doing. After all,
+Dart on the server can enjoy end-to-end type safety with Dart
+on the client and it would be a real shame to stick with untyped Maps.
+
+With this in mind, and with eyes arguably bigger than my stomach,
+I set out to *dramatically* rewrite GenLatte. Amongst my many goals:
+
+* Share models across the full stack (which was a small lift given
+ my original decision to place all data classes in a separate
+ `genlatte_data` package)
+* Reduce our Firebase Functions footprint to a single deployable function
+* Remove all client-side writes; instead focusing on calling server-side functions
+* Remove all server-side triggers and, instead,
+ explicitly perform any data mutations in server-side functions called
+ from the client
+* Persist all role-based ACL checks
+* Finally have end-to-end tests!
+
+These goals were lofty and not exactly on my 2026 roadmap, so, naturally,
+I kept my plans a secret and got typing!
+
+## Executing the migration
+
+Unlike my work on the original GenLatte, which was largely developed
+without the use of AI, I knew my crunched timeline necessitated bringing
+in a *Zoom zoom* typing assistant. My model of choice was Gemini 3.6 Flash
+and its combination of low latency and general know-how really came through for me.
+
+From a high level, I still read every line that Gemini wrote in order
+to maintain cognitive ownership of the project.
+This was made much easier by starting from a position of deep knowledge,
+and, without this commitment, I don't think the rewrite would have succeeded
+in the few days I had available. Coding assistants, for all their wonder,
+still greatly benefit from human guidance.
+
+### Sharing models
+
+Fullstack type safety means shared code across the client and server.
+As previously mentioned, this means breaking out any code that must run
+on the server into its own package that, critically,
+*must not* depend on the Flutter SDK. This is largely straightforward,
+but did require additions to my own data management package,
+[`pkg:data_layer`](https://pub.dev/packages/data_layer).
+One of its complementary packages,
+[`pkg:data_layer_firestore`](https://pub.dev/packages/data_layer_firestore),
+depends on the client-side Firebase SDK (which depends on Flutter),
+so I was forced to add
+[`pkg:data_layer_firestore_admin`](https://pub.dev/packages/data_layer_firestore_admin),
+which is pure-Dart and thus server-friendly.
+
+### Using a single Firebase function
+
+By default, each Firebase function you deploy is turned into its own
+Cloud Run service. (If you're a Firebase Functions user and this is news to you,
+navigate to Cloud Run in the Google Cloud console and check out how the Firebase
+sausage is made!)
+
+However, after leaning on over 15 separate Cloud Run services for
+GenLatte's Node.js era, I knew that I wanted a leaner setup for multiple reasons.
+First, deployments would be significantly faster, but even more importantly,
+*it would dramatically reduce GenLatte's server bill*.
+
+"But Craig!" you say, "Cloud Run spins down to zero while not in use,
+so is this actually important?"
+
+Great question. I'm glad you're paying attention.
+
+And yes, it is very important! While GenLatte was in use,
+various data writes and asynchronous tasks spun up all 15 of those services,
+and while each one would turn off while idle,
+that still had predictable effects on our server bill.
+But, to make matters worse, we set each service's minimum node count to 1
+while GenLatte was in use to avoid cold starts,
+which of course unplugged this spin-down-to-zero functionality.
+The end result was that GenLatte was surprisingly expensive to turn on.
+
+#### How to shove everything into one Firebase service
+
+To save money, I decided to take inspiration from
+[Serverpod](https://serverpod.dev) and introduce a `BackendMessage`
+[DTO](https://en.wikipedia.org/wiki/Data_transfer_object)
+to tell my singular Firebase function which inner function I wanted it to
+actually call. With some clever `pkg:freezed` trickery that Gemini helped me write,
+I was even able to get typed responses.
+
+The final class setup is a little convoluted, but is worth understanding
+if you like type-safety and saving money.
+
+**`BackendMessage`**
+
+This is the parent DTO class which I use in my actual function signatures.
+
+```dart
+abstract interface class BackendMessage {
+ /// Json serializer.
+ Json toJson();
+}
+```
+
+**`MessageParameters`**
+
+This implements `BackendMessage` and uses `pkg:freezed` to bind
+individual message types with expected response classes.
+
+The `@Implements.fromString` trick generates classes that satisfy
+the parent’s requirement to bind a `MessageResponse` type.
+And while the source class's use of raw strings feels type-dangerous,
+any typos lead to errors in the generated classes,
+meaning it functionally *is* type-safe.
+
+```dart
+@freezed
+sealed class MessageParameters with _$MessageParameters {
+ /// Creates a new latte order.
+ @Implements.fromString('BackendMessage')
+ const factory MessageParameters.saveOrder({
+ @LatteOrderConverter() required LatteOrder order,
+ }) = SaveOrderParameters;
+
+ /// Marks a latte order as completed.
+ @Implements.fromString('BackendMessage')
+ const factory MessageParameters.completeOrder({
+ required String orderId,
+ required String baristaId,
+ }) = CompleteOrderParameters;
+
+ // Many more message types...
+}
+```
+
+**`MessageResponse`**
+
+This closes the loop, declaring the response classes each method call expects.
+It offers a mix of individual response types for methods that want
+immediate feedback and empty responses to represent a sort of
+`202 Accepted` or `204 No Content` HTTP response.
+
+```dart
+@freezed
+sealed class MessageResponse with _$MessageResponse {
+ /// Return data for [SaveOrderParameters].
+ const factory MessageResponse.saveOrder({required LatteOrder? latteOrder}) =
+ SaveOrderResponse;
+
+ // Many more response types...
+
+ /// Placeholder for method calls which require no return value.
+ ///
+ /// This is typically because the client will pick up any state changes via
+ /// watched Firestore collections.
+ const factory MessageResponse.empty() = EmptyResponse;
+}
+```
+
+With my DTOs in order, I needed a singular server-side function to receive
+and route each incoming `BackendMessage` accordingly.
+
+```dart
+firebase.https.onCallWithData(
+ (request) {
+ final MessageResponse response = switch (request.data) {
+ SaveOrderParameters msg => saveOrder(msg),
+ CompleteOrderParameters msg => completeOrder(msg),
+ ...
+ };
+ return response.toJson();
+ },
+ ...
+}
+```
+
+Lastly, I needed `saveOrder` and `completeOrder` methods that
+satisfied their contract.
+
+```dart
+// Other Dart files
+
+Future saveOrder(SaveOrderParameters message) {}
+Future completeOrder(CompleteOrderParameters message) {}
+// Many more functions...
+```
+
+With these systems in place, I was able to shove arbitrarily many
+server-side operations into a single Dart function. *Helloooooooo savings!*
+
+### Refactoring all writes
+
+The above `BackendMessage` system set the stage to immediately achieve
+three more of my sub-goals:
+
+1. I removed all client-side writes by blocking them in my `firestore.rules` file.
+ I also refactored my data management layers to call the singular
+ backend function instead of ever calling direct Firestore functions
+ like `docRef.set()`.
+1. I similarly removed all Firestore triggers, but reinstantiated the missing
+ functionality into functions I could explicitly call from the client.
+1. I maintained GenLatte's strict permissions model by introducing ACL checks
+ in Dart code, which, as a testable system, helped me sleep at night.
+
+### Adding end-to-end tests
+
+For my final trick, I was able to bark orders at Gemini to write both
+server-side tests and, very excitingly, mock backend behavior in
+client-side tests that suddenly became end-to-end tests.
+Given my type-safe return values,
+this led to stability and app reliability that I was happy with!
+
+## The joy of a full-stack language
+
+Whether you like relational data and thus use something like
+[Serverpod](https://serverpod.dev), or a non-relational solution like Cloud Firestore,
+writing in the same language across your entire stack is a dream.
+And the wins just keep on coming if that language is Dart and you find yourself
+basking in the glow of fully sound null-safe type-safety.
+It truly creates the illusion that computers are actually there to be your friend!
+
+I enjoyed converting GenLatte to use Dart everywhere, and in doing so,
+sliced its server bill to a tiny fraction of what it was with Node,
+and increased reliability *and* performance.
+The money I spent on Gemini tokens executing the change was easily recouped
+after only a few minutes of operation. Now GenLatte can keep serving
+personalized coffees to event attendees long into the future,
+or until we get fed up running it.