The Hosting Rejection Tour: Render, AWS EC2, Oracle, and How I Ended Up on a €3.29/mo VPS

Before the stack worked, three hosting options failed. Each one looked perfect on paper. Each one had a specific, concrete reason it couldn’t work. This is the rejection tour.

The constraint: Medusa v2 is stateful. It needs PostgreSQL, Redis for BullMQ, and enough RAM to run the event bus and workflow engine without getting killed. That rules out serverless. (Medusa can technically start without Redis — it falls back to an in-process event bus — but that means losing job queuing, workflow retries, and reliable event delivery. Fine for a quick local demo; not something you want processing real orders.) It needs persistent storage, a real process manager, and a consistent IP for outbound webhook validation. Every “just deploy it for free” option assumes your workload is stateless. This one isn’t.

Attempt 1: Render

Render’s free tier looked fine for a low-traffic shop. 512MB RAM, easy deploys from GitHub — cold starts on the free tier, always-on on the paid plan. The plan was to pair it with Neon for PostgreSQL (generous free tier, serverless scaling) and Upstash for Redis (free tier covers BullMQ at low volume). Three managed services, total cost: $0.

I got Medusa running, ran the setup scripts, and then installed the Prodigi print fulfillment plugin — which loads product mappings and prefetches shipping zones at startup.

OOM kill on boot. 512MB wasn’t enough.

The obvious fix is to upgrade the Render service. Their starter plan is $7/mo. That’s just the API — Neon and Upstash stay free at low volume, but now I’m at $7/mo for a single service with no room to grow, and I still hadn’t solved staging. Render’s starter plan is $7/mo per service. Two environments means two API instances: $14/mo before touching the databases.

I could have stripped the fulfillment plugin to fit inside 512MB. But without it there are no print orders — Prodigi integration is how open-edition prints get produced and shipped. Dropping it to hit a RAM limit means the shop sells originals only, which wasn’t the brief.

Attempt 2: AWS EC2

After Render, AWS EC2 looked like the right move. A t4g.micro in eu-west-2 (London): 1 GiB RAM, 2 vCPU ARM64 Graviton2, always-on, full SSH access, swap configurable — all within the AWS free tier for the first 12 months. Double the memory of Render’s paid plan, at no cost. I built out the full setup: CloudFormation stack, Nginx reverse proxy, Let’s Encrypt TLS, systemd service, GitHub Actions CD, health check cron.

It ran. There were a couple of ARM64 quirks — a ts-node source-map crash under Bun that needed TS_NODE_SKIP_SOURCE_MAP_SUPPORT=1, and Node.js auto-limiting the V8 heap to ~512MB on a 1 GiB instance, fixed with NODE_OPTIONS=--max-old-space-size=1024. But Medusa came up, Prodigi loaded, the health check passed.

Then I ran a checkout flow under load. OOM kill.

The 1 GiB ceiling wasn’t enough headroom when Prodigi, Medusa, and an active checkout were all competing for memory at once. And the infrastructure complexity — CloudFormation, Nginx config, cert renewal timers, CloudWatch, a manual cutover checklist — was a real ongoing cost for a solo project. After the free year, the bill would be ~$7.90/mo anyway. At that price you can just buy a VPS that has the memory you need and none of the ceremony.

Attempt 3: Oracle Always Free

Oracle’s Always Free tier is, on paper, absurd. The A1 Flex shape gives you up to 4 OCPU and 24 GB RAM at no cost, permanently. ARM64, which is what Hetzner’s cheapest VPS uses anyway. UK London region available. I signed up.

“The selected shape is not available in this Availability Domain. Please try a different Availability Domain or shape.”

I tried every Availability Domain in UK London. Same message. I tried Frankfurt. Same message. I tried Amsterdam. Same.

This isn’t a new problem. The Oracle community forum has threads going back two years with hundreds of people reporting A1 capacity unavailable in every European region. Oracle adds capacity occasionally, and it disappears within hours as people snap it up. The forum advice is to script retries and keep trying. I tried that for a few weeks.

The Always Free tier only works if you can actually provision the instance. If capacity is gone, it’s gone indefinitely. For a project you want to actually ship, “keep retrying and hope” isn’t a deployment strategy.

What actually worked: Hetzner CAX11

Hetzner’s CAX11 is €3.29/mo. ARM64, 2 vCPU, 4 GB RAM, 40 GB NVMe SSD, 20 TB/month egress included. I signed up, got the server provisioned in about 30 seconds, and had Medusa running the same afternoon.

The setup that’s been stable since:

Two systemd services, one box. Dev and production run as separate Medusa instances — different ports, different .env files, different PostgreSQL databases. Caddy sits in front and routes api-dev.nadiapoe.co.uk and api.nadiapoe.co.uk to the right one. Each service restarts automatically on failure. No Docker, no orchestration — just two medusa.service unit files and a Caddy config.

Deploys via GitHub Actions. On push to main, a workflow SSHes in, pulls the latest code, runs bun install and bun run build, and restarts the appropriate service. Under 60 seconds end to end.

Backups to Cloudflare R2. A nightly cron runs pg_dump and uploads the compressed archive to a dedicated R2 bucket (nadiapoe-backups). R2 is already in the stack for media — adding a backup bucket costs nothing extra. Retention is 7 daily dumps on-server, 30 in R2. If the VPS burns down, restoring is pg_restore and a git pull.

The total monthly cost: €3.29. Both environments. All services. Backups included.

The lesson

“Free” hosting for stateful backend workloads usually means one of two things: a RAM ceiling that kills anything real, or a capacity queue where “available” means “available when someone else cancels.” For a stateless frontend or a simple API, free tiers are great — Cloudflare Pages handles the storefront for nothing. But the moment you have a process that needs to stay alive, own persistent state, and run at startup, a cheap paid VPS is more reliable than a free tier with asterisks.

Hetzner CAX11 at €3.29/mo is less than most people spend on a coffee a month. It’s not free. It’s better than free.

One genuine acknowledgement before moving on: the free tiers from Cloudflare, Resend, Neon, and Upstash make it possible to build and prove a business before it earns a penny. They lower the bar for anyone who wants to try something without betting money on it first. That matters, and it’s worth saying.

Live shop: nadiapoe.co.uk.

I wrote a free, open-source book on the math behind AI in freeCodeCamp

Why do a lot of people fail at AI?

Because the math scares them.

TLDR

Why I wrote this book?

I started learning machine learning before ChatGPT came out

A big problem I had back then was understanding the mathematics behind machine learning.

An engineering bachelor’s degree later (electrical and computer engineering) and hundreds of thousands of views on my freeCodeCamp blog, I decided to write a book on the math behind AI!

Also, I am currently doing a master degree in AI at northeastern university.

The book teaches the math in simple and plain English with a lot of analogies.

Currently it has +50K+ views, and the GitHub repo has +140 stars.

Also, I tried to balance the best way possible the depth and making it accessible.

Finally, I show a lot of examples where the math concepts of AI are applied in fields of engineering and how these math ideas power billion dollar industries.

What is in the book?

  • The Architecture of Mathematics: How math connects from foundations to AI, including Gödel’s paradoxes and Einstein’s relativity
  • The Field of Artificial Intelligence: From Control Theory to modern AI, understanding symbolic vs. non-symbolic AI approaches
  • Linear Algebra: Vectors, matrices, determinants, eigenvalues, and transformations that show geometry of data in machine learning
  • Multivariable Calculus: Limits, Derivatives, and Integrals.
  • Probability & Statistics: Bayesian methods, distributions, and Markov models for learning from uncertainty
  • Optimization Theory: Gradient descent, Adam optimizer, and how machines learn step by step
  • Real-World Applications: A lot of Python code examples, animated visualizations, and practical examples of where the math is applied

How can you help?

Link to the book: https://www.freecodecamp.org/news/the-math-behind-artificial-intelligence-book/

The book repo is here: https://github.com/tiagomonteiro0715/The-Math-Behind-Artificial-Intelligence-A-Guide-to-AI-Foundations

