<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: AI Alleyway</title>
    <description>The latest articles on DEV Community by AI Alleyway (@aialleyway).</description>
    <link>https://dev.to/aialleyway</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4011619%2Ff15679b8-76eb-442c-91a0-4e828c770c7c.jpg</url>
      <title>DEV Community: AI Alleyway</title>
      <link>https://dev.to/aialleyway</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aialleyway"/>
    <language>en</language>
    <item>
      <title>Google Meet ships its own AI notetaker now — so the real question is lock-in, not features</title>
      <dc:creator>AI Alleyway</dc:creator>
      <pubDate>Wed, 22 Jul 2026 03:55:44 +0000</pubDate>
      <link>https://dev.to/aialleyway/google-meet-ships-its-own-ai-notetaker-now-so-the-real-question-is-lock-in-not-features-4fc7</link>
      <guid>https://dev.to/aialleyway/google-meet-ships-its-own-ai-notetaker-now-so-the-real-question-is-lock-in-not-features-4fc7</guid>
      <description>&lt;p&gt;Google Meet now takes its own notes. Turn on "Take notes for me" and Gemini follows the call, then drops an organized summary with action items into a Google Doc in your Drive. No bot, no install — it runs &lt;em&gt;inside&lt;/em&gt; Meet.&lt;/p&gt;

&lt;p&gt;That changes the question. For years, "best AI notetaker for Meet" was a feature comparison between third-party apps. Now the platform ships the feature natively, and the interesting decision is the one every engineer recognizes when a platform absorbs a capability you used to buy: &lt;strong&gt;do you take the native integration and accept the platform's boundary, or keep an abstraction layer above it so you're not locked in?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the native path actually commits you to
&lt;/h2&gt;

&lt;p&gt;Native Gemini notes are genuinely well-integrated — that's the whole pitch. But integration is the same thing as coupling, and it's worth naming exactly what you're coupling to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's Meet-only.&lt;/strong&gt; Gemini's notetaker does nothing for your Zoom or Teams calls. Your notetaking is now a function of &lt;em&gt;which platform the meeting happened on&lt;/em&gt;, which is a strange constraint to accept for something as platform-agnostic as "write down what we decided."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's license- and admin-gated.&lt;/strong&gt; The feature needs a Gemini add-on or a Business/Enterprise Workspace edition, and a Workspace admin has to switch it on before anyone can use it. There's no free personal path. Your access to your own meeting notes is now downstream of a procurement decision and an admin toggle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The output lives in Google's sphere.&lt;/strong&gt; The notes land in a Drive Doc under your Workspace's sharing rules. That's convenient if everything you do lives in Google — and it means your meeting record is shaped by, and governed by, the platform, not you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is bad. It's the classic trade: deep integration in exchange for living inside the platform's boundary. The native notetaker is bot-free and zero-install precisely &lt;em&gt;because&lt;/em&gt; it's part of Meet — you can't have the integration without the coupling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The portable layer is the other architecture
&lt;/h2&gt;

&lt;p&gt;A third-party notetaker is the opposite design: an abstraction layer that sits &lt;em&gt;above&lt;/em&gt; the meeting platform and treats Meet, Zoom, and Teams as interchangeable inputs. It captures the call (usually via a bot that joins as a participant), and — this is the part that matters — it &lt;strong&gt;owns the output.&lt;/strong&gt; The notes live in your tool, on your terms, exportable, the same regardless of which platform hosted the call.&lt;/p&gt;

&lt;p&gt;The properties fall out of that shape. It works no matter your Workspace plan (no license, no admin). It's cross-platform by construction, so your notetaking doesn't fork every time a client sends a Zoom link instead of a Meet one. Many are free — Fathom's free plan is genuinely unlimited and produced the cleanest summary of anything we tested, and it saves the video Gemini's notes don't. The costs are the mirror image of native's benefits: a visible bot in the participant list, and one more app holding your data instead of the suite you already run.&lt;/p&gt;

&lt;p&gt;There's even a reliability wrinkle that tracks the architecture. A bot joins the call as its own participant, so it keeps recording if your laptop sleeps or your tab closes. The native/bot-free approaches capture from your session, so they depend on you staying in it. Coupling to the platform buys integration; the portable layer buys independence — including from your own flaky Wi-Fi.&lt;/p&gt;

&lt;h2&gt;
  
  
  So which boundary do you want?
&lt;/h2&gt;

&lt;p&gt;Strip it to the engineering decision and it's clean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You live entirely in Google Workspace, already pay for Gemini, and never meet anywhere but Meet&lt;/strong&gt; → take the native integration. The coupling costs you nothing you'd have used, and you get the tightest experience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You meet across Zoom/Teams too, or you want your notes to outlive your Workspace subscription, or you're on a personal account&lt;/strong&gt; → keep the portable layer. A tool like Fathom abstracts the platform away and hands you the output to own, which is worth a bot in the roster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tell is portability. If your meeting notes should be Meet-shaped — governed by Google, searchable in Drive, gone if you leave Workspace — native is the clean answer. If they should be &lt;em&gt;yours&lt;/em&gt;, independent of where the call happened, keep the layer. (Either way, the tools record a conversation, so disclose it on external calls — in two-party-consent regions that's the law, and Meet shows a note-taking banner regardless.)&lt;/p&gt;

&lt;p&gt;I broke down the native setup, the best portable tools, the bot-vs-bot-free reliability tradeoff, and the exact steps for each in the &lt;a href="https://aialleyway.com/ai-notetaker-for-google-meet/" rel="noopener noreferrer"&gt;full guide to AI notetakers for Google Meet&lt;/a&gt;. But the fast way to decide isn't a feature grid — it's the boundary question: are you coupling to the platform, or keeping a layer above it?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>saas</category>
      <category>webdev</category>
    </item>
    <item>
      <title>There are only three ways to tap a Zoom call — and it decides everything about your notetaker</title>
      <dc:creator>AI Alleyway</dc:creator>
      <pubDate>Thu, 16 Jul 2026 08:46:54 +0000</pubDate>
      <link>https://dev.to/aialleyway/there-are-only-three-ways-to-tap-a-zoom-call-and-it-decides-everything-about-your-notetaker-22og</link>
      <guid>https://dev.to/aialleyway/there-are-only-three-ways-to-tap-a-zoom-call-and-it-decides-everything-about-your-notetaker-22og</guid>
      <description>&lt;p&gt;Every "best AI notetaker for Zoom" list ranks tools by features. That's the wrong layer. Underneath the feature grid, there are only &lt;strong&gt;three places a piece of software can physically tap a Zoom call&lt;/strong&gt; to get the audio, and which one a tool uses determines almost everything you actually care about — who sees it, whether it works on the free Zoom tier, whether it follows you to Google Meet, and whether you can go back and re-watch. Once you see the three attach points, the tool choice mostly falls out. I've tested tools from each category; here's the taxonomy.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Bot-as-participant
