<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Build in Public</title><link>https://build.ralphmayr.com/</link><description>Recent content on Build in Public</description><generator>Hugo</generator><language>en-us</language><copyright>©️ Ralph Mayr 2026</copyright><lastBuildDate>Tue, 07 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://build.ralphmayr.com/index.xml" rel="self" type="application/rss+xml"/><item><title>Supabase is my new favorite database</title><link>https://build.ralphmayr.com/posts/98-supabase-is-my-new-favorite-database/</link><pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/98-supabase-is-my-new-favorite-database/</guid><description>&lt;p&gt;Since the early days of poketto.me, I&amp;rsquo;ve been dissatisfied with my persistence architecture. I started with Google Cloud SQL, but I quickly realized that it was
&lt;a href="../6-cloudsql-is-prohibitively-expensive-at-least-for-small-projects/"&gt;too expensive&lt;/a&gt; for a small app. I switched to Firebase and
&lt;a href="../7-refactoring-legacy-code-let-the-ai-handle-it/"&gt;had Claude do the replatforming&lt;/a&gt;, if you remember. However, when I introduced full-text search, I had to
&lt;a href="../94-bigquerys-search-function-only-works-with-ascii-characters/"&gt;add BigQuery as a second database&lt;/a&gt; just for that because Firebase doesn&amp;rsquo;t have full-text indexing. The point is: It&amp;rsquo;s a lot of headaches for something that should be easy.&lt;/p&gt;</description></item><item><title>Automating Instagram is a nightmare!</title><link>https://build.ralphmayr.com/posts/97-automating-instagram-is-a-nightmare/</link><pubDate>Sun, 05 Oct 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/97-automating-instagram-is-a-nightmare/</guid><description>&lt;p&gt;This one isn't entirely related to poketto.me, but it's still an interesting lesson I learned the hard way the other day: If you want to build a &amp;quot;simple&amp;quot; app that automatically posts content to your Instagram account, it's much more difficult than you'd think.&lt;/p&gt;
&lt;p&gt;First, there's a GitHub project called instagrapi that seems to offer a nice Python automation library for Instagram. It can post photos, videos, etc. Looks good, right? Don't use it. Ever. It&amp;rsquo;s based on unofficial APIs, and using it &amp;mdash; especially in a script running in a cloud environment &amp;mdash; will get your Instagram account blocked almost instantly.&lt;/p&gt;</description></item><item><title>Stopping the scrape: Why I switched to the Wikimedia API</title><link>https://build.ralphmayr.com/posts/96-stopping-the-scrape-why-i-switched-to-the-wikimedia-api/</link><pubDate>Sat, 04 Oct 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/96-stopping-the-scrape-why-i-switched-to-the-wikimedia-api/</guid><description>&lt;p&gt;I&amp;rsquo;ve noticed a welcome uptick in users saving Wikipedia articles to poketto.me recently.&lt;/p&gt;
&lt;p&gt;But until now, the app treated Wikipedia just like any other website: it scraped the raw HTML. Turns out, for Wikipedia, that is far from ideal:&lt;/p&gt;
&lt;p&gt;🤯 Artifacts: The extracted content often included UI clutter like &amp;ldquo;Edit&amp;rdquo; buttons, navigation links, and &amp;ldquo;Citation missing&amp;rdquo; tags.&lt;/p&gt;
&lt;p&gt;📋 Rendering issues: The standard HTML → Markdown → HTML conversion pipeline introduced plenty of ugly formatting glitches specific to wikis.&lt;/p&gt;</description></item><item><title>Be careful when counting your whitespace</title><link>https://build.ralphmayr.com/posts/95-be-careful-when-counting-your-whitespace/</link><pubDate>Fri, 03 Oct 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/95-be-careful-when-counting-your-whitespace/</guid><description>&lt;p&gt;One of the great things about working on poketto.me is that I'm constantly learning about fascinating linguistic subtleties. For instance, while working on automatic content summaries and extracting key facts and figures, I came across an interesting issue with token counting in Chinese script.&lt;/p&gt;
&lt;p&gt;I had put a safeguard in place so that poketto.me would only attempt to summarize content longer than 100 words. This works well for German and English content, but when I tested the feature on an article published by Xinhua, a Chinese news agency, my code said the article had only about 12 words, which was obviously incorrect, so it didn't produce a summary.&lt;/p&gt;</description></item><item><title>BigQuery’s SEARCH function only works with ASCII characters</title><link>https://build.ralphmayr.com/posts/94-bigquerys-search-function-only-works-with-ascii-characters/</link><pubDate>Thu, 02 Oct 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/94-bigquerys-search-function-only-works-with-ascii-characters/</guid><description>&lt;p&gt;Admittedly, it may not have been my brightest idea to use &lt;strong&gt;BigQuery&lt;/strong&gt; as the search backend for &lt;strong&gt;poketto.me&lt;/strong&gt;. But since Firebase doesn&amp;rsquo;t have built-in full-text search, I would have needed to add another tool to my stack anyway. I figured BigQuery would be easier than managing something external like &lt;strong&gt;Apache Lucene&lt;/strong&gt; or &lt;strong&gt;Elasticsearch&lt;/strong&gt;. Plus, BigQuery has a built-in &lt;strong&gt;SEARCH(...)&lt;/strong&gt; function, so why not give it a try?&lt;/p&gt;
&lt;p&gt;As it turns out, &lt;strong&gt;SEARCH(...)&lt;/strong&gt; is really more of a token-based text analyzer than a true search function:&lt;br&gt;
1️⃣ It splits both the search term and the text into tokens (words).&lt;br&gt;
2️⃣ It matches &lt;em&gt;full&lt;/em&gt; tokens only.&lt;br&gt;
3️⃣ It returns just &lt;strong&gt;TRUE&lt;/strong&gt; or &lt;strong&gt;FALSE&lt;/strong&gt;&amp;mdash;no offsets, no match lengths.&lt;br&gt;
🤯 And worst of all: &lt;strong&gt;It only works with ASCII.&lt;/strong&gt; Yes. Like it&amp;rsquo;s 1979.&lt;/p&gt;</description></item><item><title>Use virtue signalling (but only if you’re actually planning to be virtuous!)</title><link>https://build.ralphmayr.com/posts/93-use-virtue-signalling-but-only-if-youre-actually-planning-to-be-virtuous/</link><pubDate>Wed, 01 Oct 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/93-use-virtue-signalling-but-only-if-youre-actually-planning-to-be-virtuous/</guid><description>&lt;p&gt;In
&lt;a href="../90-use-friction-to-your-advantage/"&gt;Use friction to your advantage&lt;/a&gt;, I talked about ways to nudge users toward desired&amp;mdash;or away from undesired&amp;mdash;behaviours. Take this idea too far, however, and you end up with Dark Patterns: UI tricks that deliberately deceive users.&lt;/p&gt;
&lt;p&gt;E-commerce is full of bad examples:&lt;/p&gt;
&lt;p&gt;&amp;ndash; The &amp;ldquo;Only 3 Left In Stock!&amp;rdquo; badges that try to rush you into a purchase&lt;/p&gt;
&lt;p&gt;&amp;ndash; The awkward games and bonus points on the Temu app&lt;/p&gt;
&lt;p&gt;&amp;ndash; Cookie consent banners where the &amp;ldquo;Accept all&amp;rdquo; button looks like the only real option&lt;/p&gt;</description></item><item><title>Angular data binding with arrays: Yes, it can work!</title><link>https://build.ralphmayr.com/posts/92-angular-data-binding-with-arrays-yes-it-can-work/</link><pubDate>Tue, 30 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/92-angular-data-binding-with-arrays-yes-it-can-work/</guid><description>&lt;p&gt;Angular is awesome. And data binding, in particular, has been a game changer for developing modern web apps. Something changes somewhere and&amp;mdash;magically&amp;mdash;every part of your UI that needs to respond does so.&lt;/p&gt;
&lt;p&gt;However, I&amp;rsquo;ve always struggled with one corner case: What if you&amp;rsquo;re binding to an array, and the change that occurs is that something gets added or removed, and you need to respond to the change programmatically (not just in your HTML template)?&lt;/p&gt;</description></item><item><title>How to fix the ominous Android Status Bar Issue</title><link>https://build.ralphmayr.com/posts/91-how-to-fix-the-ominous-android-status-bar-issue/</link><pubDate>Mon, 29 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/91-how-to-fix-the-ominous-android-status-bar-issue/</guid><description>&lt;p&gt;Remember
&lt;a href="../28-capacitor-android-status-bar/"&gt;Capacitor + Android Status Bar = 🤯&lt;/a&gt;?&lt;/p&gt;
&lt;p&gt;🪲 This bug has haunted me for months&amp;mdash;stalling the Android release of poketto.me and draining way too much mental energy. It&amp;rsquo;s one of those dreaded dev problems: no obvious solution, hard to debug, and endless rabbit holes.&lt;/p&gt;
&lt;p&gt;Eventually, I had to bite the bullet and dig in. Here&amp;rsquo;s what I found:&lt;/p&gt;
&lt;p&gt;💣 Issue #1: The Capacitor status bar plugin only half-works. There&amp;rsquo;s a StatusBar.setOverlaysWebView(true/false) API, but on modern Android versions it doesn&amp;rsquo;t behave as advertised. Why?&lt;/p&gt;</description></item><item><title>Use friction to your advantage</title><link>https://build.ralphmayr.com/posts/90-use-friction-to-your-advantage/</link><pubDate>Sun, 28 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/90-use-friction-to-your-advantage/</guid><description>&lt;p&gt;Amazon in general, and Jeff Bezos in particular, are famous for &amp;ldquo;reducing friction.&amp;rdquo; Case in point: One-click checkout. Allegedly, it was Bezos himself who obsessed over removing as many steps as possible from the customer&amp;rsquo;s path to purchase. Enter your shipping address? Confirm your credit card number? Validate the details? Gone. Just &amp;ldquo;Buy now,&amp;rdquo; and the goods will be in your mailbox tomorrow.&lt;/p&gt;
&lt;p&gt;For poketto.me, I applied this principle to the signup process. What&amp;rsquo;s the simplest way to enroll for a new product or service? Do you really want to enter your name (first and last), email address (twice), a password (with arbitrary security criteria), then get a confirmation mail, click the link, and finally find yourself in another browser tab (or window)? By that time, many users would surely have given up.&lt;/p&gt;</description></item><item><title>Google’s Play Store review process is pure torture</title><link>https://build.ralphmayr.com/posts/89-googles-play-store-review-process-is-pure-torture/</link><pubDate>Sat, 27 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/89-googles-play-store-review-process-is-pure-torture/</guid><description>&lt;p&gt;In
&lt;a href="../14-the-process-to-get-an-app-into-google-play-is-byzantine/"&gt;TIL #14&lt;/a&gt;, I called getting an Android app into the Play Store &amp;ldquo;byzantine.&amp;rdquo; Turns out, I was being too generous to Google and too strict on the ancient kingdom of Byzantium.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what really gave me headaches over the last few months:&lt;/p&gt;
&lt;p&gt;🤕 Headache #1: The forms&lt;/p&gt;
&lt;p&gt;Before Google even looks at your app, you&amp;rsquo;re drowning in bureaucracy: ticking the &amp;ldquo;my app doesn&amp;rsquo;t process health data&amp;rdquo; box 12 times, pasting links to T&amp;amp;Cs and privacy policies, verifying your name, intentions, identity, blood type, shoe size, the maiden name of your mom&amp;rsquo;s dog, &amp;hellip;&lt;/p&gt;</description></item><item><title>The memory consumption patterns of LangChain are… disturbing</title><link>https://build.ralphmayr.com/posts/88-the-memory-consumption-patterns-of-langchain-are-disturbing/</link><pubDate>Fri, 26 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/88-the-memory-consumption-patterns-of-langchain-are-disturbing/</guid><description>&lt;p&gt;As I said in
&lt;a href="../21-no-you-dont-have-to-learn-langchain/"&gt;No, you don&amp;rsquo;t have to learn LangChain&lt;/a&gt;, we shouldn&amp;rsquo;t get distracted by the artificial complexity introduced by our frameworks. LangChain is mostly a wrapper around the REST APIs of various LLM providers. Useful? Yes&amp;mdash;switching between models becomes easy.&lt;/p&gt;
&lt;p&gt;But here&amp;rsquo;s a mystery I can&amp;rsquo;t explain.&lt;/p&gt;
&lt;p&gt;When I added Gemini as a fallback to DeepSeek (see yesterday&amp;rsquo;s post about DeepSeek refusing to touch Chinese politics), I thought it would be straightforward:&lt;/p&gt;</description></item><item><title>DeepSeek really won’t touch anything related to Chinese politics</title><link>https://build.ralphmayr.com/posts/87-deepseek-really-wont-touch-anything-related-to-chinese-politics/</link><pubDate>Thu, 25 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/87-deepseek-really-wont-touch-anything-related-to-chinese-politics/</guid><description>&lt;p&gt;For most use cases in poketto.me, I&amp;rsquo;m pretty happy with #DeepSeek: it&amp;rsquo;s cheap, reliable, and the output quality matches any other LLM I&amp;rsquo;ve tried.&lt;/p&gt;
&lt;p&gt;But there&amp;rsquo;s one big caveat: anything related to Chinese politics can trigger an immediate refusal. Example: Right after the launch of poketto.me, a user tried saving an article about the September 3rd Beijing meeting between Xi Jinping, Vladimir Putin, Kim Jong Un, et al. (
&lt;a href="https://orf.at/stories/3404330/" target="_blank" rel="noopener noreferrer"&gt;https://orf.at/stories/3404330/&lt;/a&gt;)&lt;/p&gt;</description></item><item><title>AI can’t replace a great marketing team (but sometimes it’s better than nothing)</title><link>https://build.ralphmayr.com/posts/86-ai-cant-replace-a-great-marketing-team-but-sometimes-its-better-than-nothing/</link><pubDate>Wed, 24 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/86-ai-cant-replace-a-great-marketing-team-but-sometimes-its-better-than-nothing/</guid><description>&lt;p&gt;Marketing done well is so much more than cranking out ad copy or polishing sales slides. In all my corporate product roles&amp;mdash;Fabasoft, Borland / Micro Focus, smec&amp;mdash;I got to work with fantastic marketing teams. They shaped products, challenged ideas, and saw the bigger picture we tech-focused product folks often missed.&lt;/p&gt;
&lt;p&gt;With poketto.me, though, it was just me. So I leaned on ChatGPT, Grok, and Gemini more often than I liked&amp;mdash;sometimes with good results, sometimes&amp;hellip; not so much.&lt;/p&gt;</description></item><item><title>Klaviyo: Much more than a Shopify plug-in</title><link>https://build.ralphmayr.com/posts/85-klaviyo-much-more-than-a-shopify-plug-in/</link><pubDate>Tue, 23 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/85-klaviyo-much-more-than-a-shopify-plug-in/</guid><description>&lt;p&gt;As I mentioned in
&lt;a href="../22-gmails-spam-filter-is-pretty-weird/"&gt;GMail&amp;rsquo;s spam filter is pretty weird&lt;/a&gt;, reliably sending emails is harder than it looks. But email marketing automation is also one of the most powerful tools you can add to your stack.&lt;/p&gt;
&lt;p&gt;So, I went looking for a simple, cheap, API-based solution I could plug into poketto.me. Ideally:&lt;/p&gt;
&lt;p&gt;✔️ Free for small usage (given my current revenue = zero)&lt;br&gt;
✔️ Easy integration with my stack existing stack (Python backend, Posthog for analytics)&lt;br&gt;
✔️ Scalable once I need more&lt;/p&gt;</description></item><item><title>You can customize the text selection menu in your Android app</title><link>https://build.ralphmayr.com/posts/84-you-can-customize-the-text-selection-menu-in-your-android-app/</link><pubDate>Mon, 22 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/84-you-can-customize-the-text-selection-menu-in-your-android-app/</guid><description>&lt;p&gt;As I mentioned in
&lt;a href="../77-in-place-dom-manipulation-thorny-as-ever/"&gt;In-place DOM manipulation: Thorny as ever&lt;/a&gt;, text selection in browsers can get tricky fast. For poketto.me, the challenge was even bigger: the web app runs inside a hosted WebView in an Android app.&lt;/p&gt;
&lt;p&gt;Mobile text selection already behaves differently from desktop, and the embedding scenario didn&amp;rsquo;t make things easier. After wrestling with a few awkward bugs, though, I found an elegant solution.&lt;/p&gt;
&lt;p&gt;On the web, when a user finishes selecting text, poketto.me immediately converts the selection into a highlight (default color). On Android, however, a tiny &lt;strong&gt;system context menu&lt;/strong&gt; pops up with entries like &lt;em&gt;Copy, Select All, Web Search,&lt;/em&gt; etc. At first this got in the way&amp;mdash;but it turns out you can &lt;strong&gt;customize this menu on a per-activity basis!&lt;/strong&gt;&lt;/p&gt;</description></item><item><title>The Gemini API for Video Understanding is surprisingly good</title><link>https://build.ralphmayr.com/posts/83-the-gemini-api-for-video-understanding-is-surprisingly-good/</link><pubDate>Sun, 21 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/83-the-gemini-api-for-video-understanding-is-surprisingly-good/</guid><description>&lt;p&gt;As I mentioned in
&lt;a href="../82-geminis-url-context-feature-is-90-hype-10-value/"&gt;Gemini&amp;rsquo;s URL Context feature is 90% hype, 10% value&lt;/a&gt;, I was pretty disappointed with Gemini&amp;rsquo;s &amp;ldquo;URL Context&amp;rdquo; feature. But &amp;ldquo;Video Understanding&amp;rdquo;? That one actually works like a charm.&lt;/p&gt;
&lt;p&gt;How it works:&lt;/p&gt;
&lt;p&gt;👉 Provide a YouTube video link&lt;br&gt;
👉 Ask Gemini questions about the video&lt;br&gt;
👉 Get a structured response back&lt;/p&gt;
&lt;p&gt;For poketto.me, this unlocks a really neat feature: users can save any YouTube video in the app and either watch it later &lt;em&gt;or&lt;/em&gt; read a textual description of the video.&lt;/p&gt;</description></item><item><title>Gemini’s “URL Context” feature is 90% hype, 10% value</title><link>https://build.ralphmayr.com/posts/82-geminis-url-context-feature-is-90-hype-10-value/</link><pubDate>Sat, 20 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/82-geminis-url-context-feature-is-90-hype-10-value/</guid><description>&lt;p&gt;I&amp;rsquo;ll admit&amp;mdash;I was pretty excited when Google announced that the Gemini API would support a new &amp;ldquo;URL Context&amp;rdquo; tool. The idea: you could &amp;ldquo;ask&amp;rdquo; Gemini about the content of a specific web page, with Google handling all the heavy lifting.&lt;/p&gt;
&lt;p&gt;The
&lt;a href="https://www.youtube.com/watch?v=4-6WQl-Hls0" target="_blank" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; even shows a neat example: send Gemini two recipe URLs and prompt it to compare ingredients and cooking times. If it worked, this would&amp;rsquo;ve been a game-changer for poketto.me:&lt;/p&gt;</description></item><item><title>LinkedIn isn’t working</title><link>https://build.ralphmayr.com/posts/81-linkedin-isnt-working/</link><pubDate>Fri, 19 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/81-linkedin-isnt-working/</guid><description>&lt;p&gt;For the last 91 days, I&amp;rsquo;ve posted one of these &amp;ldquo;things I learned when building poketto.me&amp;rdquo; every day here on LinkedIn. What was my motivation for that?&lt;/p&gt;
&lt;p&gt;1️⃣ To reflect more deeply on the countless things I&amp;rsquo;ve learned. Think of it like a gratitude journal: by writing down the small technical quirks, process hacks, and organizational lessons, I hoped to make them stick better.&lt;/p&gt;
&lt;p&gt;2️⃣ To help others avoid some of the many mistakes I've made.&lt;/p&gt;</description></item><item><title>There is no cloud (it’s just somebody else’s computer)</title><link>https://build.ralphmayr.com/posts/80-there-is-no-cloud-its-just-somebody-elses-computer/</link><pubDate>Thu, 18 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/80-there-is-no-cloud-its-just-somebody-elses-computer/</guid><description>&lt;p&gt;&amp;hellip;and ultimately, that &amp;ldquo;someone&amp;rdquo; is going to send you a real invoice for actual money. In my case, I&amp;rsquo;m running poketto.me almost entirely on Google Cloud. While it&amp;rsquo;s not terribly expensive right now, it is a cost I had to factor into my pricing strategy.&lt;/p&gt;
&lt;p&gt;But here&amp;rsquo;s the good news: Google offers various programs to support early-stage startups! Check out
&lt;a href="https://cloud.google.com/startup?hl=en" target="_blank" rel="noopener noreferrer"&gt;https://cloud.google.com/startup?hl=en&lt;/a&gt; for details.&lt;/p&gt;
&lt;p&gt;Today, I&amp;rsquo;m happy to share that poketto.me made it into the &amp;ldquo;START&amp;rdquo; tier of that program, which means: no Google Cloud bills in my mailbox for the foreseeable future!&lt;/p&gt;</description></item><item><title>How to plan a time-based launch 🚀</title><link>https://build.ralphmayr.com/posts/79-how-to-plan-a-time-based-launch/</link><pubDate>Wed, 17 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/79-how-to-plan-a-time-based-launch/</guid><description>&lt;p&gt;Since early August, I&amp;rsquo;d been toying with the idea of taking poketto.me &amp;ldquo;out of beta.&amp;rdquo; But with travel planned for late September through mid-October, timing became critical. I needed to launch before leaving, so I set &amp;ldquo;early September&amp;rdquo; as the latest possible date&amp;mdash;giving myself at least two weeks to handle any post-launch chaos.&lt;/p&gt;
&lt;p&gt;The first question I asked: What does &amp;ldquo;launch&amp;rdquo; actually mean? What&amp;rsquo;s different afterwards?&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what came to mind:&lt;/p&gt;</description></item><item><title>Pricing: 🎨 Art + 🧪 Science + 🪄 Alchemy</title><link>https://build.ralphmayr.com/posts/78-pricing-art-science-alchemy/</link><pubDate>Tue, 16 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/78-pricing-art-science-alchemy/</guid><description>&lt;p&gt;Finding the right price point&amp;mdash;for anything&amp;mdash;is part science, part art, part alchemy&amp;hellip; and maybe a sprinkle of luck.&lt;/p&gt;
&lt;p&gt;Charge too little, and you leave money on the table. Charge too much, and you don&amp;rsquo;t close the deal. This trade-off is as old as commerce itself, but it&amp;rsquo;s especially tricky for intangible products like software&amp;mdash;particularly when selling subscriptions instead of one-offs and purely product-led (without the benefit of a human sales manager in the loop).&lt;/p&gt;</description></item><item><title>In-place DOM manipulation: Thorny as ever 🥀</title><link>https://build.ralphmayr.com/posts/77-in-place-dom-manipulation-thorny-as-ever/</link><pubDate>Mon, 15 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/77-in-place-dom-manipulation-thorny-as-ever/</guid><description>&lt;p&gt;The &amp;ldquo;highlights&amp;rdquo; feature in the poketto.me reader looks deceptively simple: select a text range, and it gets highlighted in yellow. Clicking a highlight opens a small context menu to change its color or delete it.&lt;/p&gt;
&lt;p&gt;But the implementation? Much thornier than you&amp;rsquo;d think. Why?&lt;/p&gt;
&lt;p&gt;1️⃣ Browser selections are relative to visible text, not the underlying DOM tree of the rendered HTML.&lt;br&gt;
2️⃣ A selection is defined by the text plus its start offset (characters before the selection) and end offset.&lt;br&gt;
3️⃣ To do anything useful with a selection (beyond copying text to the clipboard), you have to map it back to its position in the DOM tree.&lt;br&gt;
4️⃣ That &amp;ldquo;mapping&amp;rdquo; is, to some extent, impossible to get perfectly right. A selection can span multiple nodes, start and end at different levels of depth, and include irrelevant nodes.&lt;/p&gt;</description></item><item><title>Posthog also works well for feature flagging</title><link>https://build.ralphmayr.com/posts/76-posthog-also-works-well-for-feature-flagging/</link><pubDate>Sun, 14 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/76-posthog-also-works-well-for-feature-flagging/</guid><description>&lt;p&gt;Since adopting
&lt;a href="https://posthog.com/" target="_blank" rel="noopener noreferrer"&gt;Posthog&lt;/a&gt; for user analytics at poketto.me, I&amp;rsquo;ve grown pretty fond of the tool. Beyond the
&lt;a href="../68-product-analytics-posthog-is-my-tool-of-choice/"&gt;basics&lt;/a&gt; and
&lt;a href="../72-product-analytics-is-more-than-dau-and-wau/"&gt;advanced insights&lt;/a&gt;, I&amp;rsquo;m now also using it for feature flagging.&lt;/p&gt;
&lt;p&gt;Fine-grained control (and easy rollback) of new features or major changes is becoming increasingly important as the poketto.me user base grows. Why?&lt;/p&gt;
&lt;p&gt;🤕 In a B2C app, you can&amp;rsquo;t count on users to complain when something breaks&amp;mdash;they&amp;rsquo;ll just leave. Especially for early-stage products, every irritated user is a missed opportunity.&lt;/p&gt;</description></item><item><title>The freemium trap (or why free trials don’t work)</title><link>https://build.ralphmayr.com/posts/75-the-freemium-trap-or-why-free-trials-dont-work/</link><pubDate>Sat, 13 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/75-the-freemium-trap-or-why-free-trials-dont-work/</guid><description>&lt;p&gt;Let&amp;rsquo;s face it: It&amp;rsquo;s hard to get a freemium model right. While thinking through pricing and packaging for poketto.me, I looked at a lot of other B2C apps&amp;mdash;and most of them had some flaw, inconsistency, or irritation in their approach.&lt;/p&gt;
&lt;p&gt;One striking example is
&lt;a href="https://strava.com" target="_blank" rel="noopener noreferrer"&gt;Strava&lt;/a&gt;. The fitness app is wildly popular (150M+ users worldwide) and valued at $2.2B. But their free-to-paid conversion strategy seems to be struggling. Why?&lt;/p&gt;
&lt;p&gt;👉 The core value (activity tracking) is fully commoditized, with little room to differentiate (Garmin Connect, Nike Run Club, Apple Health, etc. essentially all do the same thing).&lt;/p&gt;</description></item><item><title>Firefox extensions (mostly) work on Firefox on Android as well</title><link>https://build.ralphmayr.com/posts/74-firefox-extensions-mostly-work-on-firefox-on-android-as-well/</link><pubDate>Fri, 12 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/74-firefox-extensions-mostly-work-on-firefox-on-android-as-well/</guid><description>&lt;p&gt;A poketto.me Firefox user recently pointed out that Firefox on Android also supports browser extensions&amp;ndash; but that the poketto.me extension didn&amp;rsquo;t appear on Mozilla&amp;rsquo;s Add-Ons page there.&lt;/p&gt;
&lt;p&gt;Turns out: Any modern browser extension built on the standard APIs (Chrome, Firefox, Edge) can also run in Firefox on Android. But when publishing, you have to explicitly test and target Android.&lt;/p&gt;
&lt;p&gt;Technically, the extension &amp;ldquo;just works&amp;rdquo;&amp;mdash;as long as you&amp;rsquo;re not doing anything exotic with the APIs. But aesthetically, there&amp;rsquo;s one big caveat.&lt;/p&gt;</description></item><item><title>trafilatura’s image extraction is a bit too cautious for my taste</title><link>https://build.ralphmayr.com/posts/73-trafilaturas-image-extraction-is-a-bit-too-cautious-for-my-taste/</link><pubDate>Thu, 11 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/73-trafilaturas-image-extraction-is-a-bit-too-cautious-for-my-taste/</guid><description>&lt;p&gt;A poketto.me user recently filed a curious bug: They had saved a page that clearly contained images &amp;mdash; but in the reader view, no images showed up.&lt;/p&gt;
&lt;p&gt;I expected some quirky HTML. But when I checked, the &amp;lt;img&amp;gt; tags looked perfectly normal (see Exhibit A). Yet, after passing the HTML through trafilatura (which I use to convert HTML to Markdown), the images had simply vanished.&lt;/p&gt;
&lt;p&gt;🔎 The culprit? Trafilatura is very cautious with images. It only accepts &amp;lt;img src=...&amp;gt; URLs that end with a known extension (.jpg, .png, .gif, &amp;hellip;). The site in question served images from URLs without extensions &amp;mdash; so trafilatura just ignored them.&lt;/p&gt;</description></item><item><title>Product analytics is more than DAU and WAU</title><link>https://build.ralphmayr.com/posts/72-product-analytics-is-more-than-dau-and-wau/</link><pubDate>Wed, 10 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/72-product-analytics-is-more-than-dau-and-wau/</guid><description>&lt;p&gt;
&lt;a href="../68-product-analytics-posthog-is-my-tool-of-choice/"&gt;Recently&lt;/a&gt;, I wrote about adopting Posthog for poketto.me. At first, I thought I&amp;rsquo;d use it for the basics:&lt;/p&gt;
&lt;p&gt;📆 Daily &amp;amp; weekly active users (DAU/WAU)&lt;br&gt;
📎 Core events (URLs saved, links shared, etc.)&lt;br&gt;
🚨 Error tracking and alerting&lt;/p&gt;
&lt;p&gt;But then I realized: analytics can do much more. In fact, Posthog replaced one of my home-grown tools &amp;mdash; my &amp;ldquo;podcast heuristic accuracy guestimator.&amp;rdquo; Let me explain.&lt;/p&gt;
&lt;p&gt;When a user adds content to their podcast feed, poketto.me has to gauge three things:&lt;/p&gt;</description></item><item><title>Entitlements are easy (until they’re not)</title><link>https://build.ralphmayr.com/posts/71-entitlements-are-easy-until-theyre-not/</link><pubDate>Tue, 09 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/71-entitlements-are-easy-until-theyre-not/</guid><description>&lt;p&gt;Early-stage products are all about uncertainty. With poketto.me, I started by building something I wanted to use &amp;mdash; and gave it away for free. Then came early adopters asking for features, and eventually I began experimenting with monetizable &amp;ldquo;premium&amp;rdquo; features: personalized podcasts, news aggregation, summaries, contextualization, etc.&lt;/p&gt;
&lt;p&gt;That third bucket quickly gave me headaches. I needed a way to put usage guardrails around these features:&lt;/p&gt;
&lt;p&gt;👉 to separate free from premium,&lt;br&gt;
👉 to distinguish &amp;ldquo;beta&amp;rdquo; from production-ready,&lt;br&gt;
👉 and to keep my future monetization options open.&lt;/p&gt;</description></item><item><title>A room without a dustbin will never be clean</title><link>https://build.ralphmayr.com/posts/70-a-room-without-a-dustbin-will-never-be-clean/</link><pubDate>Mon, 08 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/70-a-room-without-a-dustbin-will-never-be-clean/</guid><description>&lt;p&gt;This is a roundabout way of saying: make &amp;ldquo;good&amp;rdquo; behavior the easy choice. But there&amp;rsquo;s an interesting backstory to the saying.&lt;/p&gt;
&lt;p&gt;I come from a family of manual laborers. My dad, all of my uncles, and most of my many cousins work in trades ranging from carpentry to plumbing to house painting. And one of the first things any good craftsman does when setting up at a new site? Installing a dustbin.&lt;/p&gt;</description></item><item><title>Debugging your Stripe integration is easy with Stripe CLI</title><link>https://build.ralphmayr.com/posts/69-debugging-your-stripe-integration-is-easy-with-stripe-cli/</link><pubDate>Sun, 07 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/69-debugging-your-stripe-integration-is-easy-with-stripe-cli/</guid><description>&lt;p&gt;I put off building checkout and payments for the premium version of poketto.me for way too long. The thought of integrating with Stripe &amp;mdash; keeping &amp;ldquo;their&amp;rdquo; and &amp;ldquo;my&amp;rdquo; data in sync, handling callbacks, connecting the UI, smoothing out user flows&amp;hellip; it always felt like &lt;em&gt;too much&lt;/em&gt; to tackle on any given day.&lt;/p&gt;
&lt;p&gt;But once I committed to a GA date for the premium version, I had to bite the bullet.&lt;/p&gt;</description></item><item><title>Product analytics? PostHog is my tool of choice!</title><link>https://build.ralphmayr.com/posts/68-product-analytics-posthog-is-my-tool-of-choice/</link><pubDate>Sat, 06 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/68-product-analytics-posthog-is-my-tool-of-choice/</guid><description>&lt;p&gt;Event tracking and analytics is one of those cross-cutting topics I mentioned back in
&lt;a href="../27-you-dont-need-to-bring-out-the-big-guns-right-away-but-its-good-to-know-them-anyway/"&gt;You don&amp;rsquo;t need to bring out the big guns right away (but it&amp;rsquo;s good to know them anyway)&lt;/a&gt;: in the beginning, it doesn&amp;rsquo;t really matter if or how you do it. Often, you can just hack something together and move on to more important things. For the longest time, for example, poketto.me just had a hard-coded email notification that let me know whenever a new user signed up. That was enough to give me a general sense of what was going on.&lt;/p&gt;</description></item><item><title>Define what ‘done’ looks like (and stick to it!)</title><link>https://build.ralphmayr.com/posts/67-define-what-done-looks-like-and-stick-to-it/</link><pubDate>Fri, 05 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/67-define-what-done-looks-like-and-stick-to-it/</guid><description>&lt;p&gt;Working solo on poketto.me &amp;mdash; without the built-in &amp;ldquo;pressure to deliver&amp;rdquo; that comes with a corporate environment &amp;mdash; comes with an interesting challenge: actually shipping stuff.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example. When I started building the &amp;ldquo;Share&amp;hellip;&amp;rdquo; feature, I had something super simple in mind: users should be able to create shareable links to their content, which others could view on poketto.me without logging in. I figured this would create a self-reinforcing loop: existing users share content → new people enjoy the clean, distraction-free reading experience → they sign up → save their own content → share it → and the cycle continues.&lt;/p&gt;</description></item><item><title>Your browser extension doesn’t necessarily need that many permissions</title><link>https://build.ralphmayr.com/posts/66-your-browser-extension-doesnt-necessarily-need-that-many-permissions/</link><pubDate>Thu, 04 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/66-your-browser-extension-doesnt-necessarily-need-that-many-permissions/</guid><description>&lt;p&gt;The other week, I was building a neat little feature for the poketto.me browser extensions (Chrome, Firefox, and Edge): when you save a web page, the extension should capture not just the URL, but the entire content you&amp;rsquo;re looking at in that moment.&lt;/p&gt;
&lt;p&gt;🤨Why does this matter?&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re signed in on a page (say, with your New York Times subscription), poketto.me can capture the full article &amp;mdash; just as you, the paying subscriber, see it. Otherwise, poketto.me tries to fetch the article in the background&amp;hellip; but it will run straight into the paywall (see TIL #59).&lt;/p&gt;</description></item><item><title>It’s more than a marathon</title><link>https://build.ralphmayr.com/posts/65-its-more-than-a-marathon/</link><pubDate>Wed, 03 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/65-its-more-than-a-marathon/</guid><description>&lt;p&gt;I promise I won&amp;rsquo;t turn into my Strava feed. But there&amp;rsquo;s something to be said about the similarities between running long distances and building products.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ve probably heard the &amp;ldquo;it&amp;rsquo;s a marathon, not a sprint&amp;rdquo; metaphor before &amp;mdash; but I want to add one more twist.&lt;/p&gt;
&lt;p&gt;You can approach running a marathon in very different ways. Some people go from zero-to-marathon in 12 weeks of intense training. It&amp;rsquo;s doable, but those gut-wrenching training plans&amp;hellip; you really don&amp;rsquo;t want to go through that. And often, this results in a once-in-a-lifetime, superhuman effort. It looks hard. It feels hard. Sure, you can pat yourself on the back afterwards if you make it &amp;mdash; but the risk of injury is high, the odds of repeating the feat are low, and the long-term benefits are minimal.&lt;/p&gt;</description></item><item><title>Always suspect your own code first</title><link>https://build.ralphmayr.com/posts/64-always-suspect-your-own-code-first/</link><pubDate>Tue, 02 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/64-always-suspect-your-own-code-first/</guid><description>&lt;p&gt;I physically winced when a beta tester of the poketto.me Android app reported this bug:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;Sometimes, adding new tags to a Save doesn&amp;rsquo;t work. I open the tagging dialog, type a new tag, click &amp;lsquo;save&amp;rsquo; &amp;mdash; the input field clears, but the new tag is nowhere to be seen.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I was able to reproduce the issue on my phone &lt;em&gt;and&lt;/em&gt; in an Android emulator.&lt;br&gt;
Gut reaction: &lt;em&gt;&amp;ldquo;Ah, must be the WebView. WebViews always behave differently than a normal browser. This will be a mess.&amp;rdquo;&lt;/em&gt;&lt;/p&gt;</description></item><item><title>Social media “previews” are tricky (part 2)</title><link>https://build.ralphmayr.com/posts/63-social-media-previews-are-tricky-part-2/</link><pubDate>Mon, 01 Sep 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/63-social-media-previews-are-tricky-part-2/</guid><description>&lt;p&gt;Yesterday, I described my elegant solution to the social media &amp;ldquo;preview&amp;rdquo; problem: Direct the share URL to the Python backend, render a skeleton HTML page with machine-readable metadata, and send real browsers to the Angular app&amp;rsquo;s proper &amp;ldquo;read&amp;rdquo; page.&lt;/p&gt;
&lt;p&gt;But&amp;hellip; how exactly do you redirect the user?&lt;/p&gt;
&lt;p&gt;My first, naive idea was to use the age-old HTML redirect tag:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;meta http-equiv=&amp;quot;refresh&amp;quot; content=&amp;quot;0; url=http://example.com/&amp;quot; \&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;My assumption: social media bots wouldn&amp;rsquo;t follow these &amp;ldquo;semantic&amp;rdquo; redirects, but they would follow HTTP redirects (which they definitely do). This worked perfectly &amp;mdash; except for LinkedIn 😅&lt;/p&gt;</description></item><item><title>Social media “previews” are tricky (part 1)</title><link>https://build.ralphmayr.com/posts/62-social-media-previews-are-tricky-part-1/</link><pubDate>Sun, 31 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/62-social-media-previews-are-tricky-part-1/</guid><description>&lt;p&gt;When you add a link to a website to post on Bluesky, Facebook, or LinkedIn, your post will often contain a &amp;ldquo;preview&amp;rdquo; of the target page. That&amp;rsquo;s great &amp;mdash; it makes the post more appealing and more likely to get clicks. But how do social media platforms fetch the exact content they embed in that preview? That&amp;rsquo;s&amp;hellip; a tiny rabbit hole.&lt;/p&gt;
&lt;p&gt;Enter: the &amp;ldquo;Share&amp;hellip;&amp;rdquo; feature of poketto.me. You can share any saved content with others via poketto.me. Anyone who opens the link gets the same distraction-free reading experience, whether or not they use poketto.me.&lt;/p&gt;</description></item><item><title>Firefox and Chrome (finally!) support the same extension API (and so does Microsoft Edge)</title><link>https://build.ralphmayr.com/posts/61-firefox-and-chrome-finally-support-the-same-extension-api-and-so-does-microsoft-edge/</link><pubDate>Sat, 30 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/61-firefox-and-chrome-finally-support-the-same-extension-api-and-so-does-microsoft-edge/</guid><description>&lt;p&gt;I hadn&amp;rsquo;t actually planned to build a Firefox extension for poketto.me. But among the first wave of &amp;ldquo;Pocket Converts&amp;rdquo; &amp;mdash; users who turned to poketto.me after reading about it as an alternative to Pocket &amp;mdash; several asked for a Firefox extension. So, I decided to do a quick technical feasibility check.&lt;/p&gt;
&lt;p&gt;As it turns out: The Chrome extension works in Firefox without a single code change!&lt;/p&gt;
&lt;p&gt;Apparently, there are a few APIs that Firefox doesn&amp;rsquo;t support (see:
&lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Browser_support_for_JavaScript_APIs%29" target="_blank" rel="noopener noreferrer"&gt;https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Browser_support_for_JavaScript_APIs)&lt;/a&gt;, but luckily, I didn&amp;rsquo;t rely on any of those.&lt;/p&gt;</description></item><item><title>Don’t generalize too soon. But do generalize.</title><link>https://build.ralphmayr.com/posts/60-dont-generalize-too-soon-but-do-generalize/</link><pubDate>Fri, 29 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/60-dont-generalize-too-soon-but-do-generalize/</guid><description>&lt;p&gt;It&amp;rsquo;s generally good practice among developers to break things down into small, independent, reusable components. But, like all good things, it can become problematic when taken too far too soon.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example from poketto.me:&lt;/p&gt;
&lt;p&gt;A few weeks ago, I introduced colored tags. Users can choose one of eight colors for each tag to help them quickly find what they&amp;rsquo;re looking for and organize their tags visually. (Personally, I use one color for all location-related tags, another for tech topics, etc.)&lt;/p&gt;</description></item><item><title>Show. And Tell. And Write.</title><link>https://build.ralphmayr.com/posts/59-show-and-tell-and-write/</link><pubDate>Thu, 28 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/59-show-and-tell-and-write/</guid><description>&lt;p&gt;The other week, a post by someone at Google went viral. The gist: PMs should focus on building prototypes that show what the product is supposed to do, rather than merely telling through written PRDs.&lt;/p&gt;
&lt;p&gt;That resonated with me.&lt;/p&gt;
&lt;p&gt;As a developer-turned-PM, I always found it limiting that &amp;ldquo;building&amp;rdquo; was considered out of my scope. I got to write specs, user stories, and requirement docs &amp;mdash; but those often failed to capture the intent behind an idea, leading to botched execution of a feature. A lot got lost in translation.&lt;/p&gt;</description></item><item><title>The occasional toast can’t hurt</title><link>https://build.ralphmayr.com/posts/58-the-occasional-toast-cant-hurt/</link><pubDate>Wed, 27 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/58-the-occasional-toast-cant-hurt/</guid><description>&lt;p&gt;Some of the designers and frontend devs I&amp;rsquo;ve worked with may remember my rants against toast notifications:&lt;/p&gt;
&lt;p&gt;&amp;ldquo;Why do I need a success message for every action? I expect things to work. Only tell me when something breaks.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;But&amp;hellip; I&amp;rsquo;ll admit it: Sometimes toasts do have their merit.&lt;/p&gt;
&lt;p&gt;Two examples from poketto.me:&lt;/p&gt;
&lt;p&gt;🔹 Copy to clipboard (Podcast feed URL):&lt;/p&gt;
&lt;p&gt;When users click the &amp;ldquo;copy&amp;rdquo; button, the action happens instantly. But without any feedback, it feels&amp;hellip; awkward.&lt;/p&gt;</description></item><item><title>Multi-threaded TTS: A bad idea</title><link>https://build.ralphmayr.com/posts/57-multi-threaded-tts-a-bad-idea/</link><pubDate>Tue, 26 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/57-multi-threaded-tts-a-bad-idea/</guid><description>&lt;p&gt;Running text-to-speech in the cloud is fun&amp;mdash;until it isn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;Early on, I didn&amp;rsquo;t think much about thread safety. During my own testing, rarely would more than one TTS task be running in parallel, so there were no big issues. But once more users started using the feature, strange bugs popped up:&lt;/p&gt;
&lt;p&gt;Errors like &amp;ldquo;Assertion srcIndex &amp;lt; srcSelectDimSize failed&amp;rdquo; started showing up in the logs&amp;mdash;and worse, once triggered, the entire Cloud Run instance would become unusable until a redeploy.&lt;/p&gt;</description></item><item><title>State diagrams are fun!</title><link>https://build.ralphmayr.com/posts/56-state-diagrams-are-fun/</link><pubDate>Mon, 25 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/56-state-diagrams-are-fun/</guid><description>&lt;p&gt;One would think that poketto.me is so straightforward that any kind of architectural documentation would be unnecessary. But then...&lt;/p&gt;
&lt;p&gt;Whenever a user saves something, the app first fetches the raw content from the relevant webpage. Then, the content undergoes an asynchronous 'cleanup' process (as I mentioned previously, I use an LLM to fix broken formatting, etc.). If the user has turned on that setting, the content is then translated. This already results in five different states that a Save can be in: New (no content available yet), Extracted (raw content available), Processing (raw content cleaned up), Translating (content being translated), and 'None' (everything done). Of course, the frontend needs to be mindful of what to allow the user to do in each of these states.&lt;/p&gt;</description></item><item><title>UX details sometimes make all the difference</title><link>https://build.ralphmayr.com/posts/55-ux-details-sometimes-make-all-the-difference/</link><pubDate>Sun, 24 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/55-ux-details-sometimes-make-all-the-difference/</guid><description>&lt;p&gt;In
&lt;a href="../51-websockets-more-than-tech-vanity/"&gt;WebSockets: More than tech vanity&lt;/a&gt;, I talked about how poketto.me uses WebSockets to keep the UI in sync with async backend processes&amp;mdash;like translating, TTS, etc. Neat? Sure. Game-changing? Probably not. Just like I asked about AI in
&lt;a href="../54-ai-is-not-a-value-proposition/"&gt;AI is not a value proposition&lt;/a&gt;: where would you put that on the Business Model Canvas? It doesn&amp;rsquo;t really fit as a Value Proposition, does it?&lt;/p&gt;
&lt;p&gt;But that doesn&amp;rsquo;t mean it&amp;rsquo;s not important. Take Strava, for example.&lt;/p&gt;</description></item><item><title>AI is not a value proposition</title><link>https://build.ralphmayr.com/posts/54-ai-is-not-a-value-proposition/</link><pubDate>Sat, 23 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/54-ai-is-not-a-value-proposition/</guid><description>&lt;p&gt;I didn&amp;rsquo;t coin this phrase (sadly), but it keeps proving itself true&amp;mdash;especially now that I&amp;rsquo;m working on GTM details for some of the more advanced features in poketto.me.&lt;/p&gt;
&lt;p&gt;Most users don&amp;rsquo;t care how your app works. They care what it does for them&amp;mdash;and whether that&amp;rsquo;s worth paying for.&lt;/p&gt;
&lt;p&gt;Since LLMs became easy to embed, companies started slapping &amp;ldquo;powered by AI&amp;rdquo; stickers on everything as if that alone justified a price tag. But unless the user clearly feels the value, it doesn&amp;rsquo;t matter what's under the hood. Case in point: Garmin&amp;rsquo;s hilariously underwhelming $7/month &amp;ldquo;AI subscription&amp;rdquo;. The so-called &amp;ldquo;insights&amp;rdquo; offered
&lt;a href="https://www.techradar.com/health-fitness/smartwatches/garmins-new-subscription-ai-feature-is-hilariously-bad-so-far" target="_blank" rel="noopener noreferrer"&gt;nothing users couldn&amp;rsquo;t deduce themselves&lt;/a&gt;&amp;mdash;or the app couldn&amp;rsquo;t have generated with much simpler logic.&lt;/p&gt;</description></item><item><title>Passion is what gets you started…</title><link>https://build.ralphmayr.com/posts/53-passion-is-what-gets-you-started/</link><pubDate>Fri, 22 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/53-passion-is-what-gets-you-started/</guid><description>&lt;p&gt;You&amp;rsquo;ve heard it before: It&amp;rsquo;s a marathon, not a sprint. That&amp;rsquo;s true for almost everything worthwhile&amp;mdash;learning a new skill, building healthy habits, nurturing relationships&amp;hellip; and yes, building, maintaining, and growing something like poketto.me, even if it's &amp;ldquo;just&amp;rdquo; a side project.&lt;/p&gt;
&lt;p&gt;Psychologist Angela Duckworth frames this through the lens of &lt;strong&gt;grit&lt;/strong&gt;&amp;mdash;not sheer &lt;strong&gt;willpower&lt;/strong&gt; (which is fleeting and highly dependent on external factors), but the combination of &lt;strong&gt;passion&lt;/strong&gt; (what gets you started) and &lt;strong&gt;perseverance&lt;/strong&gt; (what keeps you going).&lt;/p&gt;</description></item><item><title>Object.assign(...) in JavaScript helps with asynchronous updates</title><link>https://build.ralphmayr.com/posts/52-object-assign-in-javascript-helps-with-asynchronous-updates/</link><pubDate>Thu, 21 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/52-object-assign-in-javascript-helps-with-asynchronous-updates/</guid><description>&lt;p&gt;In
&lt;a href="../51-websockets-more-than-tech-vanity/"&gt;WebSockets: More than tech vanity&lt;/a&gt;, I talked about the implicit user value of asynchronous UIs. Today, here&amp;rsquo;s a neat little JavaScript trick I hadn&amp;rsquo;t been aware of that makes implementing them a bit easier: &lt;code&gt;Object.assign(...)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the situation:&lt;/p&gt;
&lt;p&gt;Your Angular frontend initially fetches a list of items from the backend. Thanks to Angular&amp;rsquo;s powerful data binding, many components reference properties of these objects. Later, the backend sends an updated version of one of these items via WebSocket. What now?&lt;/p&gt;</description></item><item><title>WebSockets: More than tech vanity</title><link>https://build.ralphmayr.com/posts/51-websockets-more-than-tech-vanity/</link><pubDate>Wed, 20 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/51-websockets-more-than-tech-vanity/</guid><description>&lt;p&gt;As I said in
&lt;a href="../18-socket-science-isnt-rocket-science-or-websockets-flask-socketio/"&gt;Socket science isn&amp;rsquo;t rocket science&lt;/a&gt;, WebSockets are quite easy to use with Python + Flask + Socket.IO on the backend and Angular + Socket.IO + RxJS on the frontend. But why bother? Dealing with asynchronous requests is more of a hassle than not. And for a &amp;ldquo;boring&amp;rdquo; app like poketto.me, aren&amp;rsquo;t synchronous HTTP requests/responses good enough?&lt;/p&gt;
&lt;p&gt;Here are two use cases where, despite the app&amp;rsquo;s apparent simplicity, asynchronous backend/frontend communication adds real value:&lt;/p&gt;</description></item><item><title>Prompt engineering: A task best left to the machines</title><link>https://build.ralphmayr.com/posts/50-prompt-engineering-a-task-best-left-to-the-machines/</link><pubDate>Tue, 19 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/50-prompt-engineering-a-task-best-left-to-the-machines/</guid><description>&lt;p&gt;Under the hood, poketto.me makes heavy use of LLMs. The podcast feature is a great example: Users can turn any web content into a podcast, but often that content isn&amp;rsquo;t well-suited for listening. LLMs are great at optimizing this&amp;mdash;simplifying complex sentences, turning headlines into enumerations, describing images verbally, etc.&lt;/p&gt;
&lt;p&gt;But the challenge: How do you craft a single, generic prompt that works across all types of content and runs unsupervised via the API?&lt;/p&gt;</description></item><item><title>How to (not) do paywalls</title><link>https://build.ralphmayr.com/posts/49-how-to-not-do-paywalls/</link><pubDate>Mon, 18 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/49-how-to-not-do-paywalls/</guid><description>&lt;p&gt;As I pointed out a few weeks ago: The web, as it's designed today, is not ready for &amp;ldquo;Agents&amp;rdquo; of any kind&amp;mdash;AI-driven or just plain old automation scripts. Why? Because there&amp;rsquo;s no agreed-upon way for machines to interact with websites on behalf of a user.&lt;/p&gt;
&lt;p&gt;Case in point: Paywalls.&lt;/p&gt;
&lt;p&gt;Publishers are getting more creative in protecting their content from scraping, and rightly so: no one wants their work stolen by AI companies or repackaged by Google. But at the same time, they want to provide a good user experience for those who pay.&lt;/p&gt;</description></item><item><title>Tackle the Monkey First</title><link>https://build.ralphmayr.com/posts/48-tackle-the-monkey-first/</link><pubDate>Sun, 17 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/48-tackle-the-monkey-first/</guid><description>&lt;p&gt;I don&amp;rsquo;t remember where I first came across this metaphor, but I still love it:&lt;/p&gt;
&lt;p&gt;Suppose your task is to train a costumed monkey to recite a Shakespeare sonnet while standing on an elaborately carved wooden pedestal. Where do you start? Do you begin by picking out the wood for the pedestal? Designing the decorative motifs? Choosing which hat the monkey should wear? Or&amp;mdash;more obviously&amp;mdash;do you first figure out whether you can actually get a monkey to recite poetry in the first place?&lt;/p&gt;</description></item><item><title>Extracting Favicons: There’s No Bulletproof Way</title><link>https://build.ralphmayr.com/posts/47-extracting-favicons-theres-no-bulletproof-way/</link><pubDate>Sat, 16 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/47-extracting-favicons-theres-no-bulletproof-way/</guid><description>&lt;p&gt;A &lt;strong&gt;favicon&lt;/strong&gt; is that tiny icon you see next to a site name in your browser tab or bookmarks bar. It's one of those small UX elements that quietly plays a big role in how we recognize and visually differentiate websites.&lt;/p&gt;
&lt;p&gt;In poketto.me, I wanted to bring favicons into play for a couple of UI elements&amp;mdash;particularly when managing your &lt;strong&gt;saved news sources&lt;/strong&gt;. Seeing a little logo beside each source makes skimming, scanning, and organizing much more intuitive than reading domain names alone.&lt;/p&gt;</description></item><item><title>GCS Caching Can Be a Pain in the Neck</title><link>https://build.ralphmayr.com/posts/46-gcs-caching-can-be-a-pain-in-the-neck/</link><pubDate>Fri, 15 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/46-gcs-caching-can-be-a-pain-in-the-neck/</guid><description>&lt;p&gt;I love GCS (Google Cloud Storage). It&amp;rsquo;s a simple, robust, and powerful solution for storing files online and accessing them either programmatically or via HTTP. Storage is dirt cheap&amp;mdash;especially if you don&amp;rsquo;t need global replication or sophisticated backups. And you can even turn a GCS bucket into an HTTPS-secured, internet-facing web server for static websites.
&lt;a href="https://poketto.me" target="_blank" rel="noopener noreferrer"&gt;https://poketto.me&lt;/a&gt;, for example, runs on that architecture.&lt;/p&gt;
&lt;p&gt;Another good use case: the #podcast feature in poketto.me. Naturally, the generated MP3 files need to live somewhere, and storing them in a database or serving them through my Python web server would be&amp;hellip; silly. So I push the generated files to a GCS bucket, and all is well: HTTPS-secured, fast, and compatible with any podcast client in the world.&lt;/p&gt;</description></item><item><title>Know When to Maximize—and When to Satisfice</title><link>https://build.ralphmayr.com/posts/45-know-when-to-maximizeand-when-to-satisfice/</link><pubDate>Thu, 14 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/45-know-when-to-maximizeand-when-to-satisfice/</guid><description>&lt;p&gt;Just like optimism vs. pessimism, there's another spectrum that every builder, founder, or product person lives on: Maximizing vs. Satisficing.&lt;/p&gt;
&lt;p&gt;In behavioral economics, a maximizer tries to achieve the &lt;em&gt;best possible outcome&lt;/em&gt;. For example: spending hours to find the absolute best hotel for your vacation. A satisficer, on the other hand, picks the &lt;em&gt;first&lt;/em&gt; option that meets their basic requirements&amp;mdash;and moves on with their day.&lt;/p&gt;
&lt;p&gt;When developing products, it's incredibly useful to know where you fall on that scale because: There&amp;rsquo;s not simple answer when to apply which strategy.&lt;/p&gt;</description></item><item><title>The ABCD rule doesn’t cut it anymore</title><link>https://build.ralphmayr.com/posts/44-the-abcd-rule-doesnt-cut-it-anymore/</link><pubDate>Wed, 13 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/44-the-abcd-rule-doesnt-cut-it-anymore/</guid><description>&lt;p&gt;There was a time when the golden rule of consumer app development was as simple as &lt;strong&gt;ABCD&lt;/strong&gt;: &lt;strong&gt;Always Be Collecting Data.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The strategy?&lt;br&gt;
1️⃣ Grow your user base as fast as possible.&lt;br&gt;
2️⃣ Track every interaction, every event, every click.&lt;br&gt;
3️⃣ Figure out how to &lt;em&gt;monetize the data&lt;/em&gt; &amp;mdash; usually through targeted advertising, if you couldn&amp;rsquo;t think of anything more creative.&lt;/p&gt;
&lt;p&gt;But that game is changing. Consumers are more privacy-aware than ever. Regulators &amp;mdash; especially in the EU, California, Japan, and a few other regions &amp;mdash; have stepped in. And both founders and investors are realizing that &lt;em&gt;data-harvesting at scale&lt;/em&gt; is not a sustainable or ethical business model.&lt;/p&gt;</description></item><item><title>You Don’t Need an nl2br Pipe</title><link>https://build.ralphmayr.com/posts/43-you-dont-need-an-nl2br-pipe/</link><pubDate>Tue, 12 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/43-you-dont-need-an-nl2br-pipe/</guid><description>&lt;p&gt;This one&amp;rsquo;s a pretty common hassle: You&amp;rsquo;ve got text with line breaks (encoded as \n or sometimes \r\n &amp;mdash; thanks, Microsoft!), but when rendering that text on a webpage, those line breaks are nowhere to be seen. Naturally, browsers collapse &amp;lsquo;source&amp;rsquo; line breaks and ignore them.&lt;/p&gt;
&lt;p&gt;The usual reaction? Reach for an nl2br utility &amp;mdash; like
&lt;a href="https://www.npmjs.com/package/nl2br-pipe" target="_blank" rel="noopener noreferrer"&gt;nl2br-pipe&lt;/a&gt;&amp;mdash;to manually replace \n with &amp;lt;br /&amp;gt; tags. It&amp;rsquo;s a well-known workaround in web dev.&lt;/p&gt;</description></item><item><title>The Optimism Trap (and Why You Need Its Opposite)</title><link>https://build.ralphmayr.com/posts/42-the-optimism-trap-and-why-you-need-its-opposite/</link><pubDate>Mon, 11 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/42-the-optimism-trap-and-why-you-need-its-opposite/</guid><description>&lt;p&gt;I just finished
&lt;a href="https://ralphmayr.com/library/the-bright-side/" target="_blank" rel="noopener noreferrer"&gt;The Bright Side&lt;/a&gt; by Sumit Paul-Choudhury&amp;mdash;a solid deep-dive into the history, psychology, and applicability of &lt;em&gt;optimism&lt;/em&gt;. The social science is clear: overall, optimists tend to achieve better outcomes.&lt;/p&gt;
&lt;p&gt;Why? Because they &lt;em&gt;act&lt;/em&gt;. Optimists move toward a positive vision of the future, and in doing so, often stumble upon unexpected opportunities. Pessimists, by contrast, lean toward fatalism and inaction &amp;mdash; and the world rarely arranges itself in exactly the way they&amp;rsquo;d like, anyway.&lt;/p&gt;</description></item><item><title>Keep a List of ‘Low-Hanging Fruit’ for Days You’re Not Feeling It 🍒</title><link>https://build.ralphmayr.com/posts/41-keep-a-list-of-low-hanging-fruit-for-days-youre-not-feeling-it/</link><pubDate>Sun, 10 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/41-keep-a-list-of-low-hanging-fruit-for-days-youre-not-feeling-it/</guid><description>&lt;p&gt;One of the perks of working independently on an early-stage product is flexibility &amp;mdash; but that comes with a risk: running out of steam. Like I said in my post about #pacing, it&amp;rsquo;s key not to overextend on the good days &lt;em&gt;and&lt;/em&gt; not to check out completely when the work feels like a slog.&lt;/p&gt;
&lt;p&gt;One trick I&amp;rsquo;ve found increasingly helpful as the codebase grows? Keep a backlog of quick wins:&lt;/p&gt;</description></item><item><title>Parsing and Serializing XML Is (Still) a Pain in the Neck</title><link>https://build.ralphmayr.com/posts/40-parsing-and-serializing-xml-is-still-a-pain-in-the-neck/</link><pubDate>Sat, 09 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/40-parsing-and-serializing-xml-is-still-a-pain-in-the-neck/</guid><description>&lt;p&gt;It&amp;rsquo;s 2025, and I still can&amp;rsquo;t believe I have to say this &amp;mdash; but handling XML, especially in Python, remains frustratingly painful.&lt;/p&gt;
&lt;p&gt;Take this example: For the upcoming podcast feature of poketto.me, the app will generate a personalized podcast feed for each user, populated with text-to-speech versions of their saved content. Users can subscribe to their custom feed in any podcast app&amp;mdash;pretty handy.&lt;/p&gt;
&lt;p&gt;The feed itself isn&amp;rsquo;t complex: just an XML file hosted on a web server (in my case, a GCS bucket) containing metadata and links to episode MP3s. It just needs to comply with Apple&amp;rsquo;s
&lt;a href="https://podcasters.apple.com/support/823-podcast-requirements" target="_blank" rel="noopener noreferrer"&gt;Podcast RSS Feed Requirements&lt;/a&gt; so podcast clients can parse it correctly.&lt;br&gt;
Sounds simple, right?&lt;/p&gt;</description></item><item><title>Consistency Beats Accuracy (Part 2)</title><link>https://build.ralphmayr.com/posts/39-consistency-beats-accuracy-part-2/</link><pubDate>Fri, 08 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/39-consistency-beats-accuracy-part-2/</guid><description>&lt;p&gt;The bigger your codebase grows, the more important it becomes to stay consistent &amp;mdash; in naming things and in how you use features of your programming language.&lt;/p&gt;
&lt;p&gt;Take input parameters and variables, for example. Is the ID of a &lt;strong&gt;Save&lt;/strong&gt; object sometimes named save_id, other times saveId, and occasionally just id? Is the function to load it called load_save(...), get_save(...), or fetch_save(...)? Or maybe it&amp;rsquo;s load_save(...) for saves, but get_settings(...) and fetch_tag(...) elsewhere?&lt;br&gt;
If so, confusion is only a matter of time.&lt;/p&gt;</description></item><item><title>Consistency Beats Accuracy (Part 1)</title><link>https://build.ralphmayr.com/posts/38-consistency-beats-accuracy-part-1/</link><pubDate>Thu, 07 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/38-consistency-beats-accuracy-part-1/</guid><description>&lt;p&gt;As I add more UI-heavy functionality to poketto.me (not all of it public yet), I keep running into the same issue: it&amp;rsquo;s tempting &amp;mdash; but risky &amp;mdash; to constantly invent new UI patterns and elements.&lt;/p&gt;
&lt;p&gt;Case in point: On the &lt;strong&gt;Saves&lt;/strong&gt; page, each saved item has a &lt;strong&gt;&amp;ldquo;more&amp;rdquo;&lt;/strong&gt; menu with actions like &lt;em&gt;Archive&lt;/em&gt;, &lt;em&gt;Delete&lt;/em&gt;, or &lt;em&gt;Edit Tags&lt;/em&gt;. But when I worked on the &lt;strong&gt;News Feed&lt;/strong&gt;, I completely overlooked this. Instead, I gave each news item its own &lt;strong&gt;action bar&lt;/strong&gt; &amp;mdash; dedicated buttons to save the item or, once saved, edit its tags. (See Exhibit A.)&lt;/p&gt;</description></item><item><title>Google Requires a New versionCode for Every App Bundle You Publish</title><link>https://build.ralphmayr.com/posts/37-google-requires-a-new-versioncode-for-every-app-bundle-you-publish/</link><pubDate>Wed, 06 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/37-google-requires-a-new-versioncode-for-every-app-bundle-you-publish/</guid><description>&lt;p&gt;When publishing a new version of your Android app through the Google Play Console, you &lt;em&gt;must&lt;/em&gt; increment the versionCode in your app&amp;rsquo;s build.gradle file. This is required even if the versionName (the human-readable version string) stays the same.&lt;/p&gt;
&lt;p&gt;I found this a bit confusing at first &amp;mdash; but the rule is simple: Before you build the bundle you upload to Google Play, make sure you&amp;rsquo;ve updated the versionCode. If you don&amp;rsquo;t, your upload will be rejected immediately.&lt;/p&gt;</description></item><item><title>Know When to Explore — and When to Build</title><link>https://build.ralphmayr.com/posts/36-know-when-to-explore-and-when-to-build/</link><pubDate>Tue, 05 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/36-know-when-to-explore-and-when-to-build/</guid><description>&lt;p&gt;As I dive deeper into poketto.me, I keep running into an increasingly tricky question:&lt;br&gt;
&lt;strong&gt;How much time should I spend exploring new features &amp;mdash; and how much actually building them?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Having worked as a Product Owner, Manager, and Director, envisioning exciting new features comes naturally. And with poketto.me, the possibilities seem endless:&lt;br&gt;
🎧 Personalized podcasts&lt;br&gt;
🗞️ AI-curated newsfeeds&lt;br&gt;
📝 Automatic summaries&lt;br&gt;
📬 Individualized daily digests&lt;br&gt;
🖊️ Highlights, annotations, organization tools&lt;br&gt;
🔍 Full-text search and even personal knowledge management (PKM)&lt;/p&gt;</description></item><item><title>LLM-Based Translations: The Good, the Bad, and the Ugly</title><link>https://build.ralphmayr.com/posts/35-llm-based-translations-the-good-the-bad-and-the-ugly/</link><pubDate>Mon, 04 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/35-llm-based-translations-the-good-the-bad-and-the-ugly/</guid><description>&lt;p&gt;Automatic content translation has been a key feature of poketto.me from day one. Why? Because I believe there&amp;rsquo;s immense value in making content accessible to non-native speakers.&lt;/p&gt;
&lt;p&gt;Personally, I&amp;rsquo;m deeply interested in developments in countries like India, Pakistan, and China &amp;mdash; but the best publications from those regions often don&amp;rsquo;t publish in English. Being able to read and compare both &lt;em&gt;Dawn News&lt;/em&gt; (Pakistan) and the &lt;em&gt;Hindustan Times&lt;/em&gt; (India) coverage of tensions between the two countries &amp;mdash; in English &amp;mdash; for example is fascinating.&lt;/p&gt;</description></item><item><title>‘Cloud Idenity’ is a secret well kept by Google</title><link>https://build.ralphmayr.com/posts/34-cloud-idenity-is-a-secret-well-kept-by-google/</link><pubDate>Sun, 03 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/34-cloud-idenity-is-a-secret-well-kept-by-google/</guid><description>&lt;p&gt;Permissions on Google Cloud resources are assigned to principals. Principals, in principle 😏, are Google accounts. My personal @gmail.com address, for example, is the principal that &amp;quot;owns&amp;quot; most of my Google Cloud stuff. So far, so good.&lt;/p&gt;
&lt;p&gt;However, in some cases, I was required to delegate ownership of a resource to a principal with an @poketto.me email address &amp;mdash; a domain for which I don&amp;rsquo;t have a Google Workspace account. Consequently, these addresses aren&amp;rsquo;t recognized as regular Google Accounts. (See exhibit A)&lt;/p&gt;</description></item><item><title>Mind the Gap — flex-gap!</title><link>https://build.ralphmayr.com/posts/33-mind-the-gap-flex-gap/</link><pubDate>Sat, 02 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/33-mind-the-gap-flex-gap/</guid><description>&lt;p&gt;I use flex-based layouts a lot in the #Angular frontend of poketto.me. But I never realized there&amp;rsquo;s a neat little property called gap (or flex-gap) that lets you define spacing between flex items directly.&lt;/p&gt;
&lt;p&gt;For the longest time, I worked around this with clumsy constructs where I&amp;rsquo;d set margins on child elements &amp;mdash; and then unset them on the last child:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;.container {
display: flex;
flex-direction: column;
.child {
margin-bottom: 1rem;
&amp;amp;:last-child {
margin-bottom: unset;
}
}
}
```
When really, the world could be so much simpler:
```
.container {
display: flex;
flex-direction: column;
gap: 1rem;
}
```
No more margin hacks.
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Pace yourself!</title><link>https://build.ralphmayr.com/posts/32-pace-yourself/</link><pubDate>Fri, 01 Aug 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/32-pace-yourself/</guid><description>&lt;p&gt;Working on a substantial project without real external pressure &amp;mdash; deadlines, financial run rates, etc. &amp;mdash; comes with a huge risk: you can easily run out of steam.&lt;/p&gt;
&lt;p&gt;When your only driver is your own motivation, you have to manage that resource wisely. Case in point: when I started tinkering with poketto.me, I thought I&amp;rsquo;d just replicate Pocket&amp;rsquo;s feature set and be done within four weeks. Initial success came quickly &amp;mdash; things worked the way I&amp;rsquo;d hoped, the UI kept getting better, AI coding tools helped kick-start the boilerplate work on infrastructure&amp;hellip; But then I started slacking off. I kept postponing work on the Chrome extension, the website, or the GTM strategy, and turned my attention to other side hustles instead. The initial drive I&amp;rsquo;d had simply faded.&lt;/p&gt;</description></item><item><title>No, AI will not take McKiney or BCG out of business any day soon</title><link>https://build.ralphmayr.com/posts/31-no-ai-will-not-take-mckiney-or-bcg-out-of-business-any-day-soon/</link><pubDate>Thu, 31 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/31-no-ai-will-not-take-mckiney-or-bcg-out-of-business-any-day-soon/</guid><description>&lt;p&gt;Despite what the &amp;ldquo;God of Prompt&amp;rdquo; (sic!) or any other self-proclaimed &amp;ldquo;AI expert&amp;rdquo; is trying to tell you, none of the current AI models will replace a multi-hundred-thousand-dollar product strategy project.&lt;/p&gt;
&lt;p&gt;First of all, the people making these claims are, most likely, just trying to sell you their overpriced list of &amp;ldquo;magic&amp;rdquo; prompts &amp;mdash; and hoping for endorsement from the big AI companies or a retweet from Elon Musk.&lt;/p&gt;
&lt;p&gt;But giving the AI tools the benefit of the doubt, I tried using Grok, ChatGPT, and Claude to iterate on a commercial strategy for poketto.me. The results were&amp;hellip; disappointing. Here are the main issues:&lt;/p&gt;</description></item><item><title>Firebase as very peculiar limits, or: You don’t want to test in production 🙄</title><link>https://build.ralphmayr.com/posts/30-firebase-as-very-peculiar-limits-or-you-dont-want-to-test-in-production/</link><pubDate>Wed, 30 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/30-firebase-as-very-peculiar-limits-or-you-dont-want-to-test-in-production/</guid><description>&lt;p&gt;My &amp;ldquo;user admin&amp;rdquo; section for poketto.me is quite simple: it lists all registered users, shows when they first (and most recently) used the app, and lets me assign &amp;ldquo;entitlements&amp;rdquo; (features like the experimental podcasts and newsfeeds). Works perfectly well on my development instance &amp;mdash; but in production, it kept crashing.&lt;/p&gt;
&lt;p&gt;Turns out: with Firebase, querying documents where a field value is `in` a list of values isn&amp;rsquo;t very smart &amp;mdash; the list may only contain up to **30 (!) values**.&lt;/p&gt;</description></item><item><title>Good things come to those who wait ⏳</title><link>https://build.ralphmayr.com/posts/29-good-things-come-to-those-who-wait/</link><pubDate>Tue, 29 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/29-good-things-come-to-those-who-wait/</guid><description>&lt;p&gt;Remember when I was complaining about
&lt;a href="../8-running-text-to-speech-in-the-cloud-is-harder-than-you-would-think-part-one/"&gt;how hard it is&lt;/a&gt; to run even basic ML workloads on GCP? Turns out, Google has listened 😊 (well, probably not to me personally, but in general).&lt;/p&gt;
&lt;p&gt;You can now request GPUs for Cloud Run instances in the UI as well as on the command line. That means all the hassle I went through deploying my text-to-speech service into a Docker environment running inside a preemptible VM with GPUs&amp;mdash;and then figuring out how to start, stop, and deploy the VM automatically&amp;mdash;was&amp;hellip; well, not exactly wasted, but at least: not necessary anymore.&lt;/p&gt;</description></item><item><title>Capacitor + Android Status Bar = 🤯</title><link>https://build.ralphmayr.com/posts/28-capacitor-android-status-bar/</link><pubDate>Mon, 28 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/28-capacitor-android-status-bar/</guid><description>&lt;p&gt;The Android status bar (that is, the small bar on top of the screen that shows the current time, the batter status, and notification icons) is really, really weird.&lt;/p&gt;
&lt;p&gt;It turns out, Capacitor will, by default, make your app a &amp;ldquo;fullscreen&amp;rdquo; app: It&amp;rsquo;ll render your MainActivity (the one that hosts the WebView which in turn hosts your web app) in such a way that the status bar isn&amp;rsquo;t there at all&amp;ndash;on most devices. On some devices, however, the status bar will be visible, but transparently overlay your app 🤨And, to add insult to injury: It&amp;rsquo;ll change the font color of the status bar to white, so that if you rapp comes with a white background, all the user sees are some weird optical artifacts at the top of their screen.&lt;/p&gt;</description></item><item><title>You don’t need to bring out the big guns right away (but it’s good to know them anyway)</title><link>https://build.ralphmayr.com/posts/27-you-dont-need-to-bring-out-the-big-guns-right-away-but-its-good-to-know-them-anyway/</link><pubDate>Sun, 27 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/27-you-dont-need-to-bring-out-the-big-guns-right-away-but-its-good-to-know-them-anyway/</guid><description>&lt;p&gt;It&amp;rsquo;s surprisingly hard to settle on a &amp;ldquo;fit-for-purpose&amp;rdquo; technology and tool stack for a modern SaaS / Cloud app.&lt;/p&gt;
&lt;p&gt;First of all, there are the technical decisions:&lt;/p&gt;
&lt;p&gt;🔀 Which frontend, backend, and persistence stack do you use? Angular vs. React, Java vs. Python, Spring Boot vs. Rails vs. Django vs. Flask, MongoDB vs. Firebase vs. MySQL vs. Postgres&amp;hellip;&lt;/p&gt;
&lt;p&gt;🔀 Do you run it on AWS (Amazon), GCP (Google Cloud), or Azure (Microsoft)?&lt;/p&gt;</description></item><item><title>For non-urgent LLM tasks, DeepSeek has offers great value for money</title><link>https://build.ralphmayr.com/posts/26-for-non-urgent-llm-tasks-deepseek-has-offers-great-value-for-money/</link><pubDate>Sat, 26 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/26-for-non-urgent-llm-tasks-deepseek-has-offers-great-value-for-money/</guid><description>&lt;p&gt;AI is not at the core of what poketto.me does, but it helps a lot: I&amp;rsquo;m using LLMs to translate saved content and to smooth out formatting issues (especially with PDF content). Any old LLM can do these things quite well, but when it comes to pricing, none beats &lt;strong&gt;DeepSeek&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;When using their
&lt;a href="https://api-docs.deepseek.com/quick_start/pricing" target="_blank" rel="noopener noreferrer"&gt;API&lt;/a&gt;, processing a million input tokens can be as cheap as &lt;strong&gt;$0.035&lt;/strong&gt;, and a million output tokens will cost you at most &lt;strong&gt;$1.10&lt;/strong&gt;. To give you an example: A typical 1,500-word essay will come down to about 2,000 tokens (input and output combined).&lt;/p&gt;</description></item><item><title>Never trust ChatGPT</title><link>https://build.ralphmayr.com/posts/25-never-trust-chatgpt/</link><pubDate>Fri, 25 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/25-never-trust-chatgpt/</guid><description>&lt;p&gt;I may sound like a broken record on this, but I&amp;rsquo;ve seen it over and over again while working with AI tools on poketto.me: Don&amp;rsquo;t trust the chatbots. Ever.&lt;/p&gt;
&lt;p&gt;ChatGPT in particular has two immense problems: sycophancy and accuracy.&lt;/p&gt;
&lt;p&gt;Regarding the former: It&amp;rsquo;s trying to please you&amp;mdash;the user&amp;mdash;to the point where it feels like every response is prefaced with a compliment that&amp;rsquo;s only designed to keep you engaged. Some examples?&lt;/p&gt;</description></item><item><title>Scaling down screen recordings with ffmpeg is fast, easy, and super useful</title><link>https://build.ralphmayr.com/posts/24-scaling-down-screen-recordings-with-ffmpeg-is-fast-easy-and-super-useful/</link><pubDate>Thu, 24 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/24-scaling-down-screen-recordings-with-ffmpeg-is-fast-easy-and-super-useful/</guid><description>&lt;p&gt;I use the free version of Monosnap to record short feature videos for poketto.me&amp;mdash;the ones I post here, on X, and on Bluesky. It works great, but the default encoding produces videos that are far too large for other contexts&amp;mdash;especially email.&lt;/p&gt;
&lt;p&gt;Rather than fiddling with different encodings, I use the command-line tool ffmpeg to scale down the videos. A typical Mac screen recording often has dimensions well over 2000×1500 pixels&amp;mdash;when half (or even a quarter) of that would easily do the job.&lt;/p&gt;</description></item><item><title>Don’t attach yourself to outcomes</title><link>https://build.ralphmayr.com/posts/23-dont-attach-yourself-to-outcomes/</link><pubDate>Wed, 23 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/23-dont-attach-yourself-to-outcomes/</guid><description>&lt;p&gt;This one&amp;rsquo;s a bit more philosophical&amp;mdash;but stay with me: There are things in life we can control, and things we can&amp;rsquo;t. That distinction lies at the heart of Stoic philosophy, most famously articulated by Epictetus in the first century BC.&lt;/p&gt;
&lt;p&gt;What does that have to do with product development?&lt;/p&gt;
&lt;p&gt;A lot, actually.&lt;/p&gt;
&lt;p&gt;When you&amp;rsquo;re working on a small, independent project like poketto.me, it&amp;rsquo;s easy to grow frustrated with a lack of resonance. LinkedIn posts don&amp;rsquo;t get the traction you hoped for. Journalists don&amp;rsquo;t reply. Mozilla doesn&amp;rsquo;t respond, even after you&amp;rsquo;ve tried to nudge them on all imaginable platforms,. It can feel like you&amp;rsquo;re putting something good into the world&amp;mdash;and the world is simply ignoring it.&lt;/p&gt;</description></item><item><title>GMail’s spam filter is pretty weird</title><link>https://build.ralphmayr.com/posts/22-gmails-spam-filter-is-pretty-weird/</link><pubDate>Tue, 22 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/22-gmails-spam-filter-is-pretty-weird/</guid><description>&lt;p&gt;I thought I was being clever when I implemented sign-in via email + one-time token for poketto.me:&lt;/p&gt;
&lt;p&gt;✅ No passwords to store&lt;/p&gt;
&lt;p&gt;✅ No reliance on external login providers like Google or Facebook&lt;/p&gt;
&lt;p&gt;And I had two reliable email services to send those tokens: Hetzner, via their all-inclusive web + mail hosting package I&amp;rsquo;m using for
&lt;a href="https://ralphmayr.com" target="_blank" rel="noopener noreferrer"&gt;ralphmayr.com&lt;/a&gt;, and Zoho Mail, as a lightweight, email-only solution I&amp;rsquo;m using for poketto.me.&lt;/p&gt;
&lt;p&gt;On paper, everything looked fine: The SMTP server accepted the mail, and the message was sent. Simple, right? Wrong&amp;mdash;especially when Gmail is on the receiving end.&lt;/p&gt;</description></item><item><title>No, you don’t have to learn LangChain</title><link>https://build.ralphmayr.com/posts/21-no-you-dont-have-to-learn-langchain/</link><pubDate>Mon, 21 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/21-no-you-dont-have-to-learn-langchain/</guid><description>&lt;p&gt;...or LangGraph, or LlamaIndex, or RAG, or whatever new AI-hype framework is trending this week in order build an AI-powered app.&lt;/p&gt;
&lt;p&gt;More often than not, these frameworks are just &lt;em&gt;wrappers&lt;/em&gt; around basic functionality&amp;mdash;in this case, calling an API. And the layers of abstraction they introduce can make even simple things (&amp;ldquo;prompt an LLM&amp;rdquo;) feel unnecessarily complex.&lt;/p&gt;
&lt;p&gt;Take RAG, for example. All it really does is frontload your prompt with additional context. That&amp;rsquo;s it. In practice, it boils down to concatenating a few strings&amp;mdash;something you can do in five lines of code. But LangChain adds layer upon layer of custom methods, config objects, routing logic, etc., that often just get in the way.&lt;/p&gt;</description></item><item><title>High-quality screenshots? is your friend! 🖼️</title><link>https://build.ralphmayr.com/posts/20-high-quality-screenshots-is-your-friend/</link><pubDate>Sun, 20 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/20-high-quality-screenshots-is-your-friend/</guid><description>&lt;p&gt;Taking good screenshots of a web or mobile app is an art in itself:&lt;/p&gt;
&lt;p&gt;🤨Which screens do you show?&lt;br&gt;
🤨What demo data should appear?&lt;br&gt;
🤨How much (or how little) functionality and complexity do you reveal?&lt;/p&gt;
&lt;p&gt;For poketto.me (the landing page), I chose a fairly minimal set of screens. But I still wanted them to look &lt;em&gt;polished&lt;/em&gt;&amp;mdash;whatever that means.&lt;/p&gt;
&lt;p&gt;Turns out,
&lt;a href="https://browserframe.com" target="_blank" rel="noopener noreferrer"&gt;BrowserFrame.com&lt;/a&gt; makes this super easy:&lt;/p&gt;
&lt;p&gt;➡️ Upload a raw screenshot of your app&lt;br&gt;
➡️ Pick from a range of realistic browser window styles (Chrome, Safari, Edge, etc.)&lt;br&gt;
➡️ Download a slick, framed version&amp;mdash;complete with browser chrome and a subtle drop shadow&lt;/p&gt;</description></item><item><title>Feedback is key 🔑</title><link>https://build.ralphmayr.com/posts/19-feedback-is-key/</link><pubDate>Sat, 19 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/19-feedback-is-key/</guid><description>&lt;p&gt;Back in my corporate days, I didn&amp;rsquo;t always give feedback the attention it deserves. But building poketto.me as a solo endeavour has reminded me just how crucial it really is.&lt;/p&gt;
&lt;p&gt;Working alone has its perks: you can move fast, make bold decisions, and follow your own vision. But it also comes with pitfalls:&lt;/p&gt;
&lt;p&gt;➡️ You get blindsided by your own past choices&lt;br&gt;
➡️ You can waste time iterating on suboptimal ideas&lt;br&gt;
➡️ You miss what&amp;rsquo;s obvious to others&lt;/p&gt;</description></item><item><title>Socket science isn’t rocket science! 🚀 Or: WebSockets + Flask + SocketIO = ❤️</title><link>https://build.ralphmayr.com/posts/18-socket-science-isnt-rocket-science-or-websockets-flask-socketio/</link><pubDate>Fri, 18 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/18-socket-science-isnt-rocket-science-or-websockets-flask-socketio/</guid><description>&lt;p&gt;For some reason, async communication between a web app backend and the browser causes more anxiety than it should (guilty 🙋). I&amp;rsquo;ve had my fair share of headaches with WebSocket frameworks before, but for poketto.me, I tried a much simpler stack&amp;mdash;and was pleasantly surprised:&lt;/p&gt;
&lt;p&gt;➡️http backend: &lt;strong&gt;Flask&lt;/strong&gt; (
&lt;a href="https://flask.palletsprojects.com/en/stable/" target="_blank" rel="noopener noreferrer"&gt;https://flask.palletsprojects.com/en/stable/&lt;/a&gt;)&lt;br&gt;
➡️websocket backend: &lt;strong&gt;Flask-Socketio&lt;/strong&gt; (
&lt;a href="https://flask-socketio.readthedocs.io/en/latest/" target="_blank" rel="noopener noreferrer"&gt;https://flask-socketio.readthedocs.io/en/latest/&lt;/a&gt;)&lt;br&gt;
➡️websocket frontend: &lt;strong&gt;socketio-client&lt;/strong&gt; (
&lt;a href="https://www.npmjs.com/package/socket.io-client" target="_blank" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/socket.io-client&lt;/a&gt;)&lt;/p&gt;
&lt;h4 id="-backend-handling-connections-is-dead-simple"&gt;&lt;strong&gt;🔌 Backend: Handling connections is dead simple&lt;/strong&gt;&lt;/h4&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;@socketio.on(&amp;#39;connect&amp;#39;)
def handle_connect():
_, user_id = authenticate()
sid = request.sid
connected_clients[user_id] = sid
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="-sending-data-to-a-connected-client"&gt;&lt;strong&gt;📤 Sending data to a connected client&lt;/strong&gt;&lt;/h4&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sid = connected_clients.get(user_id)\
if sid:
socketio.emit(&amp;#39;message&amp;#39;, {
&amp;#39;message_type&amp;#39;: &amp;#39;save_changed&amp;#39;,
&amp;#39;data&amp;#39;: {
&amp;#39;id&amp;#39;: save_id,
&amp;#39;state&amp;#39;: &amp;#39;archived&amp;#39;
}
}, to=sid)
#### **🖥️ Frontend: Receiving messages is just as easy**
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;const socket = io(environment.WS_BASE_URL, {&lt;/p&gt;</description></item><item><title>Designing for mobile after the fact is painful</title><link>https://build.ralphmayr.com/posts/17-designing-for-mobile-after-the-fact-is-painful/</link><pubDate>Thu, 17 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/17-designing-for-mobile-after-the-fact-is-painful/</guid><description>&lt;p&gt;I&amp;rsquo;ll admit it: when I built the frontend for [poketto.me, I focused mostly on desktop browsers&amp;mdash;except for the reading experience, which was always meant to be mobile-first. But optimizing the rest of the UI for smaller screens later on? A &lt;strong&gt;real hassle&lt;/strong&gt;&amp;mdash;and one that could&amp;rsquo;ve been avoided had I truly embraced &lt;em&gt;mobile-first&lt;/em&gt; design from the start.&lt;/p&gt;
&lt;p&gt;Here are a few key lessons the hard way taught me:&lt;/p&gt;
&lt;p&gt;📱 *&lt;em&gt;Thumb-friendly design matters*&lt;/em&gt;
The
&lt;a href="https://www.zilliondesigns.com/blog/infographics/mobile-app-design-thumb-friendly/" target="_blank" rel="noopener noreferrer"&gt;Rule of Thumbs&lt;/a&gt; is real:&lt;/p&gt;</description></item><item><title>Material’s tooltip interferes with touch-scrolling on mobile</title><link>https://build.ralphmayr.com/posts/16-materials-tooltip-interferes-with-touch-scrolling-on-mobile/</link><pubDate>Wed, 16 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/16-materials-tooltip-interferes-with-touch-scrolling-on-mobile/</guid><description>&lt;p&gt;As I mentioned the other day, I love Angular Material. However, I was quite surprised to discover that the matTooltip directive can negatively impact the user experience on mobile 🤯&lt;/p&gt;
&lt;p&gt;I noticed that users couldn&amp;rsquo;t 'drag to scroll' on poketto.me on their phones (in both the Android app and the browser) when they started to drag particular elements on the screen, such as the save title images.&lt;/p&gt;
&lt;p&gt;After a lot of analysis and debugging, I discovered that the matTooltip directive, which I use to render tooltips displaying the full save title in case it is abbreviated, sets touch-action: none; on that element. This means: On a mobile device, users can't start scrolling the page with that element as an anchor. It feels totally weird!&lt;/p&gt;</description></item><item><title>Cloud Build beats GitLab CI for my use case.</title><link>https://build.ralphmayr.com/posts/15-cloud-build-beats-gitlab-ci-for-my-use-case/</link><pubDate>Tue, 15 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/15-cloud-build-beats-gitlab-ci-for-my-use-case/</guid><description>&lt;p&gt;When I set up my personal blog,
&lt;a href="https://ralphmayr.com" target="_blank" rel="noopener noreferrer"&gt;ralphpmayr.com&lt;/a&gt;, years ago, I opted for a GitLab CI pipeline to build and deploy it. However, for poketto.me, I was looking for something faster, cheaper and more closely integrated with the Google Cloud ecosystem.&lt;/p&gt;
&lt;p&gt;I opted for Google's own CloudBuild and discovered that it integrates seamlessly with GitLab.com. In GitLab, all you need to do is create two API keys (one for read access and one for edit access), configure these in Google Cloud and CloudBuild will then be able to fetch and build any GitLab project.&lt;/p&gt;</description></item><item><title>The process to get an app into Google Play is… byzantine</title><link>https://build.ralphmayr.com/posts/14-the-process-to-get-an-app-into-google-play-is-byzantine/</link><pubDate>Mon, 14 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/14-the-process-to-get-an-app-into-google-play-is-byzantine/</guid><description>&lt;p&gt;🏗️First, of course, you actually got to build your app. Then you register at the &lt;strong&gt;Google Play Console&lt;/strong&gt; (and fork over $25). Then you provide your name, contact details, etc. and then the fun starts: You need official ID (drivers license, passport), proof of residence (&amp;ldquo;Meldezettel&amp;rdquo; in Austria), proof that you own an Android device, install the Google Play Console App on that device, and verify your contact phone number.&lt;/p&gt;</description></item><item><title>"Token used too early" — the weirdest Google Sign-In error.</title><link>https://build.ralphmayr.com/posts/13-token-used-too-early-the-weirdest-google-sign-in-error/</link><pubDate>Sun, 13 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/13-token-used-too-early-the-weirdest-google-sign-in-error/</guid><description>&lt;p&gt;I&amp;rsquo;ve had my ups and downs with &amp;ldquo;Sign in with Google&amp;rdquo;:&lt;/p&gt;
&lt;p&gt;⬆️It&amp;rsquo;s simple and works well on the web&lt;/p&gt;
&lt;p&gt;⬇️It&amp;rsquo;s a complete hassle inside a hosted WebView in a native mobile app&lt;/p&gt;
&lt;p&gt;But here&amp;rsquo;s something really funny: I local clock is &lt;em&gt;ahead&lt;/em&gt; of Google's &amp;mdash; sometimes by as little as a few milliseconds &amp;ndash; the sign in call will fail with &amp;ldquo;Token used too early.&amp;rdquo; 🤯&lt;/p&gt;
&lt;p&gt;✅ Solution: resync your system clock (in my case, my MacBook with Apple's time server).&lt;/p&gt;</description></item><item><title>WebView ↔ Native app communication is easier than you think.</title><link>https://build.ralphmayr.com/posts/12-webview-native-app-communication-is-easier-than-you-think/</link><pubDate>Sat, 12 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/12-webview-native-app-communication-is-easier-than-you-think/</guid><description>&lt;p&gt;Even without relying on the utilities provided by Capacitor, you can easily pass data back and forth between your web app and its native counterpart:&lt;/p&gt;
&lt;p&gt;➡️In JavaScript / TypeScript you can register global functions on the window object. The native code can call these via executeJavaScript() on the WebView.&lt;/p&gt;
&lt;p&gt;➡️In the other direction, the native code can register a JavaScript interface on the&lt;/p&gt;
&lt;p&gt;WebView:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;webView.addJavascriptInterface(new Object() {
@JavascriptInterface
public void onSessionId(String value) {
SaveUrlHandler.instance.setSessionId(value);
}
@JavascriptInterface
public void closeApp() {
MainActivity.this.finish();
}
}, &amp;#34;Android&amp;#34;);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;which the JavaScript / TypeScript code can call whenever it wants:&lt;/p&gt;</description></item><item><title>The Noah-principle (still) doesn’t work</title><link>https://build.ralphmayr.com/posts/11-the-noah-principle-still-doesnt-work/</link><pubDate>Fri, 11 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/11-the-noah-principle-still-doesnt-work/</guid><description>&lt;p&gt;&lt;em&gt;&amp;ldquo;Build it and they will come&amp;rdquo; is&lt;/em&gt; allegedly what God told Noah when he wondered how all the animals would find the ark.&lt;/p&gt;
&lt;p&gt;As builders, we often fall into the same trap &amp;mdash; assuming that once the product is done, users will magically appear. And when they don&amp;rsquo;t, it&amp;rsquo;s not just disappointing &amp;mdash; it&amp;rsquo;s exhausting and demotivating.&lt;/p&gt;
&lt;p&gt;Turns out, even launching something small (like poketto.me) requires way more go-to-market work than expected. And honestly? That part&amp;rsquo;s less fun than building.&lt;/p&gt;</description></item><item><title>Running text-to-speech in the #Cloud is harder than you would think (part three)</title><link>https://build.ralphmayr.com/posts/10-running-text-to-speech-in-the-cloud-is-harder-than-you-would-think-part-three/</link><pubDate>Thu, 10 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/10-running-text-to-speech-in-the-cloud-is-harder-than-you-would-think-part-three/</guid><description>&lt;p&gt;So, after finally setting up a dedicated virtual machine (VM) to run my text-to-speech workloads and wiring up all the build and deployment scripts, I got a bit excited. Could I reduce the TTS latency even further if the VM had GPU power?&lt;/p&gt;
&lt;p&gt;In theory: Yes. In practice: Google doesn't give you access to their GPUs straight away. There&amp;rsquo;s a special quota setting for VM instances with GPUs, and by default that&amp;rsquo;s set to zero. As a regular user, you cannot increase this without contacting Google Cloud Support.&lt;/p&gt;</description></item><item><title>Running text-to-speech in the #Cloud is harder than you would think (part two)</title><link>https://build.ralphmayr.com/posts/9-running-text-to-speech-in-the-cloud-is-harder-than-you-would-think-part-two/</link><pubDate>Wed, 09 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/9-running-text-to-speech-in-the-cloud-is-harder-than-you-would-think-part-two/</guid><description>&lt;p&gt;Do you remember when I mentioned the difficulty of running 🐸 CoquiTTS in the cloud yesterday? My first experiment was to run it directly in my Cloud Run backend service. In theory, this could have worked, but you'll never guess why it failed in practice.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;x86 CPUs&lt;/strong&gt;. Really. Like the ones we had in our computers in the 90s. How did I figure this out? After taking a horribly long time to start up, the TTS service failed with a message saying that it was running on an 'incompatible' CPU architecture. Specifically, 32-bit x86 CPUs.&lt;/p&gt;</description></item><item><title>Running text-to-speech in the #Cloud is harder than you would think (part one)</title><link>https://build.ralphmayr.com/posts/8-running-text-to-speech-in-the-cloud-is-harder-than-you-would-think-part-one/</link><pubDate>Tue, 08 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/8-running-text-to-speech-in-the-cloud-is-harder-than-you-would-think-part-one/</guid><description>&lt;p&gt;For the podcast automation feature that I&amp;rsquo;m planning for a future version of poketto.me, I&amp;rsquo;ve been experimenting with various text-to-speech solutions. The easiest and highest-quality approach would have been the ElevenLabs API. However, considering the &amp;ldquo;throwaway&amp;rdquo; nature of these audio files &amp;ndash; most of which would only be listened to once by one person &amp;ndash; and the cost structure that this would introduce, I desperately need a cheaper approach.&lt;/p&gt;
&lt;p&gt;The Python library 🐸 CoquiTTS is pretty awesome: There are many different models to choose from, ranging from 'super low latency' to 'high quality' (including voice cloning). Therefore, poketto.me users could choose from many different voices, and from a commercial perspective, I could set different price points for different levels of quality and latency. However, they all require significant computing power to function.&lt;/p&gt;</description></item><item><title>Refactoring “legacy” code? Let the AI handle it!</title><link>https://build.ralphmayr.com/posts/7-refactoring-legacy-code-let-the-ai-handle-it/</link><pubDate>Mon, 07 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/7-refactoring-legacy-code-let-the-ai-handle-it/</guid><description>&lt;p&gt;For reasons outlined in yesterday's post, I had to switch poketto.me from #CloudSQL (MySQL) to a completely different database architecture: Firebase 🔥&lt;/p&gt;
&lt;p&gt;At that stage, the Python backend code base wasn&amp;rsquo;t huge, but it was already fairly substantial. It included CRUD operations for several entities, as well as some basic lookup logic. Rewriting the whole thing would have taken me at least half a day.&lt;/p&gt;
&lt;p&gt;Instead, I asked my good friend #Claude to take care of things. And, to my surprise, the result worked straight away! 🎁The &amp;ldquo;dorp in&amp;rdquo; replacement generated by the AI immediately passed my unit tests, and also the chatbot&amp;rsquo;s instructions for how to set up and configure #Firebase were actually useful.&lt;/p&gt;</description></item><item><title>CloudSQL is prohibitively expensive (at least for small projects)</title><link>https://build.ralphmayr.com/posts/6-cloudsql-is-prohibitively-expensive-at-least-for-small-projects/</link><pubDate>Sun, 06 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/6-cloudsql-is-prohibitively-expensive-at-least-for-small-projects/</guid><description>&lt;p&gt;When I started setting up the cloud infrastructure for &lt;strong&gt;poketto.me&lt;/strong&gt;, I didn&amp;rsquo;t give much thought to costs. I thought it was such a small project that it just wouldn&amp;rsquo;t matter. I launched a #&lt;strong&gt;CloudSQL&lt;/strong&gt; (MySQL) database with pretty much the default settings and was quite happy with it &amp;ndash; until I checked the billing dashboard a couple of days later and realised that I was already spending almost €4 per day on the database alone. 120 euros per month just for a few MySQL tables? That couldn&amp;rsquo;t be right.&lt;/p&gt;</description></item><item><title>There’s no “npx cap remove” 🤦‍♂️</title><link>https://build.ralphmayr.com/posts/5-theres-no-npx-cap-remove/</link><pubDate>Sat, 05 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/5-theres-no-npx-cap-remove/</guid><description>&lt;p&gt;#Capacitor comes with a user-friendly command line interface. To add a new mobile platform to your project, simply run &amp;ldquo;&lt;strong&gt;npx cap add [android | ios]&lt;/strong&gt;&amp;rdquo;. And to remove one? Exactly &amp;mdash; you guessed it: &lt;strong&gt;'npx cap remove&lt;/strong&gt;...' But: That command isn&amp;rsquo;t implemented &amp;ndash; for understandable reasons. The interesting thing is, though, that it's &amp;quot;plausible&amp;quot; that it would be there, right?. So it's not surprising that #Claude insists it exists.&lt;/p&gt;
&lt;p&gt;This once again highlights a major issue with LLMs that I just can&amp;rsquo;t shut up about: Just because what the chatbot says sounds 'plausible' doesn't mean it's correct. In the case of AI-assisted coding, that&amp;rsquo;s not such a big deal &amp;ndash; you, the developer, will eventually realise that the AI was wrong. But what about the many other use cases where we blindly trust the AI and put whatever it says into action? 🤔&lt;/p&gt;</description></item><item><title>Multi-threaded webservers in Python: A rabbit hole you don’t want to get into.</title><link>https://build.ralphmayr.com/posts/4-multi-threaded-webservers-in-python-a-rabbit-hole-you-dont-want-to-get-into/</link><pubDate>Fri, 04 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/4-multi-threaded-webservers-in-python-a-rabbit-hole-you-dont-want-to-get-into/</guid><description>&lt;p&gt;Put simply, serving web requests properly in Python is not easy. poketto.me uses a fairly basic off-the-shelf stack (#&lt;strong&gt;Flask&lt;/strong&gt; as a web framework and #&lt;strong&gt;SocketIO&lt;/strong&gt; for websocket communication), and you would never guess the issues you could encounter with it. For starters, Flask comes with a built-in web server (&amp;ldquo;werkzeug&amp;rdquo;), which is convenient for development, but absolutely not for production (it even warns you in bright red).&lt;/p&gt;
&lt;p&gt;🧵It runs on a single thread &amp;ndash; I'm not kidding. This means it can only handle one web request at a time. If that request involves any actual work, such as extracting web content, it cannot handle any other requests at the same time.&lt;/p&gt;</description></item><item><title>Building a Chrome Extension is easier than I thought — but still a bit of a hassle</title><link>https://build.ralphmayr.com/posts/3-building-a-chrome-extension-is-easier-than-i-thought-but-still-a-bit-of-a-hassle/</link><pubDate>Thu, 03 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/3-building-a-chrome-extension-is-easier-than-i-thought-but-still-a-bit-of-a-hassle/</guid><description>&lt;p&gt;To my surprise, building a Chrome Extension is &lt;em&gt;technically&lt;/em&gt; quite straightforward. You need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A bit of HTML (the popup UI)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A bit of JavaScript (your logic)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;And a $5 one-time fee to publish in the Chrome Web Store&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The
&lt;a href="https://developer.chrome.com/docs/extensions/reference/api" target="_blank" rel="noopener noreferrer"&gt;Chrome Extensions API&lt;/a&gt; is pretty minimal, but it covers all the basics:&lt;/p&gt;
&lt;p&gt;✅ You can persist data (e.g. auth tokens)&lt;br&gt;
✅ You can query open tabs, grab the current URL&lt;br&gt;
✅ You can even inject JavaScript into pages &amp;mdash; though this last bit triggers stricter review from Google, especially if you request broad access (like &amp;quot;&amp;lt;all_urls&amp;gt;&amp;quot;).&lt;/p&gt;</description></item><item><title>Extracting web content is still… messy.</title><link>https://build.ralphmayr.com/posts/2-extracting-web-content-is-still-messy/</link><pubDate>Wed, 02 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/2-extracting-web-content-is-still-messy/</guid><description>&lt;p&gt;You can talk about autonomous #AIAgents roaming the web and performing all kinds of tasks 'just as a human would' as much as you like, but technically, some of the very basics are still lacking. For example, there is no free, easy-to-use, off-the-shelf solution for extracting web content.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what I mean: Think of poketto.me as a very basic 'agent': you tell it to save a URL, and then it talks to the website 'on your behalf' to access its content. For this use case, the &lt;strong&gt;Newspaper3k&lt;/strong&gt; Python library is pretty good: it teases out structured metadata, but occasionally misses basic things like the content language. To retrieve the actual content, &lt;strong&gt;Trafilatura&lt;/strong&gt; (
&lt;a href="https://github.com/adbar/trafilatura" target="_blank" rel="noopener noreferrer"&gt;https://github.com/adbar/trafilatura&lt;/a&gt;) appears to be the best option at the moment. However, even that doesn't work well with all sites. For edge cases, I actually had to fall back on parsing the raw HTML myself using Beautiful Soup (
&lt;a href="https://beautiful-soup-4.readthedocs.io/en/latest/%29" target="_blank" rel="noopener noreferrer"&gt;https://beautiful-soup-4.readthedocs.io/en/latest/)&lt;/a&gt;. (And yes, I&amp;rsquo;m sending that through an LLM later to streamline the content so all the tiny formatting issues Trafilatura introduces get smoothed out again.)&lt;/p&gt;</description></item><item><title>Hybrid apps &amp; social logins: tread carefully.</title><link>https://build.ralphmayr.com/posts/1-hybrid-apps-social-logins-tread-carefully/</link><pubDate>Tue, 01 Jul 2025 00:00:00 +0000</pubDate><guid>https://build.ralphmayr.com/posts/1-hybrid-apps-social-logins-tread-carefully/</guid><description>&lt;p&gt;Remember when I talked about &amp;ldquo;Sign in with Google?&amp;rdquo; That works really well on the web, but: Once you&amp;rsquo;re wrapping your web app into a native mobile app, things turn very ugly very soon. In a hosted WebView, Google won&amp;rsquo;t let you render the sign in button to begin with &amp;ndash; unless you override the WebView&amp;rsquo;s user agent string. After that, you&amp;rsquo;re still screwed if you rely on the default sign in workflow as this would open a popup window from which the user then can&amp;rsquo;t navigate back into your app.&lt;/p&gt;</description></item></channel></rss>