If it’s useful to you:

  • A star helps more people find it
  • Suggest topics you’d want covered next in a new version of the book!

Saw a mistake in the book?

Send me an email!

[email protected]

Before You Put a Fabric AI Agent in Production, Steal This Checklist

Medium hero

Originally published at https://shai-kr.github.io/data-ninja-ai-lab/blog/2026-05-17-fabric-ai-agent-production-checklist.html.

A Fabric AI Agent demo can become useful faster than most teams expect.

Connect it to a semantic model. Ask a few business questions. Add context from Eventhouse, a Lakehouse, or a Warehouse. Suddenly the demo feels close to something people could use.

That is exactly where teams need to slow down for one hour.

Not to block the idea. To stop the first working demo from becoming a messy production workload.

This is the checklist I would use before moving a Fabric AI Agent past pilot stage.

Medium hero

1. Give the agent its own identity

A demo can run under a human account. Production should not.

If an agent depends on one person’s access, the operating model is fragile. Permissions change when that person changes role. Ownership becomes unclear. Offboarding becomes risky. Troubleshooting becomes personal instead of operational.

For a production agent, the better pattern is workload identity.

That means the agent has a dedicated service principal, with access that can be granted, reviewed, rotated, and removed without depending on someone’s user account.

This is the first line I would draw between a pilot and something ready for business users.

2. Start with one narrow use case

The easiest way to make an AI agent hard to govern is to connect it to everything.

Start smaller.

A useful production candidate sounds like this:

  • Explain sales variance from a governed semantic model
  • Summarize operational events from Eventhouse
  • Answer inventory questions for a specific operations team
  • Help finance users understand reconciliation status
  • Query a curated warehouse table for one business workflow

A weak production candidate sounds like this:

“Let it answer questions about our data.”

That is too broad. It gives the agent no clear boundary and gives the team no clean way to review access.

Reference architecture

3. Map the data sources before adding them

For every data source the agent can reach, write down why it needs it.

Not in a 20-page governance document. A short access inventory is enough:

  • Workspace
  • Semantic model, Eventhouse, Lakehouse, or Warehouse
  • Read-only or operational access
  • Business owner
  • Approval date
  • Review date

The point is simple: someone should be able to look at the agent and understand its blast radius.

If nobody can explain what the agent can reach, the agent is not ready.

4. Separate dev, test, and production

Most demos start in one workspace, with one identity, and one person who understands the setup.

That is normal.

Leaving it that way is the problem.

Before production, I would want a clean path across environments:

  • Development for experimentation
  • Test for validation
  • Production for the restricted, supported version

The identities and permissions do not always need to be complicated. They do need to be deliberate.

If dev and production use the same broad access, every experiment becomes a production risk.

Checklist

5. Confirm the audit path

If the agent gives a bad answer, uses the wrong source, or becomes part of a business process it was not designed for, you need evidence.

Before launch, answer these questions:

  • Which identity did the agent use?
  • Which data source was involved?
  • Who can review activity?
  • Who investigates issues?
  • How do we separate an agent issue from a model issue?

This is where a lot of AI work gets uncomfortable. The demo focuses on the answer. Production needs the trail behind the answer.

6. Treat new data sources as a change request

The first agent will not stay still.

Someone will ask to add finance data. Then operations data. Then a shortcut. Then an Eventhouse function. Then a warehouse table.

Some of those requests will be valid.

They should still trigger a review.

Every new data source changes the agent’s scope. That means the identity, permissions, audit path, and owner should be checked again.

Demo to production

The short version

Before a Fabric AI Agent goes live, I would want these six checks done:

  1. Dedicated service principal
  2. Narrow use case
  3. Known data sources
  4. Separated environments
  5. Least-privilege permissions
  6. Clear audit path and owner

If those answers are vague, the agent is still a pilot.

That is not a failure. It just means the platform work is not finished.

The goal is not to slow down AI agents. The goal is to make them safe enough to use with real business data.

Shai Karmani

Let’s connect on LinkedIn

Beyond useEffect: Specialized Effect Hooks for Async, Deep Comparisons, and SSR

Beyond useEffect: Specialized Effect Hooks for Async, Deep Comparisons, and SSR

React gives you exactly one effect hook: useEffect. Every other effect pattern — running once after mount, skipping the first render, comparing object dependencies, handling async work without a race condition, running layout effects on the server without a warning — is something you have to assemble yourself. Most teams end up shipping the same five or six wrapper hooks in a utils/hooks.ts file. Different teams write subtly different versions of the same thing. Some of those versions are broken.

This is the kind of repetitive infrastructure that does not belong in your codebase. ReactUse ships the specialized effect hooks already — small, focused wrappers around useEffect and useLayoutEffect that close the most common gaps. This post walks each of nine of them: what useEffect makes awkward, what the hook does differently, and a concrete example of where it fits.

If you already use ReactUse for timers, observers, or browser APIs, you have already imported some of these without thinking about it. The point of going through them deliberately is to know what is in the toolbox before you write the wrapper one more time.

Why useEffect Alone Is Not Enough

Take a single line from a real component:

useEffect(() => {
  fetch(`/api/user/${id}`).then((r) => r.json()).then(setUser);
}, [id]);

That snippet has four problems on day one and a fifth one a month later:

  1. No abort. If id changes mid-flight, the old request resolves after the new one and overwrites the newer data — the classic race condition.
  2. No async/await. You cannot mark the effect callback async because React expects either undefined or a cleanup function, not a Promise. So every async effect either uses .then chains or wraps an IIFE.
  3. No skip-on-mount option. Sometimes you want to react to id changing but not run the effect when the component first renders (the parent already gave you the initial data). Plain useEffect always runs at least once.
  4. No deep compare on deps. If id is { workspace: "a", user: "b" }, every render produces a new object reference and the effect runs every time, even though nothing changed.
  5. SSR + useLayoutEffect. A month later someone switches the component to use useLayoutEffect for some DOM measurement, and now SSR logs a warning on every page render.

Each of these is fixable, but the fix takes 5 to 30 lines and is easy to get subtly wrong. The hooks below close each gap directly.

1. useAsyncEffect — async/await Without the IIFE

The pattern everyone writes the first time:

useEffect(() => {
  let cancelled = false;
  (async () => {
    const r = await fetch(`/api/user/${id}`);
    const data = await r.json();
    if (!cancelled) setUser(data);
  })();
  return () => { cancelled = true; };
}, [id]);

That works. It is also six lines of boilerplate to do what async () => { setUser(await fetch(...).then((r) => r.json())); } would do if React allowed it. useAsyncEffect closes that gap:

import { useAsyncEffect } from "@reactuses/core";

useAsyncEffect(async () => {
  const r = await fetch(`/api/user/${id}`);
  setUser(await r.json());
}, [id]);

The hook accepts an async callback directly and ignores the Promise return value (no false-positive cleanup warning). It does not handle cancellation for you — that is the next hook’s job, or a manual AbortController. Use useAsyncEffect when the async body is short and you do not need to bail out partway through. When you do need cancellation, wire up an AbortController:

useAsyncEffect(async (signal) => {
  const r = await fetch(`/api/user/${id}`, { signal });
  setUser(await r.json());
}, [id]);

The hook passes an AbortSignal as the first argument and aborts it on cleanup, so the in-flight request is cancelled instead of resolving into a stale state setter.

This single hook removes about 80% of the “I should have used a wrapper” moments in a typical codebase. Most data-fetching effects are short, async, and want to be cancelled on change. useAsyncEffect is exactly that.

2. useUpdateEffect — Skip the Mount

useEffect always runs after the first render. Sometimes that is wrong: if a component already received its initial value from props, running the effect on mount duplicates work or fires a “value changed” notification when nothing has actually changed yet.

The workaround in plain React is a ref:

const isFirst = useRef(true);
useEffect(() => {
  if (isFirst.current) { isFirst.current = false; return; }
  onChange(value);
}, [value]);