&lt;/h2&gt;

&lt;p&gt;The most common approach: a bot joins the meeting as a visible participant and records from the inside. Fathom and Otter do this by default. It's the most portable design — because the bot just needs to be admitted to the call, it works on &lt;strong&gt;any&lt;/strong&gt; Zoom tier (including free Zoom) and the same tool usually covers Google Meet and Teams too. Fathom even ships an official Zoom Marketplace app for one-click setup.&lt;/p&gt;

&lt;p&gt;The cost is visibility. There's a labeled "AI Notetaker" sitting in the participant list, and once everyone on a call runs their own, you get the now-familiar pile-up of three or four notetaker bots in the roster. On external calls that's awkward at best and against the other side's policy at worst.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade:&lt;/strong&gt; maximum reach and cross-platform, minimum discretion.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Inside-the-platform
&lt;/h2&gt;

&lt;p&gt;The second approach doesn't join the call — it runs &lt;em&gt;inside&lt;/em&gt; it. Zoom's native AI Companion (its "My Notes" feature) executes server-side within Zoom itself, captures the meeting, and emails a summary afterward. Nothing appears in the participant list because nothing joined; it's the platform taking its own notes.&lt;/p&gt;

&lt;p&gt;The economics are the appeal: where Google charges for Gemini and Microsoft charges for Copilot, Zoom bundles the core AI Companion note-taking into its paid &lt;strong&gt;Zoom Workplace&lt;/strong&gt; plans (roughly $13–18/user/month) at no extra cost — so if your org already pays for Zoom, you may already have this and just need an admin to switch it on. (Zoom has started layering premium AI tiers on top, so the frontier keeps moving, but the basic summary comes with the plan.)&lt;/p&gt;

&lt;p&gt;The constraints are the flip side of living inside one platform: it's &lt;strong&gt;Zoom-only&lt;/strong&gt; (nothing for your Meet or Teams calls), it needs an &lt;strong&gt;admin/host to enable&lt;/strong&gt; it, and it's &lt;strong&gt;not available on the free Zoom tier&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade:&lt;/strong&gt; cheapest and cleanest &lt;em&gt;if&lt;/em&gt; you're already a paying, single-platform Zoom shop; useless outside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Local device-audio capture
&lt;/h2&gt;

&lt;p&gt;The third approach ignores the call entirely and captures what your &lt;strong&gt;own machine hears&lt;/strong&gt;. Granola grabs your device audio locally; browser tools like Tactiq transcribe from a Chrome extension. No bot, no participant, nothing added to the meeting — the quietest possible footprint, which is exactly why it's the pick for confidential client calls.&lt;/p&gt;

&lt;p&gt;The architectural catch is a hard one: these tools capture what &lt;em&gt;you&lt;/em&gt; can hear, so they work best when you're an &lt;strong&gt;active participant&lt;/strong&gt; actually in the audio path, and they lean on the notes you take rather than a pristine multi-speaker server-side transcript. (Worth stating plainly: bot-free is not the same as recording in secret — in two-party-consent regions you still disclose you're transcribing.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade:&lt;/strong&gt; minimum footprint and maximum privacy, at the cost of capture fidelity and needing to be present.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick the attach point, then the tool
&lt;/h2&gt;

