<?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: Olivier Leplus</title>
    <description>The latest articles on DEV Community by Olivier Leplus (@olivierleplus).</description>
    <link>https://dev.to/olivierleplus</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%2F154793%2F08cbc391-e0c9-4412-b8dd-61b21fb6eea2.jpeg</url>
      <title>DEV Community: Olivier Leplus</title>
      <link>https://dev.to/olivierleplus</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/olivierleplus"/>
    <language>en</language>
    <item>
      <title>Meet Cloudagotchi : Building a virtual pet with a cloud brain (Part 1)</title>
      <dc:creator>Olivier Leplus</dc:creator>
      <pubDate>Tue, 21 Jul 2026 09:38:15 +0000</pubDate>
      <link>https://dev.to/aws/meet-cloudagotchi-building-a-virtual-pet-with-a-cloud-brain-part-1-54j5</link>
      <guid>https://dev.to/aws/meet-cloudagotchi-building-a-virtual-pet-with-a-cloud-brain-part-1-54j5</guid>
      <description>&lt;p&gt;Remember Tamagotchis? Those little egg-shaped keychains from the 90s with a pixelated pet that got hungry, got sad, and, if you were a negligent eight-year-old like me, got &lt;em&gt;dead&lt;/em&gt; during math class.&lt;/p&gt;

&lt;p&gt;I've been looking for an excuse to play with the &lt;a href="https://www.waveshare.com/esp32-s3-touch-amoled-1.8.htm" rel="noopener noreferrer"&gt;Waveshare ESP32-S3 Touch AMOLED 1.8"&lt;/a&gt;, a $30 board with a gorgeous 368×448 AMOLED touchscreen, an accelerometer, a microphone, and a speaker. And I found one: I'm building a Tamagotchi. But with a twist that makes it worth a four-article series&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%2Fwlqe0dllq7hmnrathipe.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%2Fwlqe0dllq7hmnrathipe.png" alt=" " width="799" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pet's body lives on the device. Its brain lives in AWS.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The device renders an adorable pet and captures your taps and shakes. But its hunger, mood, and energy are rows in DynamoDB. It gets hungry overnight because EventBridge Scheduler says so. And, my favorite part (and because it's 2026, we can't not have genAI in a project 🤪), every morning it fetches the AWS news, has Amazon Bedrock rewrite them in its own squeaky personality, and reads them to me out loud through Amazon Polly. A Tamagotchi with a job.&lt;/p&gt;