That works, but every team has at least three versions of this in their codebase. useUpdateEffect is the same thing as useEffect minus the first run:

import { useUpdateEffect } from "@reactuses/core";

useUpdateEffect(() => {
  onChange(value);
}, [value]);

The most common use case is controlled-component change notifications. You want to call onChange when the internal value updates, not when the parent first mounts the component with an initial value. The plain useEffect version fires on mount and the parent gets a spurious onChange(initialValue) before the user has done anything.

A second use case is analytics: “fire a viewed_filter event when the filter changes.” The mount is not a change; it is the starting state.

3. useMount — The “Run Once on Mount” Idiom

useEffect(() => { /* ... */ }, []) is technically correct for “run once on mount.” It is also visually noisy and a common lint-rule misfire (eslint’s exhaustive-deps will complain if the callback closes over any variables, even when you genuinely mean “snapshot at mount”).

useMount is a one-purpose alias that documents intent:

import { useMount } from "@reactuses/core";

useMount(() => {
  trackPageView();
  initialiseSentry();
});

Functionally identical to useEffect(fn, []), but the name is the documentation. When you see useMount, you know without reading the deps that the callback fires exactly once. When you see useEffect(fn, []), you have to scan the body to confirm there are no closed-over reactive variables that should have been in the deps.

4. useUnmount — Cleanup Without the Empty Effect

The mirror of useMount. The plain-React version of “do X on unmount” is:

useEffect(() => () => doCleanup(), []);

That parses as “the effect callback returns a cleanup function.” It is correct, but the inner double arrow is the kind of thing nobody reads twice. useUnmount is the explicit version:

import { useUnmount } from "@reactuses/core";

useUnmount(() => {
  socket.close();
  flushAnalytics();
});

The hook captures the latest callback (via a ref internally) so you get the most recent values at unmount time, not the values from mount. That fixes a subtle bug in the plain-React version: if you write useEffect(() => () => doCleanup(value), []), value is captured at mount and the cleanup runs against stale data. useUnmount does not have that bug.

5. useDeepCompareEffect — When Your Deps Are Objects

React compares effect deps with Object.is. If the dep is an object or array, every parent re-render produces a new reference and the effect runs even though the contents are identical. The “fix” most teams reach for is JSON.stringify-ing the dep, which works for shallow data and breaks for anything with functions, Dates, or non-serializable values.

useDeepCompareEffect replaces Object.is with a structural deep equality check:

import { useDeepCompareEffect } from "@reactuses/core";

useDeepCompareEffect(() => {
  fetcher.run(query);
}, [query]); // query is { workspace: "a", filters: { ... } }

When the parent re-renders and produces a new query object with the same contents, the effect does not re-run. When the contents actually change, it does. The trade-off is that deep equality is O(n) on the dependency size — it is not free. Reach for it when you have a small object dep you cannot memoize at the source. If you can useMemo the dep, prefer that.

There is one trap: do not use useDeepCompareEffect with primitive-only deps. The hook throws if you pass [someString, someNumber] — for those, useEffect is the right tool, and the hook fails loudly so you do not silently slow down an effect that did not need it.

6. useCustomCompareEffect — Deep Compare, Your Rules

Sometimes the equality you want is neither shallow nor fully structural. Two cases come up often:

  • Compare by a single key (e.g., prev.id === next.id).
  • Compare with a library you already depend on (e.g., lodash.isEqual, dequal).

useCustomCompareEffect takes a third argument: a comparator that decides whether the new deps should trigger the effect.

import { useCustomCompareEffect } from "@reactuses/core";
import { dequal } from "dequal";

useCustomCompareEffect(
  () => loadDashboard(filters),
  [filters],
  (prev, next) => dequal(prev, next),
);

The benefit over useDeepCompareEffect is you control the cost. Deep equality on a 200-key config object is slow; (prev, next) => prev.version === next.version is one comparison. If you have a version field, use it.

This is also the right hook for fuzzy equality — e.g., “treat two scroll positions as equal if they are within 5 pixels of each other.” The plain useEffect version requires a wrapper ref and a manual comparison inside the effect body; the custom-compare version puts the equality logic next to the deps.

7. useOnceEffect — Run Exactly Once, With Reactive Deps

useEffect(fn, []) runs once on mount, but the callback closes over whatever the deps would have been at that moment — usually undefined or initial values. If you actually want the first non-loading value of user to trigger an effect, neither useEffect(fn, [user]) (runs every time user changes) nor useEffect(fn, []) (runs at mount when user is still null) is correct.

useOnceEffect runs the effect the first time any dep changes from its initial value, then never again:

import { useOnceEffect } from "@reactuses/core";

function PersonalisedGreeting() {
  const { user } = useAuth(); // user is null until loaded

  useOnceEffect(() => {
    track("personalised_greeting_seen", { userId: user.id });
  }, [user]);

  return user ? <h1>Hi, {user.name}!</h1> : null;
}

The effect fires once — the first time user becomes non-null — and never fires again, even if user changes later. This is the right shape for first-paint analytics, one-time onboarding triggers, and “do this once the prerequisite is ready” patterns. The plain-React version of this is a ref-and-flag dance that everyone has written and nobody enjoys reading.

useOnceEffect also has a layout-effect sibling, useOnceLayoutEffect, for the same pattern when you need DOM measurements before paint.

8. useIsomorphicLayoutEffect — Stop the SSR Warnings

useLayoutEffect runs synchronously after DOM mutations and before paint. It is the right hook for reading layout (measuring an element’s size) and writing to the DOM in the same tick (positioning a tooltip relative to a trigger). It is also the hook that prints this warning during server-side rendering:

useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer’s output format.

The standard fix is to swap useLayoutEffect for useEffect when typeof window === "undefined". That is what useIsomorphicLayoutEffect does:

import { useIsomorphicLayoutEffect } from "@reactuses/core";

useIsomorphicLayoutEffect(() => {
  const { width } = ref.current!.getBoundingClientRect();
  setWidth(width);
}, []);

On the server, this is useEffect (which is a no-op during SSR — fine, because there is no layout to measure). On the client, it is useLayoutEffect (which fires synchronously, which is what you want for layout reads). One import, no warning, no special-casing.

This is the single most-copied snippet in the React ecosystem. If you have a useLayoutEffect anywhere in an SSR codebase (Next.js, Remix, Astro with islands), this hook should be the default.

9. useUpdateLayoutEffect — The Layout Variant of useUpdateEffect

The layout-effect sibling of useUpdateEffect. Same pattern: skip the first render, run on every subsequent dep change, but at layout-effect time so the DOM mutations happen before paint.

useUpdateLayoutEffect shines for layout-driven animations:

import { useUpdateLayoutEffect } from "@reactuses/core";

useUpdateLayoutEffect(() => {
  const el = listRef.current;
  if (!el) return;
  el.style.transform = `translateY(${activeIndex * itemHeight}px)`;
}, [activeIndex]);

Why not useUpdateEffect? Because useEffect fires after paint, and the slide animation will visibly start from the previous position before snapping to the new one. useLayoutEffect runs before paint, which means the new transform is applied in the same frame. Why not plain useLayoutEffect? Because on the first render activeIndex is the initial value and there is no animation to start.

The combination “layout effect that skips mount” is the right shape for “animate a change but not the initial value.” It is also the right shape for managed focus: focus the new tab content when activeTab changes, but not when the component first mounts with activeTab="home".

When to Use Which: A Decision Table

The full set, in one place:

Situation Hook
async/await effect body, want cancellation useAsyncEffect
Skip the first run, react to subsequent changes useUpdateEffect
Same as above but for layout effects useUpdateLayoutEffect
Run a callback once on mount (intent is clearer) useMount
Run a callback once on unmount (no stale capture) useUnmount
Effect deps are objects, want structural equality useDeepCompareEffect
Effect deps need a custom equality check useCustomCompareEffect
Run once, but wait for a dep to become “ready” useOnceEffect
Same as above for layout effects useOnceLayoutEffect
Layout effect that doesn’t warn during SSR useIsomorphicLayoutEffect