&lt;p&gt;Read your situation through the three and the shortlist writes itself: on a &lt;strong&gt;free Zoom account or bouncing between Zoom/Meet/Teams&lt;/strong&gt;, you need a bot-as-participant tool (Fathom is the one I'd start with — unlimited free plan, official Zoom app, cross-platform). Already a &lt;strong&gt;paying, Zoom-only org&lt;/strong&gt;, the inside-the-platform AI Companion is the cheapest clean option you may already own. Running &lt;strong&gt;discreet external calls&lt;/strong&gt;, local capture (Granola) keeps the meeting clean.&lt;/p&gt;

&lt;p&gt;I laid out both routes on Zoom specifically — the native AI Companion setup, the free third-party apps, the multi-bot problem, and how to enable each — in &lt;a href="https://aialleyway.com/ai-notetaker-for-zoom/" rel="noopener noreferrer"&gt;our AI notetaker for Zoom guide&lt;/a&gt;, built from the tools we actually ran rather than a marketplace listing. But the mental model is the durable part: three attach points, three sets of tradeoffs, and the tool is downstream of which one fits your call.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>saas</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The "AI avatar generator" category is three rendering architectures wearing one label</title>
      <dc:creator>AI Alleyway</dc:creator>
      <pubDate>Thu, 09 Jul 2026 11:42:15 +0000</pubDate>
      <link>https://dev.to/aialleyway/the-ai-avatar-generator-category-is-three-rendering-architectures-wearing-one-label-j8i</link>
      <guid>https://dev.to/aialleyway/the-ai-avatar-generator-category-is-three-rendering-architectures-wearing-one-label-j8i</guid>
      <description>&lt;p&gt;If you evaluate AI avatar tools as if they're interchangeable — type a script, get a talking head — you'll pick the wrong one and then blame the tool. I tested the three market leaders hands-on, giving each the same nine-second script, and the thing that actually separates them isn't avatar quality. It's the rendering architecture underneath. And once you see the architecture, two things you'd written off as pricing quirks turn out to be inevitable consequences of it.&lt;/p&gt;

&lt;p&gt;There are three architectures.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Batch render behind a moderation gate (Synthesia)
&lt;/h2&gt;

&lt;p&gt;Synthesia takes a script, screens it for policy issues &lt;em&gt;before&lt;/em&gt; generating, renders the avatar, then moderates the &lt;em&gt;finished&lt;/em&gt; video before it will release it to you. In my test, a nine-second clip took four to five minutes to appear — and almost all of that time was the moderation step, not the rendering.&lt;/p&gt;

&lt;p&gt;That reads like a performance problem. It isn't. It's the whole value proposition. The moderation gate is why Synthesia is the tool most of the Fortune 100 standardize on: a security or compliance team can sign off on a system that refuses to emit an avatar saying something off-policy. You cannot buy your way past the latency, because the latency &lt;em&gt;is&lt;/em&gt; the guardrail. There's no "fast mode" tier — a fast mode would mean turning off the exact thing enterprises are paying for.&lt;/p&gt;

&lt;p&gt;The billing model follows from the architecture too. Because every output is a discrete, moderated artifact, Synthesia meters by the finished minute. Its plans are minute allowances — a mid-tier plan is a few hundred minutes a &lt;em&gt;year&lt;/em&gt; — and the effective cost lands around two dollars per finished minute. A per-artifact price on a per-artifact pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Fast render, metered by compute (HeyGen)
&lt;/h2&gt;

&lt;p&gt;HeyGen returned a comparable nine-second clip in about a minute, with no moderation queue in the way. Same job description — script in, talking head out — completely different pipeline: no policy gate, and the render is the entire wait.&lt;/p&gt;

&lt;p&gt;But the interesting part is the meter. HeyGen doesn't bill by the minute of video; it bills by &lt;em&gt;credits&lt;/em&gt;, and the credit burn rate depends on which motion engine you pick. The older engine costs a few credits per minute. The newer, lifelike engines — the entire reason you'd choose HeyGen — cost around 20 credits per minute. That's roughly a 7× spread in cost for the same &lt;em&gt;duration&lt;/em&gt; of output, decided by one dropdown.&lt;/p&gt;

&lt;p&gt;That's not an arbitrary gimmick. Credits are a compute passthrough: the lifelike engines are far more expensive to run, so they drain the meter far faster. The consequence for a buyer is that a plan's headline number ("600 credits") tells you almost nothing until you know which engine you're running — 600 credits is ~30 minutes of the good engine, or ~200 minutes of the cheap one. The unit that matters is credits-per-minute-of-&lt;em&gt;the-engine-you-actually-want&lt;/em&gt;, and the pricing page won't do that division for you.&lt;/p&gt;

&lt;p&gt;There's a trap that falls straight out of this architecture: a new HeyGen project defaults to the premium engine, which the free plan won't render — so a first-time free user hits Generate, gets an upsell instead of a video, and concludes the tool is broken. It isn't; you have to manually downgrade the engine. That's the compute-metered design leaking into the onboarding.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Real-time inference, metered by the conversation (Tavus)
&lt;/h2&gt;

&lt;p&gt;Tavus doesn't render a file at all. It generates the avatar &lt;em&gt;live&lt;/em&gt;, during what feels like a video call — you talk, it talks back, rendered in real time. I held an actual conversation with its built-in agent, and the real-time face was the closest thing to talking to a person I've tried.&lt;/p&gt;

&lt;p&gt;Different architecture, different failure mode: it's still in beta, and my first call dropped before connecting on a retry. A batch renderer can't "drop a call" — it either returns a file or errors. Real-time inference carries a whole class of reliability problems the other two don't, because there's a live session to keep alive.&lt;/p&gt;

&lt;p&gt;And, predictably, the meter changes again. There's no finished minute to sell and no render credit to burn, so Tavus prices by &lt;em&gt;conversational&lt;/em&gt; minutes, with pay-as-you-go overage. That's the only honest unit for a pipeline whose output is a session, not an artifact — and it's also why costs are harder to forecast than a flat per-video plan: you're paying for time-on-call, which your users control, not you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway for anyone choosing
&lt;/h2&gt;

&lt;p&gt;"Which AI avatar tool is best" has no answer because the three leaders aren't competing on one axis — they're three different systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Playback of a governed artifact&lt;/strong&gt; → batch-plus-moderation (Synthesia). Slow and per-minute &lt;em&gt;on purpose&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playback of a high-quality artifact, fast&lt;/strong&gt; → compute-metered render (HeyGen). Fast, but the credit &lt;em&gt;rate&lt;/em&gt; — not the plan price — is your real bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A conversation&lt;/strong&gt; → real-time inference (Tavus). No file; per-conversational-minute; and a live session's reliability tax.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can't upgrade a batch tool into a real-time one, and you can't make a moderated tool fast without removing the moderation that's the point of it. So decide which architecture your job needs &lt;em&gt;first&lt;/em&gt;; the tool, and the shape of its bill, follow from that. I put all six tools we looked at — the three leaders plus the specialists — through exactly this lens in &lt;a href="https://aialleyway.com/best-ai-avatar-generator/" rel="noopener noreferrer"&gt;our hands-on best AI avatar generator roundup&lt;/a&gt;, where the render times, credit math, and per-tool verdicts are laid out in full.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Prices and credit rates above are from our hands-on tests in July 2026; vendors move these around, so treat the exact figures as directional and the architecture as the durable part.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>video</category>
      <category>architecture</category>
    </item>
    <item>
      <title>InVideo's credit meter is a GPU bill: the same video, priced 20x apart</title>
      <dc:creator>AI Alleyway</dc:creator>
      <pubDate>Thu, 09 Jul 2026 03:46:44 +0000</pubDate>
      <link>https://dev.to/aialleyway/invideos-credit-meter-is-a-gpu-bill-the-same-video-priced-20x-apart-4aei</link>
      <guid>https://dev.to/aialleyway/invideos-credit-meter-is-a-gpu-bill-the-same-video-priced-20x-apart-4aei</guid>
      <description>&lt;p&gt;"InVideo AI is $20 a month" is true and almost useless. The plan fee is the price of the door; the credits are the price of the video, and the credit cost of a single 30-second clip swings about 20x depending on one choice you make each time you hit generate. Once you understand &lt;em&gt;why&lt;/em&gt;, it stops being a mystery bill and becomes something you can actually budget — the same way you'd budget metered cloud compute.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not a video tool with a price. It's a model hub with a meter.
&lt;/h2&gt;

&lt;p&gt;InVideo bills itself as a hub with access to 200-plus models. You don't pay per video; you pay a flat monthly fee for access, and then every generation spends from a monthly &lt;strong&gt;credit pool&lt;/strong&gt; that refills with your plan (75 credits on the $20 Plus plan, 390 on Max, 800 on Generative, 4,250 on Elite).&lt;/p&gt;

&lt;p&gt;The important part is what sets the cost of a single generation. It's not the length or the plan — it's &lt;strong&gt;the compute behind the model you invoke.&lt;/strong&gt; Pulling a licensed stock clip is nearly free to run, so it costs almost nothing. Running a frontier video model like Google's Veo 3.1 or OpenAI's Sora 2 is expensive to run, so it costs a lot. InVideo is passing that compute cost straight through to you, per generation.&lt;/p&gt;

&lt;p&gt;Here's what that looks like on the exact same 30-second brief, from InVideo's in-app generate screen (as of my testing, mid-2026):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Footage setting&lt;/th&gt;
&lt;th&gt;What runs&lt;/th&gt;
&lt;th&gt;Credits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stock (Basic)&lt;/td&gt;
&lt;td&gt;licensed stock clips&lt;/td&gt;
&lt;td&gt;~2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Efficient generative&lt;/td&gt;
&lt;td&gt;InVideo's cheaper models&lt;/td&gt;
&lt;td&gt;~15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium (Pro)&lt;/td&gt;
&lt;td&gt;Veo 3.1 / Sora 2&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same brief. Twenty times the price. That is not a pricing quirk — it's the cost model. Your plan sets the size of the pool; your footage choice sets how fast you drain it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part every engineer will flinch at: no retry discount
&lt;/h2&gt;

&lt;p&gt;Here's the detail that turns this from "metered" to "watch out." &lt;strong&gt;There is no idempotency discount on a regeneration.&lt;/strong&gt; If a clip comes out wrong and you generate it again, InVideo charges the full rate a second time.&lt;/p&gt;

&lt;p&gt;Generative prompts rarely land on the first try — you tweak the wording, regenerate, adjust a scene, regenerate again. Two or three attempts at 40 credits each is 80 to 120 credits for &lt;em&gt;one finished premium clip&lt;/em&gt; — more than the entire 75-credit monthly pool on the $20 plan. So the real unit of cost isn't "a video." It's &lt;strong&gt;a finished video, including the retries it took to get there&lt;/strong&gt;, and that number is unknowable in advance because it depends on how many attempts your prompt needs.&lt;/p&gt;

&lt;p&gt;That's the mechanism behind the recurring review complaint of "I paid for videos I never finished." The meter runs on every attempt, hit or miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to actually budget it
&lt;/h2&gt;

&lt;p&gt;Stop thinking in "videos per plan." Think in cost-per-finished-clip, like a cloud bill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;effective cost  ≈  (credits per generation)
                 ×  (expected attempts to get a keeper)
                 ×  (dollars per credit on your plan)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the numbers and the split is stark. On the same $20 Plus plan, a stock-first creator effectively pays about &lt;strong&gt;50 cents a video&lt;/strong&gt; (that $20 spread across ~37 stock clips), while a premium-generative creator pays closer to &lt;strong&gt;$11 a clip&lt;/strong&gt; — and once you fold in the regenerations a Veo or Sora prompt usually needs, a single finished premium clip can cost more than the entire monthly plan on its own.&lt;/p&gt;

&lt;p&gt;So the budgeting rule is: decide your footage mix first, compute credits-per-&lt;em&gt;finished&lt;/em&gt;-clip (retries included), then pick the plan whose pool clears that with margin. The 40-credit premium tier is the on-demand GPU instance of this analogy — remarkable value if you use it sparingly, ruinous if you lean on it without watching the meter.&lt;/p&gt;

&lt;p&gt;I worked the full thing out — every plan, the credit-to-dollar math, the stock-license second meter, and where the free plan actually stops being usable — in &lt;a href="https://aialleyway.com/invideo-pricing/" rel="noopener noreferrer"&gt;what a video actually costs on InVideo&lt;/a&gt;. But the one idea that saves you the most is this: the number to write down before you subscribe isn't $20. It's how many premium clips a month you actually need, because that's the variable the whole bill turns on.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>video</category>
      <category>pricing</category>
    </item>
    <item>
      <title>The line in the LICENSE file that reorders your n8n alternatives</title>
      <dc:creator>AI Alleyway</dc:creator>
      <pubDate>Wed, 08 Jul 2026 07:16:37 +0000</pubDate>
      <link>https://dev.to/aialleyway/the-line-in-the-license-file-that-reorders-your-n8n-alternatives-3ia3</link>
      <guid>https://dev.to/aialleyway/the-line-in-the-license-file-that-reorders-your-n8n-alternatives-3ia3</guid>
      <description>&lt;p&gt;Most "n8n alternatives" lists compare the wrong column.&lt;/p&gt;

&lt;p&gt;They line up connector counts, per-execution pricing, cron support, and a screenshot of the canvas. Useful, up to a point. But if you're planning to &lt;em&gt;self-host&lt;/em&gt; the engine — the reason you're looking past the SaaS incumbents in the first place — there's a field none of those tables show, and it sits in a file at the root of every repo: &lt;code&gt;LICENSE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We run n8n in production. The entire AI Alleyway content pipeline is about ten self-hosted n8n workflows on a single box. So this isn't a "we surveyed the landscape" post — it's the one architectural question I wish more alternatives lists led with, because it's the one that quietly decides whether a tool is even &lt;em&gt;eligible&lt;/em&gt; for your use case before you've compared a single feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three licenses, three completely different contracts
&lt;/h2&gt;

&lt;p&gt;"Open source" gets used as a single adjective. For self-hosted automation engines it's actually three different legal contracts, and they diverge exactly where it matters — redistribution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;n8n ships under the Sustainable Use License (v1.0).&lt;/strong&gt; This is source-available, not an OSI-approved open-source license. n8n's own umbrella term for the model is "fair-code." You can read the source, self-host it, and modify it — but the grant is scoped. Straight from the repo's &lt;code&gt;LICENSE.md&lt;/code&gt;, you may use or modify the software "only for your own internal business purposes or for non-commercial or personal use," and you may distribute it to others "only if you do so free of charge for non-commercial purposes." In plain terms: run it for yourself all you want; you cannot turn around and offer n8n as a hosted service to third parties, or bundle-and-resell it commercially, without a separate enterprise license.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Activepieces ships under MIT&lt;/strong&gt; (its Community Edition). MIT is the permissive, OSI-approved baseline — embed it, fork it, resell it, close your fork, no copyleft strings. The honest nuance: MIT covers the Community Edition core; Activepieces keeps some enterprise features under a separate commercial license, so "MIT" describes the open core, not every paid add-on. But the core you'd self-host and build on is genuinely permissive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node-RED ships under the Apache License 2.0&lt;/strong&gt;, stewarded by the OpenJS Foundation. Also OSI-approved and permissive, and Apache 2.0 goes one step past MIT by including an explicit patent grant — the contributors license the patents needed to use their contribution, and there's a patent-retaliation clause. If your legal team cares about patent exposure in a shipped product, that clause is a feature, not boilerplate.&lt;/p&gt;

&lt;p&gt;Three tiers: source-available-with-a-redistribution-fence, permissive, permissive-with-a-patent-grant. Same "open" label on the tin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the license is an architectural constraint, not a footnote
&lt;/h2&gt;

&lt;p&gt;Here's the reframe: the license isn't a compliance checkbox you clear at the end. It's a boundary on your &lt;em&gt;architecture&lt;/em&gt;, because it constrains where the automation engine is allowed to live in your system.&lt;/p&gt;

&lt;p&gt;Ask one question — &lt;strong&gt;is the automation layer something you run, or something you ship?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you &lt;em&gt;run&lt;/em&gt; it — internal ETL, ops glue, a content pipeline like ours, back-office orchestration that never leaves your own walls — then n8n's fair-code fence never touches you. "Internal business purposes" is exactly what you're doing. That's precisely our situation: we chose n8n knowingly, and the Sustainable Use License was a non-issue because we don't resell the engine or expose it as a product to anyone. In that world you should optimize for depth, and n8n's depth is real — on the order of 1,100 integrations, strong branching and error handling, and a node ecosystem that means most of what you need already exists.&lt;/p&gt;

&lt;p&gt;If you &lt;em&gt;ship&lt;/em&gt; it — the automation engine is embedded in a product your customers touch, or you're offering "workflows" as a feature of your SaaS, or you're a platform letting &lt;em&gt;your&lt;/em&gt; users build automations — you've crossed the exact line the fair-code license draws. Now n8n's restriction is load-bearing. Offering it as a service to third parties is the thing the license reserves. At that point the feature-richest tool is no longer the honest answer; the &lt;em&gt;eligible&lt;/em&gt; tools are the permissively licensed ones. Activepieces (MIT) and Node-RED (Apache 2.0) let you embed, white-label, fork, and resell without asking anyone. That's not a marketing claim — it's what those license texts grant.&lt;/p&gt;

&lt;p&gt;This is why a license-blind alternatives list can actively mislead. It'll rank n8n at the top for a reader who's building a product &lt;em&gt;on top of&lt;/em&gt; an automation engine — a reader for whom n8n's own license says "not like this." The ranking is correct for the internal-use reader and wrong for the productizing reader, and the table gives you no way to tell which one you are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the hosted tools fit (and why they dodge the question)
&lt;/h2&gt;

&lt;p&gt;For completeness: we've also driven Make (built a real scenario against its API) and Zapier (ran its MCP across Gmail, Calendar, and Slack). Both are fine tools. But they're hosted SaaS — there's no repo to self-host and no &lt;code&gt;LICENSE&lt;/code&gt; file to read, so the whole license-tier question is moot. You're renting capacity and accepting the terms of service, full stop. That's a legitimate choice; it's just a &lt;em&gt;different&lt;/em&gt; decision than the one this post is about. The license axis only exists once you've committed to self-hosting, which is where the "n8n alternatives" search usually lands you anyway.&lt;/p&gt;

&lt;p&gt;The same "it's a service, not a license" framing applies to most of the other hosted names that show up in these roundups — Pipedream, Workato, Power Automate, Gumloop, Tray.ai. I'm assessing those from their docs and terms, not from production use, and none of them change the core point: if you're not self-hosting, you're evaluating a contract, not a license.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision, compressed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Running automations internally?&lt;/strong&gt; Fair-code is fine. Optimize for depth and ecosystem — n8n earns its spot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shipping the automation layer inside a product, or need to fork/embed/resell freely?&lt;/strong&gt; The permissive licenses are the honest destination, not the feature-count winner. Look hard at Activepieces (MIT) and Node-RED (Apache 2.0), and read their &lt;code&gt;LICENSE&lt;/code&gt; files yourself before you commit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not self-hosting at all?&lt;/strong&gt; The license tier doesn't apply — you're picking a SaaS on terms-of-service and pricing, and Make/Zapier/Pipedream are competing on that axis instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical move: before you shortlist &lt;em&gt;any&lt;/em&gt; self-hosted automation engine, open its &lt;code&gt;LICENSE&lt;/code&gt; file first and answer the run-it-vs-ship-it question. That one field filters the list faster than any feature matrix, and it filters it &lt;em&gt;correctly&lt;/em&gt; — because it filters on what you're actually allowed to do, not on what the tool can do.&lt;/p&gt;

&lt;p&gt;When I sanity-checked our own stack against this lens, I ended up pulling together &lt;a href="https://aialleyway.com/n8n-alternatives/" rel="noopener noreferrer"&gt;the full n8n alternatives breakdown, sorted by why you're leaving&lt;/a&gt; — nine tools grouped by the reason someone actually migrates (license, hosting model, pricing shape, integration gaps) rather than by a single leaderboard. If you're weighing a switch, reading it by &lt;em&gt;your&lt;/em&gt; reason-for-leaving is a lot more useful than reading it top-to-bottom.&lt;/p&gt;

&lt;p&gt;Read the LICENSE file first. It's the cheapest architecture decision you'll make all quarter.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>automation</category>
      <category>n8n</category>
      <category>devops</category>
    </item>
    <item>
      <title>Generate your Open Graph images with React (Remotion), not a design tool</title>
      <dc:creator>AI Alleyway</dc:creator>
      <pubDate>Mon, 06 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/aialleyway/generate-your-open-graph-images-with-react-remotion-not-a-design-tool-1093</link>
      <guid>https://dev.to/aialleyway/generate-your-open-graph-images-with-react-remotion-not-a-design-tool-1093</guid>
      <description>&lt;p&gt;Every blog post, product page, and share link wants its own Open Graph image — the card that shows up when the URL is posted to X, LinkedIn, Slack, or iMessage. Hand-making those in Figma doesn't scale: the moment you have a few dozen pages, you're copy-pasting a template and re-exporting PNGs by hand.&lt;/p&gt;

&lt;p&gt;I moved mine to code. One React component, one CLI command per card, and a size-optimization step. Here's the whole setup — including the two gotchas that cost me an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Remotion?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.remotion.dev/" rel="noopener noreferrer"&gt;Remotion&lt;/a&gt; is "React for videos" — but it renders &lt;strong&gt;still frames&lt;/strong&gt; just as happily as video, and a 1200×630 OG card is just a single frame of a React component. That means your card is a normal component: props, flexbox, your real fonts and brand tokens, conditional layout. No design-tool round-trip, no drift between your site's styles and your cards.&lt;/p&gt;

&lt;h2&gt;
  
  
  The composition
&lt;/h2&gt;

&lt;p&gt;Register a composition sized to the OG spec and drive everything from &lt;code&gt;inputProps&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OgCard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;productName&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;productName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rating&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AbsoluteFill&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;linear-gradient(135deg,#0b0b0f,#1a1330)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#fff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;justifyContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;space-between&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lineHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontFamily&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Outfit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;headline&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;productName&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;productName&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="nx"&gt;rating&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;` · &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/5`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AbsoluteFill&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// index.ts&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Composition&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"OgCard"&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;OgCard&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;630&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;durationInFrames&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;defaultProps&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rendering one card per page
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;renderStill&lt;/code&gt; writes a single PNG. Feed it the per-page props as JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx remotion still src/index.ts OgCard out/my-post.png &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--props&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{"headline":"The cheaper voice tool wasn'&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'t on the pricing page","productName":"Acme TTS","rating":4}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Loop that over your content collection and you have a card per URL, regenerated on every build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't ship the PNG — convert to WebP
&lt;/h2&gt;

&lt;p&gt;A 1200×630 PNG out of a headless browser is big — mine came out around &lt;strong&gt;770 KB&lt;/strong&gt;. That's absurd for a social card. One conversion step drops it to &lt;strong&gt;~17 KB&lt;/strong&gt; with no visible loss:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ImageMagick&lt;/span&gt;
magick out/my-post.png &lt;span class="nt"&gt;-quality&lt;/span&gt; 82 &lt;span class="nt"&gt;-define&lt;/span&gt; webp:method&lt;span class="o"&gt;=&lt;/span&gt;6 out/my-post.webp
&lt;span class="c"&gt;# or sharp: sharp(png).webp({ quality: 82 }).toFile(webp)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point your &lt;code&gt;og:image&lt;/code&gt; / &lt;code&gt;twitter:image&lt;/code&gt; at the WebP. ~45× smaller, same card.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 1: &lt;code&gt;selectComposition&lt;/code&gt; needs the SAME inputProps as &lt;code&gt;renderStill&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If you render programmatically (Node API) rather than the CLI, you call &lt;code&gt;selectComposition()&lt;/code&gt; first, then &lt;code&gt;renderStill()&lt;/code&gt;. &lt;strong&gt;Pass your &lt;code&gt;inputProps&lt;/code&gt; to BOTH.&lt;/strong&gt; Anything resolved at selection time — most importantly &lt;code&gt;staticFile()&lt;/code&gt; references and anything derived from props in &lt;code&gt;calculateMetadata&lt;/code&gt; — uses &lt;code&gt;defaultProps&lt;/code&gt; if you only handed props to &lt;code&gt;renderStill&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The symptom is baffling the first time: every card comes out byte-identical (the default props rendered), or an image &lt;code&gt;staticFile('logo.png')&lt;/code&gt; silently resolves to the placeholder. It's not a caching bug — it's the selection step running on defaults.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inputProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;productName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rating&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;comp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;selectComposition&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;serveUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OgCard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inputProps&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// ← here&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;renderStill&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;composition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;comp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;serveUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inputProps&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;        &lt;span class="c1"&gt;// ← and here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Gotcha 2: &lt;code&gt;renderStill({ frame })&lt;/code&gt; requires &lt;code&gt;durationInFrames &amp;gt; frame&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If you register a still-only composition with &lt;code&gt;durationInFrames: 1&lt;/code&gt; and then ask for &lt;code&gt;frame: 30&lt;/code&gt; (say, to let an entrance animation settle), Remotion throws:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RangeError: Cannot use frame 30: Duration of composition is 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Give the composition enough frames for the frame you sample. I register OG/still compositions with &lt;code&gt;durationInFrames: 60&lt;/code&gt; (2s @ 30fps) and render whatever frame I want within that.&lt;/p&gt;

&lt;h2&gt;
  
  
  That's the whole pipeline
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One React component = your card, in your real styles.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;renderStill&lt;/code&gt; + a JSON props blob = one card per page, on every build.&lt;/li&gt;
&lt;li&gt;A PNG→WebP step = ~45× smaller files.&lt;/li&gt;
&lt;li&gt;Remember: same &lt;code&gt;inputProps&lt;/code&gt; to &lt;code&gt;selectComposition&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;renderStill&lt;/code&gt;, and &lt;code&gt;durationInFrames&lt;/code&gt; bigger than the frame you sample.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once it's wired, adding a new page's social card is zero manual work — it falls out of the build. Worth the afternoon.&lt;/p&gt;

</description>
      <category>react</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Shipping one Manifest V3 extension to Chrome, Edge, and Firefox from a single source</title>
      <dc:creator>AI Alleyway</dc:creator>
      <pubDate>Sun, 05 Jul 2026 10:38:26 +0000</pubDate>
      <link>https://dev.to/aialleyway/shipping-one-manifest-v3-extension-to-chrome-edge-and-firefox-from-a-single-source-cep</link>
      <guid>https://dev.to/aialleyway/shipping-one-manifest-v3-extension-to-chrome-edge-and-firefox-from-a-single-source-cep</guid>
      <description>&lt;p&gt;I recently shipped a small Manifest V3 browser extension — a toolbar popup plus an address-bar (omnibox) search, zero permissions — and wanted it on all three major stores: the Chrome Web Store, Edge Add-ons, and Firefox's AMO.&lt;/p&gt;

&lt;p&gt;The pitch for MV3 is that it's the shared standard, so "write once, ship everywhere." That's mostly true — but the small divergences are exactly the kind of thing that fails a store review at 11pm. Here's the complete list of what actually differs, plus a tiny build script that emits all three packages from one source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chrome and Edge are the same package
&lt;/h2&gt;

&lt;p&gt;Good news first: &lt;strong&gt;Edge Add-ons accepts the exact same MV3 zip as the Chrome Web Store.&lt;/strong&gt; Edge is Chromium, &lt;code&gt;background.service_worker&lt;/code&gt; works as-is, and the &lt;code&gt;chrome.*&lt;/code&gt; APIs are identical. You upload the same artifact to both. (I keep them as separately-named zips only for a clean per-store upload trail.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Firefox needs three manifest tweaks — and zero code changes
&lt;/h2&gt;

&lt;p&gt;Firefox is where it gets interesting. The &lt;em&gt;code&lt;/em&gt; didn't change at all — the extension uses only &lt;code&gt;chrome.omnibox&lt;/code&gt;, &lt;code&gt;chrome.runtime&lt;/code&gt;, and &lt;code&gt;chrome.tabs&lt;/code&gt;, all of which Firefox exposes via the &lt;code&gt;chrome.*&lt;/code&gt; alias. Only the manifest needs three edits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;background.service_worker&lt;/code&gt; → &lt;code&gt;background.scripts&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Firefox's MV3 support prefers an event page over a service worker for the widest compatibility. If your background script only registers listeners at the top level (no service-worker-only globals), it runs fine as an event-page script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Chrome&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Edge&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"background"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"service_worker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"background.js"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Firefox&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"background"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"background.js"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;browser_specific_settings.gecko.id&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AMO requires an explicit extension id. Chrome/Edge derive one for you; Firefox wants it declared:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"browser_specific_settings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gecko"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-ext@yourdomain.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;data_collection_permissions&lt;/code&gt; — and the version floor it drags in&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the one that surprised me. Newer Firefox requires every extension to &lt;em&gt;explicitly declare&lt;/em&gt; what user data it collects — even when the answer is "nothing." Omit it and &lt;code&gt;web-ext lint&lt;/code&gt; fails with &lt;code&gt;MISSING_DATA_COLLECTION_PERMISSIONS&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"gecko"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-ext@yourdomain.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"strict_min_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"142.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data_collection_permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gotcha is the version floor. &lt;code&gt;data_collection_permissions&lt;/code&gt; is only supported from Firefox &lt;strong&gt;140 on desktop&lt;/strong&gt; and &lt;strong&gt;142 on Android&lt;/strong&gt;. Set &lt;code&gt;strict_min_version&lt;/code&gt; below 142 and the linter throws &lt;code&gt;KEY_FIREFOX_UNSUPPORTED_BY_MIN_VERSION&lt;/code&gt; — first for desktop, then again for Android. &lt;code&gt;142.0&lt;/code&gt; is the floor that satisfies both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The omnibox trap on Firefox for Android
&lt;/h2&gt;

&lt;p&gt;If your extension has an omnibox keyword like mine, do &lt;strong&gt;not&lt;/strong&gt; tick "Firefox for Android" compatibility on AMO. The &lt;a href="https://extensionworkshop.com/documentation/develop/differences-between-desktop-and-android-extensions/" rel="noopener noreferrer"&gt;&lt;code&gt;omnibox&lt;/code&gt; API is not supported on Firefox for Android&lt;/a&gt; — the popup still works as an overlay, but the address-bar keyword silently does nothing. Ship desktop-only until you've adapted and tested for mobile, or you'll be shipping a broken core feature to Android users.&lt;/p&gt;

&lt;h2&gt;
  
  
  One source, three zips
&lt;/h2&gt;

&lt;p&gt;Rather than maintain three manifests by hand, keep one and transform it at build time. The whole script is ~40 lines; the interesting part is the Firefox transform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;firefoxManifest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;structuredClone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;fx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;background&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;scripts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;background.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;fx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;browser_specific_settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;gecko&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-ext@yourdomain.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;strict_min_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;142.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;data_collection_permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// pack("chrome", base);&lt;/span&gt;
&lt;span class="c1"&gt;// pack("edge",   base);              // === Chrome, kept separate for a clean upload trail&lt;/span&gt;
&lt;span class="c1"&gt;// pack("firefox", firefoxManifest(base));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;pack()&lt;/code&gt; stages the shared files (&lt;code&gt;popup.*&lt;/code&gt;, &lt;code&gt;background.js&lt;/code&gt;, icons, plus the per-store manifest) and zips them with paths at the archive root (&lt;code&gt;manifest.json&lt;/code&gt; must sit at the top level, not under a subfolder).&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate before you upload
&lt;/h2&gt;

&lt;p&gt;Run &lt;a href="https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#web-ext-lint" rel="noopener noreferrer"&gt;&lt;code&gt;web-ext lint&lt;/code&gt;&lt;/a&gt; — the same validator AMO runs — on the Firefox package &lt;em&gt;before&lt;/em&gt; submitting. It catches the version-floor and data-collection issues above locally, instead of after a rejected upload. My bar was 0 errors / 0 warnings / 0 notices before the zip went anywhere near the store.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chrome == Edge&lt;/strong&gt; — one MV3 zip, upload to both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firefox&lt;/strong&gt; — three manifest lines (&lt;code&gt;background.scripts&lt;/code&gt;, &lt;code&gt;gecko.id&lt;/code&gt;, &lt;code&gt;data_collection_permissions&lt;/code&gt;) and one version floor (&lt;code&gt;142.0&lt;/code&gt;). No code changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Omnibox on Android&lt;/strong&gt; — unsupported; don't tick the box.&lt;/li&gt;
&lt;li&gt;Lint locally with &lt;code&gt;web-ext&lt;/code&gt; so the store never says no.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're maintaining separate repos or manifests per browser, collapsing to one source + a build transform is an afternoon well spent.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>firefox</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Two architectures for "script to video", and why the credit meter follows from the design</title>
      <dc:creator>AI Alleyway</dc:creator>
      <pubDate>Fri, 03 Jul 2026 07:09:17 +0000</pubDate>
      <link>https://dev.to/aialleyway/two-architectures-for-script-to-video-and-why-the-credit-meter-follows-from-the-design-489f</link>
      <guid>https://dev.to/aialleyway/two-architectures-for-script-to-video-and-why-the-credit-meter-follows-from-the-design-489f</guid>
      <description>&lt;p&gt;Two AI video tools take the same input — a short script — and hand back the same shape of output: a captioned vertical clip with a voiceover. Feed the same brief to both and one produces a clip for the equivalent of about 2 credits; the other, on its premium setting, burns about 40 credits for a single 30-second clip on a 75-credit monthly plan.&lt;/p&gt;

&lt;p&gt;A 20× spread on identical-looking output is the kind of thing that looks like arbitrary pricing until you look at the pipeline. It isn't arbitrary. The credit meter is a projection of the architecture, and once you see the two designs, the prices — and the free-tier policies, and where each tool spends its quality budget — all fall out of the diagram.&lt;/p&gt;

&lt;p&gt;I tested both (Fliki and InVideo) hands-on for a comparison, so to be clear about scope up front: I can't see either company's source. What follows is the architecture the &lt;em&gt;observable behavior&lt;/em&gt; implies — the models each one exposes and the credit costs I actually watched tick down. Treat it as a systems read, not an internal spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pipeline A: assemble a voice, backfill the picture
&lt;/h2&gt;

&lt;p&gt;Fliki's design is a &lt;strong&gt;text-to-speech assembly pipeline&lt;/strong&gt;. Trace a script through it and the stages look roughly like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Parse the script and segment it into scenes.&lt;/li&gt;
&lt;li&gt;Synthesize narration from a large TTS voice library — 2,000+ voices across 80+ languages.&lt;/li&gt;
&lt;li&gt;Time captions to the audio.&lt;/li&gt;
&lt;li&gt;Backfill each scene's background from stock or an AI-generated still.&lt;/li&gt;
&lt;li&gt;Mux audio + captions + background into a clip.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The thing to notice is &lt;em&gt;where the cost and the differentiation live&lt;/em&gt;. Every stage except one is cheap and mostly deterministic — segmentation, caption timing, and asset lookup are database-and-glue work. The one stage that's both expensive and the actual product is &lt;strong&gt;TTS synthesis&lt;/strong&gt;. That's why Fliki pours its quality budget into the voice library and lets the visuals stay basic: the visuals are a lookup, the voice is the inference.&lt;/p&gt;

&lt;p&gt;TTS is also, in compute terms, &lt;em&gt;cheap inference&lt;/em&gt; relative to what's coming in pipeline B. A few seconds of neural speech is orders of magnitude less GPU time than a few seconds of generated video. So the marginal cost of one Fliki clip is close to flat, and low. Two consequences fall straight out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The free tier can actually export a finished (watermarked, 720p) video.&lt;/strong&gt; A free export costs Fliki almost nothing, so it can afford to give one away as a real test drive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The paid floor is low&lt;/strong&gt; — about $8/month for the entry plan — because the pipeline it's amortizing is cheap.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pipeline B: generate the footage, then narrate
&lt;/h2&gt;

&lt;p&gt;InVideo's design is a &lt;strong&gt;generative-model orchestration layer&lt;/strong&gt;. The stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take a one-line prompt and expand it into a storyboard/plan (an LLM step).&lt;/li&gt;
&lt;li&gt;For each scene, call a text-to-video model — it reaches Veo 3.1, Sora 2, Kling, and Seedance, 200+ models in all — to &lt;em&gt;generate&lt;/em&gt; original footage.&lt;/li&gt;
&lt;li&gt;Generate a voiceover.&lt;/li&gt;
&lt;li&gt;Assemble.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here the expensive stage isn't the voice — it's &lt;strong&gt;step 2&lt;/strong&gt;, and it's expensive by a different order of magnitude. Generating a few seconds of novel video from a frontier diffusion/video model is among the most compute-heavy inferences you can buy right now. That single stage dominates the cost function so completely that everything else rounds to zero.&lt;/p&gt;

&lt;p&gt;Now the 20× makes sense. When InVideo pulls a &lt;em&gt;stock&lt;/em&gt; clip, step 2 degrades to a lookup and the clip costs ~2 credits — same class of operation as Fliki's backfill. When it &lt;em&gt;generates&lt;/em&gt; a premium Veo/Sora clip, you're paying for GPU-seconds of a frontier model, and that's the ~40-credit clip. Same tool, same UI, two completely different cost regimes depending on whether step 2 retrieves or generates.&lt;/p&gt;

&lt;p&gt;And the same two consequences invert:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The free tier cannot export a usable video&lt;/strong&gt;, because a single free generation is real, non-trivial GPU cost — you can't give that away the way you give away a TTS clip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The paid floor is higher&lt;/strong&gt; (about $20/month entry) and the meter is a &lt;em&gt;monthly&lt;/em&gt; pool of ~75 credits that one ambitious clip can gut, rather than the slow-draining yearly pool an assembly tool can offer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The meter is a shadow of the pipeline
&lt;/h2&gt;

&lt;p&gt;Put the two side by side and the pricing stops looking like a marketing decision and starts looking like an accounting identity:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Observable&lt;/th&gt;
&lt;th&gt;Fliki (assembly)&lt;/th&gt;
&lt;th&gt;InVideo (generation)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dominant-cost stage&lt;/td&gt;
&lt;td&gt;TTS synthesis&lt;/td&gt;
&lt;td&gt;per-scene video generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marginal cost per clip&lt;/td&gt;
&lt;td&gt;low, ~flat&lt;/td&gt;
&lt;td&gt;low for stock, high for generative&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credit cost, one clip&lt;/td&gt;
&lt;td&gt;~2-equivalent&lt;/td&gt;
&lt;td&gt;~2 stock / ~40 generative&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free tier exports?&lt;/td&gt;
&lt;td&gt;yes (cheap pipeline)&lt;/td&gt;
&lt;td&gt;no (a free gen is real GPU cost)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meter shape&lt;/td&gt;
&lt;td&gt;slow yearly pool&lt;/td&gt;
&lt;td&gt;monthly pool one clip can drain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entry price&lt;/td&gt;
&lt;td&gt;~$8/mo&lt;/td&gt;
&lt;td&gt;~$20/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where quality is spent&lt;/td&gt;
&lt;td&gt;the voice&lt;/td&gt;
&lt;td&gt;the footage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of the right-hand column is a pricing "choice" in isolation. It's what the generative pipeline &lt;em&gt;costs to operate&lt;/em&gt;, expressed as credits. The left column is the same story for a cheap pipeline.&lt;/p&gt;

&lt;p&gt;This generalizes past these two tools, and it's the actually-useful part if you build or buy in this space: &lt;strong&gt;find the dominant-cost stage of a pipeline and you've predicted its pricing model, its free-tier policy, and where it spends quality.&lt;/strong&gt; A tool whose expensive stage is retrieval will have a generous free tier and a flat, low meter. A tool whose expensive stage is frontier-model inference will gate the free tier and meter aggressively, because it has to — the unit economics don't allow anything else. When a pricing page confuses you, reverse the arrow: ask what the tool must be spending compute on, and the meter usually explains itself.&lt;/p&gt;

&lt;p&gt;For the buyer, the practical read is the same one the architecture predicts: if the &lt;em&gt;voice&lt;/em&gt; carries your video, the assembly pipeline is the efficient match and you'll pay a low, predictable meter. If the &lt;em&gt;footage&lt;/em&gt; is the product, you're buying GPU-time-as-credits and your job is to ration the generative stage — treat the 40-credit clip as a deliberate spend, not a default.&lt;/p&gt;

&lt;p&gt;Both, incidentally, export clean 1080p on their paid plans, which is the tell that the resolution was never the differentiator — what fills the frame is. One holds a generic AI still; the other holds generated footage. That's the whole 20×.&lt;/p&gt;

&lt;p&gt;I scored the two 4.3 and 4.2 respectively, and if you want the full head-to-head — the credit math on a real posting cadence, the voice-library depth, and which one to pick by what carries your videos — I wrote it up here: &lt;strong&gt;&lt;a href="https://aialleyway.com/fliki-vs-invideo/" rel="noopener noreferrer"&gt;Fliki vs InVideo&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But the pricing itself you can now read straight off the diagram: cheap pipeline, cheap meter, free export; expensive pipeline, expensive meter, no free export. The credit gap was the architecture talking the whole time.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>systemdesign</category>
      <category>video</category>
    </item>
  </channel>
</rss>