&lt;p&gt;In this series, I will walk you through the whole build:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;This article&lt;/strong&gt; : the architecture, and connecting the ESP32-S3 to AWS IoT Core (the pet's nervous system).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 2&lt;/strong&gt; : giving it a face: animations and touch with LVGL on the AMOLED.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 3&lt;/strong&gt; : the serverless brain: Lambda, DynamoDB, and a pet that gets hungry while you sleep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 4&lt;/strong&gt; : Bedrock + Polly: my Tamagotchi reads me the AWS news.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Reality check before you get too excited (sorry 😅):&lt;/strong&gt; this is a hobby project, not a product. You'll need the specific Waveshare board (or the patience to adapt the code to yours), an AWS account, and a USB-C cable. The AWS bill for the whole series is well under a dollar a month, but it is not zero, and neither is the time you'll spend staring at &lt;code&gt;idf.py monitor&lt;/code&gt;. Worth it, though.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;All the code lives in &lt;a href="https://github.com/tagazok/cloudagotchi" rel="noopener noreferrer"&gt;this repository&lt;/a&gt;. Each article has its own branch. For this one, &lt;code&gt;git checkout article-1&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why put a pet's brain in the cloud?
&lt;/h2&gt;

&lt;p&gt;Fair question. The original Tamagotchi ran on a chip with less power than my USB cable. Why does mine need a cloud?&lt;/p&gt;

&lt;p&gt;Three reasons, and they're the same reasons real IoT products are built this way:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The pet needs to...&lt;/th&gt;
&lt;th&gt;Which means...&lt;/th&gt;
&lt;th&gt;AWS gives us...&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Get hungry &lt;em&gt;while the device is off&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Time must pass server-side&lt;/td&gt;
&lt;td&gt;EventBridge Scheduler + Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remember it was sad yesterday&lt;/td&gt;
&lt;td&gt;State must outlive reboots&lt;/td&gt;
&lt;td&gt;DynamoDB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Say something new every day&lt;/td&gt;
&lt;td&gt;Logic must evolve without reflashing&lt;/td&gt;
&lt;td&gt;Bedrock, deployed behind Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is going to save us a lot of time, trust me (been there flashing the device for every little change I did...). When the pet's brain is a Lambda function, giving it a new behavior is just a &lt;code&gt;cdk deploy&lt;/code&gt;, not a firmware update. The device stays dumb and stable; the personality iterates daily.&lt;/p&gt;

&lt;p&gt;This is, not coincidentally, the exact architecture of every serious connected device you own. Your robot vacuum is a Tamagotchi with a motor. We're just building the fun version.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;Here's the full picture we're building toward across the series:&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%2F8gm4nxh46xywggoyz2r7.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%2F8gm4nxh46xywggoyz2r7.png" alt="Cloudagotchi architecture diagram" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Today we're building the arrow in the middle. No pet on screen yet. First, the nervous system, then, the face. (If you came here for cuteness, part 2 will deliver, I promise.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The pet's vocabulary: MQTT topics
&lt;/h2&gt;

&lt;p&gt;The device and cloud talk over MQTT, the lingua franca of IoT: a lightweight pub/sub protocol where everyone publishes to &lt;em&gt;topics&lt;/em&gt;. Designing the topic space first is the IoT equivalent of designing your API routes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cloudagotchi/{thing}/hello&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;device → cloud&lt;/td&gt;
&lt;td&gt;"I just woke up, what did I miss?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cloudagotchi/{thing}/interaction&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;device → cloud&lt;/td&gt;
&lt;td&gt;"The human fed/petted/played with me"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cloudagotchi/{thing}/state&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cloud → device&lt;/td&gt;
&lt;td&gt;"Here are your current stats, render them"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cloudagotchi/{thing}/briefing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cloud → device&lt;/td&gt;
&lt;td&gt;"Fresh AWS news, go fetch the audio"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;{thing}&lt;/code&gt; is the device's unique name (&lt;code&gt;cloudagotchi-01&lt;/code&gt;). Baking it into the topic path matters for security, as you're about to see.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1 — The cloud side: an IoT policy with trust issues
&lt;/h2&gt;

&lt;p&gt;A device talking to AWS IoT Core authenticates with an &lt;strong&gt;X.509 certificate&lt;/strong&gt;, a private key that lives on the device and never leaves it. No API keys in the firmware, no secrets in your repo.&lt;/p&gt;

&lt;p&gt;But authentication says &lt;em&gt;who you are&lt;/em&gt;, not &lt;em&gt;what you may do&lt;/em&gt;. That's the &lt;strong&gt;IoT policy&lt;/strong&gt;, and here's the part that trips people up: a lazy policy (&lt;code&gt;iot:*&lt;/code&gt; on &lt;code&gt;*&lt;/code&gt;, every tutorial's dirty secret) means any compromised device can impersonate the whole fleet. Ours pins every permission to the device's own identity:&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;// backend/lib/iot-stack.ts — the interesting part.&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CloudagotchiDevicePolicy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;policyName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CloudagotchiDevicePolicy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;policyDocument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2012-10-17&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Statement&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;span class="c1"&gt;// 1. You may connect ONLY under your own name...&lt;/span&gt;
        &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Allow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iot:Connect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`arn:aws:iot:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:client/&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{iot:Connection.Thing.ThingName}`&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;span class="c1"&gt;// 2. ...publish ONLY on your own topics...&lt;/span&gt;
        &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Allow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iot:Publish&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`arn:aws:iot:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:topic/cloudagotchi/&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{iot:Connection.Thing.ThingName}/*`&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;span class="c1"&gt;// 3. ...and listen ONLY to your own topics.&lt;/span&gt;
        &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Allow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="p"&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;iot:Subscribe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`arn:aws:iot:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:topicfilter/cloudagotchi/&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{iot:Connection.Thing.ThingName}/*`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="c1"&gt;// (+ iot:Receive, same resource shape — see the repo)&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;The magic is &lt;code&gt;${iot:Connection.Thing.ThingName}&lt;/code&gt;, an IoT policy variable that resolves &lt;em&gt;at connection time&lt;/em&gt; to the name of the thing attached to the certificate. One policy serves the whole fleet, yet &lt;code&gt;cloudagotchi-01&lt;/code&gt; physically cannot publish on &lt;code&gt;cloudagotchi-02&lt;/code&gt;'s topics. If your pet ever gets kidnapped at a conference (mine will, eventually), the blast radius is one sad pet.&lt;/p&gt;

&lt;p&gt;Deploy it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;backend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx cdk deploy CloudagotchiIotStack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2 — A birth certificate for the pet
&lt;/h2&gt;

&lt;p&gt;Certificates are secrets, so they don't belong in CloudFormation state, we create them per-device with a small script (&lt;a href="https://github.com/tagazok/cloudagotchi/blob/main/scripts/provision-device.sh" rel="noopener noreferrer"&gt;&lt;code&gt;scripts/provision-device.sh&lt;/code&gt;&lt;/a&gt; in the repo). The gist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create the thing (the device's identity in AWS).&lt;/span&gt;
aws iot create-thing &lt;span class="nt"&gt;--thing-name&lt;/span&gt; cloudagotchi-01

&lt;span class="c"&gt;# Create a certificate + private key, saved straight into the firmware tree.&lt;/span&gt;
&lt;span class="nv"&gt;CERT_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws iot create-keys-and-certificate &lt;span class="nt"&gt;--set-as-active&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--certificate-pem-outfile&lt;/span&gt; firmware/main/certs/device-cert.pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--private-key-outfile&lt;/span&gt;   firmware/main/certs/device-key.pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; certificateArn &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Tie it all together: this cert may act as this thing, under this policy.&lt;/span&gt;
aws iot attach-policy &lt;span class="nt"&gt;--policy-name&lt;/span&gt; CloudagotchiDevicePolicy &lt;span class="nt"&gt;--target&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CERT_ARN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
aws iot attach-thing-principal &lt;span class="nt"&gt;--thing-name&lt;/span&gt; cloudagotchi-01 &lt;span class="nt"&gt;--principal&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CERT_ARN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command in the repo version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/provision-device.sh cloudagotchi-01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;The &lt;code&gt;.pem&lt;/code&gt; files are gitignored, keep it that way.&lt;/strong&gt; The private key is the pet's identity. Anyone who has it &lt;em&gt;is&lt;/em&gt; your pet, as far as AWS is concerned. The repo ignores &lt;code&gt;certs/*.pem&lt;/code&gt; except Amazon's public root CA, and you should never weaken that line.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 3 — The firmware: Wi-Fi, then mutual TLS
&lt;/h2&gt;

&lt;p&gt;The firmware is plain &lt;strong&gt;ESP-IDF&lt;/strong&gt; (Espressif's native framework) plus the official Waveshare board support package. Why not Arduino? ESP-IDF's built-in &lt;code&gt;esp-mqtt&lt;/code&gt; client does mutual TLS natively, and the BSP hands us the display, touch, and audio drivers we'll need in parts 2 and 4, less glue code overall, which matters when every line has to fit in a blog post.&lt;/p&gt;

&lt;p&gt;The build embeds the three credential files into the binary, no filesystem juggling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cmake"&gt;&lt;code&gt;&lt;span class="c1"&gt;# firmware/main/CMakeLists.txt&lt;/span&gt;
&lt;span class="nf"&gt;idf_component_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    SRCS &lt;span class="s2"&gt;"main.c"&lt;/span&gt; &lt;span class="s2"&gt;"app_wifi.c"&lt;/span&gt; &lt;span class="s2"&gt;"app_mqtt.c"&lt;/span&gt;
    INCLUDE_DIRS &lt;span class="s2"&gt;"."&lt;/span&gt;
    EMBED_TXTFILES
        &lt;span class="s2"&gt;"certs/AmazonRootCA1.pem"&lt;/span&gt;
        &lt;span class="s2"&gt;"certs/device-cert.pem"&lt;/span&gt;
        &lt;span class="s2"&gt;"certs/device-key.pem"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the MQTT connection is disarmingly small. This is the entire handshake with AWS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// firmware/main/app_mqtt.c — the heart of it.&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;esp_mqtt_client_config_t&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;broker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 1. AWS IoT speaks MQTT over TLS on port 8883.&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"mqtts://"&lt;/span&gt; &lt;span class="n"&gt;CONFIG_CLOUDAGOTCHI_IOT_ENDPOINT&lt;/span&gt; &lt;span class="s"&gt;":8883"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// 2. We verify we're talking to the real AWS...&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;certificate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;root_ca_pem&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;span class="n"&gt;credentials&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 3. ...and AWS verifies it's talking to the real us.&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authentication&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;certificate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;device_cert_pem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;device_key_pem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="c1"&gt;// 4. Our policy requires client id == thing name. Not optional!&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONFIG_CLOUDAGOTCHI_THING_NAME&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;span class="n"&gt;s_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;esp_mqtt_client_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;esp_mqtt_client_start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_client&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's mutual TLS: both sides prove who they are before a single byte of MQTT flows. Step 4 is the one people forget, if the client id doesn't match the thing name, our policy's &lt;code&gt;iot:Connect&lt;/code&gt; statement rejects the connection, and you get the single most cryptic failure mode in IoT: an endless connect/disconnect loop with no error message. You've been warned. 😅&lt;/p&gt;

&lt;p&gt;On connect, the pet introduces itself and subscribes to its own topics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;MQTT_EVENT_CONNECTED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;// Listen to everything addressed to *this* pet...&lt;/span&gt;
    &lt;span class="n"&gt;esp_mqtt_client_subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"cloudagotchi/"&lt;/span&gt; &lt;span class="n"&gt;THING&lt;/span&gt; &lt;span class="s"&gt;"/#"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ...and announce we're awake. In part 3, a Lambda answers&lt;/span&gt;
    &lt;span class="c1"&gt;// this with the pet's current state. Today, only silence. 🥲&lt;/span&gt;
    &lt;span class="n"&gt;app_mqtt_publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;firmware&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;article-1&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;}"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main loop just heartbeats every 30 seconds so we have a pulse to watch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Proof of life
&lt;/h2&gt;

&lt;p&gt;Configure and flash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;firmware
idf.py set-target esp32s3
idf.py menuconfig     &lt;span class="c"&gt;# → "Cloudagotchi Configuration": Wi-Fi + IoT endpoint + thing name&lt;/span&gt;
idf.py flash monitor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Your IoT endpoint: &lt;code&gt;aws iot describe-endpoint --endpoint-type iot:Data-ATS&lt;/code&gt;.)&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%2F6pltx1edswatzaozos1f.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%2F6pltx1edswatzaozos1f.png" alt="Cloudagotchi menuconfig screenshot" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Region gotcha that will produce the most confusing failure of your build.&lt;/strong&gt; The device's endpoint and your deployed stacks must be in the &lt;strong&gt;same region&lt;/strong&gt;, the endpoint hostname looks almost identical across regions (&lt;code&gt;xxxx-ats.iot.us-east-1...&lt;/code&gt; vs &lt;code&gt;xxxx-ats.iot.us-west-2...&lt;/code&gt;), and if they don't match, TLS connects fine and then AWS silently drops you: an endless connect/EOF loop with no error message anywhere. Always take the endpoint from the same CLI region you deployed to (check with &lt;code&gt;aws configure get region&lt;/code&gt;), and pass that same value everywhere it's needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the gods of embedded development smile upon you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I (2841) app_wifi: Wi-Fi connected
I (4127) app_mqtt: Connected to AWS IoT Core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the fun part. Open the &lt;strong&gt;AWS IoT console → MQTT test client&lt;/strong&gt;, subscribe to &lt;code&gt;cloudagotchi/#&lt;/code&gt;, and watch your pet's heartbeat arrive from your desk:&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%2Fs28671i3jwi5rxgot8ek.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%2Fs28671i3jwi5rxgot8ek.png" alt="AWS IoT MQTT test client showing heartbeat messages" width="800" height="854"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;cloudagotchi/cloudagotchi&lt;/span&gt;&lt;span class="mi"&gt;-01&lt;/span&gt;&lt;span class="err"&gt;/heartbeat&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"alive"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And talk back! Publish &lt;code&gt;{"msg":"hello little one"}&lt;/code&gt; to &lt;code&gt;cloudagotchi/cloudagotchi-01/greeting&lt;/code&gt; and watch it land in your serial monitor:&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%2F3t42r69d5rl9yglrckc0.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%2F3t42r69d5rl9yglrckc0.png" alt="Publishing a message from the AWS IoT console to the device" width="800" height="480"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;I&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;91204&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cloudagotchi:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Cloud&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;says&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;cloudagotchi/cloudagotchi&lt;/span&gt;&lt;span class="mi"&gt;-01&lt;/span&gt;&lt;span class="err"&gt;/greeting&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"hello little one"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A message typed in a browser, through AWS, to a $30 board on your desk, in about 100 ms. It never stops being cool.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Everything we built today (certificate identity, least-privilege topic isolation, a scoped policy shared by a fleet) is the same foundation you'd lay for a real product with a million devices. The difference between a toy IoT tutorial and production IoT isn't the hardware; it's whether &lt;code&gt;${iot:Connection.Thing.ThingName}&lt;/code&gt; shows up in the policy. Now it's in ours, and we never have to think about it again.&lt;/p&gt;

&lt;p&gt;Our pet has a nervous system. It can feel (receive) and speak (publish). What it doesn't have is a face, it's currently the philosophical-zombie phase of the project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next article:&lt;/strong&gt; we fix that. LVGL on the AMOLED, a pet with blinking eyes and moods, touch interactions, and the accelerometer, because a pet you can't tickle by shaking the board is just a dashboard.&lt;/p&gt;




&lt;h3&gt;
  
  
  Try it yourself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/tagazok/cloudagotchi" rel="noopener noreferrer"&gt;The Cloudagotchi repo&lt;/a&gt; : &lt;code&gt;git checkout article-1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.waveshare.com/wiki/ESP32-S3-Touch-AMOLED-1.8" rel="noopener noreferrer"&gt;Waveshare ESP32-S3 Touch AMOLED 1.8 wiki&lt;/a&gt; : board docs and schematics&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/iot/latest/developerguide/thing-policy-variables.html" rel="noopener noreferrer"&gt;AWS IoT Core policy variables&lt;/a&gt; : the &lt;code&gt;ThingName&lt;/code&gt; trick, officially&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/protocols/mqtt.html" rel="noopener noreferrer"&gt;ESP-MQTT docs&lt;/a&gt; : the client we used&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Disclosure:&lt;/strong&gt; I'm a Developer Advocate at AWS, that's the cloud my Tamagotchi thinks with. The device-side patterns (mutual TLS, scoped topics) apply to any MQTT broker.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>aws</category>
      <category>iot</category>
      <category>esp32</category>
      <category>ai</category>
    </item>
    <item>
      <title>I built a Tamagotchi with its brain in AWS (and it reads me the news)</title>
      <dc:creator>Olivier Leplus</dc:creator>
      <pubDate>Mon, 20 Jul 2026 11:30:09 +0000</pubDate>
      <link>https://dev.to/aws/i-built-a-tamagotchi-with-its-brain-in-aws-and-it-reads-me-the-news-481n</link>
      <guid>https://dev.to/aws/i-built-a-tamagotchi-with-its-brain-in-aws-and-it-reads-me-the-news-481n</guid>
      <description>&lt;p&gt;Remember Tamagotchis? Those little egg-shaped keychains with a pixelated pet that got hungry, got sad, and, if you were a negligent eight-year-old like me, got &lt;em&gt;dead&lt;/em&gt; during math class.&lt;/p&gt;

&lt;p&gt;I built one. Except mine has a 368×448 AMOLED screen, an accelerometer, a speaker... and its brain runs on AWS.&lt;/p&gt;

&lt;p&gt;Meet &lt;strong&gt;Cloudagotchi&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/5WiaUMA3A3Q"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It &lt;strong&gt;gets hungry while the device is off&lt;/strong&gt; : time passes in the cloud, not on the chip. EventBridge Scheduler decides when it's dinner time.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;remembers you ignored it yesterday&lt;/strong&gt; : its feelings are a DynamoDB row. You can't reset a guilt trip by rebooting.&lt;/li&gt;
&lt;li&gt;You &lt;strong&gt;feed it by tapping an avocado&lt;/strong&gt;, pet it by tapping its face, play with it by shaking the board. Every interaction is an MQTT round trip through AWS IoT Core.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;dozes off&lt;/strong&gt; if you leave it alone, complete with a little &lt;em&gt;zzz&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;And my favorite part: &lt;strong&gt;every morning at 7:03, it reads me the AWS news out loud&lt;/strong&gt; : Amazon Bedrock rewrites the headlines in the pet's cute voice, Amazon Polly gives it speech, and the little face moves its mouth while it talks. A Tamagotchi with a job.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it works (the 30-second version)
&lt;/h2&gt;

&lt;p&gt;The device is the body; AWS is the brain. The ESP32 renders sprites and captures your taps. But every feeling, every stat, every decision comes from the cloud:&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%2Fnk991ulg0j8z9nwjzqfx.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%2Fnk991ulg0j8z9nwjzqfx.png" alt="Cloudagotchi Architecture Diagram" width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The stack, at a glance: &lt;strong&gt;ESP-IDF + LVGL&lt;/strong&gt; on a &lt;a href="https://www.waveshare.com/esp32-s3-touch-amoled-1.8.htm" rel="noopener noreferrer"&gt;Waveshare ESP32-S3 Touch AMOLED 1.8"&lt;/a&gt;, talking mutual-TLS MQTT to &lt;strong&gt;AWS IoT Core&lt;/strong&gt;, with &lt;strong&gt;Lambda&lt;/strong&gt;, &lt;strong&gt;DynamoDB&lt;/strong&gt; and &lt;strong&gt;EventBridge Scheduler&lt;/strong&gt; as the serverless brain, and a &lt;strong&gt;Bedrock → Polly → S3&lt;/strong&gt; pipeline for the morning briefing. All infrastructure defined with &lt;strong&gt;CDK&lt;/strong&gt;. Total running cost: well under a dollar a month.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Not coincidentally, this is the exact architecture of every serious connected device you own. Your robot vacuum is a Tamagotchi with a motor. We're just building the fun version.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The series
&lt;/h2&gt;

&lt;p&gt;I'm turning the whole build into four articles, each with its own git branch so you can follow along commit by commit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;This is article 0 ^^&lt;/strong&gt;: It's a teaser so you come back to see the rest of the project 🤫&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/aws/meet-cloudagotchi-building-a-virtual-pet-with-a-cloud-brain-part-1-54j5"&gt;Meet Cloudagotchi: A virtual pet with a cloud brain&lt;/a&gt;&lt;/strong&gt; : the architecture, and connecting the board to AWS IoT Core with mutual TLS (plus the IoT policy trick that makes a kidnapped pet a non-event).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[Coming July 23rd] Giving it a face&lt;/strong&gt; : sprite-based LVGL UI, blinking, dozing, shake detection... and the very honest story of how my first "elegant" procedural face became a corrupted mess on real hardware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[Coming July 27st] It gets hungry while You sleep&lt;/strong&gt; : the serverless state machine: Lambda, DynamoDB, and the lazy-time-accounting trick that makes a pet age without a single always-on process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[Coming July 29st] My tamagotchi reads me the AWS News&lt;/strong&gt; : Bedrock as a squeaky-voiced news anchor, Polly's one-parameter trick that deleted an entire firmware subsystem, and the four embedded-streaming gotchas nobody warns you about.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fair warning: this series contains real war stories. Panels that wedge themselves, buffers defeated by presigned URLs, a TCP window doing trans-Atlantic math against me. The failures were the best teachers, so they're all in there.&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%2Fwb868sgv6tr3orc0jp5y.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%2Fwb868sgv6tr3orc0jp5y.png" alt="The Cloudagotchi device on my desk" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 1 drops on Tuesday, July 20th. Follow me to catch it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The code is open source: &lt;a href="https://github.com/tagazok/cloudagotchi" rel="noopener noreferrer"&gt;github.com/tagazok/cloudagotchi&lt;/a&gt;. And, if you build one, please show me your pet. 👻&lt;/p&gt;

</description>
      <category>aws</category>
      <category>iot</category>
      <category>esp32</category>
      <category>ai</category>
    </item>
    <item>
      <title>AI Without a Backend: The Browser's Built-in AI APIs for Web Developers</title>
      <dc:creator>Olivier Leplus</dc:creator>
      <pubDate>Wed, 08 Jul 2026 14:12:29 +0000</pubDate>
      <link>https://dev.to/olivierleplus/ai-without-a-backend-the-browsers-built-in-ai-apis-for-web-developers-2745</link>
      <guid>https://dev.to/olivierleplus/ai-without-a-backend-the-browsers-built-in-ai-apis-for-web-developers-2745</guid>
      <description>&lt;p&gt;For years, "adding AI to your web app" meant the same thing: sign up for an API key, wire up a backend proxy so you don't leak that key to the browser, meter your token usage, handle rate limits, and pray your bill doesn't explode when a feature goes viral. Every summary, every classification, every bit of generated text was a round-trip to someone else's data center.&lt;/p&gt;

&lt;p&gt;Chrome is quietly changing that. A family of &lt;strong&gt;built-in AI APIs&lt;/strong&gt; now ships a language model — Google's Gemini Nano — &lt;em&gt;inside the browser&lt;/em&gt;. Your JavaScript can summarize an article, translate a comment, or generate structured data from an image without a single network call to an inference server. No API key. No backend. No per-token bill. And because the model runs on-device, the user's data never leaves their machine.&lt;/p&gt;

&lt;p&gt;In this article, I will walk you through what these APIs are, how to use them safely, and two hands-on examples: summarizing user reviews, and turning a product photo into structured JSON with the Prompt API.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Reality check before you get too excited (sorry 😅):&lt;/strong&gt; these APIs are desktop-only for now (no mobile), require a fairly capable machine, and the model is a ~4 GB download on first use. They're production-viable as &lt;em&gt;progressive enhancements&lt;/em&gt;, not as your only code path. We'll cover graceful degradation throughout.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The lineup: task APIs vs. the Prompt API
&lt;/h2&gt;

&lt;p&gt;Chrome's built-in AI comes in two flavors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Task-specific APIs&lt;/strong&gt; do one job well, with a clean, purpose-built interface:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Translator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Translates text between languages on-device.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Language Detector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Detects the language of a piece of text.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Summarizer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Condenses long text into key points, a TL;DR, a teaser, or a headline.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Writer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Generates new text from a prompt (e.g. a product description).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rewriter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Restructures or improves existing text (shorter, more formal, etc.).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The Prompt API&lt;/strong&gt; (also called the &lt;strong&gt;LanguageModel API&lt;/strong&gt;) is the general-purpose escape hatch. When no task API fits, you talk to Gemini Nano directly with your own prompts — and it's &lt;strong&gt;multimodal&lt;/strong&gt;, accepting text, images, and audio as input.&lt;/p&gt;

&lt;p&gt;The mental model: reach for a &lt;strong&gt;task API&lt;/strong&gt; when your need matches one (translation, summarization, rewriting) — it's simpler and more predictable. Reach for the &lt;strong&gt;Prompt API&lt;/strong&gt; when you need something custom, structured, or multimodal.&lt;/p&gt;

&lt;p&gt;All of these share the same lifecycle, so once you learn one, you know them all.&lt;/p&gt;




&lt;h2&gt;
  
  
  The lifecycle every built-in AI API shares
&lt;/h2&gt;

&lt;p&gt;Every API follows the same three-step pattern: &lt;strong&gt;feature-detect → check availability → create and use.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Feature detection
&lt;/h3&gt;

&lt;p&gt;The interfaces live as globals (&lt;code&gt;Summarizer&lt;/code&gt;, &lt;code&gt;LanguageModel&lt;/code&gt;, &lt;code&gt;Translator&lt;/code&gt;, …). Check before you touch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summarizer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// The Summarizer API exists in this browser.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Availability and the model download
&lt;/h3&gt;

&lt;p&gt;Here's the part that trips people up. The API being &lt;em&gt;present&lt;/em&gt; doesn't mean the model is &lt;em&gt;ready&lt;/em&gt;. Gemini Nano is downloaded separately the first time an origin uses it — and it's a multi-gigabyte download.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;availability()&lt;/code&gt; tells you where you stand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Summarizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;availability&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// "unavailable"  → not supported on this device/config&lt;/span&gt;
&lt;span class="c1"&gt;// "downloadable" → supported, but the model must download first&lt;/span&gt;
&lt;span class="c1"&gt;// "downloading"  → download in progress&lt;/span&gt;
&lt;span class="c1"&gt;// "available"    → ready to go right now&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Create the instance (and show download progress)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;create()&lt;/code&gt; instantiates the API. If the model still needs downloading, &lt;code&gt;create()&lt;/code&gt; triggers it — so &lt;strong&gt;always give the user feedback&lt;/strong&gt;, because "downloading 4 GB" is not something to do silently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summarizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Summarizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nf"&gt;monitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;downloadprogress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Model download: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loaded&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;%`&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;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The download happens once per device, not once per site.&lt;/strong&gt; After Gemini Nano is on disk, every origin that uses these APIs shares it. Chrome manages it like a cache — if disk space runs low it may evict the model and re-download later.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;create()&lt;/code&gt; &lt;strong&gt;should be triggered by a user gesture.&lt;/strong&gt; Kicking off a huge download on page load is hostile. Gate it behind a click, and check &lt;code&gt;navigator.userActivation.isActive&lt;/code&gt; if you want to be strict.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Use case #1 — Summarizing user reviews
&lt;/h2&gt;

&lt;p&gt;Picture a product page with dozens of customer reviews. Nobody reads all of them. A classic use case: a "Summarize reviews" button that distills them into a few key points, computed entirely in the browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  The summarizer's knobs
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Summarizer.create()&lt;/code&gt; takes options that shape the output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;type&lt;/code&gt; — &lt;code&gt;"key-points"&lt;/code&gt; (default), &lt;code&gt;"tldr"&lt;/code&gt;, &lt;code&gt;"teaser"&lt;/code&gt;, or &lt;code&gt;"headline"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;format&lt;/code&gt; — &lt;code&gt;"markdown"&lt;/code&gt; (default) or &lt;code&gt;"plain-text"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;length&lt;/code&gt; — &lt;code&gt;"short"&lt;/code&gt; (default), &lt;code&gt;"medium"&lt;/code&gt;, or &lt;code&gt;"long"&lt;/code&gt;. What that means depends on the type: a short &lt;code&gt;key-points&lt;/code&gt; summary is 3 bullets; a short &lt;code&gt;tldr&lt;/code&gt; is one sentence.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sharedContext&lt;/code&gt; — background info that helps the model do a better job.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Putting it together
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;summarizeReviews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reviewsText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Feature detection — degrade gracefully if unsupported.&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="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summarizer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;supported&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Availability check.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;availability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Summarizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;availability&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="nx"&gt;availability&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unavailable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;supported&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Create the summarizer, tuned for product reviews.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summarizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Summarizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;sharedContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;These are customer product reviews from an e-commerce store. &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summarize the overall sentiment and the points customers mention most.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;key-points&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;markdown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;monitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;downloadprogress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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="nf"&gt;updateProgressUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loaded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// your UI hook&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;span class="c1"&gt;// 4. Summarize.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;summarizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reviewsText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Focus on product quality, value for money, and shipping.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;supported&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="nx"&gt;summary&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;Wiring it to a button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;summarizeBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// innerText, not innerHTML — feed the model clean text, not markup.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reviewsText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#reviews&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;summarizeBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;supported&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;summarizeReviews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reviewsText&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;supported&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Progressive enhancement: fall back to your server, or just hide the feature.&lt;/span&gt;
    &lt;span class="nx"&gt;summaryEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Review summaries aren’t available on this device.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;summaryEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;summarizeBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Feed it clean text.&lt;/strong&gt; Use &lt;code&gt;innerText&lt;/code&gt;, not &lt;code&gt;innerHTML&lt;/code&gt;. The model summarizes better when it isn't wading through &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; soup, and you don't waste the context window on markup. If you retrieve the feedback from an API, you can also give it the json directly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Streaming for long input
&lt;/h3&gt;

&lt;p&gt;For a big block of text, waiting for the whole summary feels sluggish. Stream it instead and render tokens as they arrive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;summarizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summarizeStreaming&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reviewsText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Focus on product quality and value for money.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;summaryEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;summaryEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// each chunk is an incremental piece of text&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A note on languages
&lt;/h3&gt;

&lt;p&gt;The Summarizer currently supports &lt;code&gt;en&lt;/code&gt;, &lt;code&gt;ja&lt;/code&gt;, &lt;code&gt;es&lt;/code&gt;, &lt;code&gt;de&lt;/code&gt;, and &lt;code&gt;fr&lt;/code&gt;. Declaring the languages up front lets the browser reject an unsupported combination early instead of failing mid-request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summarizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Summarizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;key-points&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expectedInputLanguages&lt;/span&gt;&lt;span class="p"&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;en&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;fr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;outputLanguage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&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;h2&gt;
  
  
  Use case #2 — From a product photo to structured JSON with the Prompt API
&lt;/h2&gt;

&lt;p&gt;The task APIs are great when your need fits one of them. But what about something bespoke — like: &lt;em&gt;"Here's a photo of a product. Generate a draft review and auto-fill the review form."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's a job for the &lt;strong&gt;Prompt API&lt;/strong&gt;, and it shows off two of its best features: &lt;strong&gt;multimodal input&lt;/strong&gt; (an image) and &lt;strong&gt;structured output&lt;/strong&gt; (a JSON schema).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Create a multimodal session
&lt;/h3&gt;

&lt;p&gt;Because we're sending an image, we declare &lt;code&gt;image&lt;/code&gt; (and &lt;code&gt;text&lt;/code&gt;) as expected inputs when creating the session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;LanguageModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;expectedInputs&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;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;languages&lt;/span&gt;&lt;span class="p"&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;en&lt;/span&gt;&lt;span class="dl"&gt;'&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;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;expectedOutputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;languages&lt;/span&gt;&lt;span class="p"&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;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="nf"&gt;monitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;downloadprogress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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="nf"&gt;updateProgressUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loaded&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;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2 — Define the shape you want back
&lt;/h3&gt;

&lt;p&gt;Prompting &lt;em&gt;"reply in JSON"&lt;/em&gt; and hoping for the best is a recipe for parse errors. Instead, pass a &lt;strong&gt;JSON Schema&lt;/strong&gt; via &lt;code&gt;responseConstraint&lt;/code&gt;, and the model is &lt;em&gt;constrained&lt;/em&gt; to produce output matching it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const reviewSchema = {
  title:       { type: 'string' },
  description: { type: 'string' },
  rating:      { type: 'number', minimum: 1, maximum: 5 },
  pros:        { type: 'array', items: { type: 'string' } },
  cons:        { type: 'array', items: { type: 'string' } },
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Mf-OAGLmUe4"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — Prompt with text + image, constrained to the schema
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;content&lt;/code&gt; of a message is an array of typed parts. We combine a text instruction with the image, and pass the schema as a constraint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateReviewFromPhoto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prompt&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;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&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;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
              &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are given a photo of a product. Write a short, honest &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
              &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer-style review of it. Return the result as JSON.&lt;/span&gt;&lt;span class="dl"&gt;'&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;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;imageElement&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;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;responseConstraint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reviewSchema&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Guaranteed to match the schema — safe to parse.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&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;Calling it against an image on the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;photo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#product-photo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// an &amp;lt;img&amp;gt; element&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;review&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateReviewFromPhoto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;review&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// {&lt;/span&gt;
&lt;span class="c1"&gt;//   title: "Sleek and sturdy everyday mug",&lt;/span&gt;
&lt;span class="c1"&gt;//   description: "A clean matte-white ceramic mug that looks premium...",&lt;/span&gt;
&lt;span class="c1"&gt;//   rating: 4,&lt;/span&gt;
&lt;span class="c1"&gt;//   pros: ["Solid build", "Comfortable handle"],&lt;/span&gt;
&lt;span class="c1"&gt;//   cons: ["Slightly heavier than expected"]&lt;/span&gt;
&lt;span class="c1"&gt;// }&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the output is schema-constrained, you skip the usual defensive gymnastics — no regex-scraping a JSON blob out of a chatty response, no &lt;code&gt;try/catch&lt;/code&gt; around a hopeful &lt;code&gt;JSON.parse&lt;/code&gt;. You get an object shaped exactly like your database row.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Why the schema matters.&lt;/strong&gt; Without &lt;code&gt;responseConstraint&lt;/code&gt;, the model &lt;em&gt;might&lt;/em&gt; honor a "reply in JSON" instruction — most of the time. With it, the output is structurally guaranteed. In production, "most of the time" is where the 2 a.m. pages come from. Use the schema!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Reusing the session
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;LanguageModel&lt;/code&gt; session keeps context across prompts, so you can hold a conversation or run several images through the same instance. When you're done, free the resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Building responsibly: this is progressive enhancement
&lt;/h2&gt;

&lt;p&gt;The single most important design principle for built-in AI: &lt;strong&gt;treat it as an enhancement, never a hard dependency.&lt;/strong&gt; A big chunk of your users won't have it — wrong OS, mobile device, insufficient hardware, or the model simply hasn't downloaded yet.&lt;/p&gt;

&lt;p&gt;A robust feature does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getSummarizer&lt;/span&gt;&lt;span class="p"&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="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summarizer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;availability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Summarizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;availability&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="nx"&gt;availability&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unavailable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Summarizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* … */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summarizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getSummarizer&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="nx"&gt;summarizer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// On-device path: instant, private, free.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Fallback: call your server-side summarizer, or hide the feature entirely.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep these in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Desktop only, for now.&lt;/strong&gt; The foundation-model APIs (Prompt, Summarizer, Writer, Rewriter) don't run on Android or iOS yet. Translator and Language Detector are desktop-only too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real hardware requirements.&lt;/strong&gt; Roughly: &amp;gt;4 GB of GPU VRAM &lt;em&gt;or&lt;/em&gt; a decent CPU with 16 GB RAM, plus ~22 GB free disk for Chrome's profile volume. Not every laptop qualifies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first run is a download.&lt;/strong&gt; Multiple gigabytes. Always show progress, and never block first paint on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy is a genuine selling point.&lt;/strong&gt; Inference is local; user data never hits a server. For a reviews summarizer or a content classifier handling user text, that's a real advantage — say so.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  When the browser can't: a cloud fallback
&lt;/h2&gt;

&lt;p&gt;Progressive enhancement is only half a strategy if the "else" branch just hides the feature. For a lot of products you want the capability to work &lt;em&gt;everywhere&lt;/em&gt; — on the mobile users, the older laptops, the browsers that haven't shipped these APIs yet — while still giving the on-device path to everyone who can use it.&lt;/p&gt;

&lt;p&gt;Think of it as a spectrum rather than a rivalry:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On-device (built-in AI):&lt;/strong&gt; free, private, offline-capable, zero infra. Ideal for lightweight, high-frequency, privacy-sensitive tasks — summarizing reviews, classifying text, drafting a first pass. The trade-off is a small model with a modest context window, and it's not on every device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud (a hosted model):&lt;/strong&gt; handles heavy reasoning, large context windows, and a specific frontier model when you need it — at the cost of latency, per-token pricing, and a backend to secure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The nice part: your feature-detection branch is already the seam between them. Where the on-device path falls through, call your cloud model instead. Here's the same &lt;code&gt;getSummarizer()&lt;/code&gt; pattern with a fallback to &lt;a href="https://aws.amazon.com/bedrock/" rel="noopener noreferrer"&gt;Amazon Bedrock&lt;/a&gt;, which exposes many foundation models behind one API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Client side — same seam as before, now with a real fallback.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summarizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getSummarizer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;summary&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="nx"&gt;summarizer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// On-device path: instant, private, free.&lt;/span&gt;
  &lt;span class="nx"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;summarizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reviewsText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Cloud path: works on any device, at the cost of a network round-trip.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/summarize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reviewsText&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;span class="nx"&gt;summary&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Server side (Node/Lambda) — a thin proxy to Amazon Bedrock.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BedrockRuntimeClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ConverseCommand&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@aws-sdk/client-bedrock-runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BedrockRuntimeClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-east-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConverseCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anthropic.claude-3-5-haiku-20241022-v1:0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Summarize these product reviews as key points:\n\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;summary&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;Same feature, two engines: the browser serves everyone it can for free, and the cloud quietly covers the rest. Keeping the API key server-side (never in the browser) is exactly the backend concern the on-device path lets you &lt;em&gt;skip&lt;/em&gt; for the majority of your users — so you pay for cloud inference only on the requests that genuinely need it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Disclosure:&lt;/strong&gt; I'm a Developer Advocate at AWS, so Bedrock is the cloud fallback I reach for. The pattern is vendor-neutral — any hosted model API slots into the same &lt;code&gt;else&lt;/code&gt; branch.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Built-in AI shifts a whole class of features from "backend project with ongoing costs" to "a few dozen lines of client-side JavaScript." No key to rotate, no proxy to secure, no bill that scales with usage, and no user data leaving the device.&lt;/p&gt;

&lt;p&gt;It won't replace big cloud models for heavy reasoning, and it isn't everywhere yet. But for the everyday tasks that make up so much of real web work — summarize this, classify that, translate the other thing, extract structured data from a mess — the model is right there in the browser, waiting for a function call.&lt;/p&gt;




&lt;h3&gt;
  
  
  Try it yourself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.chrome.com/docs/ai/" rel="noopener noreferrer"&gt;Chrome Built-in AI docs&lt;/a&gt; — start here&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Summarizer_API" rel="noopener noreferrer"&gt;Summarizer API doc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/ai/prompt-api" rel="noopener noreferrer"&gt;Prompt API guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/ai/structured-output-for-prompt-api" rel="noopener noreferrer"&gt;Structured output with the Prompt API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Tip: install the &lt;code&gt;@types/dom-chromium-ai&lt;/code&gt; npm package for TypeScript typings.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>bedrock</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