Three rules to keep in mind:

  1. Default to useEffect. Specialized hooks are for the cases above; do not reach for them speculatively.
  2. Match layout to layout, async to async. If you are doing DOM measurement, the right family is the layout-effect hooks. If you are doing data fetching, the right family is useAsyncEffect. Mixing them produces flicker or race conditions.
  3. useUpdateEffect is not “useEffect optimization.” It changes behavior, not performance. The first render still happens, you just do not run the effect on it. If your goal is performance, look at the deps array, not at the hook.

A Realistic Combination

A common React pattern: a “search results” panel that fetches when the query changes, skips fetching on mount (the parent passed initial results), and announces “search updated” to screen readers — but not on mount, where the heading already conveys the same info.

import {
  useAsyncEffect,
  useUpdateEffect,
  useIsomorphicLayoutEffect,
} from "@reactuses/core";

function SearchResults({ query, initialResults }: {
  query: string;
  initialResults: Result[];
}) {
  const [results, setResults] = useState(initialResults);
  const announceRef = useRef<HTMLDivElement>(null);

  // Skip mount; fetch on every subsequent query change.
  useUpdateEffect(() => {
    let cancelled = false;
    fetch(`/api/search?q=${encodeURIComponent(query)}`)
      .then((r) => r.json())
      .then((data) => { if (!cancelled) setResults(data); });
    return () => { cancelled = true; };
  }, [query]);

  // Layout effect: read the results count and update aria-live before paint.
  // Skips mount because the initial heading already says it.
  useIsomorphicLayoutEffect(() => {
    if (!announceRef.current) return;
    announceRef.current.textContent = `${results.length} results for ${query}`;
  }, [results, query]);

  return (
    <>
      <div ref={announceRef} role="status" aria-live="polite" className="sr-only" />
      <ul>{results.map((r) => <li key={r.id}>{r.title}</li>)}</ul>
    </>
  );
}

Three behaviors, three hooks, no refs-and-flags. Replace the first useUpdateEffect with useAsyncEffect if the body grows complicated enough to want async/await; the rest stays the same.

Try Them Out

Every hook above has a runnable demo on its docs page. Read the demo, change the deps, watch what fires:

Install with npm install @reactuses/core (or pnpm add @reactuses/core) and import directly. No provider, no peer dependencies beyond React 16.8+. The full hook list and source for everything we discussed is at reactuse.com.

useEffect is a primitive. These hooks are the language you would build on top of it if you did it once and stopped re-inventing it across projects.

Wiring up a hybrid WebRTC + LL-HLS live stack (the protocol decision tree that actually works)

TL;DR

We’re going to build a hybrid live stack: presenters connect over WebRTC for sub-second feedback, the SFU re-publishes to RTMP, and the audience plays it back over LL-HLS at ~2 seconds. By the end you’ll have a decision tree, a working ingest config, an LL-HLS player setup, and a list of the failure modes that bit me in production.

📦 Code: github.com/USER/llhls-webrtc-hybrid, replace before publishing

The “WebRTC vs LL-HLS” debate is mostly fake. In production, you almost always want both. The presenter needs sub-second so they can talk over their co-host without that awful video-call lag, and the 5,000 people watching just want a stream that doesn’t buffer on their phone. We’re going to wire up that pattern, end to end.

💡 Tip: If your concurrent viewer count will stay under 500 and the experience is genuinely interactive, skip the LL-HLS half of this article. Pure WebRTC is fine for you. The hybrid pattern is for products where the audience grows past what your media server can hold.

1. The decision tree, codified

Before you write a line of code, ask two questions:

Q1: Does any viewer need sub-second latency (auctions, real-time interaction)?
    → YES: WebRTC is mandatory for those viewers.
    → NO:  LL-HLS only. You're done. Stop reading.

Q2: How many concurrent viewers in 18 months?
    → < 500:    All-WebRTC. Skip LL-HLS.
    → 500–10K:  Hybrid. WebRTC for presenters, LL-HLS for the rest.
    → 10K+:     Hybrid mandatory. LL-HLS is the only thing that scales here.

The math under that tree: a 4-core media server fits roughly 200 WebRTC viewers vs roughly 1,000 LL-HLS viewers. LL-HLS sits behind a CDN cleanly; WebRTC is per-session and can’t.

2. The stack we’re building

[Presenter]  --WebRTC-->  [SFU]  --RTMP-->  [Ingest]  --LL-HLS-->  [CDN]  -->  [Viewers]
                            |
                            +--WebRTC-->  [Co-presenter / Guest]

For the SFU, I’ll use LiveKit because it has a clean cloud-or-self-hosted story and its egress feature pushes RTMP out of the box. For ingest + packaging, I’ll use FFmpeg (free, hackable, the reference for everything). For playback, HLS.js with low-latency mode enabled.

⚠️ Note: This article isn’t a LiveKit tutorial. Substitute any SFU that supports RTMP egress (Janus, mediasoup, Daily, etc.). The pattern is the same.

3. Wire up the WebRTC half

A minimal browser-side publish using livekit-client. This is the presenter joining the stage:

// app/stage.js
import { Room, RoomEvent, createLocalTracks } from 'livekit-client';

const room = new Room({
  adaptiveStream: true,
  dynacast: true,
  publishDefaults: { simulcast: true },
});

await room.connect(import.meta.env.VITE_LIVEKIT_URL, await fetchJoinToken());

const tracks = await createLocalTracks({ audio: true, video: { resolution: { width: 1280, height: 720, frameRate: 30 } } });
for (const track of tracks) await room.localParticipant.publishTrack(track);

room.on(RoomEvent.ConnectionStateChanged, (state) => {
  console.log('[stage] state =', state);
});

The token is signed server-side. Here’s the minimal Node endpoint:

// server/token.js
import { AccessToken } from 'livekit-server-sdk';

export function joinToken(identity, room) {
  const at = new AccessToken(process.env.LIVEKIT_API_KEY, process.env.LIVEKIT_API_SECRET, { identity });
  at.addGrant({ roomJoin: true, room, canPublish: true, canSubscribe: true });
  return at.toJwt();
}

That gets the presenter and co-presenters on WebRTC. Their glass-to-glass is sub-300 ms on a decent network.

4. Re-publish the room to RTMP

LiveKit’s egress service can record or relay a room composition out to an RTMP endpoint. We’ll point it at our LL-HLS ingest.

# scripts/start-egress.sh
curl -X POST "$LIVEKIT_URL/twirp/livekit.Egress/StartRoomCompositeEgress" 
  -H "Authorization: Bearer $LIVEKIT_API_TOKEN" 
  -H "Content-Type: application/json" 
  -d '{
    "room_name": "live-show",
    "layout": "speaker",
    "audio_only": false,
    "stream_outputs": [
      { "protocol": "RTMP", "urls": ["rtmp://ingest.example.com/live/show1"] }
    ]
  }'

Now your SFU is pushing the composed stream into your ingest server as plain RTMP. Anything downstream that speaks RTMP can pick it up.

5. Package to LL-HLS with FFmpeg

This is the part most teams get wrong on the first try. The LL-HLS spec requires CMAF partial segments (typically ~200 ms), preload hints, and an EXT-X-SERVER-CONTROL tag in the manifest. FFmpeg 6.0+ supports this directly:

# scripts/package-llhls.sh
ffmpeg -i rtmp://ingest.example.com/live/show1 
  -c:v libx264 -preset veryfast -tune zerolatency -g 60 -keyint_min 60 -sc_threshold 0 
  -c:a aac -ar 48000 -b:a 128k 
  -hls_time 2 
  -hls_playlist_type event 
  -hls_segment_type fmp4 
  -hls_fmp4_init_filename init.mp4 
  -hls_segment_filename "/var/www/hls/seg_%05d.m4s" 
  -hls_flags independent_segments+program_date_time+append_list 
  -hls_list_size 6 
  -master_pl_name master.m3u8 
  -strftime 1 
  -method PUT 
  -http_persistent 1 
  -ldash 1 
  -window_size 6 
  -extra_window_size 3 
  -streaming 1 
  -seg_duration 2 
  -frag_duration 0.2 
  /var/www/hls/stream.m3u8

