AI 资讯
I built a link shortener with FastAPI and htmx (no JS framework) — the parts that were actually hard
"A URL shortener" sounds like a weekend project. Slug in, long URL out, 302 , done. That's what I thought too. Then real usage showed up: links opened inside Instagram's in-app browser and didn't convert, bot traffic wrecked the analytics, and one link needed to send a US visitor somewhere different from an EU visitor. Suddenly the "trivial" part was 5% of the work. I built the whole thing on FastAPI + Redis + MySQL + htmx , deliberately with no frontend framework . This post is about the parts that turned out to be interesting — the redirect hot path, geo/device routing, and escaping in-app browsers — and why htmx was the right call for a one-person team. Disclosure: I build tapurl.io , a link shortener for marketers. This is a write-up of the engineering behind it, not a pitch — everything below is patterns you can apply to any shortener. The redirect is a hot path, so treat it like one Every other page in the app can be a bit slow. The redirect cannot. It sits in front of someone's click, and it runs on every click, so it has to be a tight, predictable read. The naive version hits your database for every redirect: @app.get ( " /{slug} " ) async def redirect ( slug : str ): link = await db . fetch_link ( slug ) # DB round-trip on every click if not link : raise HTTPException ( 404 ) return RedirectResponse ( link . destination , status_code = 302 ) That's fine until you have traffic. The slug-to-link lookup is a near-perfect cache candidate — a slug maps to the same link record every time. So the real path reads from Redis first and only falls back to MySQL on a miss: async def resolve ( slug : str ) -> Link | None : cached = await redis . get ( f " link: { slug } " ) if cached : return Link . parse_raw ( cached ) link = await db . fetch_link ( slug ) if link : await redis . set ( f " link: { slug } " , link . json (), ex = 3600 ) return link Two things worth saying out loud: Cache the lookup, not the decision. You cache the link record, but the actual destination
AI 资讯
Agentic AI: Good Upfront Design Pays You Back Later
I spend a lot of time preaching architecture and constraints, so it is always nice when a side project gives me receipts. Adding this new feature to DumbQuestion.ai was a good reminder that a well-structured first version lets you spend your next iteration on value, not repair. Below, you will find a few relatively simple challenges and how thoughtful, upfront design made the changes effortless. To vibe or not to vibe ... Many developers jump right in and just rip out an app, ship fast, let the coding agent sort it out, come back and deal with it later. To be fair, that absolutely can get you to first release faster. But even on a solo project, a little proper SDLC discipline pays back later when you want to extend the product without turning every feature into a rescue mission, which is a theme that already runs through how I have been building DumbQuestion.ai. Extend this to the enterprise and you turn a little upfront effort into potential huge savings on token spend Roasting starup pitches (for sport) ... The core idea for Startup Roast was simple enough: take a startup pitch, roast it, and add a reality-check section so the output is not just mockery for mockery’s sake. To illustrate (and avoid just vaguely describing the feature) I picked a random but highly upvoted pitch from Product Hunt: Vida . Vida, which pitches itself as an “AI clone” that learns how you work, remembers what matters, and becomes a “second you,” with early use cases like Reply Rescue, Prompt Rescue, Resume Rescue, Workspace Cleanup, and Daily Wrap. This is a pretty common target use case of agentic AI making it a solid candidate. If you want to skip ahead, here's an example roast for Vida. Combining a preliminary web "market search" into the content yielded a result that was not just sarcastic, but informed. The roast hit the obvious AI-clone positioning, questioned whether the product was really a clone versus a macro suite, and then turned the market context into a sharper Reality Check
AI 资讯
Htmx fragment caching with Accept-Version
IF YOU'VE been developing htmx apps for a while, you might have tried to cache the HTML fragments generated by your server as htmx responses. Caching htmx fragments is the equivalent of caching JSON responses in a SPA. Eg, you might have a fragment response from GET /users/:id that renders a user detail view. You might want to cache this view to avoid expensive queries in the backend if you know the user details haven't changed. But when you start caching htmx fragments, a problem pops up: the style doesn't match the rest of your app. You might be rapidly iterating on the app and making adjustments (small or big) to its CSS. You quickly start to notice that annoyingly frequently, your user fragments are not updating to the latest style. Sure, you can do a hard reload and force the fragment to have the latest style. But surely there must be an easier way? Content negotiation Enter the version headers: Accept-Version : a request header set by your frontend to instruct the backend what version of a resource it wants Version : a response header set by your backend to inform the frontend what version of the resource it is serving. Basically, the backend and frontend have to agree on the version, otherwise they automatically do a hard reload. You can think of this as a lightweight form of content negotiation. Here's a pseudo-code for a backend middleware that shows the rules: if Accept-Version header not in request then continue with request pipeline else if Accept-Version header value = the expected version then continue with request pipeline else if request method is GET then respond with 200 OK empty body and a response header HX-Redirect: request target else continue with request pipeline finally add response header Version: expected version end The meat of this middleware is the redirect if the expected and actual versions don't match. This ensures that the response htmx fragment style can't drift out of sync with the rest of the app. Now, let's look at some of the d
AI 资讯
Ditch Electron: Securing Local Socket Communications using Opaque Tokens
Part 3 of the ERTH Architecture Series: Preventing port-scanning attacks and local socket hijacking in multi-process desktop apps. In the second part of this series , we built a self-healing Watchdog daemon in Bun to monitor and resurrect our Python sidecar backend (Robyn). Now, our desktop app is extremely stable. But it is also extremely insecure. You might think: "This is a desktop app running entirely on 127.0.0.1 (localhost). People from the internet can't access it, so why do I need security?" This is a classic cognitive blind spot in desktop app development. In reality, your local loopback interface is shared globally by the operating system. Any script running in the user’s web browser (e.g., a malicious website they happen to visit) can aggressively scan local ports (from 10000 to 65535). Once it hits your Robyn sidecar's dynamic port, it can send unauthenticated POST requests to delete databases, read private files, or trigger system actions. To prevent this, we must build a Zero-Trust Shield using Opaque Tokens to lock down all communication between the frontend WebView and the Python sidecar. The Zero-Trust Security Model To block unauthorized local traffic, the frontend and backend must share a cryptographically secure, short-lived token. Any request lacking this token will be instantly rejected by Robyn with a 403 Forbidden response. Here is how the defense line functions: Let's implement this architecture step-by-step. Step 1: Generating the Ephemeral Token in Bun Rather than saving credentials to a local config file (which could be read by malware on the system), we generate a random UUIDv4 in Bun’s process memory at startup. This token exists only during the application's runtime. // src-app/frontend/src/bun/index.ts // Generate a cryptographically secure, one-time Opaque Token in memory const agentSecretToken = crypto . randomUUID (); Next, we inject this token into the child process's environment variables when we spawn the Python sidecar: // Spaw
AI 资讯
Ditch Electron: Spawning a Rust-Powered Python Sidecar from Bun
Part 1 of the ERTH Architecture Series: Launching local backends on Port 0, dynamic port negotiation, and establishing the dual-core desktop backbone. If you are building a modern desktop application, you are probably tired of the same old options. On one hand, you have Electron . It’s the industry standard, but it forces you to bundle a full Chromium browser and a Node.js runtime with every app. Even a simple "Hello World" takes up 200MB+ of disk space and eats hundreds of megabytes of RAM. For background utility utilities or AI assistants that need to be nimble, this is a massive tax. On the other hand, you have Tauri . It solves the bundle size issue by binding to OS-native WebViews and using Rust for the backend. But unless you are already a Rust expert, you will find yourself fighting the compiler's borrow checker and async lifecycles, slowing down your development velocity. But what if you want to use Python for its rich AI ecosystem (Ollama, SQLModel, PyTorch), but still keep the UI lightweight, fast-loading, and responsive? Welcome to the ERTH Stack ( E lectroBun + R obyn + T urso + H TMX). In this first post of our 5-part series, we will break down how to launch a high-performance Python sidecar backend directly from a Bun-based desktop shell, bypassing the bloated Electron environment entirely. The Concept: Heterogeneous Dual-Core In the ERTH architecture, the desktop app is split into two physical processes: The Main Process (Bun) : Responsible for native OS window management, IPC (Inter-Process Communication), and hosting the HTML/CSS view. We use ElectroBun —a next-generation, ultra-lightweight wrapper that binds directly to the OS-native WebKit engine (no Chromium bloat!). The Sidecar Process (Python/Robyn) : Responsible for heavy computations, database access, and local LLM orchestration. We use Robyn , an incredibly fast, Rust-based async Python web framework. Here is how the lifecycle and process boundaries interact: Step 1: Spawning the Robyn Sidec
AI 资讯
Why I Abandoned Electron & SPAs to Build a 128MB Local-First Desktop AI Agent
How the ERTH Architecture (ElectroBun + Robyn + Turso + HTMX) breaks the obesity of modern desktop app development. If you’ve tried to build a cross-platform desktop application recently, you’ve likely faced the classic developer’s dilemma: Electron makes developer velocity fast, but at the cost of dragging a bloated Chromium kernel and Node.js runtime into every build. A simple "Hello World" easily eats up 200MB+ of disk space and hundreds of megabytes of RAM. Tauri solves the footprint issue by using OS-native WebViews and Rust, but forces you onto Rust’s steep learning curve, sacrificing the agility of rapid prototyping. The Python Distribution Hell : With the rise of local LLMs and Edge AI, Python is the de facto language for AI orchestration. Yet, packaging Python, its heavy dependencies, and databases into a double-click-to-run package for non-technical users remains a nightmare. Faced with these shackles, I decided to take a step back and rewrite the physical laws of desktop development. Today, I’m introducing the ERTH Stack ( E lectroBun + R obyn + T urso + H TMX)—a heterogeneous, local-first, zero-JS desktop application architecture designed for independent full-stack creators. And yes, the entire bundle—including a browser shell, a high-performance Python sidecar, a local database, and local AI agent execution—packages into a single 128MB standalone binary. Our open-source implementation is live on GitHub: 👉 GitHub Repository: bnpysse/erth_assistant The Core Pillars of the ERTH Architecture To achieve a minimalist footprint without sacrificing developer velocity, we structured the architecture into four core layers: ERTH ARCHITECTURE [E]lectroBun (UI Shell) ───[HTMX]───► [H]TMX (Zero-JS Frontend) │ ▲ (IPC) (HTTP) ▼ │ [R]obyn (Python Sidecar) ───────────► [T]urso / libSQL (Local-First DB) 1. [E]lectroBun: The Lightweight Shell Instead of Electron’s heavy Chromium, ElectroBun binds directly to the OS-native WebKit engine (Cocoa WebKit on macOS, WebView2 on W
AI 资讯
# Next MDL Update: Security-First Adapters and HTMX Support
In the last update, I introduced MDL as an HTML-first language for building websites and apps with less noise. This update is about the next layer: adapters . The goal is simple: MDL source should describe intent. Adapters decide how that intent becomes deployable HTML. Why adapters? I don’t want MDL to become locked into one frontend framework. Instead, the same MDL structure should be able to target different deployment styles: static HTML MDL-native runtime attributes HTMX future template or framework adapters So this: form@api(post /api/login)@result(loginResult)@swap(replace): .input@type(email)@required .btn-primary@type(submit)(Sign in) status@id(loginResult): Waiting. Can become plain HTML: html <form method= "post" action= "/api/login" > Or HTMX: html <form hx-post= "/api/login" hx-target= "#loginResult" hx-swap= "outerHTML" > Security before convenienceThe important part is that MDL does not pass behavior attributes through blindly. Raw HTMX attributes like this are blocked: mdl form@hx-post(/api/login): Instead, MDL uses intent-based attributes: mdl form@api(post /api/login): Then the adapter validates and translates it. Current safety rules include: external api(...) URLs are rejected raw @hx-* attributes are blocked raw browser events like onclick(...) are blocked unsafe URL schemes like javascript: are blocked broad form inclusion like @params( ) is blocked @inherit( ) is blocked @disinherit(*) is allowed because disabling inherited behavior is safer That means MDL can support HTMX without making MDL source depend directly on HTMX’s full raw surface area. New HTMX adapter attributesThe HTMX v2 adapter now supports more behavior attributes: mdl @select-oob(...) @swap-oob(...) @disabled(...) @disinherit(...) @encoding(...) @history-elt(...) @inherit(...) @params(...) @preserve(...) @prompt(...) @replace(...) @request(...) @sync(...) @validate(...) Example: mdl form@api(post /api/profile)@result(profileResult)@swap(replace)@params(email csrfToken)@disable