# Yoosuf Mohamed > Systems Architect and AI engineer helping startups and enterprises build reliable AI automation, LLM/RAG products, and scalable software. > Full text of 41 published posts. Written by Yoosuf Mohamed. Based in Colombo, Sri Lanka. ## Pages - [Home](https://yoosuf.me/): Personal site — AI automation, systems architecture, and AI-first products - [Services](https://yoosuf.me/services/): Engagement models and starting prices - [About](https://yoosuf.me/about/): Background, skills, and architectural philosophy - [Contact](https://yoosuf.me/contact/): How to reach Yoosuf Mohamed - [Yoosuf](https://yoosuf.me/yoosuf/): Canonical identity page - [Postwire](https://yoosuf.me/postwire/): Open-source local auth testing tool - [Messenger](https://yoosuf.me/messenger/): Open-source relational chat schema - [Blog](https://yoosuf.me/blog/): All posts - [Terms](https://yoosuf.me/terms/): Terms of service - [Terms and payments](https://yoosuf.me/terms-payments/): How engagements are billed - [Privacy](https://yoosuf.me/privacy/): What is collected, which is almost nothing --- ## Jev Returns a Type, Not a String URL: https://yoosuf.me/blog/jev-decision-model-routing/ Published: 28 Sep, 2026 Topics: AI & Tech, Engineering, AI, LLM, Architecture, Model Routing, JEV, Performance JEV returns typed values with probabilities instead of text. What that changes for AI model routing: the control plane, the failure modes, the benchmarks. Two weeks ago, TypeSafe released a model that cannot generate text. This week, OpenRouter wired it into the routing path for ordinary LLM calls — reported by PANews via Gate and Phemex, integrated as typesafe/jev-router with cache-aware selection, so a request can pick its model and its inference intensity per turn. That's the news. The part worth your afternoon is what it means that the thing doing the routing can't write. I've reviewed enough agent systems that my default reaction to a new routing layer is "where's the fallback". So I went and read the docs, the launch post, the evals, the pricing, and — because this is exactly the class of claim that deserves it — the independent benchmark numbers. Then I read TypeSafe's own "Nuance" sections, which turned out to be the most interesting thing they published. Routing was always a control-plane problem Here's the reframe. Almost every "LLM router" you've used is middleware sitting in the request path: it inspects the request, picks a model, forwards the bytes. It looks like part of the data plane because it lives in the data path. It isn't. It's a control plane. The generation is the work. The routing is a decision about the work, and decisions have completely different engineering requirements than work: they need a confidence signal, a failure policy, an audit trail, and someone who owns the threshold. [diagram] The whole game is the left box. Everything interesting about routing as a category has always been control-plane work — evaluation harnesses, cost curves, abstention policies, drift. The data plane is a solved problem you rent. What changed is that the decision is no longer a string you scraped out of a completion. It's a typed value with a probability attached, produced in 70 to 500 milliseconds for $0.042 per million input tokens with output billed at zero. Three primitives, and that's the entire type system Jev answers three kinds of question, and you can mix all three in one call: • Choice — pick one of your keys. Returns the key, a probability for every key, and a confidence value. Cardinality goes up to 255, above which TypeSafe scores independently and then makes an explicit choice. • Score — where does this fall on an ordered set of levels you describe? Returns the probability-weighted average position, a legend mapping each index to its description, per-level probabilities, and confidence. Capped at ten levels. • Noul — what's the probability this proposition holds? One number. No separate confidence field, because the number is the answer. No free text. No reasoning trace. No JSON body to parse, no schema validation, no retry loop for a malformed tool call. If your pipeline ends in JSON.parse followed by a switch, that shape is exactly what this replaces — and OpenRouter's own framing of it is that you swap the model, keep the LLM for the branch that needs prose, and measure both. The cost shape deserves a second look, because it's structurally different and not obviously a bargain. Generative models charge heavily for output, roughly 5x input on typical pricing. Jev bills input only, because the output is a distribution over a set you defined, and the sets are small. A three-question support-ticket call runs about 450 input tokens, which OpenRouter's writeup puts at roughly two thousandths of a cent. A million tickets of that shape: about $19. You are not buying intelligence. You're buying a function call with a probability attached. The schema is also the prompt, and your key names aren't in it This is the detail I think most teams will trip over, and it's one line in TypeSafe's API reference: the question id is never sent to the model. When you write team: { type: 'choice', ... }, the string team does not reach Jev. Only the instructions and the criteria do. So all the meaning lives in the descriptions. Which means: • Your variable names are invisible. intent versus primarytopic versus q1 makes zero difference to the output. Only the prose does. • Criteria are read literally, so they have to be written like a specification, not a label. OpenRouter's example spends a full sentence per option explaining exactly which customer complaints fall in it. • A refactor that renames a key is a no-op. A refactor that tightens a sentence is a model behaviour change, and it will show up in your routing distribution with no deploy, no commit, and nothing in your changelog. • Irrelevant detail in the state measurably lowers accuracy. TypeSafe documents this as jaggedness, and OpenRouter's guidance is blunt: send only the state each question needs. That last one turns out to be the most consequential, so I'll give it its own section. Confidence is not accuracy, and the numbers move Two properties of the output that will bite you, both documented, neither widely discussed. Confidence measures concentration, not correctness. TypeSafe derives confidence from the shape of the distribution, not from whether the answer is right. A model that puts all its weight on one option scores 1.0 whether it's right or confidently, catastrophically wrong. Confidence tells you how torn the model was between the options you offered. Whether those options are the right set is entirely your problem. The same input returns different numbers on different calls. OpenRouter documented this by accident, which is the best kind of documentation. The same muddy billing ticket returned billing at 0.79 with confidence 0.69 in one run and 0.84 with confidence 0.77 in another. A severity Score came back 1.15 on one call and 1.19 on the next. A refund Noul on a deliberately ambiguous message returned 0.52 and then 0.49 on the verbatim same text. That second property is the one that changes your code. You cannot write if (confidence > 0.8). You have to write bands, because a point threshold on a distribution that moves between calls is a coin flip near the boundary. The practical shape: • High confidence: act automatically. • Middle band: this is a third outcome, not a coin toss. Ask a follow-up, or send it to a person. • Low confidence, or a Noul hovering near 0.5: the model is telling you it doesn't know, and that's the most valuable thing it will ever tell you. The Noul-near-0.5 case deserves emphasis. A customer who wrote "there are two charges on my card this month, if one of them is a mistake, what are my options?" has not asked for a refund. Jev correctly declined to pretend they had — 0.52, then 0.49. A pipeline that thresholds at 0.5 and then acts on that would have refunded a customer who didn't ask. The right read is that 0.5 is a routing signal, not a boolean. Projecting state is the real engineering If irrelevant detail degrades accuracy, then a decision plane forces a new stage into your request path that you didn't have before: a projection, per question set, of the state each question actually reads. [diagram] The 32,000-token context window is not a budget you get to spend. It's a liability, because everything you put in it is competing with the signal. This is the same shape as the chunking problem in the AEO piece — the unit that gets judged isn't the document, it's the passage that was retrieved. Here the unit that gets judged is the projected state, and if you can't explain which fields a given question reads, you don't have a projection, you have a habit. Two more constraints that are architectural rather than model-shaped: Jev takes text only (no images, audio, or video), and it is explicitly not the place for arithmetic, counting, or date comparison. Compute those first, hand it the semantic remainder. "Is this order late" is a question for your database. "Does this customer sound like they're about to churn" is a question for Jev. The numbers, including the inconvenient ones OpenRouter ran a triage benchmark on 60 support tickets across five intents plus an escalation flag, and a prompt-injection screen on 40 messages, all through the same API on 19 September 2026. Small sets, one prompt, one day — the shape of the tradeoff rather than a leaderboard, which is the correct way to describe it. Ticket triage: • Jev — 59/60 intent (98.3%), 60/60 escalation, 194ms median, 633ms p95, $0.0248 per 1,000. • GPT Luna — 59/60, 60/60, 1,106ms median, $0.0921 per 1,000. • Claude Opus — 60/60, 59/60, 1,957ms median, $2.88 per 1,000. Prompt-injection screening: Jev 40/40 at 194ms median and $0.0161 per 1,000; Luna 39/40; Opus 40/40 at $1.5889 per 1,000. Injections scored 0.86 to 0.99, ordinary messages 0.01 to 0.20. That's a clean separation, and it's the kind of margin a threshold can actually sit in. But the number in that writeup matters more than the accuracy figures, and it's the strongest argument for confidence existing at all. Jev misfiled exactly one ticket — a question about splitting a refund on a returned item — and filed it under return or refund at a confidence of 0.56. It was the only ticket in the run below 0.8. A 0.8 threshold would have sent it to a person, so the pipeline got the case right because of the confidence signal while the accuracy metric reported a miss. That's the whole argument. Aggregate accuracy is the wrong metric for a decision plane. What you care about is the error rate inside the band where you act, and only calibration gives you that number. Now the independent numbers, which are more interesting because they're less flattering. A community effort benchmarking Jev as a router on RouterArena (ICLR 2026) reported two results. On a 13-model flagship pool via LLMRouterBench — 11,668 queries, zero inference cost because it routes by lookup against precomputed answers — the router hit 62.4% at $26.73 per 1,000 against GPT-5's 60.3% at $31.55. Or 60.3% at $21.09, which is best-single accuracy at 33% lower cost. Both are genuine routing wins on a strong, heterogeneous pool. Then the ablation. The same pipeline with Jev removed and replaced by a retrieval prior scored 62.4% at $26.69. Identical. The authors' own conclusion: the win came from the retrieved neighbour evidence, not from the decision model, and Jev's difficulty signal was "largely redundant" on this benchmark. And on the three-cheap-model pilot, nothing beat gemini-2.0-flash-001 on its own — 77.1% at $0.048 per 1,000, cheapest and most accurate simultaneously, which is a position no router can improve on because every misroute is a pure loss. The README puts it better than I can: routing only pays off when no single model dominates the pool. That's a property of the pool, not of the router. The same report contains the most useful number nobody in this space talks about: the oracle — the cheapest model that got each query right — sits at 82.6%. The router reached 62.4%. Twenty points of headroom, and the authors correctly identify it as a model-recall problem rather than a difficulty-estimation problem. Almost all the remaining value in routing is knowing which model to pick, not knowing how hard the question is. If you're building a router, that's your roadmap. Not a better classifier. Better recall of which model wins on requests like this one. The reference standard is the actual benchmark TypeSafe's launch post has a "Nuance" section under every claim, and it's the most useful page they published, because it's a benchmark-design confession. The workflow evals don't have ground truth. They use the predictions of the largest and most expensive external models as reference probabilities — specifically the average of GPT-6 Astra and Fable 5.1. TypeSafe states plainly that this "biases answers towards OpenAI and Anthropic's models" and that they likely underestimate their own model relative to DeepSeek's. The latency figures were measured from laptops on the West Coast, where the service currently lives. On cost: "we can't prove it isn't subsidised." And the hallucination comparison was run through OpenRouter, where, in their words, "more complex queries might be routed to better models" — which means the baseline got a quiet quality upgrade the challenger didn't. None of that makes the claims false. It makes them incompletely verified, in a specific and locatable way, and I respect that far more than the usual launch post which simply omits the caveats and lets you assume. The transferable lesson is the one I keep landing on in the search piece: the measurement design is the system, and the ranking of the results is downstream of choices somebody made about what counts as ground truth. A benchmark that defines truth as the average of two competitors' opinions is measuring agreement with those two models. It may still be a useful signal. It is not accuracy, and it should never be reported as accuracy. If you're evaluating any router — including this one — the question to ask is not "how accurate is it" but "who defined correct, and would they have defined it this way if a competitor had won." Your decision plane needs a failure policy Here is the part almost nobody writes down. A decision model is a network dependency in front of a dependency you already had, it's probabilistic, and it's a third party. When it is down, slow, or unsure, the system must do something deliberate. AutoJev's own model-routing guidance is the cleanest statement of the policy I've seen, and it's worth reading as a template: • Remove candidates that violate context, residency, permission, or budget constraints before asking anything. • Keep the current or safe default model when the decision service fails. • Avoid downgrading when low confidence, or a large cached context, makes switching risky. • Record the candidate list, the selected model, the confidence, and the fallback reason. [diagram] Four things I'd add to that, from having been on the wrong side of this: • Give the decision call a smaller timeout than your end-to-end SLO. A 194ms median with a 633ms p95 is fine. A 194ms median with an unbounded tail is a latency incident you'll attribute to the model, because that's where the token count is. • Log the signals, not the state. Log the request id, the question names, the probabilities, the threshold you applied, and the outcome. OpenRouter's guidance on this is explicit: keep the state out of the log, because tickets and documents are full of customer data. A router log is a compliance liability the moment it stores prompts. • Ship it in shadow mode first. This is the pattern I trust most, and it's what opencode-jev-router does by default: run the decision, log what it would have done, act on nothing, and measure disagreement against your human labels for a couple of weeks. Then promote. Every router I've seen launched by flipping the switch on day one, and every one of them learned its thresholds in production. • Decide now what happens on a data-residency conflict. The open-source LiteLLM routers are explicit that routing sends a minimised summary of your request to a third party, and they fall back to local behaviour with no key configured. If you have data that can't leave a region, your decision plane has to be deployable inside it. That constraint will determine your vendor list, and it's cheaper to find out before you've routed a customer's medical records through a routing API. Cache-aware routing is the constraint nobody mentions The OpenRouter integration is described as cache-aware: it detects cache hits from the context in order to avoid redundant computation and token waste. That's a small clause in a news item and it's the most important architectural detail in the whole deployment. Because provider-side prompt caching is keyed on the token prefix, model choice is not a per-request decision, it's a per-context decision. A router that optimises each turn independently will change the model mid-thread, invalidate the prefix cache, and pay full price for the entire conversation again — on every turn where it disagrees with the previous choice. [diagram] So the routing policy has to be sticky within a context, and the cache hit rate becomes a first-class metric alongside cost and quality. This also quietly caps how clever routing can get: the cheapest correct model for turn nine is not worth re-paying for turns one through eight. I'd expect most router evaluations to miss this entirely, because the public benchmarks route one-shot queries where no prefix exists. It's the same blind spot as the Jevons problem in a different costume, and I expect it to be the thing that decides whether per-turn routing survives contact with real agent workloads. The Jevons part TypeSafe named the model after W. Stanley Jevons, and the reasoning is stated in the launch post: steam efficiency didn't reduce coal consumption, it increased it. Every order of magnitude drop in the cost of intelligence, they argue, unlocks orders of magnitude more use cases. That's usually read as a growth thesis. As an architect I read it as a demand thesis, and it's the bit that should worry you. A decision that costs two thousandths of a cent is not a decision you make once. It's a decision you make on every tool call, every permission prompt, every routing hop, every retry, every output, on every request. The economics don't make the bill smaller, they make the number of decisions explode. Nobody's cost model is built for the volume they're about to generate. Which means the interesting question stops being "is routing cheaper" and becomes "how many decisions can I afford to make, and who reviews them." A pipeline that today makes four model-driven decisions per request will, at this price, make forty, and every one of them is a place where a wrong answer is silent. The failure mode isn't an exception. It's a confidently wrong route that nobody sees until a customer complains. What I'd actually build Concrete, and short, because the pattern is not complicated: • One switch statement first. Find the LLM call in your codebase that ends in JSON.parse and then branches. That's the first candidate, and it's the cheapest to measure. Leave everything else alone. • Shadow before you switch. Log the decision, compare it to your labels, and only then let it act. • Project the state per question set, and write down which fields each question reads. If you can't, you don't have a projection. • Thresholds as bands, not points, derived from a few hundred of your own labelled examples, and re-derived every time you edit a criteria string. • Sticky routing per context, with cache hit rate on the same dashboard as cost and quality. • Fallback that doesn't need the model up, plus a log full of probabilities and none of your customers' data. • Measure cost per correct decision. Cost per request is the metric that made everyone optimistic about routing in the first place, and it's the one that hides the failure. The uncomfortable part A few beliefs that cut against the launch: • The routing win is real and it isn't Jev's. The strongest independent result came from retrieval evidence, and the ablation matched it to within a rounding error. That's not a criticism of the model; it's a statement about where the value is in routing generally. • A 20-point oracle gap means the hard problem is recall, not judgement. Everyone optimising their classifier is working on the easy half. • Calibration is a property of the aggregate, not of your request. A model that's right 80% of the time at 0.8 confidence is still wrong one time in five, and your escalation path has to be sized for that, not for the marketing number. • "Can't hallucinate" is true and narrower than it sounds. It can't emit a type error because it emits no text. It can absolutely be wrong about which of your options is correct, confidently, with a probability attached. Type safety moved the failure from loud to quiet, and quiet failures are more expensive. • And the structural one: a router that picks your model is picking your quality floor, and nobody is on the hook for that floor. When the cheap model handles the hard prompt and the answer is subtly worse, there is no exception, no stack trace, and no alert. It just shows up as a support ticket about the product getting dumber, six weeks later. Where this lands A decision plane is a new kind of dependency. It sits in front of a model you already depend on, it's probabilistic, it's somebody else's server, and its failure mode is a confidently wrong answer that looks exactly like a right one. The technology is genuinely interesting. The early numbers look real, the cost curve is a different shape from anything in the stack, and the fact that it can't generate prose is a much bigger deal than it first appears — it removes an entire category of bug that we've spent two years building frameworks to tolerate. But the thing that decides whether it's good engineering is the boring part. Project the state. Threshold in bands. Fall back without it. Shadow before you switch. Log the signals, not the data. Measure cost per correct decision. Routing was never really about picking the best model. It was about owning the decision. Jev just made the decision a first-class object with a probability attached to it — which means the parts that were always the hard part are now the entire product. — Yoosuf --- ## SEO Is a Distributed System URL: https://yoosuf.me/blog/how-search-works-architecture/ Published: 26 Sep, 2026 Topics: Engineering, AI & Tech, SEO, Architecture, Distributed Systems, Performance, RAG How search ranking actually works: the crawl subsystem and its budget, the inverted index, canonicalisation as clustering, and the ranking cascade. I don't trust SEO advice, and I've never trusted it. Not because the practitioners are dishonest — most are competent — but because the genre has a structural blind spot. The advice assumes you're talking to a rubric. Add these keywords. Use this heading depth. Put the keyword in the title. Get these seven backlinks. The mental model is a grader with a checklist, and that model is wrong in a specific, predictable way. It's wrong because there is no rubric. There is a large distributed system, running somewhere you can't see, with a queue, a budget, a stale-data problem, and a cascade of increasingly expensive machine-learned stages. Your page is a message in that system. It gets filtered, scored, maybe dropped, maybe promoted, and eventually either rendered into a list of ten links or discarded without telling you. Once you draw the actual architecture, about 90% of the folklore evaporates and the remaining 10% makes obvious sense. The crawler is a rate-limited distributed system This is the part I think SEO practitioners underrate most. The crawler is not a program that fetches your page. It's a distributed crawl engine with a priority queue, and your site is competing for a slice of a fixed resource. The canonical description of this architecture comes from Andrei Broder's 2002 paper The Anatomy of a Large-Scale Hypertext Search Engine, and it's worth internalising because every modern search engine still resembles it: [diagram] Read that carefully, because it explains most crawl behaviour you'll ever complain about. The known URL store is the graph of everything discovered. The URL filter rejects what's disallowed, already fresh, or duplicate. The frontier is a priority queue, and — this is the important part — the priority order is not yours. You do not get crawled because you published something good. You get crawled because the scheduler's priority function decided your URL was worth the bandwidth, and you do not have a vote. The per-host politeness constraint is why large sites crawl slowly. It's a deliberate throttle so one origin can't monopolise capacity. If you have 400,000 URLs, your own volume is now an argument against you. And the render queue is a separate system with its own backlog, which I'll come back to, because it breaks a lot of confident architecture decisions. Your sitemap is a priority queue you write for someone else's scheduler This reframing is the single most useful thing about the crawl architecture, so let me sit with it. A sitemap isn't a list for human auditors. It's a machine-readable assertion about which URLs matter, how they relate, when they last changed, and how important each one is. The lastmod field isn't a suggestion to a crawler; it's an input to the freshness heuristic. The set is the population you're asking to be scheduled. Practical consequences that follow directly from the architecture: • A sitemap that lists 200,000 URLs including every facet permutation doesn't just waste budget. It tells the scheduler your URL space is enormous and low-signal, and you lose the argument on priority. • lastmod that changes on every build is worse than no lastmod. A freshness signal that always fires is a signal that carries zero information, and the scheduler learns to ignore it. • Orphans — pages with no internal link pointing at them — are structurally invisible. They may appear in a sitemap, but the frontier is seeded overwhelmingly from the link graph, and a page nothing links to has no path in. Google has been explicit that there is no maximum site size and no "page count limit" to optimise around. That's true and it's also somewhat beside the point. The constraint isn't a cliff, it's a gradient. A ten-thousand-page site with clean internal linking gets crawled more thoroughly than a five-hundred-page site where two-thirds of the URLs are duplicates of the other third. Faceted navigation is a combinatorial URL explosion Here's where real-world architectures go wrong, and it's worth drawing because the failure is structural rather than a bug you can patch. [diagram] Twelve filters, each with five values, and you've generated a URL space in the billions. Every one of those is fetchable, discoverable via internal links, and returns a 200 with a listing. From the crawler's perspective you've built a distributed denial-of-service attack against your own site. This is the actual reason faceted navigation needs handling — not because of "duplicate content" as a moral failing, but because you're consuming a shared resource budget and the pager for the important pages is behind you in the queue. The fixes are boring and architectural, which is the tell: noindex on filter permutations, rel=nofollow or non-linked JS controls on filter UI, canonical on the clean category URL, disallowing parameter patterns in robots.txt. Choose based on whether you need the pages indexed, but make the choice explicitly rather than by omission. The index is not one index The thing people call "the Google index" is not a database. It's a cluster of specialised indexes, and the partitioning is functional rather than arbitrary — the image index, video index, news index, local index, and the main web index answer different queries with different ranking logic and different latency budgets. The main web index is an inverted index: a map from term to a postings list of every document containing it, with the positions, field weights, and term frequencies attached. It's the same structure a database uses to answer "which rows match this predicate" without scanning them, which is why the whole thing returns in a fraction of a second. Google's own documentation frames the scale as "hundreds of billions of web pages." Two things about this stage matter for how you work: Fields are weighted differently. A term in the title isn't the same evidence as the same term in a footer. A term in an alt attribute counts. A term in the anchor text of an inbound link counts, and counts differently again. This is why "put the keyword in the title" works — not as a checklist item, but because title matches get structurally more weight in a fielded postings list. Indexing is explicitly not guaranteed. Google's documentation says it directly: not every page processed gets indexed. A page can be crawled, parsed, understood, and still be dropped. Which means there's a filter between "in the index" and "shown," and the distance between those two states is where most of the frustration lives. Canonicalisation is a clustering problem This one reframes nicely, and Google describes it in these terms: pages with similar content get grouped into a cluster, and then one is selected as the most representative. The others are treated as alternates for specific contexts. So rel="canonical" is not a directive. It's a vote you're casting in an election you don't run. [diagram] The consequences of "hint, not command" are the ones that bite: • If two pages point canonical at each other, both are ambiguous and the signal is discarded. • If you noindex a page, Google will ignore its canonical, because a page you've told them not to index can't be elected as the indexable representative. • If the elected canonical itself is weak, the whole cluster is weak. You've centralised your ranking risk onto one URL. • A redirect and a canonical are different mechanisms solving an overlapping problem, and using both is the usual cause of the "canonical points to a redirect" warnings. The genuinely hard case is duplicates that are intentionally different — pagination, print views, product variants — where there's no obviously right representative. There isn't a clean answer. You pick one and accept the consequences. The ranking cascade is the whole architecture Here it is. The thing everything else is in service of. The shape of this is not a secret, and it's not specific to search — it's the same candidate generation → scoring → re-ranking cascade that every large recommendation system uses, which Google documents in its own ML material. YouTube's candidate generator reduces billions of videos to hundreds or thousands, exactly the shape here. [diagram] Each stage exists because the one before it is cheap and the one after it is expensive. That's the entire design constraint. You cannot run your most precise model over the whole index, and you cannot serve results from an index scan. So you narrow brutally and early, and you spend precision only where the shortlist is small enough to afford it. Some specifics that are public: • Candidate generation is a fan-out, structurally similar to the query fan-out I described in the AEO piece. Multiple candidate generators may nominate different subsets, then get fused. • Feature scoring is where the classic signals live — index relevance, PageRank, freshness, page-level quality. • The machine-learned stage narrows to "several hundred," per Pandu Nayak's public description of the pipeline. • The neural rerank operates on the top twenty or thirty only, precisely because it's too expensive to run on hundreds of candidates. RankBrain is the named system; it was also the first to handle queries Google hadn't previously seen. • SERP assembly isn't just "take the top ten." There's a shuffle stage and layout logic, and SERP features get inserted into positions determined by their own logic. The architect's takeaway is the one people miss: because the cascade is cheap-early and precise-late, relevance is decided at a stage where your page is one of thousands, and it can be eliminated long before anything as thoughtful as "is this actually a good page" runs. No amount of excellent content can recover from being filtered out at candidate generation, because the system never forms an opinion about quality — it just never considers you again. The link graph is a graph database problem PageRank is the original random surfer model: a view of the web as a probability distribution over a directed graph, where a link is a vote and the rank flows. [diagram] Two architectural consequences: Navigation is a ranking decision. Every link in your header, footer, sidebar, and breadcrumbs is an edge in a graph computation. A sitewide template link passes authority to the same page on every URL. Sitewide navigation dilutes the signal it carries, and a page linked from everywhere carries no more than one linked from three places, because the graph is about proportion, not count. Internal linking is information architecture. A well-structured hierarchy with a shallow click depth distributes rank to more of the site. A flat architecture of 40,000 URLs linked from a single index page makes every one of them look identical to the graph, which is to say: interchangeable and individually insignificant. Google has said PageRank has "evolved a lot" since 1998 and remains part of the core systems. Treat "backlinks" as link acquisition and internal linking as architecture, because they are the same problem at different scales. A meaningful chunk of ranking is trained on click behaviour This is the section the SEO industry skips, and it's the one that changes how you think about the whole thing. A large family of Google's systems — Navboost and its relatives Glue and Instant Glue — are trained on user interaction data: clicks, dwell, reformulations, repeat searches, return-to-SERP behaviour. These have been discussed publicly by Google's Pandu Nayak, who described Navboost as one of the important signals and a kind of memorisation system, with Glue aggregating interaction signals historically and Instant Glue running on a much tighter window — on the order of the last 24 hours, with roughly ten minutes of latency. Think about what that means operationally: • Click-through rate relative to your expected position is an input. A result that historically gets far fewer clicks than its rank predicts gets demoted, and vice versa. Your title and snippet are therefore part of the ranking feedback loop, not just the presentation layer. • Interaction data is normalised per query. You can't game it by being clickable; you can only be more or less clickable than expected for that specific query. • Fresh interaction data is scarce for new pages. A page with no history gets the prior, not the measurement. This is a real, structural disadvantage for new content, and it's the actual mechanism behind why "publish and wait" works better than any amount of impatient optimisation. This is also why the SEO advice around clickbait-style titles is more than a user-trust issue. It's a feedback-loop violation. You are optimising a ranking input with a technique that degrades the user outcome the input is supposed to measure. The render queue is a separate system and it will disappoint you "Modern frameworks handle SEO" is the claim I'm most tired of, because it usually means "we send HTML to the browser" and stops there. The crawler fetches raw HTML. If your content is client-rendered, the fetch succeeds and returns a shell. The render queue then handles it — but it's a distinct subsystem with a distinct backlog and no latency guarantee to you. [diagram] So the honest version of the rendering advice: • SSG and SSR are the safe answers. Content is in the response, in the initial HTML, and there is no queue to wait in. • ISR is fine as long as the first response contains content, which it does by construction. • Client-side-only rendering is a bet on someone else's backlog. It frequently works. It's not free, and the failure mode is silent — the page gets indexed as a shell and you find out in Search Console, weeks later. • A hybrid shell is a real pattern (server-render a meaningful subset, hydrate the rest) but it only helps if the first paint contains the part you care about. The other thing about the render queue: it makes rendering cost roughly double, which interacts badly with the crawl budget gradient from earlier. On a large site, that's not a rounding error. Core Web Vitals are a distributed measurement system Here's the part where I think most people, including most SEO practitioners, have the wrong mental model — and the correct one is genuinely more interesting. Core Web Vitals isn't "your site has a speed." It's an aggregate of measurements taken by your visitors' own Chrome instances, bucketed, anonymised, and only surfaced once enough users have contributed. • Field data, not lab data. Lighthouse gives you a synthetic run on Google's hardware. The ranking input is what real users experienced. A 98 Lighthouse score with poor field data means real people had a bad time, and the field data is what's counted. • The 75th percentile is deliberate. Passing means three quarters of your sessions were good, not that the average was good. That's specifically to punish long-tail slow experiences on old devices on bad networks — which is most of your real traffic. • There is a privacy floor. Below a minimum user count, no field data is published at all. Which means small sites can be structurally absent from this signal, and the standard advice to "just run PageSpeed Insights and fix the red number" doesn't work for them. • The reporting window is 28 days rolling. You cannot observe the effect of a change quickly. Any workflow that promises a same-week ranking response to a performance fix is selling something the data pipeline cannot deliver. Current state, for context: the thresholds are LCP at 2.5 seconds, INP at 200 milliseconds, CLS at 0.1, and they have not moved. INP replaced FID in March 2024, and if your performance documentation still references First Input Delay, it's stale. Worth flagging, because it's currently circulating and it's false: there are posts claiming Google tightened the "good" LCP threshold from 2.5 seconds to 2 in a 2026 update. Google hasn't. The published number is still 2.5, and they have said to expect the definitions and thresholds to be stable with changes on prior notice. And the weighting, honestly: page experience is a page experience signal, not a primary one. Google's own framing of the page experience update was that it is not a single ranking signal, and the underlying metrics are best understood as a tiebreaker between pages of comparable content quality. Roughly 55% of origins now pass all three, per the January 2026 Chrome UX Report data, so if you're in the failing half this is real work. If your LCP is 2.4 seconds and your traffic is flat, the threshold is not your problem and buying a faster agency will not fix it. Structured data is a declaration, not a lever Let me be direct about this, because schema markup has accumulated a lot of cargo cult. JSON-LD structured data does two things: it makes your content machine-interpretable in a way that's genuinely useful for entity extraction, and it makes you eligible for rich results. That's it. It is not a ranking signal, and no amount of schema on a page with no authority behind it will do anything for visibility. The most common misuse is marking up things that aren't visible on the page. Google's guidelines are explicit that structured data should represent content a user would see, and marking up invisible content is a violation that can cost you a manual action. It's a shortcut with a real downside, not a free upside. The architectural case for schema is different from the marketing case: it's the most explicit machine-readable statement you can make about what a thing is, and that's worth having in a world of ambiguous HTML. Do it because it's correct, not because it bought you position last time. Internationalisation is distributed configuration hreflang is the one genuinely distributed part of SEO, and it's where good architectures go wrong most reliably. The requirements are mechanical and strict: annotations must be reciprocal (if the English page lists the French one, the French page must list the English one), each page must self-reference, and you need x-default as a fallback. And it composes with canonical in a way that produces the classic footgun — hreflang and canonical must point at consistent URLs, or you end up with a canonical that contradicts the alternates and the cluster resolution goes unpredictable. [diagram] The failure is not always visible. You can get a page that indexes fine, ranks fine in one locale, and quietly never surfaces in the other — which is exactly the bug that's miserable to diagnose six months later. What to measure, and what to distrust The measurement architecture is weak, and knowing which parts are load-bearing saves a lot of time. Log files are ground truth. Your server logs record every crawl hit: the user agent, the status code, the URL, the response time, the byte count, the timestamp. You can compute your own crawl distribution, find your slowest pages, count how many times bots hit your 404s, and see exactly which parameters crawlers are exploring. It's unglamorous and it answers questions nothing else does. Search Console is authoritative but heavily sampled. It's a lossy window into the index. It tells you what Google believes about your site, which is genuinely valuable, but it is a sample and it is not a census. Treat it as a health check, not a measurement instrument. Rank tracking is a simulation. Third-party tools re-run queries against a set of locations and devices. The engine is stochastic and personalised, so a rank number is an estimate with a wide error bar, not a fact. It's useful for tracking relative movement over time. It is not useful for day-to-day decisions, and anyone treating a one-point rank change as a signal has misunderstood what they're measuring. Traffic is the lagging indicator of all of it. And it degrades for reasons outside your control — seasonality, algorithm updates, and increasingly AI answers taking the click without taking the visit. The habit I'd actually build: watch your own server logs for crawler behaviour weekly, and treat Search Console coverage as an error report. Fix what the logs tell you is actually broken before optimising anything the tools have opinions about. The uncomfortable part A few things I believe that cut against the industry: • Most SEO work is really distributed-systems hygiene with a marketing surface. Duplicate content is a caching and canonicalisation problem. Crawl waste is a queue-depth problem. Slow templates are a resource-contention problem. None of these are content problems and none of them are mysterious once you see the pipeline. • Most ranking factors are not things you control directly. Links are a social outcome. Authority is accumulated over years. Interaction signals are a consequence of whether your page satisfied someone. The controllable surface is smaller than the industry's product pricing implies. • The feedback loop is real and it's mostly virtuous. Write something genuinely useful, someone lands on it from search, they stay, they come back, the interaction data improves the ranking, more people see it. That's the whole game and it needs no tricks. The tricks are mostly attempts to short-circuit a loop that only runs once, at the cost of the thing the loop measures. • And the honest one: SEO is not a channel you own. You never had it. You were always a guest in someone else's distributed system, whose capacity decisions, crawl budgets, and model weights are all outside your control. The best strategy is to make your pages cheap for that system to fetch, understand, and trust — and then to be surprised less often. Where this lands Search is a system that reads the web continuously, compresses it into an index, and then answers questions by narrowing from millions of candidates down to ten through a cascade where the cheap decisions happen first and the careful decisions happen only for the survivors. Your job, in architectural terms, is to make sure your pages survive every stage: discoverable, not blocked, renderable, non-duplicate, canonical, fast enough to pass a tiebreaker, and structured enough to be interpreted rather than guessed at. Almost none of that is writing. Almost all of it is delivery. The folklore gets the ordering wrong. It tells you to optimise the content and assume the delivery will follow. The architecture says the opposite: delivery decides whether the content is ever read. A page that isn't crawled, isn't rendered, or isn't canonical never gets to have its quality assessed by anything. Which is, in the end, the same lesson the AEO piece arrives at from a different direction. The engines got better at reading. The bar for being readable didn't move. — Yoosuf --- ## Answer Engine Optimization Is an Architecture Problem URL: https://yoosuf.me/blog/answer-engine-optimization-architecture/ Published: 25 Sep, 2026 Topics: AI & Tech, Engineering, AEO, Architecture, AI, RAG, LLM, SEO AEO as systems architecture: the indexing and query planes, chunking as the unit of ranking, why citation is separate from retrieval, and measuring lift. There's a flood of AEO advice right now, and almost all of it is written by people who have never traced a request through a retrieval pipeline. The advice goes: add FAQ schema, sprinkle statistics, quote authoritative sources, use question-shaped headings, publish an llms.txt. Some of that's real. Most of it is folklore, and some of it is superstition being sold at a markup. The problem isn't that the advice is useless. The problem is that it's advice about the output when the actual mechanism operates somewhere else entirely. I'm a systems architect. When someone tells me a system is misbehaving, I don't start by tuning the copy. I start by drawing the pipeline and finding the stage where the expectation and the reality diverge. So that's what I did with answer engines, and this is the map I ended up with. The pipeline, end to end An answer engine isn't one component. It's two planes, and the gap between them is where almost all AEO confusion lives. [diagram] Read that left to right and you'll notice something: your content passes through a chunker and a renderer before anything about "optimization" even becomes relevant. A page that isn't renderable, isn't chunkable, or isn't retrievable cannot be rescued by any amount of good writing. That's the whole game, and most of the popular advice skips the left half of the diagram. The indexing plane runs whether you're looking or not The offline plane is a batch pipeline. A crawler shows up, your server returns something, and that something goes through several filters before it lands in a structure designed to be found by a machine that is not reading it — it's retrieving it. The stages that actually decide your fate: • Render. The crawler executes JavaScript, and often not immediately. Modern crawlers queue raw HTML, render it later, and compare. If your content only exists after a client-side fetch, you're gambling on the render queue's latency and on whether the rendered DOM is treated as equivalent to your server HTML. • Extract. Boilerplate, nav, cookie banners, footers, and comment threads get stripped. What's left is a linear text stream plus a graph of links, headings, and entities pulled from markup like tables, lists, and definition structures. • Chunk. The stream gets cut into passages. This is the stage I'd argue about hardest, and I'll come back to it. • Index twice. Once lexically (BM25 over tokens — the classic inverted index your site is already feeding without knowing it) and once semantically (a dense embedding per passage, stored in an approximate-nearest-neighbour structure so the query vector can find neighbours in sub-linear time). Two indices, not one. Which means a paragraph can be findable by exact terminology or by meaning, and there's a real class of content that only wins on one axis. The query plane fans out, then narrows hard The user question hits a language model that decomposes it. Google calls this query fan-out in AI Overviews and AI Mode: one question becomes a spray of related sub-searches, whose results get blended. Independent tracking puts AI Mode at roughly fifteen citations per answer, AI Overviews around eleven, and the standalone Gemini app closer to seven — which tells you the fan-out depth isn't uniform, and that each surface is genuinely its own retrieval system. Then retrieval runs hybrid. Lexical scores and vector similarity get fused, and the surviving candidates go through a reranker — usually a smaller, more expensive model that reads query and passage together and produces a much better relevance judgment than cosine similarity ever could. After that, context assembly. Here's the number that should shape everything you write: the context window is finite. Maybe 8,000 tokens, maybe 128,000. The passages that survive reranking get stuffed into it, and if your best sentence on the topic didn't make the cut, nothing you say about optimization matters. CXL sampled Google AI Overview citations and found 55% of cited passages came from the first 30% of the page, 24% from the middle, and 21% from the bottom 40%. That distribution is not evidence that the bottom of your page is worthless. It's evidence that retrieval is lazy and proximity beats persuasion. Chunking is the real unit of ranking This is the part I think most AEO content gets quietly wrong. We talk about "pages ranking." Pages don't rank in an answer engine. Passages compete. A single 4,000-word article might be chopped into sixty passages, and the engine will happily cite sentence twelve of your post as the authoritative answer to a question you didn't realize the post was about. That changes what "good content" means. A passage is retrieved in isolation. It arrives in a context window with no memory of the section it came from. So: [diagram] The right-hand column is where a lot of beautifully written content goes to die. Not because it's bad — because when the engine cut it out of the middle of a paragraph, the sentence stopped meaning anything. Practical consequences: • Headings are load-bearing. They're the only context a chunk inherits about its own subject. ## Sizing a Postgres connection pool is a chunk boundary marker and a semantic label. • Answer the question at the top of the section, not the bottom. Bottom answers lose to the next heading boundary. • Prefer definitions, named mechanisms, and concrete numbers. They're semantically dense and survive chunking. An anecdote about my dog doesn't. • Front-load the entity. Say what thing this is, then the nuance. The embedding isn't subtle about topic; the reranker is strict about whether the passage actually answers the query. This is why the Aggarwal et al. KDD 2024 paper on Generative Engine Optimization found that adding statistics, quotations, and citations moved visibility up to 40% on its benchmark — and up to 37% on Perplexity specifically. The mechanism isn't magic. Those elements are quotable, self-contained, and dense with the kind of claims an answer engine wants to attribute. A 2026 Hypertext paper formalised something similar and measured structured HTML at 2.6× higher citation rates and 4× higher extraction fidelity than equivalent unstructured content. Two retrieval paths, and they need different things This is the distinction I think causes the most operational confusion, so let me be precise. There are two ways your content reaches an answer, and they have completely different requirements. [diagram] Path A is pre-computed. You cannot make it fresher than your last crawl. Path B happens during the answer, which means your server's response time, error rate, and render behaviour are now part of someone's answer. And the crawler taxonomy is more granular than "AI bots": | Agent | Purpose | When it hits you | |---|---|---| | OAI-SearchBot | Builds the index ChatGPT search cites from | Scheduled, off-peak | | Claude-SearchBot | Anthropic's search index | Scheduled | | PerplexityBot | Perplexity's index | Scheduled | | GPTBot / ClaudeBot | Model training corpora | Scheduled, bulk | | ChatGPT-User / Claude-User / Perplexity-User | User-triggered page visits | Mid-answer, synchronous | | OAI-AdsBot | Validates ad landing pages | On ad submission | The last row of scheduled crawlers (training) and the first two rows (search index) are frequently conflated, and they're independent switches. OpenAI documents them separately: blocking GPTBot keeps your content out of training while leaving you fully citable, and blocking OAI-SearchBot removes you from ChatGPT search answers while your content still goes to training. The two most consequential decisions on this list are also the two most commonly bungled by a well-meaning "block all AI crawlers" default that someone added in 2023 and never revisited. Two operational notes from OpenAI's crawler docs worth internalising: they publish IP ranges per bot (openai.com/searchbot.json and friends) so you can verify rather than trust the user-agent string, and robots.txt changes take about 24 hours to propagate. So "I blocked it" and "it's blocked" are different states separated by a day. Retrieval is not citation Here's the architectural insight I'd most like people to internalise: being retrieved and being cited are two separate decisions, and passing the first says almost nothing about the second. [diagram] Your content can be inside the window, have shaped the answer, and receive no link. The attribution step is downstream of the LLM's own choice of which claims to make. This is where the platform-level asymmetry shows up. A mid-2026 study that analysed 100,000+ prompt responses across 100+ brands found roughly 78% of cited sources were corporate websites, and that the single most-cited content format was the "best-of" listicle at about 21% of all citations. Those two facts are related. Listicles make assertions that are easy to attribute and hard to argue with, because the whole genre is a set of claims in citation-shaped slots. There's also a ceiling nobody advertises. That same study found a clean tier structure in first-mention visibility: household-name brands appeared in 73% of relevant AI answers on a first run, established mid-market brands in 44%, and niche or small brands in 11%. Roughly thirty percentage points per step. AEO amplifies what already exists. It does not manufacture authority, and if your mental model is "optimise my content and I'll catch up," that model is going to disappoint you at exactly the moment you expect it to work. The failure modes are all architectural Not "SEO mistakes." Structural faults in how content is served. • Client-rendered content only. Served, but delayed through a render queue, and sometimes indexed as a shell. • noindex left on by a staging deploy. The most common AEO catastrophe I hear about, and it always has the same shape: a template change, one environment, no catch. • Canonical pointing at the homepage. Completely valid HTML, collapses every passage in your article into one undifferentiated document. • Blocking the search crawler while allowing the trainer. You've paid for model training and bought yourself nothing. • Login walls and interstitials. The crawler gets a 200 and a cookie banner. This is the worst possible outcome: technically crawled, substantively empty. • Giant tables with no container. A 40-column comparison table gets extracted as an unparseable wall of tokens, and so does the sentence you were proud of in row three. • Answer text that only exists in an image or a canvas. Not extracted. Not chunked. Not retrievable. Full stop. Most of these are the same class of bug as a missing foreign key: not a content problem, a plumbing problem, and invisible until someone traces the path. llms.txt, honestly There's a proposed convention where you publish a /llms.txt — a markdown map of your site for language models. It got a lot of attention, then a lot of debunking. The debunking is fair on one specific point: no operator has published evidence that reading llms.txt improves citation rates. It's a convention adopted by a critical mass of sites, which is genuinely useful, but it is not a ranking signal and anyone selling it as one is selling a plugin. Where I think it does earn its keep is different, and it's about the live fetch path. When an assistant decides at answer time that your domain looks like the authoritative source for a topic, the difference between a site that hands it a clean, structured map and one that makes it guess is real. It's a routing hint for a machine with limited patience and a hard latency budget. I publish one on this site, and I also generate llms-full.txt — every post as plain text, built from the content collection. Cost: about a minute of build config. Benefit: plausible, unverifiable, and I still think it's worth it because it's cheap and the alternative is being unindexable on purpose. I'll happily be wrong about this. The honest framing: llms.txt is a content-hygiene practice, not an optimisation technique. Measuring this without lying to yourself This is where the discipline either shows up or doesn't, and it's the part I find genuinely hard. The measurement problem is that AEO didn't arrive in a vacuum. ChatGPT's own user base was growing fast the entire time anyone was "measuring AEO lift." So a chart showing AI referrals up 6× tells you almost nothing about whether your changes did anything. A 2026 natural experiment on this exact problem used a single high-traffic domain and let the untreated remainder of the same site act as a contemporaneous control, which absorbs the platform-level tailwind. Total ChatGPT referrals grew 5.7× while untreated pages on the same domain grew 3.5×. The intervention-aligned effect estimate was 1.82×, with a 95% confidence interval of 1.31 to 2.54. And then — this is the part I love — a conservative placebo-in-time permutation test came back at p = 0.16. Suggestive, not conclusive, on a short and noisy pre-period. The authors said so plainly instead of shipping the 1.82× as a case study. That's the level of rigour this field needs and mostly doesn't get. If you run AEO experiments, you need: • A prompt panel, not a vibe. A fixed set of 50 to 200 real prompts, run on a schedule. Prompts are stochastic — the same question sampled five times at temperature 0.7 gives different citations. Single-run comparisons are noise. • A control. Untreated pages on your own domain, so the platform's own growth doesn't get billed to your content strategy. • Citation share, not mention count. A 2026 Hypertext paper proposed Generative Share of Voice — your expected share of citations on a prompt, under the acknowledged randomness of retrieval — as a better target than raw mentions. I think that's the right unit. • Sentiment tracked separately. In the same brand study, whether a brand was framed positively or negatively flipped about 6.7× more often than whether it was mentioned at all. Sentiment is the volatile signal. Mentions are the stable one. Optimising mentions and being surprised by tone is a common and avoidable error. • Google clicks as a guardrail. AI Overviews cut clickthrough to the top-ranking result by 58% on December 2025 data, up from 34.5% in April 2025. You are trading something measurable for something less measurable. Know the exchange rate before you make it. One more number that should reframe your SEO instincts: Moz analysed nearly 40,000 queries and found 88% of AI Mode citations were not present in the organic SERP for the exact-match query. Ranking well for your head term no longer buys you the answer. Those are increasingly separate surfaces. What I'd actually do Not a checklist — a set of decisions, in the order the pipeline imposes. 1. Serve real HTML with real content in it. Server-render or static-generate. This is the highest-leverage thing on the page and it's a build-pipeline decision, not a content decision. 2. Verify the bots by IP, not by user-agent string. Check your logs, check the published ranges, confirm 200s with actual body content. 3. Audit canonical and noindex across every environment. Especially the one that isn't production right now. 4. Decide the training/search split deliberately. Blocking training while allowing search is a supported, documented position. Doing it accidentally is not a position at all. 5. Structure for chunk extraction. One question per ##. Answer immediately under it. Concrete numbers, named mechanisms, real entities. 6. Put the load-bearing material high on the page. Not because deep content is worthless — because retrievers skim. 7. Claim things that are attributable. A number, a date, a named source, a mechanism. Those are the sentences that survive a summariser looking for something to cite. 8. Build the prompt panel before you change anything. Otherwise you'll never know whether you helped. 9. Keep publishing things only you can say. First-hand experience, your own numbers, your own opinions, your own mistakes. In a genre that is converging on identical listicles, this is the only durable differentiator I can identify. Where this lands The entire AEO industry is, functionally, a pile of folk practices wrapped around one real insight: answer engines retrieve passages and attribute claims, so write passages that are worth retrieving and make claims worth attributing. That insight is real, it's the Aggarwal paper's actual finding, and it explains most of what works. Everything else — the schema dialects, the scoring tools, the promised placement — is downstream noise dressed as architecture. The part I'd push back on hardest is the framing. AEO is usually sold as a new discipline requiring new tooling, and that framing is a commercial convenience, not a technical one. Nearly all of it is HTTP: return the document, don't hide it, structure it, say something specific, publish it where the crawler can reach it. You already know how to do this. It's called serving a website properly, and it's been the hard part since 1993. Which is the least glamorous possible conclusion, and I think it's the correct one. The engines got better at reading. The bar for being readable didn't move. — Yoosuf --- ## Implementing Wizards the Right Way URL: https://yoosuf.me/blog/implementing-wizards-the-right-way/ Published: 23 Sep, 2026 Topics: Engineering, UX, UI, Frontend, Forms, Product Design Building multi-step wizards: Duolingo's one-question onboarding, Stripe Connect's branching, and the anti-patterns that make users quit. Every product eventually grows a wizard. Onboarding, checkout, billing setup, importing data, connecting an integration — somewhere along the line someone decides the flow has "too many fields for one screen" and splits it into steps. That decision is usually right. The execution almost never is. Most wizards I use are a worse version of the form they replaced: progress bars that lie, back buttons that eat your input, validation that only fires when you hit final submit, and nine steps where three would do. The good ones are recognizable because they're the ones you finish without noticing. Stripe checks out without you thinking about it. Duolingo gets you into your first lesson in under a minute. TurboTax makes you feel like you're answering questions a person would ask. These companies didn't stumble into that — the patterns are specific, and they're reproducible. I've built enough of these to have strong opinions. Here's how I think about implementing a wizard properly, with the real products as proof. First, earn the right to be a wizard A wizard is not a free UX upgrade. It's a trade. You're buying cognitive ease per screen at the cost of perceived effort overall — "four short steps" often feels like more work than one scrollable page, because every Next click is a commitment. The research on this is old but it holds up. Luke Wroblewski's Web Form Design: Filling in the Blanks made the case two decades ago that people slow down, reread, and quit as forms grow — and that breaking them into chunks helps as long as the chunks are coherent and the progress is honest. The failure mode is chopping by arbitrary field count — "step 3 of 7" where each step is two inputs — which just multiplies clicks. [diagram] You can see the numbers in commerce. Baymard Institute's checkout research found the average checkout still demands roughly fifteen form fields, and "too long / complicated checkout" has been a leading user-cited reason for abandonment for years. That's the real cost of a bad flow, in lost revenue. So I use a wizard when: • The steps have a natural order, and later answers depend on earlier ones. • Each step needs focus — payment details, mapping review, API scopes — not just fewer fields. • The flow branches. The user's answers should decide what they see next. I don't use one when it's just a long flat form. Split that page, section it, use progressive disclosure (as Nielsen Norman Group has documented for decades). Don't fake a sequence that doesn't exist. One decision per step The single most useful design rule: each step should answer one question, or complete one coherent chunk of work. [diagram] Notice none of the steps are "Account details part 2." They're questions a human would ask out loud. When you can name the step as a question, you've probably grouped it right. When you can't, you've probably dumped leftover fields into a bucket called "Additional information" — which is where completion rates go to die. The canonical proof is Duolingo's onboarding. It's one question per screen: what language do you want to learn, why are you learning it, how much time per day, how comfortable are you. Four screens, each asking exactly one thing the next screen depends on. Duolingo's language was famously "no signup, just start" — the flow moves you into content almost before you've committed to anything, and the single-question pacing is why it never feels like a chore. Typeform built its entire business on the same instinct: a form as a conversation, one question at a time. The corollary: put the step title on the screen as that question, not as a label lifted from your database schema. TurboTax's interview asks "Do you own your home?" and "Did you have medical expenses?" — translated from Form 1040 line items into questions a person would recognize. "Configure SAML attributes" is an implementation detail. "Which fields should we sync?" is the question the user is actually there to answer. Progress has to be honest A progress indicator isn't decoration — it's a promise. The contract is simple: show where I am, how much is left, and what's behind the steps I haven't done yet. Break that promise and you've made the flow worse than no indicator at all. Things that break it: • Linear bars on branching flows. If step 3 can be skipped or step 5 can become three steps, a percentage bar will be wrong by construction. Use a step list with checkmarks, and mark the skipped branch explicitly when it comes back into the trunk. • "Step 2 of 9" where later steps are conditional. Count the steps the user will actually take, or don't count at all. • Indeterminate spinners as steps. A loading state inside a step is fine. A loading state as a step ("Processing...") with no idea how long it lasts is where people close tabs. GitHub's importer is the reference here. When you migrate a repository from another host, it names the stages outright — Analyzing, Migrating, Finalizing — and shows exactly which one you're in, next to a short list of the steps ahead. You know precisely what's happening and what's left, and because the stages aren't given a fake percentage, the page never promises something it can't deliver while the import stalls on a giant repo. [diagram] For short flows (three to five steps), a labelled step list beats a percentage bar every time — users can see that "Review" is the last real work, and that's what they're pacing themselves against. When a branch skips a step, the indicator should show the skip happening rather than silently; the moment it's a percentage again, you're lying about the count. Back is not optional This is the one I will die on. The back button must exist on every step, it must preserve everything the user typed, and it must never re-run a side effect. Users make mistakes on step 4 that they only notice on step 6. Without back, they have three options: abandon, or start over, or — worst — guess blindly and fix it later in some settings screen you didn't build. All three are failures. Airbnb's host onboarding is a good model. New hosts wander through the listing wizard across days, and everything — photos, dates, house rules — survives round trips between edits and even between sessions. Back just works, and the draft is never lost. At the other extreme, you still get finance flows that silently discard your input when you click back, which is how a fifteen-minute application becomes a "I'll finish this never." The implementation details matter here: • Keep a single source of truth for form state (a reducer, a form library, whatever your stack uses) keyed by step, not scattered across step components that unmount and remount. The classic React bug is step components holding their own useState, then unmounting on navigation and taking the user's data with them. • Going forward validates the current step. Going back never validates — you're already past it, and blocking reverse navigation with a validator is how you trap people. • Never re-submit on back/forward. If step 3 fired an API call when you clicked Next, revisiting it later must not fire it again. Idempotency isn't just a backend concern; it's a navigation concern. [diagram] Forward validates, back preserves, and the browser's own back/forward buttons should behave the same as the in-flow ones — which means the step belongs in the URL (more on that below), not in a state variable that dies on refresh. Validate per step, inline, immediately The cruelest wizard pattern is collecting six steps of input and then telling the user, on the final screen, that something on step 2 is wrong. Now they're clicking back through a flow hunting for the red text. [diagram] Validate each step when the user tries to leave it — or better, as they type, once they've blurred a field. Errors should sit next to the field they belong to, in plain language ("That card expires in the past"), not as a toast that vanishes before it's read. Stripe Checkout is the polished example: card numbers are grouped and verified as you type, expiry and CVC are spot-checked on blur, and the error appears the moment it's detectably wrong, inline, next to the exact field. You're never told "there's a problem" with no idea where. Stripe's own form research repeatedly lands on this — show the mistake while it costs nothing to fix, not at the end when it costs a reload of upstream validation. Two rules people skip: • Don't disable Next. Disabling the button gives the user no idea what's wrong. Let them click, show them the errors, focus the first invalid field. A disabled button with an empty required field is a puzzle; a button that answers with feedback is a conversation. • Server-side errors belong to the step that caused them. If the API rejects the connection string, that error lives on the data-source step, not as a global banner above step 4. The user should be able to hit Back and see the problem exactly where the fix goes. Branch without losing the thread Branching is where wizards earn their keep — and where most implementations quietly fall apart. The moment answers change what comes next, three things get hard: progress, back navigation, and state. [diagram] Real products branch hard, the moment answers change what comes next. Stripe Connect's onboarding starts with country — then company type, then business details, then ownership and bank account — and each answer rewrites the requirement list that follows. An "individual" in India sees a different set of questions than a private company in Ireland. Shopify's store setup does the same thing from the first question: what kind of stuff will you sell, do you already sell somewhere, which platform are you migrating from — and it turns you down a completely different path for WooCommerce, BigCommerce, or a CSV import. These flows work because the branches are real, not cosmetic. Do the branching explicitly in your step graph, not with if statements smeared across the UI. Model the flow as data — an ordered list of steps, each with an isVisible(state) predicate or a route guard — and derive the visible sequence from current state. When you do that, the progress indicator, the back button, and the "which step am I on" logic all fall out of the same list, and adding a branch becomes a data change instead of a navigation refactor. The subtle bug: when a branch collapses (say the user switches from CSV to S3), any state owned by the abandoned branch should be discarded or parked — not silently kept and submitted later. [diagram] I've seen flows where someone filled half the CSV mapping, switched sources, and shipped orphaned field mappings to the API because nothing cleared them on the branch change. The branch switch is a destroy/reload boundary, not an overlay on the same form. Persist like the user will disappear — because they will People start wizards on a phone on the train and finish on a laptop at lunch. Or they hit a step that requires their IT admin's SSO metadata and don't get it back for two days. A wizard that only lives in component state is a wizard you're asking users to finish in one sitting. This is exactly the problem the big application flows solved. Rocket Mortgage's home-loan wizard famously lets you start on your phone, abandon it, and resume on the desktop site days later in the same spot — the step, the values, everything. Airbnb saves your listing as a draft on every step, so hosts can come back weeks later and pick up where they parked. Uber's driver onboarding knows you'll hit a step that needs a document from your glove compartment, and it lets you close the flow and return without losing the paperwork you'd already entered. [diagram] Minimum bar: • Persist draft state server-side, keyed to the session or account, at least on each step transition. • Deep-link steps. If I can send you /setup/source?step=mapping, support it — and derive the step from the URL rather than from an index variable, so back/forward and refresh behave like a real flow. • On return, restore everything and land the user on the step they left, with any unfinished validation surfaced gently. "You stopped here" beats dumping them back at step 1 with everything wiped. • Local-only fallbacks (sessionStorage) are fine for anonymous flows, but know their limits: clear the tab, lose the work. This is also the answer to the oldest complaint about long forms. The reason people abandon multi-step flows isn't usually the length — it's the fear that the work is perishable. Make it durable and you'll watch completion climb. Give them a review step before the point of no return If the wizard ends in anything consequential — a charge, a live integration, a bulk import, an irreversible config change — the last step should be a read-only summary of what's about to happen, with inline edit links back to each section. TurboTax has done this for decades with its "your return at a glance" review before filing: every number that matters on one screen, editable by jumping back to the exact section. Shopify's import flow shows you a mapping preview before a single row touches your store. Stripe shows a review before you take a verification-failed workspace live, so you know exactly what you're committing to. [diagram] A review step converts the whole flow from a leap into a confirmation. It also catches the class of errors that per-step validation can't: individually valid answers that don't make sense together. Review screens are where the user (not your validator) does the final coherence check. Then — and this matters — make the final action's label describe the actual action. "Confirm and start import," not "Next." If the button charges money, say so. Ambiguous terminal buttons are how you earn disputes and churn. Accessibility is part of the pattern, not a pass at the end Wizards are pure focus-management territory, and most of them are hostile to anyone not using a mouse. • On step change, move focus to the new step's heading (with tabIndex={-1} so it's programmatically focusable), or to the first invalid field if validation failed. Without this, screen reader users click Next and hear nothing — their focus is stranded on a button that may have been replaced in the DOM. • Announce the step change. aria-live="polite" with "Step 3 of 5: Map columns" is enough. • Mark the current step with aria-current="step" in the progress list, and make each completed step a link back to itself — that's free non-linear navigation and it's what keyboard users expect from a stepper. • Errors get aria-invalid and aria-describedby pointing at the message. The invalid summary, if you have one, receives focus when you land on a step that failed. • Escape should never dump the user's work. If there's a modal wrapper (and on mobile, wizards often are one), Escape closes it with a confirm-and-discard choice, not silently. This is the same standard I hold the rest of the site to — the accordion and dialog implementations in my own components follow exactly these rules: roving tabindex, labelled regions, focus restore. A wizard is just a dialog that spans several screens. The anti-patterns, collected After enough of these, the mistakes get repetitive — and almost all of them are visible in real products: • Too many steps. If you're at nine, merge. Steps are a cognitive unit, not a database table. TurboTax's reputation for "the 25-step tax return" is half joke, half warning — every screen you add is another chance to check out. • Requiring signup before showing any value. Quora and Pinterest spent years hiding content behind registration — huge traffic, tiny conversion. Ask for the account at the end (or at the moment it's needed to save), not up front. Duolingo lets you start a lesson before it ever asks who you are. • Hiding the Next button below the fold on mobile. Sticky footers with Back and Next are non-negotiable. Thumb-reach beats elegance. • Timed steps. Sessions expiring mid-flow with no warning. The worst offenders are government and banking flows that promise X minutes and silently drop a half-hour of input. Warn at a minute out, offer renewal, never auto-advance to a destructive default. • The "one last step" that's actually four. The progress bar said Review, then came payment, then came verification, then came an upsell. Users learn fast, and they don't come back. [diagram] • Clearing state on any error. A network blip on step 5 should cost a retry, not the previous ten minutes. If wiping a single login form on a wrong password is an industry-wide crime, doing that across eight steps is a hostage situation. • Wizards for things that don't branch or sequence. If there's no dependency between the parts, you've added navigation for no reason. Section the form instead. The implementation shape I keep coming back to Strip away the framework and every good wizard I've built — the ones that behave like a Stripe onboarding or a Rocket Mortgage application — looks the same: a step graph as data, form state in one store outside the steps, navigation as pure functions over that graph, validation tied to step transitions, and persistence at every boundary. [diagram] Everything the UX needs — honest progress, working back, branching, deep links, resumability — falls out of those five pieces. Every wizard I've seen fall apart was missing one of them: state trapped in unmounted components, progress computed from a hardcoded length, persistence bolted on later as an afterthought. The takeaway A wizard is a promise: shorter screens, clear direction, nothing lost. Most implementations keep the first part and break the other two. Get the unglamorous things right — state that survives navigation, back that never bites, validation that arrives where the fix goes, progress that doesn't lie — and the flow will feel right even before you touch the visual design. Watch how the good ones behave and you'll see the same script every time: one question per screen, honest progress, back that preserves everything, validation where it can be fixed, branches that stay coherent, drafts that survive the night, and a final review before the point of no return. That's the whole trick. The polish goes on top of that foundation, never instead of it. --- ## How to Make an MVP in the AI Era URL: https://yoosuf.me/blog/how-to-make-an-mvp-in-the-ai-era/ Published: 22 Sep, 2026 Topics: AI & Tech, MVP, Startups, Product Management, Entrepreneurship, AI What an MVP needs now that AI makes building nearly free: the traps to avoid, and why the constraint moved to testing assumptions with real customers. Every week someone shows me the same message: "I built my MVP in a weekend with AI." And sometimes it genuinely is impressive. A support tool that reads emails and drafts replies. A niche note-taking app with a clean UI. Pricing page, auth, a little dashboard. All shipped in forty-eight hours. Then I ask the question the shipper usually hasn't asked yet: what did you learn? That's where the excitement dies. Because most of the time the answer is some version of "we learned we could build it." Which isn't a learning. It's a receipt. AI didn't change what an MVP is for. It changed how cheaply you can do the part that was never the hard part. So the way I'd put it now is this: read the whole history of famous MVPs and the thing you'll notice is that almost none of them were about the engineering. They were about the question. Minimum viable never meant small Eric Ries's definition, from The Lean Startup, still holds and it's quietly brutal: the minimum viable product is that version of a new product that allows a team to collect the maximum amount of validated learning about customers with the least effort. Notice what that makes explicit. The MVP isn't a feature-lite preview of your finished product. It's an experiment engineered to produce a decision. The unit of success isn't "shipped." It's "learned something we couldn't have known without a real customer." The "minimum" was never an aesthetic preference for smallness. It was a strategy for protecting a scarce resource: your budget, your time, your attention. If you've got two engineers and three months of runway, the size of the thing is the whole game. You physically cannot build everything, so the constraint forces you to choose what actually matters. That's the scarcity AI removed. And it's exactly why so many people get the AI-era MVP wrong. Building used to be the bottleneck Cast your mind back to the before times. An MVP meant months. You'd skin a design, wire up auth, stand up a database, handle payments, write copy, debug the deployment, and only then — weeks or a small fortune later — could a stranger touch your idea. There was a real cost to that. But it did a dirty job for you: it policed your scope. You couldn't build the whole dream, so you had to pick the one assumption worth risking the entire budget on. The budget enforced prioritization whether you wanted it to or not. The old MVP was the smallest product you could afford, stretched thin over the thing that mattered most. The AI-era MVP doesn't have that governor. The bottleneck moved Now the build is nearly free. Two people, one weekend. Landing page, auth, database, UI, tests, docs. A scaffold that would have taken a contractor a month costs you an afternoon of prompting. So the entire MVP discipline flips. The question stops being "what's the minimum I can build inside my budget" and becomes "what's the minimum I can build that would genuinely test my riskiest assumption?" And here's the trap hiding in that flip. Because building is cheap, the temptation is to build a full product. But a full product isn't a better MVP — it's a worse experiment. It loads a dozen untested assumptions into one lump of software. When it launches to crickets, you can't tell which assumption was wrong: the problem, the customer, the pricing, the channel, the timing? You spent a weekend (or a month) building a machine to test nothing but whether people would show up to an empty room. The point of minimum isn't to be small. It's to keep the experiment legible. If one thing fails, you want to know exactly which thing it was. What the industry actually proves Before we get to the AI-era traps, it's worth laying down the evidence. These are the MVPs that built the companies you've heard of. Read the list and notice the pattern: the product almost never came first. | Company | The MVP | What it actually tested | What happened | | --- | --- | --- | --- | | Dropbox | A three-minute video of software that didn't exist yet | Whether the idea itself created demand | Beta waitlist jumped from roughly 5,000 to 75,000 overnight | | Zappos | Buying shoes from local stores, photographing them, shipping by hand | Whether people would buy shoes online untried | A tiny proof that eventually grew into a billion-dollar retailer | | Airbnb | Three air mattresses rented out during a design conference | Whether strangers would host and rent — at all | First real bookings, then an entire category | | Groupon | A WordPress site with deals hand-written and emailed as PDFs | Whether merchants would pre-buy discounted volume | Chicago experiment that defined daily-deals | | Buffer | A landing page with pricing tiers whose "sign up" went nowhere | Whether anyone wanted the product and which plan | Early signups and tier choices collected before a line of code | | Midjourney | A Discord server where you typed prompts to generate art | Whether people would subscribe to AI art in a chat app | A million-plus-member community before any native app shipped | | ChatGPT | A thin chat wrapper over existing model technology, offered as a free research preview | Whether consumers wanted conversational AI at all | 100 million users in two months, the fastest consumer adoption then on record | Some of these "MVPs" contained almost no engineering at all — Groupon is a blog with a PDF, Midjourney is a chat channel, Dropbox is a video. The building was never the point. The question was, and the answers were unambiguous. The one caveat: ChatGPT and Midjourney are AI-era examples where the teams already owned unusual technology or community. Don't read them as "ship a wrapper and win." Read them as "make the front door the cheapest possible thing that proves whether people want what's behind it." The three traps of the AI-era MVP I've watched enough of these now to name the ways they go wrong. There are three, and most failed AI MVPs hit at least one. Trap 1: polishing, not shipping Melissa Perri calls it the Build Trap — teams that ship feature after feature and call it product work, when really they're just moving production. AI makes the Build Trap pathological, because the marginal cost of "one more thing" is a prompt. So the founder keeps adding. Better onboarding. A dashboard refinement. Export as CSV. A dark mode because dark mode is easy now. Nothing about the product gets meaningfully better for the customer, but the "MVP" keeps not launching, because it's never quite viable enough — a status anxiety that's expensive precisely because shipping is now cheap but deciding is still hard. The poster child for the Build Trap, in the pre-AI era, is Juicero. The company raised around $120 million and shipped a $400 countertop juicer — a polished, internet-connected machine with proprietary QR-coded juice packs. It was to be the Apple of fresh juice. Then a Bloomberg reporter squeezed a pack by hand and got essentially the same result the $400 machine produced. The problem — do people actually want a gadget between them and their juice? — was never the thing being tested. The build had outrun the question, and the company shut down within a year of that report. The classic MVP death was a product too small to be useful. The AI-era death is a product too finished to fail inconspicuously. Trap 2: validation by vibes Here's the one that stings. Ask an LLM whether your idea is good and it'll hand you a glowing report. It will list plausible customer pain points, compute a market size that sounds rigorous, name competitors, sketch an upside. It's confident, fluent, and completely useless as evidence. An LLM will generate the customer complaints you hoped for. That's not discovery, that's literary fiction. Asking ChatGPT whether your idea is good is like asking a mirror — you'll always get a flattering answer. Real customer discovery still has to happen in the world, with humans who have the problem and would or wouldn't pay to solve it. That part has not been automated. Anyone who tells you otherwise is selling prompts. The industry history agrees, almost cruelly. In the CB Insights autopsy of startups that failed, the most-cited symptom was running out of capital, but the leading root cause has consistently been poor product-market fit — a market that never truly existed, however much the pitch deck said otherwise. You can't confabulate your way to product-market fit. The founders who claim they "validated with AI" almost always validated with a mirror. Trap 3: mistaking attention for demand You launch the landing page, collect 500 emails, and declare validation. Five hundred emails is promise, not behavior — signups cost nothing. The gap between "interesting" and "I'll reorganize my workflow and give you money" is where real products are won and pretend products are exposed. There's a whole graveyard of examples at this exact point. Color raised $41 million before it had shipped, on the strength of its founding story and the size of the moment — investors were sold on the pitch, not the product. The attention was enormous. The retained users, essentially, were not, and the app fizzled within a couple of years. A slightly older one: Google Wave attracted outsized curiosity at launch — a million invites chased it — and the whole thing was shut down because fascination never converted into durable use. People who merely like things usually don't give you money. The honest version of the smoke test converts attention into an action with a cost: a paid pre-order, a scheduled onboarding call, a deposit. Emails are a promise. Booked conversations and charged cards are evidence. The playbook this era rewards The good news: the actual MVP process is the same as it ever was. The order is identical. Only the speeds changed. [diagram] Run the loop. Cheap loops are why the AI era is a gift for anyone who actually tests — you can fit five of these into what used to be one. 1. Name the riskiest assumption. The one thing that, if false, kills everything else. Usually it's not "can we build this" — it's "do humans with this problem exist and would they pay." Dropbox's riskiest assumption was never "can we build file sync" — the founder had built worse — it was "will people want seamless sync waiting on nothing." That's why the video was the perfect test: it isolated demand from build. 2. Choose the cheapest experiment that actually tests it. Not the cheapest thing that looks related. The cheapest thing that produces a real, falsifiable answer. Groupon's WordPress page tested merchant willingness to buy discounted volume better than any prototype could have. Midjourney's Discord server tested willingness to pay for AI art in the least buildy way imaginable. 3. Let AI compress everything except the experiment itself. Copy, code, diagrams, infrastructure, onboarding flows — all cheaper than ever. But the experiment — a real customer doing a real thing with their real time or money — never gets outsourced to a prompt. 4. Decide on evidence, in advance. Signal, threshold, pre-agreed move. I wrote about perish/pivot/persevere recently and the rules are the same here; the cheap build just lets you reach your decision point faster, for less money. The winning team in this era isn't the one that builds the most impressive single product. It's the one that runs five cheap, honest loops in the month it used to take rivals to run one expensive, vague one. The experiment is the one thing AI can't fake The cheapest test of "is this a real market" is the smoke test: one landing page, a clear offer, a waitlist or a pre-order button, then traffic. With AI you can draft the copy, build the page, and set up the funnel in an afternoon. What AI cannot do is earn the traffic or collect the receipts. That's still on you, and it's still the whole job. Then there's the concierge, or Wizard of Oz, MVP — and this is the one I suspect founders sleep on. Ship the front door of the product: a form, an email address, the promise of the service. Behind it, do the work by hand, with you and an LLM as the entire backend. This looks like cheating and it isn't. The assumption you're testing is not "can the technology do this." It's "is there a human being whose problem is painful enough that they'll trust this front door to fix it." The code is trivially replaceable later; the demand isn't. The classic name in this category is Zappos: founder Nick Swinmurn photographed shoes at local retailers, listed them, and shipped purchases by hand out of his apartment — no warehouse, no inventory system, nothing. The machinery was fake, the transaction was real. Global shoe retail online convinced itself from that preposterous little loop. The AI-era version is even better, because the backend is cheaper to fake. I've watched founders run "fully automated AI products" that were really them at a desk, pasting LLM output into an inbox — and that's not a confession, that's a technique. You're testing whether the promise of the product is worth something to someone. If you can serve a real user by hand with LLM assistance in ten minutes, you don't build software yet. You run the transaction. You learned more in one afternoon than a month of scaffolding would tell you. Scale makes a bad bet worse There's a specific failure mode worth naming for the AI era, because everyone's convinced speed means you can afford it: scaling a bad bet faster. Quibi is the expensive monument. It raised somewhere in the region of $1.75 billion, built a polished mobile streaming platform with A-list talent, launched hard with a fanfare, and shut down about six months later. The post-mortems argued about phones killed it, or COVID killed it, but the structural reading is simpler: the team validated the funding story, not the product. There was no cheap loop. There was no moment where eleven users a week told them "this isn't for me" at a cost of an afternoon. They spent venture capital the way earlier eras' losers spent years, and the decision surface only appeared once the money was already gone. This matters more now than it did before, for a counterintuitive reason. The cost of building fell, so the temptation to skip the cheap experiment and go straight to "real product" costs you less money than it used to — but you're still giving up the learning loop, which is now the only thing that was ever scarce. Cheap builds plus skipped experiments is how you spend $40,000 and a quarter thinking you validated, when a $200 landing page and forty phone calls would have told you the truth in two weeks. The parts AI didn't touch The cost of making the solution collapsed. The cost of finding the problem didn't. That asymmetry is the entire economics of the current moment, and I think it's worth stating plainly. Because everyone can ship now, shipping is worthless as a differentiator. Not worthless — table stakes. The moats that remain are the ones a prompt can't reproduce: • Distribution. Being found. How you reach buyers when a thousand other teams can now build your product by Saturday. Midjourney's early moat wasn't the model — so many teams could generate images — it was the community that grew inside Discord first. • Trust. The people who'll take a meeting with you, believe your promises, forgive your bugs. This is why the Airbnb story is still instructive: the product was trivially copyable, the trust required to host strangers in your home was not. • A genuine understanding of the problem. The founder who's lived in the customer's world wins on comprehension, not on code. Dropbox won with a video because its founder had rebuilt file sync enough times to know precisely which pain to dramatize. None of that was ever the hard part of writing software. It was always the hard part of building a company. AI just removed the noise that used to hide it. Decide on a schedule, not on a feeling The one casualty of cheap builds is discipline. When every experiment costs a weekend, it's tempting to run them forever, drifting from tweak to tweak, mistaking motion for learning. So attach a date to the experiment before you run it. "By October 15, if fewer than X people take the paid action, we change the core assumption." Cheap loops are only an advantage when you let them terminate in a decision. An experiment without a termination date isn't a learning loop — it's a hobby with a domain name. And resist the worst advice the era spawns: that you should keep AI-building while you "wait to see." You're never waiting to see. You're deferring the only two things that ever mattered — finding the problem and finding the people. Get them early, get them cheap, and let AI spend its time on everything else. The AI-era MVP is more of an MVP, not less When building is nearly free, the minimum viable product becomes nearly all product and almost none of the "building." The discipline moves fully to the harder end: naming a falsifiable assumption, choosing an experiment that honestly tests it, and sitting face to face with someone who either pays or doesn't. Look back at that table. Dropbox's MVP was a video. Groupon's was a PDF. Zappos's was a man with a camera and a post office run. Midjourney's was a chat channel. The most valuable parts of those companies were identified long before the impressive engineering existed — because the founders were testing demand with whatever was cheapest, not building toward a launch. Ship the smallest thing that teaches you something a prompt can't invent — whether a real person, with a real problem, will hand over their money, their email, or an hour of their time. Every week someone builds an "MVP" in a weekend and calls it validated. The marker of the ones that count isn't that they shipped. It's that, by the end of the weekend, they knew something they didn't know on Friday that a stranger proved to them. Build fast by all means — just make sure you're building the evidence, not just the app. --- ## 10 Event-Driven Architecture Questions That Separate Architects from Framework Users URL: https://yoosuf.me/blog/10-event-driven-architecture-questions/ Published: 14 Sep, 2026 Topics: Engineering, Architecture, Event-Driven, Distributed Systems, Kafka, RabbitMQ Ten questions that separate framework users from distributed-systems engineers: idempotency, ordering, sagas, observability, and when not to use events. Event-driven architecture is easy to start. Install Kafka. Add RabbitMQ. Configure SNS and SQS. Publish an event. Subscribe to it somewhere else. Suddenly the architecture diagram looks sophisticated. But using an event broker does not automatically mean you understand event-driven architecture. The real complexity starts after the first event has been published. What happens when the same event arrives twice? What happens when events arrive in the wrong order? What happens when the database succeeds but publishing fails? What happens when one service fails halfway through a workflow involving five other services? And perhaps the most important question: should this interaction have been event-driven in the first place? Those are the questions that separate someone who knows an event-driven framework from someone who understands distributed systems. Here are ten questions I would use to evaluate whether an event-driven architecture is actually production-ready. --- 1. What happens if the same event is delivered twice? One of the first assumptions that disappears in distributed systems is: "this message will only be processed once." In many production systems, you should assume the opposite. An event may be delivered more than once. [diagram] If the consumer blindly processes the event twice, you can end up with: • duplicate payments • duplicate invoices • duplicate emails • duplicated ledger entries • incorrect inventory movements That is why idempotency is a fundamental part of event-driven architecture. A consumer may store processed event IDs and check before processing: [diagram] But idempotency can also exist at the business-operation level. "Set payment status for ORD-10042 to CAPTURED" is easier to make idempotent than "increase captured amount by $125." The architectural question is not "can duplicates happen?" — they can. The real question is: can the system remain correct when they do? --- 2. What happens when events arrive out of order? Consider an order lifecycle: [diagram] That sequence makes sense. But distributed systems do not always deliver events exactly how humans expect them to arrive. A consumer might observe the events in a different order entirely: [diagram] Or worse — a cancelled order shipped: [diagram] Developers sometimes respond with: "Kafka guarantees ordering." That statement is incomplete. The correct question is: ordering within what boundary? Kafka can preserve ordering within a partition. That does not mean your entire distributed system has universal global ordering. You therefore need to think about: • partition keys • aggregate boundaries • sequence numbers • entity versions • stale event detection • state-machine validation An event might include an aggregate version: { "eventId": "evt-338", "aggregateId": "order-1024", "aggregateVersion": 17, "type": "OrderShipped" } If the consumer has already processed version 18, version 17 may be stale and safely ignored. Another option is to model valid state transitions. If an event attempts to transition CANCELLED → SHIPPED, the domain model should reject it. Message ordering is an infrastructure capability. Business ordering is a domain problem. --- 3. What happens if the database succeeds but publishing the event fails? This is one of the most important failure scenarios in event-driven systems. Without any safeguards, this is what happens: [diagram] The database says one thing; the rest of the system believes another. The consequences ripple outward: • the inventory service never reserves stock • the fulfillment workflow never starts • the customer never receives confirmation A common solution is the Transactional Outbox Pattern. Instead of updating the database and then publishing as separate steps, you perform both persistence operations inside the same database transaction: [diagram] The important principle is: the business state change and the intent to publish must share the same consistency boundary. Without this, it is very easy to create invisible distributed data loss. --- 4. Is this really an event, or is a command disguised as one? Naming messages correctly matters more than it appears. Consider SendInvoiceEmail and InvoiceGenerated. They are not the same thing. SendInvoiceEmail expresses intent — someone is asking another component to perform an action. That is usually a command. InvoiceGenerated represents something that has already happened. That is an event. [diagram] This distinction matters because commands and events create different coupling models. [diagram] With a command, the Order Service knows exactly what the Notification Service should do. With an event, the producer simply announces what happened and consumers independently decide whether they care. A useful test: if nobody consumes this message, has the business fact still happened? If yes, it is probably an event. If the system expects someone to perform an action because of the message, you may actually be dealing with a command. Calling everything an "event" does not make the system event-driven. --- 5. Who owns the event contract? Events become APIs. And just like APIs, they evolve. Suppose your original event looks like { "customerId": 1024, "name": "Yoosuf Mohamed" }. Six months later, someone wants to split it into firstName and lastName. That looks like a harmless refactor. It may not be. There may already be a CRM service, marketing service, billing service, analytics service, notification service, and a data warehouse all consuming name. Changing an event schema can break systems you do not even deploy together: [diagram] Architects need to think about: • backward compatibility • forward compatibility • schema evolution • additive changes • optional fields • semantic versioning • event ownership • deprecation windows • schema registries • consumer contract testing Instead of deleting fields immediately, you might temporarily evolve the event — include both name and firstName/lastName in version 2. Older consumers continue functioning. Newer consumers migrate. Eventually, the old field can be retired through an explicit compatibility process. The real question is not "can the JSON still deserialize?" It is: can dozens of independently deployed consumers survive years of contract evolution? --- 6. What happens when a consumer keeps failing? The event is retried. It fails again. Then again. What happens next? Retrying forever is not a strategy. Retrying immediately can make things worse. If the downstream system is overloaded, sending more requests every few milliseconds can turn a small outage into a much larger incident. [diagram] But adding a DLQ does not solve the operational problem. You must still answer: • Who monitors it? • When should alerts fire? • Who investigates failures? • How are messages corrected? • How are messages replayed? • What happens after replay? • Can reprocessing cause duplicate side effects? A dead-letter queue without operational ownership is simply a graveyard where business operations quietly disappear. Production EDA requires a failure strategy, not merely a failure destination. --- 7. How do you handle partial failure across multiple services? Consider an e-commerce checkout. Now imagine: order created, payment captured, inventory reserve failed. What now? You cannot usually wrap Order DB, Payment DB, Inventory DB, and Shipping DB inside one traditional ACID transaction. Instead, distributed systems often rely on Sagas — a sequence of compensating actions. [diagram] The next architectural decision is whether the workflow uses choreography or orchestration. Choreography Each service reacts to events and emits another event. No central coordinator. [diagram] This can be elegant for simple flows. But as the process grows, it can become difficult to understand. Orchestration A workflow component coordinates the process. [diagram] The orchestrator explicitly knows the workflow. This creates more centralized logic, but can significantly improve observability and control for complex business processes. Neither approach is universally correct. Architects choose between them based on: • workflow complexity • team ownership • compensation requirements • visibility • coupling • auditability • operational requirements The important thing is that distributed workflows need explicit failure semantics. --- 8. What is the source of truth? Once events enter an architecture, this question becomes critical. Where does truth live? Is it: • PostgreSQL? • Kafka? • an Event Store? • a materialized projection? These are very different architectures. In many event-driven applications, the database is the source of truth and events are notifications about changes. In Event Sourcing, events are the source of truth and current state is derived by replaying historical events. [diagram] This distinction is important: event-driven architecture does not automatically mean Event Sourcing. You can use Kafka without Event Sourcing. You can use RabbitMQ without CQRS. You can use events while PostgreSQL remains the authoritative source. Event Sourcing can be extremely powerful for domains requiring complete audit history, temporal reconstruction, complex domain transitions, and historical state replay. But it also introduces significant complexity. A strong architect knows not only how to implement Event Sourcing — they know when not to use it. --- 9. How do you debug a business transaction across ten services? In a synchronous application, debugging is relatively easy — one request, one trace, one response: [diagram] An event-driven workflow may involve events across multiple services, several databases, different queues, multiple regions, and spans of minutes or hours. Now imagine customer support asks: "why was order 83492 never shipped?" Without proper observability, answering that question can become painful. Events should carry enough metadata to reconstruct causality: [diagram] Now you can reconstruct the whole chain — correlation IDs, causation IDs, trace IDs, event IDs. A mature platform should think about: • distributed tracing • structured logs • correlation IDs • event IDs and causation IDs • metrics and consumer lag • failed-message dashboards • workflow state inspection Observability is not something you bolt onto an event-driven architecture later. It is part of the architecture itself. --- 10. Should this interaction be event-driven at all? This may be the most important question on the list. Engineers sometimes discover Kafka and suddenly every interaction becomes an event. That is usually a warning sign. Imagine you need customer details. This may be completely reasonable: GET /customers/123 You probably do not need this — a request, a broker round-trip, a response, another broker round-trip: [diagram] [diagram] Event-driven communication makes sense when you need: • asynchronous processing • temporal decoupling • fan-out to independent consumers • buffering and scalable processing • workflow propagation • integration events • eventual consistency Synchronous communication is often better when: • the caller needs an immediate response • the interaction is request-response • strong consistency is required • failure must be returned immediately • there is no meaningful reason to decouple the participants Many mature architectures intentionally use both. The architectural skill is not knowing how to introduce Kafka — it is knowing where Kafka does not belong. --- The architect-level test If I were interviewing for a system using event-driven architecture, I would ask questions like these: 1. How do you guarantee idempotent consumption? 2. How do you handle duplicate and out-of-order events? 3. How do you guarantee consistency between database changes and event publication? 4. Is this message an event, command, or query? 5. How will event contracts evolve without breaking existing consumers? 6. What is the retry, backoff, poison-message, and DLQ strategy? 7. How does the system recover from partial failure across multiple services? 8. What is the authoritative source of truth? 9. How do you trace and debug an asynchronous workflow across services? 10. Why should this interaction be event-driven in the first place? If most of the answers involve tool names — Kafka, RabbitMQ, MassTransit, Spring Boot, NestJS, MediatR, NServiceBus, AWS EventBridge — then the discussion is still mostly about tools. Those tools are useful. But architecture happens one level above them. The stronger answers start talking about: • idempotency and delivery semantics • consistency boundaries • aggregate ordering • schema evolution • failure domains • temporal coupling • compensation • observability • replay and recovery That is where event-driven architecture becomes a distributed-systems problem rather than a framework configuration exercise. --- And there is an 11th question There is one question I would add for systems handling money, emails, notifications, inventory, claims, or other external side effects: how would you safely replay six months of events without replaying six months of real-world side effects? [diagram] That distinction affects: • event replay • projections • idempotency • integration boundaries • consumer design • migration strategies • disaster recovery If a team cannot explain how replay works safely, the event-driven architecture probably is not as mature as the architecture diagram makes it look. --- Final thought Event-driven architecture is not difficult because publishing an event is difficult. Publishing is the easy part. The difficult part is accepting that once services communicate asynchronously, many assumptions from a traditional application disappear. Messages can be duplicated. Messages can arrive late. Consumers can fail. Services can disagree temporarily. Schemas evolve independently. Workflows can partially complete. Networks disappear. And eventually someone will need to understand exactly what happened to one customer transaction three months ago. That is why understanding event-driven architecture requires more than understanding Kafka, RabbitMQ, or a framework abstraction. Frameworks teach you how to publish events. Architecture teaches you how to remain correct when everything around those events starts failing. --- ## Beyond Email: Dual-Channel SMS Catching, 14 MCP Tools, and Email Analysis in Postwire URL: https://yoosuf.me/blog/postwire-sms-mcp-analysis/ Published: 4 Sep, 2026 Topics: Engineering, AI & Tech, Rust, Open Source, DevOps, AI, Testing, SMS Postwire v0.2 adds SMS catching with Twilio webhooks, 14 MCP tools for AI agents, and Litmus-style email analysis to the local mail catcher. A few days after shipping Postwire—the tiny, single-binary SMTP mail catcher with a native Model Context Protocol (MCP) server—the feedback started rolling in. Mostly from people running autonomous agents and headless browser test runners, the Claude Codes and Cursors and Windsurfs of the world, plus a fair few plain old Playwright CI pipelines. The core stuff had landed well. Server-side long-polling on /api/wait, automatic OTP extraction, no more polling loops or regex puzzles. Test suites were passing faster, CI stopped timing out on async email, and agents were completing signup flows without getting stuck on a verification link. But the same theme kept coming back, phrased slightly differently each time: "Email catching is great, but our app needs SMS 2FA for login. How does my test agent get the SMS code?" That one cut deeper than I expected. It took me a beat to realize why. Email was only half the problem Modern authentication rarely relies on email alone. Think about the actual flow of most production apps: an email confirmation link, sure, and then straight into a 6-digit SMS code sent over Twilio, AWS SNS, or Vonage. One channel verifies the address, the other proves possession of a phone. They cooperate. My mail catcher could handle the first leg flawlessly and then hit a brick wall on the second. And that's a genuinely weird failure mode, because the whole point of a local catcher is to remove the flakiness and the manual copy-pasting from your test loop. If the developer still has to fish a text message off their phone, or spin up some cloud sandbox with another API key, the tool hasn't actually finished its job. So I took it back to the workshop, and spent the last couple weeks turning a lightweight SMTP catcher into something that looks a lot more like a dual-channel authentication testing hub. Here's what changed. Dual-channel SMS catching and Twilio webhooks Postwire now ships with a real SMS catcher inside the same binary, sitting alongside the SMTP engine: ┌──────────────────────────────┐ │ Postwire │ │ │ SMTP (:1025) ───┼─► Email Store (MIME/HTML) ├─┐ │ │ │ Web UI & REST API (:8025) HTTP (:8025) ───┼─► SMS Store (JSON / Twilio) ├─┼─► 14 MCP Tools (stdio) └──────────────────────────────┘ │ Long-polling Engine └─► Signal Extractor (OTP/URLs) No separate mock service, no third-party sandbox account. It captures SMS through two routes: 1. Direct REST ingestion (POST /api/sms) — push structured JSON from your test drivers or a local notification service. 2. Native Twilio webhook parsing (POST /api/sms/webhook) — point your app's Twilio client or webhook dispatcher straight at Postwire and it unpacks the standard x-www-form-urlencoded fields (From, To, Body) into the captured log. The embedded React dashboard on localhost:8025 grew a dual-tab layout to match, so you can flip between emails and SMS in real time. New messages push in over a WebSocket (ws://localhost:8025/api/events) rather than waiting for a refresh. SMS long-polling and extraction The email wait endpoint got a sibling. You pause server-side until an SMS lands: Long-poll up to 10 seconds for an SMS to +15550100 curl "http://localhost:8025/api/sms/wait?to=%2B15550100&timeoutms=10000" And the signal extractor got one too, so the 4-to-8 digit code and any embedded URLs come back ready to use: { "codes": ["839201"], "links": [] } No regexes, no fragile string slicing in your test files. Fourteen MCP tools, or: agents can now do full MFA The original postwire-mcp server exposed seven tools. Adding the SMS channel meant I couldn't just leave it at seven—an agent that can read email but not text messages is back to the same dead end. So the MCP stdio server now exposes fourteen tools, one set per channel: | Email tools | SMS tools | |---|---| | listemails | listsms | | getemail | getsms | | waitforemail | waitforsms | | extractsignals | extractsmssignals | | sendtestemail | sendtestsms | | deleteemail | deletesms | | clearinbox | clearsmsinbox | This is the part that makes me grin. An agent driving a browser can now finish an entire multi-factor flow without breaking character: "Sign up a new user, verify their email, enable 2FA with phone +15550199, grab the SMS OTP, and complete the login." It calls waitforemail, follows the confirmation link, calls waitforsms, pulls codes[0] out of the extract response, types it into the browser, done. In seconds. Natively, over MCP, no browser vision, no webmail scraping. Email analysis, the not-so-glamorous part While I was in there, I added something that wasn't on anyone's request list but solves a quiet pain: GET /api/messages/:id/analysis. Because catching an email is one thing—knowing whether that same email is going to render or land in spam in production is another entirely. curl "http://localhost:8025/api/messages/msg01J6XYZ.../analysis" It runs two passes over each captured HTML email: 1. A compatibility check across eleven formatting factors that trip up legacy clients like Outlook, Gmail mobile, and older Apple Mail—missing , flexbox or grid where a table would be safer, un-inlined style blocks, images without alt text or explicit dimensions, payloads creeping past the 102KB clipping threshold. 2. A heuristic spam score in the spirit of SpamAssassin—text-to-HTML ratio imbalances, uppercase and exclamation-mark clusters, suspicious link shorteners or mismatched anchor hrefs, missing plain-text MIME alternatives. Neither pass replaces a real rendering test or a real spam filter. It's a fast, deterministic signal you can assert against in CI before code merges, and honestly sometimes that's all you need to catch the email that was doomed the moment it was written. Killing the race condition for real The flakiness that bites hardest in async testing is the gap between triggering an action and starting to wait for the result. The message arrives and you weren't listening yet. I wanted that gone. Both /api/wait and /api/sms/wait now accept an ISO-8601 since timestamp: // 1. Capture the exact timestamp BEFORE triggering the action const since = new Date().toISOString(); // 2. Trigger the SMS 2FA request in your app await api.requestSmsOtp({ phone: "+15550199" }); // 3. Long-poll against the pre-captured timestamp const res = await fetch(http://localhost:8025/api/sms/wait?to=%2B15550199&since=${since}); If the SMS lands in the few milliseconds before your fetch connects, Postwire checks receivedat >= since and hands it back instead of missing it. And for tidying up between test runs, there are bulk endpoints to match: POST /api/messages/bulk-delete and /api/sms/bulk-delete, plus PATCH /api/messages/bulk-read and /api/sms/bulk-read to mark batches seen. A full SMS 2FA test, start to finish Here's what the whole thing looks like from the test's point of view in modern Node.js: async function testSmsAuthentication() { const userPhone = "+15550199"; // 1. Record the timestamp prior to triggering the SMS const since = new Date().toISOString(); // 2. Ask your app to send the SMS 2FA code await fetch("http://localhost:3000/api/auth/send-otp", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phone: userPhone }), }); // 3. Long-poll Postwire until the SMS arrives const waitRes = await fetch( http://localhost:8025/api/sms/wait?to=${encodeURIComponent(userPhone)}&since=${since}&timeoutms=10000 ); const sms = await waitRes.json(); // 4. Pull the OTP straight out of the body const extractRes = await fetch(http://localhost:8025/api/sms/${sms.id}/extract); const { codes } = await extractRes.json(); const otpCode = codes[0]; // e.g. "839201" // 5. Complete the login const loginRes = await fetch("http://localhost:3000/api/auth/verify-otp", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phone: userPhone, code: otpCode }), }); console.log("MFA Login Success:", loginRes.status === 200); } testSmsAuthentication(); No arbitrary sleeps, no scraping a dashboard, no timing guesswork. Just deterministic, and it completes in well under fifty milliseconds. Installing it Nothing about the install story changed, which is the point. Still one static binary, still about 15MB, React frontend embedded inside, still no JVM or Node runtime idling in the background. There are a few more distribution paths now though: • Homebrew: brew tap yoosuf/tap && brew install postwire (and brew services start postwire if you want it running as a background service) • One-liner installers: a curl | sh script for macOS and Linux, and a PowerShell equivalent for Windows • Native packages: .deb for Debian/Ubuntu, .rpm for Fedora/RHEL, an AUR package, plus Scoop, Winget, and Chocolatey for Windows • Docker Hub: docker run -d -p 1025:1025 -p 8025:8025 yoosuf/postwire:latest Where it goes from here Postwire started as a way to stop copy-pasting OTP codes out of my own inbox, and it's grown into a single-binary multi-channel auth catcher built for an era where the "developer" testing a feature might be a spell of tokens on a headless browser. I'm not sure I fully predicted that, but I'm not complaining either. If you're writing Playwright integration tests, running CI, or standing up autonomous agents with Claude or Cursor, give it a spin against your real MFA flow and see if it holds up where the old catchers didn't. • GitHub: github.com/yoosuf/postwire • Docker Hub: hub.docker.com/r/yoosuf/postwire • Product Hunt: producthunt.com/products/postwire The repo's AGENTS.md and ARCHITECTURE.md cover integration and design. Drop an issue or a PR if you hit a wall, and tell me what feels missing next—I genuinely want to hear how this behaves under the weird auth flows people keep throwing at it. — Yoosuf --- ## Introducing Postwire: A Tiny SMTP Mail Catcher for Developers and AI Agents URL: https://yoosuf.me/blog/introducing-postwire/ Published: 1 Sep, 2026 Topics: Engineering, AI & Tech, Rust, Open Source, DevOps, AI, Testing Why I built Postwire: a single-binary SMTP mail catcher in Rust with a REST API and MCP server for automated testing and AI agents. We are living in an era where an AI coding agent can scaffold an entire full-stack application in two prompts, spin up cloud infrastructure, and generate fifty integration tests before you've finished your morning coffee. Yet the moment that automated test suite or browser agent encounters this one innocent sentence: “Please enter the 6-digit verification code sent to your email.” Everything collapses. The test suite hangs. The CI pipeline times out. The AI agent starts hallucinating or burns ten thousand tokens trying to scrape a webmail dashboard. And the developer sighs, pulls up a browser tab, clicks around an inbox UI, copies an OTP code, and pastes it into a terminal by hand. In 2026, we have solved distributed consensus, edge computing, and neural code generation. But testing email verification is still fundamentally broken. A few weeks ago, after repeating that manual dance for the fourth time on a Friday afternoon while debugging a password-reset flow, I decided I had reached my limit. So I built Postwire. The absurd problem with testing email If you build web applications, you already know the frustration. When an application sends an email during local development or end-to-end testing, you have three traditional options, and all three come with annoying compromises: 1. The Cloud Sandbox SaaS (Mailtrap, SendGrid Sandbox, etc.) They solve the basic problem of not blasting test spam to real users. But now your local test suite depends on an external network connection, third-party API keys, webhook configurations, and monthly subscription tiers. Why should testing an authentication flow running on localhost:3000 require making API calls across the Atlantic? 2. The Classic Local Catchers (MailHog, Mailpit) These are genuinely great open-source tools, and I've used Mailpit happily for years. But they were designed primarily for human eyes looking at a browser tab. When you want to automate them inside a Playwright test or a CI runner, you run into the same friction: • You have to write clumsy while true; sleep(2); pollapi() loops in your test suite to wait for the email to land. • The assertion frequently runs a split second before the SMTP server flushes to disk, resulting in flaky, non-deterministic test failures. • When the email finally arrives, you receive a massive, unparsed multipart MIME payload. You then spend the next hour writing and debugging fragile regexes to pluck out the six-digit code from deeply nested HTML tables. 3. The AI Agent Wall This is the real kicker. I am running more and more workflows where autonomous agents (Claude Code, Cursor, Windsurf, browser agents) drive our test suites and verify feature implementations end-to-end. Asking an LLM agent to navigate a webmail UI via headless browser vision is slow, fragile, and expensive. And asking it to curl an arbitrary REST endpoint and parse raw MIME headers leads to subtle hallucinations. Agents need a clean, structured, native way to interact with email. They need to be able to say: "Wait for the verification email sent to test@local, give me the OTP code, and tell me when it's done." That was the itch. Postwire is the scratch. What Postwire actually is Postwire is a lightweight, single-binary SMTP mail catcher built specifically for local development, automated test suites, and autonomous AI agents. You point your application's SMTP configuration at localhost:1025. It intercepts every email sent by your app. You can view the messages in a clean web UI at localhost:8025, query them via a REST API, or connect an AI agent directly using its native Model Context Protocol (MCP) server. It doesn't require a cloud account, doesn't require Docker (though a Docker image is available), and doesn't ask for credentials. It's a single static binary written in Rust that compiles the server, SQLite storage, and the embedded web frontend into an executable under 15MB. No JVM warming up. No Node.js runtime idling in the background. It starts in under 5 milliseconds and sits comfortably at 12MB of RAM. The two features that change everything I didn't want to build just another clone of Mailpit. Postwire was built around two specific capabilities designed to eliminate test flakiness and agent friction. 1. Long-polling built into the HTTP API (GET /api/wait) The worst part of testing asynchronous messaging is polling. You send an email in your test, and then you either throw an arbitrary sleep(3) into your code (which slows down CI) or you poll a list endpoint every 500ms (which adds noise and causes race conditions). Postwire solves this at the protocol level with a long-polling wait endpoint: Wait up to 15 seconds for an email matching these filters curl "http://localhost:8025/api/wait?to=user@example.com&subject=Verify&timeoutms=15000" The HTTP request stays open. The instant the SMTP listener finishes processing the incoming message, the server pushes the completed message payload back over the connection and closes the request. If the email doesn't arrive before the timeout, it returns a clean 408. Zero sleep() calls in your test harness. Zero polling loops. Your assertions run the millisecond the email exists. 2. Automatic signal and token extraction (/api/messages/:id/extract) In 95% of automated test scenarios, you do not care about the MIME boundary delimiters, the CSS styling of the hero header, or the tracking pixels. You care about two things: • What was the verification code? • What was the magic link / password reset URL? Postwire runs incoming email bodies through a dedicated signal extraction engine. When you query the extraction endpoint: curl "http://localhost:8025/api/messages/msg01J6XYZ.../extract" It returns structured JSON: { "codes": ["849201"], "links": [ "http://localhost:3000/auth/verify?token=d8f1e09a8b2c4d5e" ], "actionurls": [ "http://localhost:3000/auth/verify?token=d8f1e09a8b2c4d5e" ] } It recognizes 4 to 8-digit numeric and alphanumeric OTPs, magic login links, password reset URLs, and confirmation triggers. You never have to write regexes in your test scripts again. What this looks like in an automated test Here is a practical example. Suppose you are writing an end-to-end authentication test in Python using Playwright or requests. Instead of juggling browser tabs or sleeping: 1. Trigger the signup in your application signupresp = requests.post("http://localhost:3000/api/signup", json={ "email": "sarah@company.test", "password": "CorrectHorseBatteryStaple!" }) assert signupresp.statuscode == 201 2. Block until Postwire receives the verification email (no sleep needed) email = requests.get( "http://localhost:8025/api/wait", params={ "to": "sarah@company.test", "subject": "Confirm your account", "timeoutms": 10000 } ).json() 3. Pull the OTP code straight out of the parsed signals extracted = requests.get(f"http://localhost:8025/api/messages/{email['id']}/extract").json() otpcode = extracted["codes"][0] 4. Submit the verification code verifyresp = requests.post("http://localhost:3000/api/verify-email", json={ "email": "sarah@company.test", "code": otpcode }) assert verifyresp.statuscode == 200 The test is fast, robust, and completely deterministic. If the email delivery fails inside your app, the test fails immediately on the timeout without hanging indefinitely. The native MCP server for AI agents This is where things get genuinely fun. Model Context Protocol (MCP) has rapidly become the standard way AI agents interact with local developer tools. Postwire ships with a dedicated MCP server binary (postwire-mcp) that exposes the entire inbox to your AI agents as native callable tools. Whether you're using Claude Desktop, Cursor, Windsurf, Claude Code, or an autonomous framework, you configure the server in your MCP settings: { "mcpServers": { "postwire": { "command": "postwire-mcp", "env": { "POSTWIREURL": "http://localhost:8025" } } } } Once wired up, your agent has access to seven targeted tools: • waitforemail (blocks until matching email arrives) • extractsignals (pulls OTP codes and URLs) • listmessages (queries inbox with pagination) • getmessage (fetches full HTML, plain text, and MIME data) • deletemessage (cleans up a specific message) • clearinbox (resets state between test runs) • sendtestemail (injects a simulated test message) Now, you can hand your agent a high-level task: "Use the browser to register a new account as alex@test.local, wait for the confirmation email, extract the verification link, visit it to complete registration, and confirm that the user dashboard renders." The agent navigates to your signup page, fills in the form, calls the waitforemail tool, retrieves the extracted URL via extractsignals, navigates directly to the confirmation link, and validates the dashboard. No brittle webmail scraping. No custom bash glue. The agent just does the job. Why Rust? (And how it's engineered) People always ask why I chose Rust for this instead of Go or TypeScript. I don't rewrite tools in Rust just for the sake of writing Rust. But for developer tooling that sits in the background of your daily workflow, three constraints matter: 1. Zero runtime overhead: When I'm developing, Docker containers, language runtimes, and background daemons consume battery and RAM quickly. Postwire compiles to a single, statically linked binary. It starts instantly and uses barely 12MB of resident memory. 2. Single-file distribution: The web frontend (built with React and Vite) is embedded directly into the Rust executable at compile time using rust-embed. There are no nodemodules, no static asset directories to configure, and no external HTML files to keep track of. One file on disk does everything. 3. Resilience under load: SMTP connections and WebSocket streams can easily lead to memory leaks or deadlocks if not handled cleanly. Tokio's async runtime handles hundreds of simultaneous SMTP connections effortlessly, while SQLite in WAL (Write-Ahead Logging) mode guarantees fast, reliable persistence without locks freezing the UI. The codebase is organized as a clean cargo workspace with three focused crates: • postwire-core: Contains the SQLite storage engine, MIME parsing logic, signal extraction rules, and shared domain models. • postwire-server: Houses the Tokio-based SMTP server (port 1025), the Axum HTTP REST API, and the embedded WebSockets inbox dashboard (port 8025). This is the primary binary. • postwire-mcp: The Model Context Protocol stdio server that translates MCP tool calls into HTTP queries against the server. For CI environments, you can pass --memory to run Postwire with an in-memory SQLite database. Tests run at lightning speed, and when the process exits, zero temporary files are left on disk. Quick start You can get Postwire running in your environment in less than 30 seconds. Via Docker If you prefer containers, the official image is on Docker Hub: docker run -d \ --name postwire \ -p 1025:1025 \ -p 8025:8025 \ -v postwire-data:/data \ yoosuf/postwire:latest Open http://localhost:8025 to view the inbox, and point your application's SMTP host to localhost:1025. From source If you have the Rust toolchain installed: git clone https://github.com/yoosuf/postwire.git cd postwire cargo build --release ./target/release/postwire That's all it takes. What's on the roadmap Postwire is currently at v0.1.0. I've been running it in local development and in automated agentic test suites for several weeks, and it has already replaced both Mailhog and Mailtrap across my personal workflow. A few capabilities I'm actively working on for upcoming releases: • Incoming mail webhooks: Register a webhook endpoint so Postwire pushes an HTTP POST payload to your local server the moment a matching message arrives. • Configurable rate-limiting simulation: The ability to simulate real-world email provider throttling and temporary greylisting errors (421/450 responses) to test your application's email queue backoff logic. • IMAP read-only interface: For legacy apps or desktop email clients that insist on fetching mail via IMAP rather than checking a REST API. Wrap up Software development has gotten dramatically faster over the last two years, but we often overlook the mundane friction points that break our flow. Email verification shouldn't be the thing that slows down an automated deployment or confuses an AI agent. Postwire is free, open source under the MIT license, and ready for you to poke at: • GitHub repository: github.com/yoosuf/postwire • Docker Hub: hub.docker.com/r/yoosuf/postwire If you find a bug, want a new feature, or want to contribute to the codebase, open an issue or submit a pull request. I'd love to hear how you're using it in your test suites and agent setups. — Yoosuf --- ## Make the Hard Decision Before the Product Fails URL: https://yoosuf.me/blog/perish-pivot-persevere/ Published: 23 Aug, 2026 Topics: AI & Tech, Management, Product Management, Leadership, Entrepreneurship, Startups Most products don't die suddenly — they die from delayed decisions. A guide to failure signals, premortems, and perish/pivot/persevere rules. I've spent over a decade building systems for startups, and if there's one pattern I'd tattoo on the wall of every founding team, it's this: the failure was almost always visible months before anyone admitted it. You know the script. A product launches, the numbers come in soft, and everyone agrees to "give it another quarter." Then another. Then there's a board meeting with a slide full of hopeful arrows. And somewhere around month fifteen of an eighteen-month runway, somebody finally says the quiet part out loud. By then, the damage is mostly done. The runway is half gone. The best engineers — the ones who could read the writing on the wall — have already found somewhere else to work. Technical debt has piled up because nobody wanted to slow down and fix things during a "push." Customers who gave the product a chance are quietly telling their networks it didn't stick. The founders are running on fumes. And the investors have heard "it's turning around" enough times that they've stopped believing it. Here's what I actually believe about this: waiting for failure to become undeniable isn't caution. It's the most expensive decision a leadership team can make, and most teams make it by accident. Because the alternative isn't optimism or pessimism. It's deciding — in advance — what failure would look like, so you can act while acting still matters. Ask a better question The question most teams ask when things feel off is "is this product failing?" It's a bad question. It invites debate, vibes, politics, whoever speaks loudest in the room. And it frames failure as something you detect after the fact, like a smoke alarm going off in a house that's already burnt down. The much better question is: "What evidence would tell us this product is heading toward failure?" Notice what changes. Suddenly you're not arguing about whether the product feels doomed. You're naming specific, observable things that would worry you before they happen. That single reframe turns failure from a surprise into a plan. Define your failure signals before you need them Before launching anything serious, write down the assumptions you're betting on, with numbers attached. Something like: • Product: customers activate within 7 days of signing up. • Customer: mid-sized businesses have a problem painful enough to pay for this. • Commercial: we can acquire a customer for less than $500. • Retention: at least 70% of qualified customers are still active after three months. • Revenue: $50,000 MRR within 12 months. None of these are promises. They're hypotheses. And here's the part everyone skips: deciding now what happens if they don't hold. Because if you don't decide now, you'll decide later — when you're eight months in, tired, defensive, and emotionally invested in being right. Few people make their best decisions in that state. I've certainly never seen anyone do it. Turn every hypothesis into a trigger If you take one idea from this article, make it this one — it's the mechanism underneath everything else, so let me spell it out. For every hypothesis you write down, attach three things: Signal — the leading indicator that tests it. Activation rate for the product hypothesis, acquisition cost for the commercial one, 90-day retention for the retention one. Threshold — the number and the date that would count as evidence against you. Not "retention should be decent" but "below 70% after three months." The date matters as much as the number: a metric that might get there eventually is not the same as a metric that gets there by June. Pre-agreed move — what you commit to doing if the threshold trips. Investigate. Run an experiment. Change the plan. Stop. Signal, threshold, move. A hypothesis without a threshold isn't a hypothesis — it's a hope with a spreadsheet attached. A few examples built on the assumptions above: | Hypothesis | Signal | Threshold | Pre-agreed move | | --- | --- | --- | --- | | Customers activate within 7 days | Activation rate | Below 40% by week 6 | Investigate onboarding; run experiments before touching strategy | | We can acquire customers for under $500 | Blended CAC | Above $500 for two consecutive months | Pause paid spend; test channels and positioning | | 70% of qualified customers stay active at month 3 | Cohort retention | Under 70% for two consecutive cohorts | Open the pivot conversation while runway allows choices | | $50K MRR within 12 months | MRR growth rate | Off track at month 9 review | Reassess pricing and segment before spending further | Two rules make this work in practice. First, set the move while you're still optimistic. The whole point is that you're choosing with clear eyes now rather than negotiating with yourself later. Second, write the triggers where you'll actually see them — the metrics dashboard, the quarterly review agenda — not buried in a doc nobody opens. An unread trigger is just a wish. You'll notice this pattern repeats through the rest of the article, because it genuinely is the load-bearing piece: the decision matrix below pairs signals into triggers, the runway calendar puts them on a schedule, and the premortem converts imagined failures into new ones. A decision matrix beats the loudest voice in the room Strategic decisions shouldn't come out of whichever meeting had the most confident person in it. I've sat in plenty of those meetings. The loudest person is rarely the one who read the data. So pre-commit to some decision rules. For example: | Signal | What it means | Decision | | --------------------------------------------- | ------------------------------------- | ------------------------------- | | Strong retention + weak acquisition | Product works, distribution doesn't | Fix sales/marketing | | Strong acquisition + weak retention | People try it but don't stay | Fix product | | Strong usage + weak willingness to pay | Value exists, monetization is weak | Test pricing/business model | | Weak acquisition + weak retention | Fundamental problem | Reconsider product/customer fit | | Strong enterprise interest + long sales cycle | Product works, GTM is slow | Evaluate runway and sales model | | Strong demand + execution problems | Market exists, team struggles | Upgrade leadership/team | | No meaningful customer pain | Core hypothesis wrong | Pivot or stop | This table isn't sacred and yours won't look exactly like mine. That's fine. What matters is what it gives you: pre-agreed decision criteria. When the moment comes, the argument is about execution, not about whether the rules exist. That difference has saved plenty of companies. Don't let your runway make the decision Picture a company with eighteen months of cash. At month 3, traction looks weak. At month 6, it still hasn't improved. At month 9, leadership decides to "give it another six months" — which sounds patient and reasonable and wise. At month 15, panic sets in. Now the company has six months to reinvent itself. Hiring takes months. Enterprise sales cycles take longer than that. Even pivoting properly — finding a new customer, rebuilding positioning, shipping the changed product — barely fits. The runway has effectively made the decision, and it made the worst possible one: too little, too late, under maximum stress. Compare that to a team that plans its decision points up front: • Month 0 — define the hypotheses. • Month 3 — review the leading indicators. • Month 6 — make the first major strategic decision. • Month 9 — if the evidence isn't there, execute the pivot you already scoped. • Month 12 — decide whether the new strategy deserves more capital. The exact timeline varies by company. The principle doesn't: never allow your runway to become your decision-making mechanism. If you only act when the bank account forces you to, you're not doing strategy. You're negotiating with bankruptcy. Watch leading indicators, not revenue Revenue matters, obviously. But revenue is a lagging indicator — it reports on decisions made months ago. By the time revenue visibly collapses, the underlying illness has been spreading for two quarters. So track the things that lead: activation rate, weekly active users, retention, feature adoption, conversion, sales pipeline, qualified leads, time-to-value, referral rate, expansion behavior. Pick the handful that map to your hypotheses and watch those. And read them together, because the combination tells you which world you're in. If revenue is flat but activation, retention, usage and referrals are all climbing, you're probably early, not failing — that's a "keep going" signal, not a panic button. But if revenue is growing on the back of aggressive discounts while retention quietly falls apart, your headline number is lying to you. I've watched companies celebrate exactly that kind of growth. It ages badly. This is also the hardest judgment in product management: separating "not working yet" from "never going to work." The trick is to stop asking whether the numbers look good, and start asking whether the numbers are improving at a rate that gives us confidence. Look at two companies: • Company A: 10 customers, then 18, then 31, then 47, then 72. • Company B: 100 customers, then 98, then 91, then 83, then 71. In any single screenshot, B looks twice the company A is. But B is dying, and A is compounding. Direction matters more than snapshots. Any investor, board member or founder who reads a metric without reading its slope is decorating, not analyzing. Perish, pivot, or persevere Every major product initiative eventually falls into one of three buckets. Decide deliberately which one you're in, because drifting between them by default is how teams waste years. Persevere. The evidence supports the strategy. Keep executing and stop generating unnecessary strategic noise. Some of the worst advice founders get is to constantly reconsider everything — momentum deserves respect when the data backs it. Pivot. The underlying opportunity is real, but one of your assumptions was wrong. So you change the customer segment, the pricing, the distribution channel, the scope, the positioning, the business model — while preserving the assets that still have value. Slack grew out of a failed game studio's internal chat tool. Instagram began as Burbn, a check-in app so cluttered its own founders barely opened it; they cut everything except the photos. Those pivots worked because someone admitted early enough that the original hypothesis had died, while there were still assets worth carrying out of the wreckage. Perish. Boardrooms usually reach for a blunter four-letter word, but the substance is the same: there isn't enough evidence of a viable business, and continuing would destroy more resources than it could reasonably create. Stopping isn't failure. Sometimes stopping early is the single best capital-allocation decision available to you — a founder who retires a product with twelve months of runway intact still has the ability to try again. A founder who burns to zero has nothing left but the lesson. If you think letting products perish is rare, look at CB Insights' March 2026 analysis of 431 venture-backed startups that shut down since 2023. The most-cited cause of death was running out of capital — 70% of them — but CB Insights flags that as the final symptom, not the root. The leading root causes were poor product-market fit at 43%, bad timing at 29%, and unit economics that never worked at 19%. Not technology failing. Market judgment arriving too late, with the cash running out as the coroner's report. Company-level decisions, not ego-level ones Founders get emotionally welded to their products. Of course they do — years of their life, stories told to friends and investors and employees about what this was going to become. I'm not going to pretend that attachment is irrational. It's human. But the market does not care about the founder's emotional investment. A product should survive because customers value it, not because shutting it down would be embarrassing. There's a famous version of this question from Intel's history — and the way it's usually told is tidier than what actually happened, which is exactly why it's worth retelling properly. In the mid-eighties, Andy Grove and Gordon Moore were watching the memory-chip business — the product Intel was known for — get crushed by Japanese competitors. In mid-1985, Grove asked Moore a question that has outlived the crisis: "If we got kicked out and the board brought in a new CEO, what do you think he would do?" Moore's answer: get us out of memory. Grove's reply: why shouldn't you and I walk out the door, come back, and do it ourselves? That's the legend. Here's the part it usually leaves out. By then, Intel's middle managers had been quietly reallocating fab capacity away from memory and toward microprocessors for a long while — the organization had effectively voted with its capacity before leadership formally agreed. And even after the famous conversation, the actual exit took roughly another year of painful winding down. Grove himself admitted in hindsight that the clean narrative was cleaner than the reality. Notice what that does to the story. It wasn't one heroic afternoon where two brilliant men saw the truth; it was evidence accumulating until perspective finally caught up with it. Which is precisely the case for pre-agreed triggers: if the signal had been written down years earlier — "memory share below X, margin below Y" — the decision would likely have arrived a year sooner and cost less anguish. Still, the heart of the lesson stands. Same people. Same facts. Different question. The most valuable thing Grove did was step outside his own investment: if I weren't already invested in this company, would I invest in it today? If your honest answer is no, that answer deserves attention, not a follow-up meeting. Don't make employees the last line of defense When leadership delays hard decisions, the team feels it first. The engineers and support folks and account managers figure out "this isn't working" months before anyone says it officially. Meanwhile management keeps repeating "we just need another quarter." Then another. Then another. What follows is organizational fatigue, and I can describe it precisely because the sequence is remarkably consistent. The best people tend to leave first — they're usually the ones with the most options elsewhere. The people who stay get less confident. Morale calcifies into quiet cynicism. And eventually leadership points at the departures and treats them as another problem, when the departures were actually a symptom of the original one: decisions that arrived too late. Honest reporting needs psychological safety — people have to be able to bring bad news early without getting punished for it. If your team learned that flagging a dead product is career-limiting, you didn't just lose information. You lost your earliest warning system. Sometimes the problem is the leader, not the strategy There's also a point where the issue stops being the plan and starts being the person running it. You've seen the shape of it. Clear strategy, real customer demand, big enough market, adequate capital — and yet engineering repeatedly misses commitments. Requirements are mush. Production incidents climb. Customer complaints go nowhere. And the leader responsible keeps making the same mistakes with total confidence. In that situation, hiring more engineers usually makes things worse. A leadership bottleneck is rarely fixed by adding people underneath it — more often it just widens the traffic jam. Sometimes the correct, brutal move is changing the person responsible for the system — and the same logic applies to sales, product, marketing and operations, not just engineering. I wrote recently about how so many corporate failures get blamed on technology when the real culprit is delayed, defensive management. This is the same disease at product scale: the market was never the blocker. The blocker was allowed to stay. Let the evidence set the pace Here's a rule worth stealing: • Weak evidence → investigate. • Mixed evidence → experiment. • Strong positive evidence → invest. • Strong negative evidence → change direction. • Repeated negative evidence → stop. The mistake many companies make is treating every situation as though it requires another meeting. Sometimes the evidence is already overwhelming, and the only thing missing is the nerve to act on it. A leadership team's job, past a certain point, isn't to deliberate more. It's to move. Run a premortem before you commit Before sinking serious resources into a product, gather the team and ask: "Imagine it's eighteen months from now and this product has failed. What killed it?" Then write the answers down. This technique — the premortem — comes from psychologist Gary Klein, and the research behind it is solid: studies on prospective hindsight found that people identify reasons for future outcomes far more completely when they're told to assume the outcome has already happened, rather than guessing whether it might. Something about giving your brain permission to be specific. The answers tend to sound like this: wrong customer. Sales cycle too long. Customers didn't retain. Infrastructure costs underestimated. Product too complex. Competitors moved faster. Wrong leadership hired. Ran out of capital. Built features instead of solving the actual problem. Now — and this is the step that separates theater from risk management — convert each risk into a measurable signal and a trigger. For instance: • Risk: the enterprise sales cycle is too long. • Signal: average deal cycle exceeds six months. • Trigger: if three consecutive quarters show no improvement, we reconsider enterprise as our primary segment. That's it. Risk named, signal measured, trigger agreed. Now nobody has to summon courage in month fourteen — the courage was spent on day one. Ten questions to answer before you build The quality of a product decision isn't determined only by what happens after launch. It starts before engineering writes a line of code. Before committing six months of development, leadership should be able to answer: 1. Who is the customer? 2. What painful problem are we solving? 3. Why will they pay? 4. What evidence already exists? 5. What is our strongest assumption? 6. What could make this fail? 7. How much are we willing to spend testing the hypothesis? 8. What metrics determine success? 9. What metrics trigger a pivot? 10. What conditions cause us to stop? If those questions can't be answered yet, the company isn't necessarily ready to build. It might be ready to learn. Those are different budgets, different timelines, different expectations — and confusing one for the other is a classic way to spend a year learning nothing. Put the decisions on a calendar One practical habit ties all of this together: formal review points, written down, non-negotiable. • Monthly: operational metrics. • Quarterly: product and market assumptions. • Every six months: strategy and resource allocation. • Before major hiring: capability gap analysis. • Before major investment: evidence review. • Before extending runway: an honest probability-of-success assessment. The calendar matters because it removes mood from the loop. Strategy decided in reaction to the latest bad week isn't strategy — it's anxiety with a slide deck. Fail cheaply, not fatally Let me be clear about what none of this is: none of it is a recipe for eliminating failure. Some products will fail. Some features, some hires, some markets — wrong, wrong, wrong. That's normal and unavoidable, and any founder who claims otherwise is selling something. The achievable goal is narrower and much more powerful: Fail cheaply. Learn quickly. Never let a small failure grow into a company-ending one. A $20,000 experiment that prevents a $2 million mistake is a success. A three-month failed product test that prevents three years of wasted development is a win, even though it stings. A difficult leadership change made early can save the entire company. And letting a product perish while there's still runway left might be the best decision a founder ever makes — it's just a decision nobody applauds at the time. The final leadership test Nobody makes the right call every time. Not Grove, not Moore, not you, not me. Leadership isn't a perfect-prediction contest. It's creating an organization where wrong decisions become visible early enough to correct. That takes clear assumptions, measurable outcomes, defined decision points, honest reporting, strong people, the psychological safety to challenge ideas, and the courage to change direction — balanced by the discipline not to change direction without evidence. The companies that survive aren't the ones that never make mistakes. They're the ones that catch their mistakes before the mistakes become existential. That's the whole difference between a product experiment and a company-wide gamble. For anyone building with constrained resources, it's the difference between another five years and another five months. Your product will tell you the truth eventually. The only question is whether you'll still have the runway to listen. --- ## AI Didn't Cause Every Layoff. Management Did. URL: https://yoosuf.me/blog/ai-didnt-cause-every-layoff-management-did/ Published: 15 Aug, 2026 Topics: AI & Tech, AI, Layoffs, Management, Productivity, Future of Work How much of the AI layoff era is automation, and how much is management using AI as the excuse for decisions that were already coming? We're living through one of the strangest periods in the history of technology, and honestly, I'm not sure we're talking about it the right way. Companies are pouring billions into AI. Executives keep announcing "unprecedented productivity" like it's a magic number. Investors reward anything that smells like AI-driven efficiency. And in the same breath, thousands of people are being told their position has been eliminated. The obvious villain here is AI. It can write code, summarize documents, answer customers, draft marketing copy, analyze spreadsheets. Work that used to take a whole team now takes a tool and a prompt. So the story writes itself: AI is taking our jobs. But I've been around long enough to be suspicious of clean narratives. I've watched companies make bad calls for decades, long before anyone ever heard of a transformer. And my honest read is that AI gets blamed for a lot of layoffs it didn't cause. Sometimes AI genuinely replaces work. Sometimes it creates new work. And sometimes it's just the most convenient excuse for a decision that was already coming. That distinction matters. Because if we blame everything on AI, we'll miss the real problem entirely. The good Let's start with the uncomfortable truth. AI is genuinely powerful. I use it every day. A developer can now explore an unfamiliar codebase, generate tests, chase down errors and document half a project in an afternoon. A marketing team can ship drafts ten times faster. A support team can deflect the questions that used to eat entire shifts. An analyst can chew through hours of manual work in minutes. A two-person startup can do what used to require twenty people. That part is real. The International Labour Organization's 2026 review of the evidence concluded that productivity gains from generative AI are real, though uneven and often stubbornly hard to translate into measured output, earnings or employment. Here's where I think leadership gets to make a choice. The question can be "how many people can we remove?" or it can be "what can our existing people accomplish now that they have better tools?" Those are two very different management philosophies. And they lead to very different companies. The bad Now the part that worries me. Say you run a company with a thousand employees. You discover AI might make people 30% more productive. The quick arithmetic goes something like: "well, we could probably run this place with 700 people." So 300 people get cut, and the company announces "AI-driven transformation." Costs drop, the stock bumps, the board loves the story. But here's the thing nobody wants to say out loud: AI does not redesign your organization. Removing people does not create productivity. It just removes people. You still have your tangled processes, your technical debt, your meetings about meetings, your duplicated systems, your layers of management whose job is forwarding emails, your bad product calls. AI doesn't fix any of that. In some cases, layoffs just remove the only people left who understood why the whole dysfunctional system worked the way it did. Then it collapses properly, and nobody can explain what happened. The ugly Here's the part I think deserves a lot more conversation. Not every layoff blamed on AI was caused by AI. Companies over-hire in the good times. They chase markets that don't exist. They buy companies and discover they paid twice for the same product. They build things nobody asked for. They miss numbers. They have to answer to investors. Their cost base balloons. And then AI arrives. And suddenly there's this beautiful narrative available: we're transforming the company for the AI era. Doesn't that sound better than we made a few strategic mistakes and now we need to cut costs? I'm not saying every company that blames AI is lying. Plenty of layoffs genuinely are automation-driven. But when a company fires thousands while announcing a billion-dollar AI spend, you should ask how much is really the technology — and how much is restructuring wearing an AI costume. You don't have to be cynical to wonder. You just have to look at the numbers. The layoff that never makes headlines There's a quieter kind of job loss that nobody talks about. It's not the person getting fired. It's the person who never gets hired in the first place. A company doesn't have to fire a hundred juniors. It can just hire thirty fewer people this year. Then thirty fewer next year. Then it stops backfilling roles when people leave. No big announcement, no viral LinkedIn post, no news story. But the opportunities quietly disappear. This one hits graduates hardest. Entry-level work has always been how people got their start — you do the boring stuff, you watch, you learn, you earn your stripes. If AI eats all the boring stuff, companies ask why they'd hire someone with two years of experience when the tool does most of that work anyway. Which leads to a question I genuinely can't answer: how does anyone get five years of experience if nobody will give them the first job? Management isn't safe either And here's a twist a lot of people don't see coming: AI doesn't just threaten the workers. It threatens the managers. If employees can get answers, build analyses, automate their own workflows and solve problems on their own, then a chunk of traditional middle management stops making sense. A 2026 study published through the Academy of Management found that firms more exposed to generative AI cut managerial hiring by somewhere between 19 and 27 percent, and the evidence points to organizations leaning on middle-management layers a lot less. Part of me thinks that's overdue. Some companies have so many layers that a decision has to travel employee → team lead → manager → senior manager → director → VP → executive before coming back, by which point the market has moved. Flatter is usually better. But there's a danger too. Slicing out management without rethinking how the organization actually works is chaos. Good management was never just passing information up and down. It's context. It's prioritization. It's accountability, mentoring, conflict resolution, keeping people pointed in the same direction. Cut the dead weight, fine. Cut the leadership, and you'll feel it. The cost that doesn't show up on a spreadsheet There's a consequence of all this that never makes it into the quarterly report. Fear. When people believe the tool next to them is gunning for their job, they change. They stop taking risks. They stop questioning the boss. They stop experimenting. Instead of asking how AI can help us build something better, they start asking how to prove AI can't replace them. That's a terrible environment, and companies create it without meaning to. Innovation needs psychological safety. Fear produces compliance. You can accidentally build the exact opposite of the company you wanted. The productivity trap There's one more quiet way this goes wrong. AI makes everyone faster. Great. But what if the company doesn't use that speed to lighten anyone's load, and just raises expectations instead? Before AI: "finish this in three days." After AI: "why can't you do ten of these today?" The job doesn't disappear. But it gets worse. The person is expected to operate like the machine. That's a different kind of displacement — the role stays, and the human experience of it degrades. Who actually keeps the gains? And then there's the money question. A tiny company with good AI infrastructure can now take on a much bigger one. One sharp engineer with the right tools can do work that used to need a team. On paper that's great. But who captures the value? Does the employee earn more? Does the customer pay less? Does the company invest in new things? Or does the shareholder just get fatter margins? There's no law of physics that decides this. It's a management decision. It's an economic decision. And right now, most companies seem to be defaulting to the shareholder option. Forget "will AI take our jobs?" That's the wrong question, and I think we all know it by now. The useful questions are smaller and more uncomfortable. Which tasks disappear? Which jobs change? Which jobs appear? Who gets the productivity gains? Who pays for the transition? And what happens to the people who can't retrain fast enough? The World Economic Forum's 2025 outlook said it well: plenty of jobs created and plenty destroyed by 2030, not some simple collapse. The catch is that job creation and job destruction don't hit the same people at the same time. A tester who gets let go today can't become an AI systems architect tomorrow. A junior who never gets hired doesn't benefit from the senior AI roles five years down the road. A support worker can't just become a machine-learning engineer because the company tweeted a strategy. Transitions happen at the population level. People live through them one by one. The biggest mistake is the mindset The real mistake isn't adopting AI. It's adopting AI with a layoff-first mentality. The sane sequence is: understand the work → redesign the process → introduce AI → measure productivity → retrain people → redeploy talent → then restructure where you truly must. What a lot of companies actually do is: buy AI → announce transformation → cut headcount → declare victory. Those aren't the same strategy. One is transformation. The other is cost cutting with an AI sticker on the box. What good looks like If I could put a sane AI transition in front of executives, it would come down to five questions. First, what work are we actually automating? Not "which people can we remove," but "which tasks can the technology do better." Second, what happens to the people doing that work? Can they move into harder problems? Become operators of the AI? Work closer to customers? Build new products? Third, are we measuring productivity or just headcount? Cutting 20% of the team doesn't mean output went up 20%. It means you have fewer employees. Those are different sentences. Fourth, are we quietly killing the talent pipeline? Eliminate the junior roles today, and in five years you'll discover you have nobody left to promote. Fifth, who gets the dividend? If output really does jump, it shouldn't just mean more work for the same people. The gains could come back as pay, shorter hours, better products, training, new opportunities. That's a choice, not a default. Where this actually lands I don't think the future is "humans vs AI." I think it's "humans with AI vs humans without AI." That's a much more interesting conversation. The edge probably won't go to the company with the most AI. It'll go to the company that best combines people, tools, processes, domain knowledge and management that doesn't panic. AI is a tool. Management decides how the tool gets used. So yes — let's keep talking about AI-driven displacement. But let's also ask harder questions about corporate decision-making. When a company cuts thousands while spending billions on AI, ask how much is genuine displacement and how much is strategy wearing a technology costume. When a company kills its entry-level roles, ask who grows the next generation of experienced people. When a company celebrates productivity, ask who actually receives it. And when an executive says "AI is changing the workforce," ask whether AI is changing the workforce — or whether the executive is using AI to justify changes they wanted all along. Sometimes the answer is both. That's okay. The point is to actually ask. AI isn't good or bad by itself. And it certainly isn't responsible for every job lost in this wave. It's accelerating a bigger transformation — automation, globalization, cost pressure, investor demands, org redesign, new business models. Some jobs vanish, some get better, some new ones show up, and some companies will use this brilliantly while others use it as the handiest excuse they've ever had. The biggest risk was never that AI replaces humans. The biggest risk is that companies replace thoughtful management with AI-shaped cost cutting. Technology can make a bad organization faster at being bad. It can also make a good organization extraordinary. The difference isn't the AI. It's leadership. --- ## Setting Up a Legacy Rails 4.2.6 Project with Docker in 2024 URL: https://yoosuf.me/blog/setting-up-legacy-rails-project-in-2024/ Published: 18 Sep, 2024 Topics: Engineering, Ruby on Rails, Docker, Legacy Systems, DevOps How to set up and run a legacy Rails 4.2.6 project in 2024 using Docker to isolate outdated dependencies. Working on a legacy Rails 4.2.6 project in 2024 can be challenging due to outdated dependencies and potential compatibility issues. Setting up the environment with Docker is a good approach, as it can help isolate dependencies and ensure consistency across development environments. Here's how you can proceed: Setting Up the Environment with Docker 1. Install Docker: Ensure Docker is installed on your system. This includes Docker Engine and Docker Compose. 2. Create a Dockerfile: Create a Dockerfile to define the environment. Here's an example Dockerfile for Rails 4.2.6: FROM ruby:2.3.8 # Install dependencies RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs # Set up the working directory WORKDIR /app # Copy the Gemfile and Gemfile.lock into the container COPY Gemfile /app/ # Install gems RUN gem install bundler -v '<2.0' && bundle install # Copy the rest of the application code COPY . /app # Expose port 3000 to access the Rails server EXPOSE 3000 # Start the Rails server CMD ["rails", "server", "-b", "0.0.0.0"] 3. Create a docker-compose.yml File: Set up docker-compose.yml to define services. For example: version: '3' services: db: image: postgres:9.6 environment: POSTGRESUSER: postgres POSTGRESPASSWORD: password volumes: - dbdata:/var/lib/postgresql/data web: build: . command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails server -b 0.0.0.0" volumes: - .:/app ports: - "3000:3000" dependson: - db volumes: dbdata: 4. Adjust Gemfile for Compatibility: Ensure the Gemfile is compatible with Rails 4.2.6 and any other dependencies. You might need to specify older versions of gems that are compatible with Ruby 2.3.8 and Rails 4.2.6. 5. Build and Run the Docker Container: docker-compose build docker-compose up 6. Run Database Migrations: Once the container is up, run migrations if needed: docker-compose run web rake db:create db:migrate Considerations • Ruby Version: Rails 4.2.6 typically works with Ruby 2.3.x. You may need to use an older version of Ruby, like 2.3.8, which can be specified in the Dockerfile. • Gem Compatibility: Some gems used in Rails 4.2.6 may not be compatible with newer systems. Pinning gem versions in the Gemfile is important. • Security Risks: Rails 4.2.6 is no longer maintained, meaning it doesn't receive security updates. If possible, consider upgrading to a more recent Rails version. • Local Development: Docker can help mitigate issues with local environment inconsistencies, ensuring that the legacy project runs consistently across different machines. Using Docker for a Rails 4.2.6 project can streamline setting up and managing the development environment, especially with the challenges posed by running older software in modern environments. --- ## I don't use Tea bags for my pot of Tea URL: https://yoosuf.me/blog/i-dont-use-tea-bags/ Published: 5 Jun, 2023 Updated: 25 Sep, 2026 Topics: Personal, Tea, Loose Leaf Tea, Sustainability Why loose-leaf tea beats tea bags: better quality leaves, no microplastics, and a more sustainable cup of tea. Tea bags are a staple in many households, but they may not be the healthiest or most sustainable option. Here are some reasons why you might want to avoid tea bags and switch to loose leaf tea instead: Lower quality tea leaves Tea bags are often made from lower-quality tea leaves than loose leaf tea, because the leaves used in tea bags tend to be broken, crushed, or reduced to dust. These lower-quality leaves don't offer the same flavor or health benefits as whole leaf tea. Microplastics Tea bags can release microplastics and other harmful chemicals into your tea. Microplastics are tiny pieces of plastic that can enter the environment from a variety of sources, including tea bags. These microplastics have been linked to a number of health problems, including cancer and reproductive issues. Sustainability Tea bags are not as sustainable as loose leaf tea. This is because tea bags are typically made from plastic, which is a non-renewable resource. Loose leaf tea, on the other hand, can be stored in a reusable container and brewed multiple times. This makes loose leaf tea a more sustainable option. If you're looking for a healthier and more sustainable way to enjoy a cup of tea, I'd recommend switching to loose leaf. It's made from higher-quality leaves, doesn't shed microplastics, and is the more sustainable choice. Here are some of the benefits of buying loose tea: • Better flavor: Loose tea leaves have more surface area than tea bags, which means that they can release more flavor into the water. • More variety: There are many different types of loose tea available, so you can find one that suits your taste. • Fresher tea: Loose tea leaves are typically fresher than tea bags, because they are not pre-packaged. • More affordable: In the long run, loose tea can be more affordable than tea bags, because you can use the leaves multiple times. If you are ready to make the switch to loose tea, here are a few tips: • Buy loose tea from a reputable source: There are many different brands of loose tea available, so it is important to buy from a source that you trust. • Store loose tea in an airtight container: This will help to keep the tea fresh. • Brew the tea for the correct amount of time: This will help to ensure that the tea is not too bitter or too weak. With a little practice, you will be able to make delicious cups of tea with loose tea. --- ## How I do water fasting URL: https://yoosuf.me/blog/how-to-water-fasting/ Published: 1 Jun, 2023 Updated: 25 Sep, 2026 Topics: Personal, Water Fasting, Health, Fasting Guide Water fasting means consuming only water for a period of time. Here is why people do it, the methods involved, and what to expect. There are many different ways to do water fasting. Some people choose to fast for a specific number of days, while others fast until they reach a certain weight goal. There is no right or wrong way to do water fasting, as long as you are safe and comfortable. If you are considering water fasting, it is important to talk to your doctor first. Water fasting can be dangerous for people with certain medical conditions, so it is important to make sure that it is safe for you. Once you have talked to your doctor and decided that water fasting is right for you, there are a few things you need to do to prepare. First, you need to make sure that you are hydrated. This means drinking plenty of water in the days leading up to your fast. You should also eat a healthy diet in the days leading up to your fast. This will help to ensure that your body has the nutrients it needs to survive the fast. On the day of your fast, you should start by drinking plenty of water. You should also drink other clear liquids, such as unsweetened tea or coffee. These liquids will help to keep you hydrated and prevent you from feeling hungry. During your fast, it is important to listen to your body. If you start to feel lightheaded, dizzy, or nauseous, you should break your fast. You should also break your fast if you have any medical problems. Once you have completed your fast, it is important to slowly reintroduce food into your diet. This will help to prevent your body from going into shock. You should start by eating small, healthy meals. You should also avoid processed foods and any drinks that contain sugar. Water fasting can be a great way to improve your health. However, it is important to do it safely and under the supervision of a doctor. If you are considering water fasting, talk to your doctor first to make sure that it is right for you. The Liquids I Consume During Water Fasting In addition to water, there are a few other liquids that I consume during water fasting. These include: • Unsweetened tea • Unsweetened coffee (black coffee) • Water with a pinch of salt added before drinking (not recommended if you have any chronic health condition) • Lemon or lime water • Herbal tea • Green tea I avoid drinking any liquids that contain calories, sugar, or artificial sweeteners. These liquids can break your fast and make it more difficult to lose weight. If you are considering water fasting, it is important to choose liquids that are safe and healthy. Water, unsweetened tea, and unsweetened coffee are all good choices. I hope you found this article interesting. If you have any questions or comments, please feel free to contact me. And if you found this article helpful, please consider subscribing to my YouTube channel for more fitness and health related video content. --- ## Why I am practising 24-hour water fasting 2 days a week URL: https://yoosuf.me/blog/24-hours-water-fasting/ Published: 29 May, 2023 Topics: Personal, Intermittent Fasting, Water Fasting, Health Why Yoosuf Mohamed practises 24-hour water fasting two days a week, and the effects he's noticed. Fasting has been practiced for centuries for its many health benefits. In recent years, intermittent fasting — alternating periods of eating and fasting — has seen a surge of interest, and one popular form of it is a 24-hour water fast, done twice a week. There are several potential benefits to this approach: • Weight loss: Fasting can help you lose weight by reducing your calorie intake. In fact, studies have shown that intermittent fasting can be just as effective for weight loss as traditional calorie restriction. • Improved insulin sensitivity: Fasting can help improve insulin sensitivity, which can help reduce your risk of developing type 2 diabetes. • Reduced inflammation: Fasting can help reduce inflammation throughout the body, which can improve your overall health and well-being. • Increased autophagy: Autophagy is a process by which the body breaks down and recycles damaged cells. Fasting can help increase autophagy, which can help protect your cells from damage and disease. • Longer lifespan: Studies have shown that fasting can extend lifespan in animals. While there is no definitive research on the effects of fasting on lifespan in humans, it is possible that fasting could have similar benefits. The Prophet Muhammad (peace be upon him) practiced dry fasting, abstaining from both food and water during his fasts. While dry fasting isn't recommended for everyone, it's a practice that has been shown to carry its own health benefits. If you're considering a twice-weekly 24-hour water fast, talk to your doctor first — fasting isn't appropriate for everyone, and it's worth making sure it's safe for you before you start. Here are some tips for water fasting: • Start slowly: If you've never fasted before, start with a shorter fast, such as 12 hours, and gradually increase the length over time. • Stay hydrated: Drink plenty of water, along with unsweetened tea and coffee if you like. • Listen to your body: If you feel lightheaded, dizzy, or weak, break your fast. • Don't overdo it when you break your fast: Avoid a large meal — start with small, frequent meals instead. Here are some of the benefits I've experienced from fasting this way: • Weight loss: I have lost about 10 pounds since I started fasting 2 days a week. • Improved insulin sensitivity: I have noticed that my blood sugar levels are more stable since I started fasting. • Reduced inflammation: I have less joint pain and inflammation since I started fasting. • Increased energy levels: I have more energy throughout the day since I started fasting. • Mental clarity: I feel more mentally clear and focused since I started fasting. If you're considering trying this, I'd encourage you to give it a shot. It's been a genuinely beneficial experience for me, and I believe it could be for you too. Why did I choose Monday and Thursday for water fasting? I settled on Monday and Thursday for a couple of reasons. First, I wanted non-consecutive days, so my body would have a chance to recover before the next fast. Second, I wanted days that weren't too busy, so I could focus on the fast without too many distractions. Monday and Thursday have turned out to be ideal for me. Mondays are usually quiet, which gives me the space to focus on fasting. By Thursday, I'm already feeling the benefits from earlier in the week, which keeps me motivated to continue. If you're considering trying this yourself, I'd suggest starting with Monday and Thursday. These days have worked well for me, and I believe they could work well for you too. --- ## How I managed year by living a frugal lifestyle URL: https://yoosuf.me/blog/frugal-lifestyle/ Published: 25 May, 2023 Updated: 25 Sep, 2026 Topics: Personal, Frugal Living, Personal Finance, Minimalism How Yoosuf Mohamed managed a full year by living a deliberately frugal lifestyle, and what he learned. I've always been a bit of a frugal person, but I decided to take it to the next level in 2022. I was inspired by Dave Ramsey's book, The Total Money Makeover, and I wanted to see if I could live on a budget and still enjoy my life. I started by creating a budget and tracking my spending. I was surprised to see how much money I was wasting on things like eating out and shopping. Once I knew where my money was going, I started to make changes. I began cooking more meals at home and packing my lunch for work. I also started shopping at thrift stores and garage sales instead of new ones, and I made a habit of using coupons and discounts wherever possible. It wasn't always easy, but after a few months I started to see how little I actually needed to spend to live a financially minimal lifestyle. I was saving more money and felt less stressed about my finances. I also had more time and energy to spend on the things I really enjoyed. Here are some of the things that I did to live frugally in 2022-2023: • Managed every penny: Created a budget and tracked my spending. This was the first step in my journey to frugal living. It helped me to see where my money was going and where I could cut back. • Deleted the Uber Eats app: Cooked more meals at home and packed my lunch for work. This saved me a lot of money on eating out. I also found that I enjoyed cooking more when I had the time to do it. • Unsubscribed from Netflix and PeoTV: There are plenty of free or low-cost activities you can enjoy instead, such as going for walks, reading library books, or travelling by bus. • Chased everyone who owed me money: This was the hardest one — following up with everyone who owed me money. I couldn't recover every penny, so it wasn't a complete success. Some people got annoyed with me, and a few stopped answering my calls, but I felt it was worth doing regardless. • Stopped expensive hobbies: I gave up my marine aquarium, since it was a cash burn with no return. After selling off the equipment I'd bought for it, I only recovered about 20% of what I'd spent. Frugal living is not about depriving yourself of the things that you enjoy. It's about finding ways to enjoy your life without spending a lot of money. It's also about being mindful of your spending and making sure that you are not wasting money on things that you don't need. If you are looking to save money and live a more fulfilling life, I encourage you to try frugal living. It may not be easy, but it is definitely worth it. Here are some additional tips for frugal living: • Set financial goals. What do you want to save for? A down payment on a house? A new car? Retirement? Once you know what you are saving for, it will be easier to stay motivated. • Find a support system. Having friends or family members who are also interested in frugal living can be helpful. You can share tips and tricks, and you can hold each other accountable. • Don't be afraid to make changes. If you find you're not sticking to your budget, adjust it. There's no one-size-fits-all approach to frugal living — you need to find what works best for you. • Use coupons and discounts. There are many ways to find coupons and discounts, such as online, in newspapers, and in store circulars. • Shop at thrift stores and garage sales. This is a great way to find gently used items at a fraction of the price of new items. Frugal living can be a great way to save money and live a more fulfilling life. If you are willing to make some changes, you can see a big difference in your finances and your overall well-being. I plan to keep living this way — at least until I become a millionaire. --- ## How ChatGPT helps me on day to day life URL: https://yoosuf.me/blog/chatgpt-daily-life/ Published: 21 May, 2023 Updated: 25 Sep, 2026 Topics: AI & Tech, ChatGPT, AI Tools, Productivity, Artificial Intelligence How ChatGPT helps Yoosuf Mohamed in day-to-day life, from quick research to everyday problem solving. In today's digital age, the ability to communicate effectively and craft compelling content is more important than ever. As a writer, marketer, and programmer, I've always sought tools and techniques to enhance my skills and streamline my workflow. Recently, I discovered the power of ChatGPT, an AI language model developed by OpenAI. In this article, I will share my personal experience of how ChatGPT has transformed my content writing, email composition, and coding abilities. By leveraging this innovative tool, I've achieved remarkable improvements in efficiency, creativity, and overall quality. 1. Enhanced content writing One of the most significant ways ChatGPT has helped me is by enhancing my content writing process. With its vast knowledge base and ability to generate coherent and contextually relevant text, ChatGPT acts as a valuable writing companion. Whenever I encounter writer's block or need assistance with brainstorming ideas, I turn to ChatGPT for inspiration. The model's ability to generate topic introductions, provide supporting arguments, and suggest creative angles has proven invaluable. By incorporating its suggestions into my writing, I've noticed a significant improvement in the structure, flow, and overall readability of my content. ChatGPT's assistance has helped me produce engaging blog posts, articles, and website copy more efficiently than ever before. 2. Streamlined email composition Crafting effective and professional emails is essential in both personal and business communication. ChatGPT has been instrumental in helping me compose emails that strike the right tone and convey my message with clarity. By providing me with alternative phrasing, offering suggestions for introductions and conclusions, and even catching grammar errors, ChatGPT has become an indispensable tool for email composition. The AI model's ability to understand context and respond appropriately to my queries enables me to save time and effort while ensuring the emails I send are polished and impactful. Whether it's a persuasive sales pitch, a formal business proposal, or a friendly personal message, ChatGPT has helped me communicate more effectively and leave a positive impression on recipients. 3. Empowering coding skills As a programmer, I've found ChatGPT to be an excellent resource for improving my coding skills. Whether I'm looking for a quick code snippet, troubleshooting a bug, or seeking guidance on best practices, ChatGPT provides reliable assistance. Its extensive knowledge base covers a wide range of programming languages, frameworks, and development techniques. By interacting with ChatGPT, I've been able to reinforce my understanding of programming concepts, explore different approaches to problem-solving, and stay updated on the latest industry trends. This AI-powered coding companion has not only helped me save time but also encouraged me to think critically and explore new possibilities in my coding projects. ChatGPT has truly revolutionized my content writing, email composition, and coding skills. Its ability to generate creative ideas, provide relevant suggestions, and offer valuable insights has had a profound impact on the quality and efficiency of my work. By harnessing the power of this AI language model, I've become a more effective and confident writer, communicator, and programmer. However, it's important to note that while ChatGPT is a powerful tool, it should not replace human creativity, critical thinking, and expertise. It serves as a supportive resource, complementing our skills and enhancing our capabilities. With responsible usage and a thoughtful approach, ChatGPT can be an invaluable asset in our quest to write better content, compose compelling emails, and improve our coding proficiency. --- ## Becoming a ReactJS expert URL: https://yoosuf.me/blog/become-a-reactjs-expert/ Published: 18 May, 2023 Updated: 25 Sep, 2026 Topics: Engineering, React, ReactJS, JavaScript, Frontend Development A practical guide to leveling up in React, from core concepts to becoming a confident, expert-level developer. Becoming a React expert requires time, practice, and a strong dedication to learning. Here are some steps you can follow to enhance your React skills and become an expert: 1. Master the fundamentals Start by understanding the core concepts of React, such as components, JSX, props, state, and lifecycle methods. Get comfortable with creating and rendering components, passing data between components, and managing component state. 2. Build projects Practice is crucial for improving your React skills. Begin by building small, simple projects and gradually progress to more complex ones. Working on real-world projects helps you understand how React works in different scenarios and allows you to encounter and solve common challenges. 3. Study the official documentation The React documentation is comprehensive and offers a wealth of information. Read through the official documentation to gain a deep understanding of React's features, APIs, and best practices. The documentation is regularly updated, so keep an eye on new releases and changes. 4. Explore community resources The React community is vibrant, and there are numerous online resources available to learn from. Explore popular blogs, tutorials, YouTube channels, and forums to expand your knowledge. Some reputable sources include the React official website, React documentation, React blogs, and popular tutorial platforms like Udemy and Pluralsight. 5. Learn related tools and libraries React has a vast ecosystem of tools and libraries that enhance its capabilities. Familiarize yourself with popular tools like Redux (for state management), React Router (for routing), and Axios (for making HTTP requests). Understanding how these tools integrate with React will make you a more versatile developer. 6. Practice with code challenges Engage in coding challenges and exercises specific to React. Websites like Codecademy, LeetCode, and HackerRank offer React-focused coding problems that can sharpen your problem-solving skills and reinforce your understanding of React concepts. 7. Contribute to open source projects Contributing to open source projects not only helps you hone your skills but also exposes you to collaborative development environments. Start by finding beginner-friendly React projects on platforms like GitHub and make small contributions. This experience will provide you with practical exposure to real-world React codebases and teach you how to work with other developers. 8. Attend workshops and conferences Keep an eye out for React workshops and conferences happening both online and offline. These events are excellent opportunities to learn from experts, network with like-minded individuals, and stay up to date with the latest trends and advancements in React development. 9. Stay updated React is an evolving technology, and staying updated with the latest features and updates is essential. Follow React-related blogs, subscribe to newsletters, and join relevant communities or forums to stay informed about new releases, best practices, and emerging patterns. Remember that expertise comes with experience, so be patient and persistent in your learning journey. Dedicate regular time to practice, experiment, and build projects to reinforce your skills. Happy coding! --- ## What happened to my .me domain URL: https://yoosuf.me/blog/what-happened-to-my-dot-me-domain/ Published: 13 Aug, 2019 Topics: Personal, Domain Names, .me Domain, Web Hosting The story of what happened to Yoosuf Mohamed's .me domain and the lessons learned about domain management. Recently I lost access to my .me domain because of a card issue, and it seems GoDaddy may have seen an opportunity to auction it off and make some extra money. My old .me domain was seven years old and very personal to me. Like many others, I used it to host my personal blog and portfolio. I suspect GoDaddy is trying to squeeze quick money out of aging domains, though I'm not sure exactly what they'll do with mine now :) --- ## Trigger and Shoot – Slim WordPress plugin URL: https://yoosuf.me/blog/trigger-shoot-slim-wordpress-plugin/ Published: 26 Mar, 2016 Topics: Engineering, WordPress, WordPress Plugin, PHP Trigger and Shoot: a slim, lightweight WordPress plugin for triggering actions on post events. Whenever a WordPress post is created, I wanted to send that information to a separate service running on a different server. I tried several approaches and landed on this simple solution. Try it out and play around with it, and if you run into any issues, please let me know. --- ## WordPress: How to trigger the Post publish event URL: https://yoosuf.me/blog/wordpress-how-to-trigger-the-post-publish-event/ Published: 28 Feb, 2016 Topics: Engineering, WordPress, WordPress Hooks, PHP, Automation How to hook into and trigger the WordPress post-publish event for custom automation. These days I'm working on a news publishing app, focused on its back-end API. Now that we're in the final stage of development, we've started adding push notifications, so I needed a way to trigger an event the moment a WordPress post is published, in order to send push notifications to Android and iOS devices. While experimenting with WordPress functions, I came up with this solution, which is handy, and I believe it'll be useful for anyone wanting to build a similar feature. By the way, if you know a better way to handle this, please let me know. Improvements are always welcome for apps dealing with millions of users, right? {/ The gist used to be embedded with a third-party that replaced itself at runtime with an unstyled 718px table, which pushed the page into a horizontal scroll on small screens. The code lives here instead: nothing to execute, nothing to overflow, and the snippet is now part of the post's own text rather than an opaque injected widget. /} / • Add this function somewhere in functions.php • You will see there is nothing is magic in WordPress / function onallstatustransitions( $newstatus, $oldstatus, $post ) { if ('publish' == $newstatus && 'publish' != $oldstatus && 'trash' != $oldstatus) { // Your code comes here } } addaction( 'transitionpoststatus', 'onallstatustransitions', 10, 3 ); The gist is still at https://gist.github.com/yoosuf/3890f3789754afe65525 . --- ## Now I am on Github as Yoosuf URL: https://yoosuf.me/blog/now-i-am-on-github-as-yoosuf/ Published: 26 Feb, 2016 Topics: Personal, GitHub, Open Source, Developer Life A quick note on joining GitHub under the username Yoosuf and starting to share code publicly. Well, it's 2016 now, and I wanted my own name to be mine across the internet, so yesterday I decided to claim Yoosuf on GitHub. It really wasn't a hard process at all — GitHub made the migration so simple that it redirects my old projects to the new URL without me having to worry about setting up redirects myself. I also recently tried to claim the same username on Twitter, but it looks like someone inactive registered it back in 2009 and has never posted a single tweet since, which makes no sense to me at all. I really wish social media platforms would release accounts like that back to new users. --- ## Work – Beaufort House Chelsea – Redesign URL: https://yoosuf.me/blog/beaufort-house-chelsea-redesign/ Published: 12 Feb, 2016 Topics: Personal, Web Design, Redesign, Case Study Case study: redesigning the Beaufort House Chelsea restaurant site on WordPress, and the performance work that made the new build 10% faster than the old one. Recently I had the chance to work on a UK-based restaurant website. The project came to me via AVM, London, a London-based content development agency. The Beaufort House Chelsea website was built on WordPress, with full-size background videos and images. To make it possible, I used the WordPress ACF plugin for custom fields, WP Simple 301 Redirect to manage redirects, and the WordPress SEO plugin by Yoast. Performance was the word most discussed from the very first stage of the project, and every design and development decision was made with it in mind. When I benchmarked the new Beaufort House Chelsea website against the old one, performance had improved by 10%. Want to redesign your website and boost its speed? Send me a message and I'll give you a free quote. !Website after the makeover Here's what the website looked like before the makeover. !Beaufort House Chelsea website before the makeover --- ## HTTP2 for front-end web developers URL: https://yoosuf.me/blog/http2-for-front-end-web-developers/ Published: 12 Mar, 2015 Topics: Engineering, HTTP/2, Web Performance, Frontend, Networking What front-end web developers need to know about HTTP/2 and the performance gains it brings. HTTP/2 means a real change in how we should build websites — many of the best practices from the HTTP/1 world actually work against you in an HTTP/2 world. Read more --- ## Good Product Manager, Bad Product Manager URL: https://yoosuf.me/blog/good-product-manager-bad-product-manager/ Published: 22 Jul, 2014 Topics: Personal, Product Management, Leadership, Career Notes and insights on what separates a good product manager from a bad one. Good product managers know the market, the product, the product line and the competition extremely well and operate from a strong basis of knowledge and confidence. A good product manager is the CEO of the product. Good product managers take full responsibility and measure themselves in terms of the success of the product. They are responsible for right product/right time and all that entails. A good product manager knows the context going in (the company, our revenue funding, competition, etc.), and they take responsibility for devising and executing a winning plan (no excuses). Bad product managers have lots of excuses. Not enough funding, the engineering manager is an idiot, Microsoft has 10 times as many engineers working on it, I'm overworked, I don't get enough direction. [Netscape CEO] Barksdale doesn't make these kinds of excuses and neither should the CEO of a product. Good product managers don't get all of their time sucked up by the various organizations that must work together to deliver right product right time. They don't take all the product team minutes, they don't project manage the various functions; they are not gophers for engineering. They are not part of the product team; they manage the product team. Engineering teams don't consider Good Product Managers a "marketing resource." Good product managers are the marketing counterparts of the engineering manager. Good product managers crisply define the target, the “what” (as opposed to the “how”) and manage the delivery of the “what.” Bad product managers feel best about themselves when they figure out “how”. Good product managers communicate crisply to engineering in writing as well as verbally. Good product managers don't give direction informally. Good product managers gather information informally. Good product managers create collateral, FAQs, presentations, and white papers that can be leveraged. Bad product managers complain that they spend all day answering questions for the sales force and are swamped. Good product managers anticipate the serious product flaws and build real solutions. Bad product managers put out fires all day. Good product managers take written positions on important issues (competitive silver bullets, tough architectural choices, tough product decisions, markets to attack or yield). Bad product managers voice their opinion verbally and lament that the “powers that be” won't let it happen. Once bad product managers fail, they point out that they predicted they would fail. Good product managers focus the team on revenue and customers. Bad product managers focus team on how many features Microsoft is building. Good product managers define good products that can be executed with a strong effort. Bad product managers define good products that can't be executed or let engineering build whatever they want (i.e. solve the hardest problem). Good product managers think in terms of delivering superior value to the market place during inbound planning and achieving market share and revenue goals during outbound. Bad product managers get very confused about the differences amongst delivering value, matching competitive features, pricing, and ubiquity. Good product managers decompose problems. Bad product managers combine all problems into one. Good product managers think about the story they want written by the press. Bad product managers think about covering every feature and being really technically accurate with the press. Good product managers ask the press questions. Bad product managers answer any press question. Good product managers assume press and analyst people are really smart. Bad product managers assume that press and analysts are dumb because they don't understand the difference between "push" and "simulated push." Good product managers err on the side of clarity vs. explaining the obvious. Bad product managers never explain the obvious. Good product managers define their job and their success. Bad product managers constantly want to be told what to do. Good product managers send their status reports in on time every week, because they are disciplined. Bad product managers forget to send in their status reports on time, because they don't value discipline. Ben Horowitz Director of Product Management Summer 1996 --- ## Component-based Architectures in Ruby and Rails URL: https://yoosuf.me/blog/component-based-architectures-ruby-rails/ Published: 20 May, 2014 Topics: Engineering, Ruby on Rails, Component Architecture, Software Architecture, Ruby An overview of component-based architecture patterns for building maintainable Ruby on Rails applications. Stephan Hagemann's excellent talk from last year's MountainWest RubyConf on component-based architectures in Ruby and Rails. --- ## Photography, My second passion URL: https://yoosuf.me/blog/photography-my-second-passion/ Published: 29 Oct, 2013 Topics: Photography, Photography, Personal Passion, Sri Lanka Yoosuf Mohamed on photography as a second passion alongside software engineering, with sample shots. For about a year now, I've been playing around with a semi-professional DSLR (a Nikon D7000), exploring photography and learning new ways to capture photographs each day. Photography has genuinely become my second passion, alongside my work. Every time I learn something new and get a great shot, I really enjoy it — and the positive feedback from friends and family makes it even better. Below are some of my favorite shots. A full photography portfolio is coming to this website soon. Let me know your thoughts. You can also check out my previous iPhone photography post. The Buddha — captured during Vesak (Buddha's Birthday) 2013. View on 500px The Bar, Kingsbury Hotel — Sri Lanka. View on 500px Loneliness is killing everyone — a glass of water stays alone. A man mixing sugar for coffee — Java Lounge, Sri Lanka. Selection of fruits — Eden Resorts and Spa, Sri Lanka. Poolside view — Eden Resorts and Spa, Sri Lanka. View on 500px A rail bridge — connects the southern and western province, Sri Lanka. View on 500px The full moon captured through a branch. The lamp — captured during a wedding. The bridge — connects the western and southern province, Sri Lanka. Chickpeas — Sri Lanka. --- ## Just do it URL: https://yoosuf.me/blog/just-do-it/ Published: 26 Oct, 2013 Topics: Personal, Motivation, Productivity, Personal Growth A short, personal take on motivation, procrastination, and the value of just taking action. Too often we are scared. Scared of what we might not be able to do. Scared of what people might think if we tried. We let our fears stand in the way of our hopes. We say no when we want to say yes. We sit quietly when we want to scream. And we shout at others, when we should keep our mouths shut. Why? After all, we do only go around once. There’s really no time to be afraid. So stop. Try something you’ve never tried. Risk it. Enter a triathlon. Write a letter to the editor. Demand a raise. Call winners at the toughest court. Throw away your television. Bicycle across the United States. Try bobsledding. Try anything! Speak out against the designated hitter. Travel to a country where you don’t speak the language. Patent something. Call her. You have nothing to lose and everything everything everything to gain. Barry Sanders --- ## Respect people with less power than you URL: https://yoosuf.me/blog/respect-people-with-less-power-than-you/ Published: 3 Oct, 2013 Topics: Personal, Leadership, Respect, Personal Values Why treating people with less power than you with respect is a real test of character and leadership. Watch the talk An inspirational speech by Tim Minchin 1. You don't have to have a dream. 2. Don't seek happiness. 3. Remember, it's all luck. 4. Exercise. 5. Be hard on your opinions. 6. Be a teacher. 7. Define yourself by what you love. 8. Respect people with less power than you. 9. Don't rush. I found these to be amazing tips, and I believe they'll help you too. --- ## The Secret to Making Money by starting a small business URL: https://yoosuf.me/blog/the-secret-to-making-money-by-starting-a-small-business/ Published: 24 Sep, 2013 Topics: Personal, Entrepreneurship, Small Business, Money Thoughts on entrepreneurship and the realities of making money by starting a small business. Good stuff is worth sharing, and I found this video to be a great, informative watch on starting a business with a small amount of money. To start the right business, you need the right advisor to help you find your potential. --- ## Stand Against Racism URL: https://yoosuf.me/blog/stand-against-racism/ Published: 14 Aug, 2013 Updated: 25 Sep, 2026 Topics: Personal, Racism, Equality, Social Justice A personal stand against racism and a call for greater equality and respect for others. In recent years, Sri Lanka has been turning into another Burma. Deliberate acts of violence keep taking place against Christian and Muslim minorities, and after each attack, the majority community changes their Facebook profile pictures to "against racism" images and quotes. I honestly don't see the point. Each of us should reflect and root racism out of our own hearts — changing a profile picture won't change anything on its own. So instead, I want to share a pledge to stand against racism, adapted from Standagainstracism.org. Please read it with an open heart and try to apply it in your daily life. Rather than simply praying, I believe that if you and I actually live by this, we can help build a better humanity on this planet. Pledge against racism As an individual committed to social justice, I stand against racism and discrimination of any kind. I will commit to a lifetime of promoting peace, justice, freedom, and dignity for all people in my community and in the world. --- ## Menus/Navigation handlers in mobile URL: https://yoosuf.me/blog/menus-navigation-handlers-in-mobile/ Published: 22 Jun, 2013 Topics: Engineering, Mobile UX, Navigation Patterns, Mobile Web, UI Design A look at common menu and navigation patterns for mobile web and app interfaces. Navigation and menu handlers are an important element of mobile apps and mobile websites. Below is a small study I did on my iPhone, and the conclusion I came to: there's no standard for where the navigation handler sits across apps. Most websites follow the right-side menu handler, including Twitter Bootstrap. Starbucks Website's responsive navigation handler is in the right side. Microsoft's Website's responsive navigation handler is in the right side. But when it comes to apps, almost all of them follow the left-side menu handler. Path App's navigation handler is in the left. Google+ iPhone App's navigation handler is in the left. FourSquare iPhone App's navigation handler is in the left. Vine iPhone App's navigation handler is in the left. Nike+ iPhone App's navigation handler is in the left. Unfortunately, some apps still follow the right-hand side menu handler. Chrome iPhone App's navigation handler is in the right. Apple iOS's Music app's navigation handler is in the right. Nike+ iPhone App's navigation handler is in the left. As a right-handed phone user, I always prefer the menu handler on the right side, but there's clearly no consistency across apps. I'd like to see Apple let users customize the menu handler's position to suit their preference. For websites, it's a bit more complicated, though web designers can still make an informed decision based on UX testing, in-lab user testing, and anonymous data tracking with tools like Google Analytics. This becomes especially straightforward when designing for a well-defined target audience. --- ## Photography with iPhone 5 URL: https://yoosuf.me/blog/photography-with-iphone-5/ Published: 20 Jun, 2013 Topics: Photography, iPhone Photography, Mobile Photography, Photography Tips Tips and techniques for taking better photos with the iPhone 5's camera. After a long while, I finally got the chance to capture some great photos on the iPhone 5. I'm noticing a lot of improvements in the camera, and it's noticeably faster than earlier iPhone models. If you're not an iPhone user, I'd encourage you to give it a shot — at the very least, visit an Apple Store to try it out. Have a look at the photos below, leave a comment, and share the love on Facebook, Twitter, or wherever else you like to share. You can also check out my previous iPhone photography post. Galleface — a sunset view, photos by Yoosuf Mo. Galleface — a sunset view, photos by Yoosuf Mo. Galleface — a sunset view, photos by Yoosuf Mo. Sri Lanka flag, at Galleface, photos by Yoosuf Mo. A view of the twin towers and the Bank of Ceylon at Galleface, photos by Yoosuf Mohamed. Kids stuff at Galleface, photos by Yoosuf Mo. [^​1]: All photos above were captured with the iPhone 5. --- ## Performance Checklist for the Mobile Web URL: https://yoosuf.me/blog/performance-checklist-for-the-mobile-web/ Published: 6 May, 2013 Topics: Engineering, Mobile Web, Performance, Web Optimization, Frontend A practical checklist for improving performance on the mobile web, from asset loading to rendering. Colt McAnlis (@duhroach), a developer advocate at Google, makes the point that web performance is about more than how fast a page loads — it's also about the experience a user has while using your app. --- ## Going back to Sri Lanka URL: https://yoosuf.me/blog/going-back-to-sri-lanka/ Published: 27 Feb, 2013 Topics: Personal, Sri Lanka, Personal Life, Relocation Personal reflections on moving back home to Sri Lanka and what that transition meant. I came to the UK to do my MSc and build a better future. In the meantime, the immigration rules changed, bringing many new restrictions for international students. So, with a little disappointment, I'm moving back to Sri Lanka. Let's see what happens next. I'm also planning to set up a small team there, where I hope to put my experience and ability to work and keep pursuing my passion: making things. I'm hoping to return to the United Kingdom before too long. Wish me luck. --- ## Ruby 2 URL: https://yoosuf.me/blog/ruby-2/ Published: 24 Feb, 2013 Topics: Engineering, Ruby, Ruby 2.0, Programming Languages Early thoughts and reactions to the release of Ruby 2.0 and its new language features. Today, the long-awaited Ruby 2.0 was released, bringing a host of new features, compatibility improvements, better documentation, and stability fixes. Upgrade from Ruby 1.9.3 to Ruby 2 today. --- ## The Setup URL: https://yoosuf.me/blog/the-setup/ Published: 3 Feb, 2013 Updated: 25 Sep, 2026 Topics: Personal, Developer Setup, Tools, Workspace A rundown of the hardware, software, and tools Yoosuf Mohamed used for development at the time. Who am I and what I do? I'm Yoosuf Mo, yoosuf @aitchdei on X/Twitter and elsewhere, and I'm a web developer and general geek. I've done web development for companies both small and big, and I also design in a mostly black-and-white palette. These days I'm working on two personal side projects, alongside contracting and freelance work to pay the bills. What hardware do I use? I'm an Apple fanboy, although I used Windows for more than ten years before switching to Linux (Ubuntu) for a couple of years. Since 2012 I've been using a Mac for both work and personal use. I've had an iPhone 4 since 2011, and I still love it. What software do I use? I'm a big fan of trying out different software — take a look at my Applications folder and you'll see plenty of it. As a web developer, I use some of the well-known graphic editors, such as Adobe Photoshop, Adobe Fireworks, and Adobe Illustrator. For development, I mainly use Sublime Text 3 and TextMate, along with a few IDEs like Aptana Studio 3 and Eclipse, but Sublime Text 3 is where I spend most of my time. I've used pretty much every kind of version control system, but I prefer Git over SVN. I use Cyberduck (when working with more traditional clients) to upload and download files, and Source Tree as a GUI for Git. For entertainment, I use Spotify, QuickTime, and VLC Player, along with Vimeo and YouTube for watching videos. Every so often I use iTunes to back up and restore my iPhone. On my phone I use a lot of apps: Twitter, Facebook, Instagram, Camera Plus, Camera Plus Pro, and many more. I keep my to-do list in Things, which I love, and I use Spotify on the go — I think it's a genuinely innovative app. Programming I love Ruby on Rails, though I haven't yet worked with a client on a Rails project. I have worked on many PHP projects, but I haven't had the chance to dig into the latest PHP frameworks recently — something I should catch up on soon. Transport I'm a public transport user, relying mostly on the London Underground and occasionally the train. When I'm tired of walking, I take the London bus. --- ## CSS3: text-shadow URL: https://yoosuf.me/blog/css3-text-shadow/ Published: 2 Feb, 2013 Updated: 25 Sep, 2026 Topics: Engineering, CSS3, Text Shadow, Web Design, Frontend A practical look at the CSS3 text-shadow property and how to use it to add depth to web typography. I recently had the opportunity to work on a gaming startup's website, and they wanted a modern site packed with all the latest visual flourishes. I decided to lean heavily on CSS3 and jQuery interactions, and as part of that, I went searching for creative CSS3 text-shadow effects to make the site's typography cleaner and more readable. Below are some of the experiments I came across, published as fiddles. Play around with them and share your own. CSS3 LetterPress Open the CSS3 LetterPress fiddle CSS3: Cloud effect Open the CSS3 Cloud effect fiddle CSS3 Embossed Open the CSS3 Embossed fiddle CSS3: City light Open the CSS3 City light fiddle CSS3: Flame effect Open the CSS3 Flame effect fiddle CSS3 Retro effect Open the CSS3 Retro effect fiddle CSS3 Puffed effect Open the CSS3 Puffed effect fiddle --- ## A Better Way to Show WordPress Database Errors URL: https://yoosuf.me/blog/a-better-way-to-show-wordpress-database-errors/ Published: 28 Dec, 2012 Topics: Engineering, WordPress, Debugging, PHP, Database Errors A cleaner approach to displaying WordPress database errors for easier debugging during development. If you visit this website often, you may have noticed some downtime (due to issues with the Amazon Cloud servers). I got tired of seeing that ugly default error message, so I decided to override the WordPress database error message with something friendlier for users. To do this, I followed a blog post by Jeff Starr, and it worked like a charm. Better way to display WordPress Database Error Create a file named db-error.php in the wp-content directory and paste in the following code. Feel free to style it however you like. <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Terribly sorry :(</title> </head> <body> <div class="container"> <h1>Terribly sorry <span>:(</span></h1> <p>I am sorry, looks like the Amazon Cloud Servers aren't responding or are having some technical issues right now.</p> <p>Sometimes refreshing the browser may work, hit on the refresh button or press CMD + R / CTRL + F5</p> </div> <script type="text/javascript"> var gaq = gaq || []; gaq.push(['setAccount', 'YOUR GOOGLE ANALYTICS ID']); gaq.push(['setDomainName', 'YOUR DOMAIN']); gaq.push(['setAllowLinker', true]); gaq.push(['trackPageview']); gaq.push(['setCustomVar', 1, '500 Error', 'Error site Loading', 1]); (function() { var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); })(); </script> </body> </html> Feel free to share it anywhere you like. --- ## Yoosuf’s iPhone Photography URL: https://yoosuf.me/blog/yoosuf-iphone-photography/ Published: 23 Dec, 2012 Topics: Photography, iPhone Photography, Photo Collection, Mobile Photography A collection of iPhone photography by Yoosuf Mohamed, capturing everyday moments and scenes. Since becoming an iPhone user, I've been capturing photographs often, from random snapshots to a wide variety of scenes. The iPhone has been a great tool for photography, and here are some of the best shots I've taken. I hope you enjoy them. London Tower bridge — a view from London Bridge. View the British Airways crossing the sun on 500px View the rose on 500px — a rose, with a little bit of editing. I reckon you'll love it. View the sun on 500px — the sun, colour of nature. View the purple wild flower on 500px — I was trying to focus on the bee, but it flew away from the flower bud before I could. Still, I consider it a great shot. View the steam engine train on 500px — a steam engine train, photographed on my way to work. I've got plenty more, and I'm looking forward to sharing them in the future. --- ## Rails Apps: Rails Application Composer URL: https://yoosuf.me/blog/rails-apps-rails-application-composer/ Published: 22 Dec, 2012 Topics: Engineering, Ruby on Rails, Rails Application Composer, Scaffolding, Ruby A look at Rails Application Composer, a tool for scaffolding new Ruby on Rails applications faster. Recently I came across Ruby on Rails application templates (Rails Composer). If you want to quick-start application development, Rails Composer offers a wide range of options. So far I've only used it in my local development environment, but I'll consider using it more broadly in the future. --- ## Hello you! URL: https://yoosuf.me/blog/hello-you/ Published: 18 Dec, 2007 Topics: Personal, Personal Blog, Introduction, Blogging Yoosuf Mohamed's first blog post from 2007, marking the start of a personal blog after years on Geocities, Blogger, and WordPress. Thank you for visiting my blog. My first website lived on Geocities, then I moved to Blogger, and later to WordPress. Now I've decided it's time to build it on GitHub Pages instead. This space will hold updates on my life, my career, and my personal achievements. Stay in touch with me on Twitter, Facebook, and YouTube. ---