💡 Tip: The two parameters that actually matter for latency are -frag_duration 0.2 (200 ms CMAF chunks, which the LL-HLS spec wants) and -streaming 1 (write chunks as they’re produced rather than after the segment closes). Without those, you have plain HLS with extra steps.

The output is a /var/www/hls/ directory full of .m4s chunks plus a stream.m3u8 manifest with partial-segment annotations. Point your CDN at that directory and respect the chunked transfer encoding when you proxy.

6. Player setup

HLS.js with low-latency mode:

// app/player.js
import Hls from 'hls.js';

const video = document.querySelector('#viewer');
const hls = new Hls({
  lowLatencyMode: true,
  backBufferLength: 4,
  maxLiveSyncPlaybackRate: 1.5,
  liveSyncDuration: 1.5,
  liveMaxLatencyDuration: 3.5,
});

hls.loadSource('https://cdn.example.com/hls/stream.m3u8');
hls.attachMedia(video);

hls.on(Hls.Events.MEDIA_ATTACHED, () => video.play());

hls.on(Hls.Events.ERROR, (_evt, data) => {
  if (data.fatal) {
    console.error('[player] fatal', data.type, data.details);
    hls.startLoad();
  }
});

The values for liveSyncDuration and liveMaxLatencyDuration are the ones I’ve landed on after testing. They tell the player to stay 1.5 seconds from live with a 3.5-second hard cap before it resyncs. Tighter values rebuffer too often; looser values defeat the point of LL-HLS.

⚠️ Note: Safari uses its native HLS implementation, not HLS.js, and it respects the manifest’s EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES tag for low latency. FFmpeg’s LL-HLS muxer writes that tag for you when -streaming 1 is set. Verify by curling the manifest and grepping for it.

7. Verifying it actually works

The first time you wire this up, the latency will not be what you expected. Tools to sanity-check:

# Check the manifest has LL-HLS annotations
curl -s https://cdn.example.com/hls/stream.m3u8 | grep -E '(SERVER-CONTROL|PART-INF|EXT-X-PRELOAD-HINT)'

# Should print something like:
# #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6
# #EXT-X-PART-INF:PART-TARGET=0.2
# #EXT-X-PRELOAD-HINT:TYPE=PART,URI="seg_00123.m4s?byterange=..."

# Measure end-to-end latency with a clock overlay on the source
# Run on the presenter machine:
ffmpeg -re -f lavfi -i "color=c=black:s=640x360:d=600,drawtext=text='%{localtime}'" 
  -f flv rtmp://ingest.example.com/live/show1

Open the playback URL on another device, eyeball the difference between the wall clock on the source and the rendered clock in the player. Production LL-HLS on a tuned CDN gets you 2 to 3 seconds. WebRTC on the same path gets you under half a second.

8. The failure modes that bit me

A short list of things to check before you ship:

  1. CDN that doesn’t honor chunked transfer encoding. Some legacy CDNs buffer the segment fully before serving it. You’ll get plain HLS latency with all the LL-HLS code paths enabled. Test with curl: curl -v https://cdn.example.com/hls/stream.m3u8 2>&1 | grep -i chunked should show Transfer-Encoding: chunked.
  2. Player buffering “just in case”. Default HLS.js buffers more than you’d expect. The liveSyncDuration and liveMaxLatencyDuration values above are deliberate.
  3. TURN-only WebRTC sessions on the presenter side. Corporate firewalls force TURN, which adds a relay hop. Mitigate by deploying TURN close to your SFU.
  4. Egress lag from the SFU. RTMP relay from LiveKit egress has a built-in ~1-2 second buffer on top of LL-HLS latency. The hybrid pattern is fundamentally “WebRTC fast for presenters, LL-HLS slow-ish for audience”. Don’t try to make the audience side faster than the protocol allows.

What’s next

If you want to push the audience-side latency lower, the next step is HESP or WebRTC-WHEP for everyone, both of which have rougher edges than LL-HLS today but are worth tracking. If you want to reduce egress cost, look at peer-assisted delivery (Peer5, CDNBye) which works cleanly on top of LL-HLS.

For player customization (DVR window, captions, multi-audio), the HLS.js docs are the right next read. For ingest at scale, you’ll eventually outgrow single-box FFmpeg and want to look at managed packaging or a Kubernetes setup with horizontal ingest.

The protocol war is over. The interesting work in 2026 is building the right hybrid for your audience. Happy streaming.

Blast Radius Before Execution: Why Autonomous Cloud Must Check Idle Resources First

diagram

Blast Radius Before Execution: Why Autonomous Cloud Must Check Idle Resources First

Autonomous cloud remediation fails the same way every time. The recommendation is correct. The action is correct. The scope is wrong.

Stop the idle RDS instance. Correct recommendation. The instance has averaged 2% CPU for 30 days. It is genuinely idle. But it is also the database backing an internal integration endpoint that three production services call once a day, on different schedules, from different accounts. Stop it and you have a production incident at the next scheduled call time: 2:00 AM on a Tuesday.

The recommendation engine did not fail. The blast radius model was missing.

Autonomous systems that act without a blast radius check are not autonomous. They are automated. Automation executes instructions. Autonomy includes a model of consequences. Every auto-remediation action ZopNight certifies runs through a blast radius check before execution. This post defines what that check contains and how to score it.

The Correct-But-Wrong-Scope Problem

Cost tools are good at identifying idle resources. They look at CPU, memory, network, and storage I/O over a 14-30 day window. A resource below thresholds on all four dimensions is flagged as idle. The recommendation is statistically correct.

The tool does not know what depends on that resource. It cannot see the cross-account IAM role that calls the RDS instance from a Lambda in another account. It cannot see the CloudFront distribution that caches responses from the EC2 instance it flagged. It cannot see that the “idle” ElastiCache cluster is the cache warming target for a batch job that runs quarterly.

In the accounts we analyzed, 12-18% of resources flagged as idle have active downstream dependencies that would cause an incident if acted on without verification. That means 1 in 6 to 1 in 8 autonomous actions on a naive system would create an incident. No engineering team will accept that failure rate for unattended automation.

The blast radius check is the gate that separates the safe 82-88% from the risky 12-18%.

Defining Blast Radius: Three Inputs

Blast radius is the set of resources and services that fail or degrade if the target resource is stopped, modified, or deleted mid-action. It is a pre-execution estimate, not a post-incident measurement.

Three inputs define it.

The dependency graph. AWS VPC flow logs record every connection between resources in the past 14 days. A resource with no inbound or outbound connections in 14 days has a low dependency graph score. A resource with 200 connections per day across 4 VPCs has a high one. Service mesh telemetry (Istio, Linkerd) adds application-layer connection data for services that flow logs cannot see (same-host connections, gRPC multiplexed streams). The dependency graph has a 6-hour lag for VPC flow logs. Resources with less than 6 hours of flow log data default to high blast radius.

Criticality tier. Resource tags encode business context that infrastructure metrics cannot. A resource tagged env=production and tier=critical scores high blast radius regardless of its CPU utilization. A resource tagged env=dev and team=platform scores lower. Tags are not perfectly reliable: 23% of resources in typical accounts have stale or missing criticality tags. When the criticality tag is absent, blast radius defaults to medium.

Recency. A resource with no write operations in 24 hours is idle by the write signal. A resource with a write 18 hours ago is not idle. CloudTrail records write API calls against each resource. LastWriteTimestamp is the third input. Resources with writes in the last 24 hours get a recency penalty that raises their blast radius score regardless of CPU.

diagram

The Blast Radius Score: 0-100 and What Each Band Means

The three inputs produce a numeric score from 0 to 100. The score determines the action policy.

Score range Action policy What it means
0-29 Unattended execution No active dependencies, non-production tag, no recent writes. Safe to act without human review.
30-69 Notification window Possible dependencies or ambiguous tags. Action queued with 15-minute notification. Human can cancel.
70-100 Approval required Active dependencies confirmed, production tag, or recent writes. Action blocked until explicit approval.

A resource scores below 30 only if all three conditions hold: dependency graph shows no connections in 14 days, criticality tag is non-production, and no writes in 24 hours. This is the conservative definition of “safely idle.”

A resource scores above 70 if any one of these conditions holds: 50+ connections per day in flow logs, production or critical tag, or writes in the last 6 hours. One high-signal input overrides the others. An RDS instance tagged dev that has 200 daily connections scores above 70. The dev tag does not override the dependency signal.

The 30-69 band handles the ambiguous cases: resources with missing tags, flow log gaps, or moderate connection counts. The 15-minute notification window gives an engineer time to cancel an action they know is risky, without requiring them to pre-approve every action in the queue.

How ZopNight Gates Actions

Every automated remediation in ZopNight runs the blast radius check before execution. The check adds 2-4 seconds to the action pipeline. For actions that run unattended at 3:00 AM, 4 seconds is an acceptable gate latency.

diagram

In a typical month, across the ZopNight customer fleet: 67% of triggered remediations score below 30 and run unattended. 24% score 30-69 and enter the notification queue; 91% of those proceed after the window with no cancellation. 9% score above 70 and require approval; approval is granted for 78% of those, with 22% cancelled by the reviewing engineer.

The autonomous action log records the blast radius score alongside every action. This creates the audit trail: not just what action ran, but why the system considered it safe to run without human review.

The Half-Action Problem: Idempotency as a Blast Radius Input

Blast radius measures the risk of acting on a resource. It does not by itself measure the risk of acting and then failing halfway through.

A multi-step remediation that stops an EC2 instance, modifies its tags, and starts it again has three steps. If the network fails after step 1 (stopped) and before step 3 (started), the instance is stopped but not restarted. The resource is in a worse state than before the action ran. The original state was idle but running. The post-failure state is stopped unexpectedly. Recovery requires manual intervention.

Idempotency is the property that makes a remediation safe to retry from any point. A stop-and-delete action is not idempotent: running it twice on an already-deleted resource produces an error. A tag-update action is idempotent: running it twice produces the same result as running it once.

Non-idempotent remediations get a blast radius floor of 50, regardless of their dependency graph, criticality, and recency scores. This forces them into the notification queue minimum, never into the unattended queue.

Remediation type Idempotent Blast radius floor Example
Tag update Yes 0 (no floor) Add cost-center tag to EC2
Stop instance Yes 0 (no floor) Stop idle EC2 (safe to retry)
Delete snapshot No 50 Deleted snapshot cannot be recovered
Schema migration No 50 Partial schema change leaves DB inconsistent
Cross-account IAM change No 70 IAM changes have immediate effect
Stop + reconfigure + start Partially 50 Failure mid-sequence leaves wrong config

The blast radius score is a pre-execution risk estimate. It is not a guarantee. A resource that scores 15 can still cause an incident if the flow logs had a 12-hour gap and missed an overnight connection. The score reduces the probability of the wrong-scope failure from 12-18% to under 1%. It does not reduce it to zero. The ZopNight trust score model uses blast radius as one input alongside recommendation confidence and business hours context. No single signal is the gate. The gate is the composite.

Autonomous cloud is safe when the system knows what it does not know and routes accordingly. Blast radius is the model of what it does not know.

Three Open-Source Projects That Turn Your Mac Into a Private AI Workstation

Mano-P Architecture

The idea of running AI agents entirely on your laptop used to be a joke. A fun thought experiment you’d entertain over coffee before switching back to your cloud API dashboard and watching the bills pile up.

In 2026, it’s a real workflow.

Not a demo. Not a “technically possible if you squint” proof of concept. An actual, production-grade stack where a vision-language model sees your screen, operates your apps, accelerates inference on Apple Silicon, and builds entire applications from a product spec — all without a single byte leaving your machine.

At Mininglamp Technology, we’ve been building toward this with three open-source projects. Each solves a distinct piece of the on-device AI puzzle. Together, they form something we think is genuinely new: a complete private AI workstation stack that runs on a Mac.

Let’s walk through them.

1. Mano-P: The Agent That Sees Your Screen

Repo: github.com/Mininglamp-AI/Mano-P

Most “AI agents” are glorified API wrappers. They read text, call tools, and hope the tool’s interface hasn’t changed since the prompt was written. Mano-P takes a fundamentally different approach: it’s a GUI-VLA (Vision-Language-Action) model that perceives your screen the way a human does — by looking at it.

Mano-P comes in two sizes:

  • 72B (cloud/server): The full model, currently ranked #1 on OSWorld with a score of 58.2% — a significant lead over the second-place opencua-72b at 45.0%.
  • 4B (local): A distilled model designed to run entirely on-device. On an M5 Pro, it decodes at roughly ~80 tokens/second with a peak memory footprint of just 4.3GB. It runs on M4 chips with 32GB RAM.

Mano-P Architecture

What makes this interesting isn’t just the benchmark numbers — it’s the interaction model. Mano-P doesn’t need custom integrations or tool definitions. It sees buttons, text fields, menus, and dialogs the same way you do. Tell it “open Safari and find the latest Hacker News post about Rust,” and it navigates the GUI visually, clicking and typing as needed.

The 72B model also includes WebRetriever, a web navigation component that scores 41.7 on NavEval — ahead of Gemini 2.5 Pro (40.9) and Claude 4.5 (31.3). Web browsing as a first-class agent capability, not an afterthought.

Why This Matters

The traditional approach to computer-use agents is brittle. You build tool adapters, maintain API schemas, and pray that the next macOS update doesn’t break your Accessibility API hooks. A vision-first agent sidesteps all of that. If a human can use the app, Mano-P can use the app.

2. Cider: Inference Acceleration for Apple Silicon

Repo: github.com/Mininglamp-AI/cider

Running a 4B model at 80 tok/s on a Mac doesn’t happen by accident. It requires an inference engine that actually understands Apple Silicon’s hardware characteristics. That’s what Cider is.

Cider is an inference acceleration SDK built specifically for Apple’s M-series chips. Its key contribution is activation quantization — specifically W8A8 and W4A8 schemes — which fills a gap that MLX currently doesn’t cover. MLX supports weight-only quantization (W4A16, W8A16), but activations stay in full precision. Cider quantizes both weights and activations, which unlocks substantially better throughput.

Benchmark Overview

The Numbers

On an M5 Pro, Cider delivers 1.4–2.2x faster inference compared to MLX W4A16, depending on the quantization granularity you choose:

Quantization Granularity Speedup vs MLX W4A16
W8A8 / W4A8 Per-channel 1.8x (fastest)
W8A8 / W4A8 Per-group (gs=128) 1.5x
W8A8 / W4A8 Per-group (gs=64) 1.3x

There’s a tradeoff between speed and accuracy, as you’d expect. On the CUA Benchmark (M5, 16GB), W8A16 quantization maintains 58.0% accuracy while W8A8 comes in at 54.0%. Depending on your use case, that 4-point delta may or may not matter — for many agentic workflows, the speed gain is worth it.

Why Not Just Use MLX?

This isn’t about replacing MLX. MLX is excellent at what it does. But weight-only quantization hits a wall when you need both low memory and high throughput for real-time agent interactions. Activation quantization is the next lever, and right now, Cider is the open-source option that pulls it on Apple Silicon.

Think of it this way: MLX gives you the foundation. Cider fills the gap in activation quantization that lets you push throughput further on the same hardware.

3. Mano-AFK: The Autonomous App Builder

Repo: github.com/Mininglamp-AI/mano-afk

This is where things get wild.

Mano-AFK takes a PRD (Product Requirements Document) and turns it into a working application. Not a skeleton. Not boilerplate. A deployed, tested application — with zero human intervention in the loop.

Here’s the pipeline:

  1. Read the PRD — Parse requirements, extract features, identify tech stack
  2. Write the code — Generate the full application
  3. Deploy it — Spin up a local or containerized environment
  4. Test it visually — Using Mano-P’s vision model to actually look at the running app
  5. Find bugs — Compare what’s on screen to what the PRD specified
  6. Fix them — Modify code, redeploy, retest

Mano-Action Training Flow

The critical piece here is step 4. Most code-generation tools “test” by running unit tests they also generated — which is roughly as useful as grading your own homework. Mano-AFK uses Mano-P’s vision capabilities to perform visual testing: it loads the app, looks at the screen, and verifies that the UI actually matches the spec. A button that’s supposed to be blue but renders as white? Caught. A form that submits but shows no confirmation? Caught.

This closes the loop in a way that pure code generation can’t. The vision model acts as an independent quality gate that evaluates the artifact, not just the source.

What It’s Good For

Mano-AFK shines for internal tools, prototypes, and MVPs where the cost of human QA exceeds the cost of iteration cycles. It’s not going to replace your engineering team on a complex distributed system. But for “I need a dashboard that shows these metrics with these filters by Thursday”? It’s remarkably capable.

The Stack: Model → Accelerator → Builder

Here’s where the three projects become more than the sum of their parts.

┌─────────────────────────────────────────────┐
│              Your Mac (M4+ / 32GB)          │
│                                             │
│  ┌──────────┐  ┌──────────┐  ┌───────────┐ │
│  │  Mano-P  │  │  Cider   │  │ Mano-AFK  │ │
│  │  (Agent) │──│  (Accel) │──│ (Builder) │ │
│  │  4B VLA  │  │  W8A8    │  │ PRD→App   │ │
│  └──────────┘  └──────────┘  └───────────┘ │
│                                             │
│  Data stays here. Always.                   │
└─────────────────────────────────────────────┘

Mano-P provides the vision-language-action intelligence — the ability to see, understand, and act on screen content. Cider accelerates inference so that intelligence runs at interactive speeds on consumer hardware. Mano-AFK orchestrates multi-step autonomous workflows, using Mano-P as both its brain and its eyes.

The result is a stack where:

  • Your AI agent perceives and operates your entire desktop
  • Inference is fast enough for real-time interaction (not “wait 30 seconds per action” fast — actually fast)
  • Autonomous workflows can build, deploy, and quality-test applications without human involvement
  • Nothing leaves your machine. No API calls to external servers. No telemetry. No data exfiltration vectors. Your code, your screen content, your documents — they stay on your Mac.

That last point matters more than people think. Enterprise teams working with proprietary code, healthcare organizations handling patient data, legal teams reviewing confidential documents — these groups can’t use cloud AI agents, period. An on-device stack isn’t a nice-to-have for them. It’s the only option.

Hardware Requirements

Let’s be clear about what you need: Apple M4 with 32GB of RAM is the minimum for running the 4B model at usable speeds. An M5 Pro will give you the best experience. This isn’t a “runs on any Mac” situation — you need the unified memory bandwidth and Neural Engine capabilities of recent Apple Silicon.

The Bigger Picture

We’re not claiming this replaces cloud AI. The 72B model exists for a reason — some workloads need that scale, and running it requires serious hardware. What we are saying is that the gap between “cloud-only” and “runs on your laptop” has narrowed dramatically, and for a growing category of workflows, the on-device option is not just viable but preferable.

The three forces driving this:

  1. Model distillation has gotten remarkably good. The 4B Mano-P retains enough capability from its 72B parent to handle real-world GUI tasks.
  2. Apple Silicon’s unified memory architecture is uniquely suited to LLM inference. High memory bandwidth + large unified pool = exactly what transformer decoding needs.
  3. Activation quantization (via Cider) closes the remaining throughput gap. Weight-only quantization was the easy win; activation quantization is the hard one that makes real-time interaction possible.

The open-source angle matters here too. These aren’t black-box binaries. You can inspect the model weights, audit the inference engine, verify that nothing phones home. For privacy-sensitive deployments, “trust us” isn’t good enough. “Read the code” is.

Get Started

All three projects are released under Apache 2.0 — use them commercially, fork them, contribute back, or just kick the tires.

If you build something with them, we’d love to hear about it. File an issue, open a PR, or just star the repos if you think this direction is worth pursuing.

The future of AI workstations isn’t in the cloud. It’s on your desk.

Mininglamp Technology builds AI infrastructure for enterprises. Our open-source projects focus on on-device AI agents, inference optimization, and autonomous software development. Learn more at github.com/Mininglamp-AI.

How I’d Build An AI-First MVP In 2026: Tech Stack, Agents, APIs, And Cost Traps Founders Should Know

Here’s the founder trap in 2026: building an AI-first MVP is easier than ever, but building one that doesn’t burn cash, confuse users, or collapse at scale is still hard.

The tools are better. Agents are smarter. APIs are cheaper in some places and brutally expensive in others.

So the winning move is not “add AI.” It’s choosing the right stack, the right workflow, and the right cost controls from day one.

If you’re planning an AI-first MVP in 2026, this guide gives you the practical build plan I’d use before writing the first line of code for real today.

AI-First MVP In 2026 Starts With One Painful Workflow

A good AI-first MVP in 2026 does not start with model selection. It starts with one painful user workflow.

Not ten. One.

Maybe users need to summarize legal docs. Maybe they need an AI shopping assistant. Maybe they need a field-service app that turns voice notes into reports. The AI layer should remove friction from the core action, not sit on top like a shiny button.

If a founder asked me where to begin, I’d say this: define the job your AI should complete. Then build the smallest product around that job.

That’s also where an AI app development company can help early, because the hard part is not connecting an API. The hard part is knowing which AI feature is worth building first.

And yes, this matters for local product teams too. If you’re searching for Android app development Houston, the real question is not “who can build an app?” It is “who can build an AI workflow that users actually finish?”

What I’d Build First

For a founder-friendly MVP, I’d keep the first version brutally simple.

The first build should include:

  • one primary user role
  • one AI-powered workflow
  • one clean dashboard or mobile flow
  • one database
  • one model provider to start
  • basic analytics
  • a human fallback path

That is enough.

You do not need multi-agent orchestration on day one. You do not need six LLM providers. You do not need a giant vector database unless retrieval is truly part of the product.

If you are working with a mobile app development company Houston founders trust, ask them to explain the workflow before they explain the architecture. That answer tells you a lot.

The Tech Stack I’d Use

Here’s the stack I’d consider for most AI-first MVPs.

Frontend

For web, I’d use Next.js. It’s fast, flexible, and good for content-heavy products, dashboards, and SaaS MVPs.

For mobile, I’d use React Native or Flutter. Both are strong choices when speed matters. If the product needs deep Android-specific features, then native Android can make sense, especially for teams comparing Android app development Houston options.

Backend

I’d choose Node.js with NestJS or Python with FastAPI.

Use Node if your team is already strong in TypeScript. Use Python if the product depends heavily on AI pipelines, data processing, or model experimentation.

Keep the backend boring. Boring is good here.

Database

PostgreSQL is still my default. Add pgvector if your MVP needs semantic search or simple retrieval.

Do not overbuild with complex data infra too early. A lot of founders jump to advanced search stacks before they even have repeat users. That is how MVPs get heavy, slow, and expensive.

Hosting

Use Vercel for frontend, AWS or Google Cloud for backend, and managed Postgres where possible.

Managed services cost more per unit sometimes, but they save founder time. In an MVP, time is usually the real budget.

A mobile app development company in Houston should be able to help you pick this tradeoff based on launch speed, compliance, and scale goals.

The AI Layer I’d Add

Now we get to the fun part.

For the AI layer, I’d split the product into four parts:

Model API

Use OpenAI, Anthropic, Google Gemini, or another reliable provider depending on the task. OpenAI’s official pricing page currently lists GPT-5.5 at $5 per 1M input tokens and $30 per 1M output tokens, while Gemini’s pricing page shows lower-cost Flash-style options for lighter workloads. Pricing changes fast, so this must be checked before launch. (OpenAI)

Agent Logic

Start with simple tool calling, not a huge autonomous system.

Your agent should be able to:

  • read user input
  • call a function
  • fetch data
  • return a clear answer
  • ask for confirmation before risky actions

That’s it.

Agents are powerful, but uncontrolled agents are expensive. Recent reporting about large-scale AI coding agents showed API usage can explode when autonomous systems run aggressively, which is exactly the kind of cost trap founders should avoid. (Tom’s Hardware)

Retrieval

Add retrieval only when the AI needs your own data.

Examples:

  • product catalogs
  • internal docs
  • user history
  • policy content
  • support tickets

For MVPs, use small, clean retrieval before chasing advanced RAG architecture. Bad retrieval gives confident wrong answers. That’s worse than no AI.

Guardrails

Guardrails are not optional.

Add limits for:

  • token usage
  • file size
  • agent steps
  • tool calls
  • user permissions
  • sensitive actions

If a mobile app development company Houston team skips this conversation, that is a red flag. Guardrails protect product quality and your cloud bill.

APIs I’d Use Carefully

APIs are where MVPs can either move fast or bleed cash.

The key APIs I’d consider:

  • LLM API for reasoning and generation
  • embedding API for search
  • speech-to-text API for voice workflows
  • image API only if visual input is core
  • payment API like Stripe
  • auth API like Clerk, Auth0, or Firebase Auth
  • analytics API like PostHog or Mixpanel
  • notification API like Firebase Cloud Messaging

Do not connect every exciting API on day one.

Every API adds pricing, maintenance, latency, and failure points. Keep the chain short. A user does not care how cool your backend is. They care if the app works when they need it.

This is where a mobile app development company in Houston with real AI product experience can save a founder months of trial and error.

Cost Traps Founders Miss

This is the part nobody loves talking about. But it’s where MVPs either survive or quietly die.

Trap 1: Output Tokens

Output tokens cost more than input tokens on many models. Long answers feel nice, but they can get expensive fast.

Fix it with response limits, summaries, and structured outputs.

Trap 2: Agent Loops

Agents can call tools again and again. That may look smart in demos. In production, it can destroy your budget.

Set max steps. Always.

Trap 3: Reprocessing The Same Data

If the app summarizes the same document every time a user opens it, you’re wasting money.

Cache results. Store summaries. Reuse embeddings.

Trap 4: Using The Biggest Model For Every Task

Not every task needs the most expensive model.

Use cheaper models for classification, routing, formatting, and short answers. Save premium models for complex reasoning.

Anthropic also highlights prompt caching and batch processing as ways to cut costs on eligible workloads, which is a strong reminder that architecture choices directly affect AI spend. (Anthropic)

Trap 5: No Usage-Based Pricing

If your users can generate unlimited AI requests on a cheap plan, your revenue model may break.

Tie pricing to usage from the start. Even if the first version is simple.

Founders comparing Android app development Houston partners should ask one painful question: “How will we stop one power user from eating the whole AI budget?”

What The MVP Should Measure

Do not only measure signups. That’s vanity if users do nothing.

Track:

  • task completion rate
  • AI response accuracy
  • retry rate
  • time saved
  • cost per successful task
  • user approval rate
  • human fallback rate
  • retention after first AI success

That last one is gold.

The first moment where AI saves the user real time is your hook. Build around that. Make it obvious. Make it fast. Make it repeatable.

A mobile app development company Houston team that understands this will build analytics into the MVP from day one, not after launch.

What I’d Avoid In Version One

I would avoid:

  • fully autonomous agents with no approvals
  • too many model providers
  • advanced RAG before clean data
  • voice features unless needed
  • custom model training
  • complicated admin dashboards
  • over-designed onboarding
  • building for scale before proving demand

Early products need focus. Not more features.

If your MVP needs mobile from day one, pick a team that can balance AI, UX, backend, and launch discipline. That’s the real value of working with a mobile app development company in Houston that knows startup constraints.

The Build Plan I’d Follow

Here’s the practical 6-step plan.

Step 1: Validate The Workflow

Talk to users. Watch how they do the task today. Find the slow part.

Step 2: Prototype With One Model

Use one reliable AI API. Keep prompts simple. Test with real data.

Step 3: Add A Thin Product Layer

Build the interface around the task. Avoid extra screens.

Step 4: Add Guardrails And Logs

Track every AI request, cost, and failure.

Step 5: Launch To A Small User Group

Do not go wide too fast. Watch behavior, not compliments.

Step 6: Optimize Cost Before Scaling

Switch models, cache outputs, shorten prompts, and limit agent steps.

That is how you build without guessing.

And for teams looking for a custom mobile app development company, this is the kind of product thinking that matters more than a pretty proposal deck.

Where Houston Founders Should Pay Attention

Founders got a lot of product opportunities in healthcare, logistics, energy, field service, real estate, and operations-heavy businesses. These are perfect markets for AI-first MVPs because the workflows are messy and expensive.

That means demand for Android app development Houston will keep growing, but the winning apps won’t just be normal apps with chatbots added.

The winners will be:

  • AI dispatch tools
  • inspection apps with voice reports
  • healthcare intake assistants
  • logistics planning copilots
  • internal enterprise workflow agents
  • field-service documentation apps

If you are evaluating a mobile app development company Houston, ask for AI workflow examples. Ask about cost control. Ask about model fallback. Ask about hallucination handling.

And if you are choosing a mobile app development company in Houston, make sure they can speak product and engineering in the same conversation. That combo is rare, but very needed.

Final Takeaway

An AI-first MVP in 2026 should be useful before it is impressive.

Pick one painful workflow.

Build a simple stack.

Use one strong model.

Add guardrails.

Measure cost per successful task. Then improve from real usage.

That’s the play.

Whether you’re a SaaS founder, healthcare startup, logistics team, or enterprise leader exploring Android app development Houston, the advantage is not “having AI.” The advantage is building AI that saves users time and makes the product feel easier.

And if you’re comparing a mobile app development company in Houston, do not just ask what they can build.

Ask what they would remove.

15 yaş altına sosyal medya kısıtlaması 6 ay içinde hayata geçecek

yeni sosyal medya yasası

yeni sosyal medya yasasıTRT Haber’de yer alan habere göre Aile ve Sosyal Hizmetler Bakanı Mahinur Özdemir Göktaş, 15 yaş altına sosyal medya kısıtlamasının 6 ay içinde hayata geçirileceğini söyledi. Bu konuda yaptığı açıklamada, “Avustralya’nın raporunu inceledikten sonra gerekli tüm değerlendirmeleri yaptık ve eksiklikleri tamamlayarak ülkemize özgü bir model geliştirdik” ifadelerini kullanan Göktaş, ek olarak şunları söyledi: “Sosyal medya kullanımına yönelik düzenlemenin uygulamasındaki esasların belirlenmesine yönelik Bakanlığımız bünyesinde bir çalışma grubu kurduk ve süreci ilgili kurum ve kuruluşlarla tam bir koordinasyon içinde yürütüyoruz. Kamu kurumları ve diğer paydaşlarla birlikte yürüteceğimiz bu çalışma grubunda, 15 yaş altı çocukların sosyal medya kullanımına yönelik esasları kısa sürede belirleyerek ilgili yönetmeliği 6 ay içinde hayata geçireceğiz. Üzerinde çalıştığımız bu yönetmelik tamamlandığında, yeni sosyal medya düzenlemesi tüm Türkiye’de fiilen yürürlüğe girmiş olacak.” Yaş doğrulama sistemi e-Devlet ve farklı doğrulama yöntemleri üzerinden sosyal medya kullanımına yönelik yeni dönemin yalnızca yasal bir düzenlemeyle sınırlı kalmayacağını vurgulayan Göktaş, teknik altyapı ve uygulama […]

15 yaş altına sosyal medya kısıtlaması 6 ay içinde hayata geçecek yazısı ilk olarak Teknoloji haberleri – LOG üzerinde yayınlandı.