Stop managing RBAC and feature flags separately in React

Access control gets messy faster than most people expect.

Most apps handle:

  • RBAC (roles & permissions)
  • Feature flags
  • Experiments
  • Plan-based access

…as separate systems.

That usually leads to:

  • duplicated logic across frontend and backend
  • inconsistent behavior over time
  • harder scaling as the product grows

The problem

At small scale, this works fine.

But as your app grows:

  • permissions live in one place
  • feature flags in another
  • experiments somewhere else

👉 Now your logic is fragmented.

You end up asking:

  • “Is this user allowed?”
  • “Is this feature enabled?”
  • “Is this experiment active?”

…in multiple places, with different rules.

A better approach

Instead of managing all of this separately, unify everything into a single access layer.

👉 Define access once
👉 Use it everywhere

Example

const canEdit = access.can("edit_post", user)

if (canEdit) {
  return <EditButton />
}

Same logic:

  • frontend
  • backend
  • APIs

What I’ve been building

I’ve been working on a small library called React Access Engine to solve this.

It combines:

  • RBAC
  • ABAC
  • Feature flags
  • A/B experiments
  • Plan-based access
  • Remote config

👉 Into one consistent system.

Why this matters

  • No duplicated logic
  • Consistent behavior across layers
  • Easier to scale
  • Cleaner mental model

Curious how others are solving this

Would love to hear how you’re handling access control in production apps — especially at scale.

Links

GitHub: https://github.com/abhishekayu/react-access-engine

How I built a disposable email service with Haraka and Astro

I wanted a throwaway email service that wasn’t slow, wasn’t covered in ads, and worked in my language. Nothing I found checked all three boxes. So I built one.

The stack is Haraka for SMTP, Redis for real-time message delivery, SQLite for storage, and Astro for the frontend deployed on Cloudflare Pages. The whole thing runs on a single VPS plus Cloudflare’s edge network.

Why Haraka

Most guides for “build your own email server” point you at Postfix or something similar. I went with Haraka because it’s Node.js, which meant I could write plugins in a language I already knew instead of learning Postfix configuration files, which feel like they were designed to punish you.

Haraka handles inbound SMTP. When an email arrives, a custom plugin checks if the recipient address exists in the active session pool. If it does, the message gets parsed and pushed to Redis pub/sub. If it doesn’t, the email gets rejected.

The plugin is about 80 lines. It extracts the sender, subject, HTML body, and attachments, then publishes a JSON payload to a Redis channel keyed by the recipient address.

Real-time delivery with Redis

The frontend opens a Server-Sent Events connection when you load the page. On the backend, there’s a listener subscribed to the Redis channel matching your temporary address. When an email hits Haraka and gets published to Redis, the SSE connection pushes it to the browser immediately.

I tried WebSockets first but SSE was simpler for a one-directional flow. You don’t need to send messages back to the server, you just need to receive emails. SSE reconnects automatically on connection drops, which is one less thing to handle.

SQLite for storage

Emails get stored in SQLite with a TTL. A background job runs every minute and deletes anything past its expiration. I considered Postgres but SQLite was enough for this scale and means one less service to manage on the VPS.

The schema is minimal: id, recipient, sender, subject, html_body, text_body, attachments (JSON), created_at, expires_at.

Astro on Cloudflare Pages

The frontend is Astro with islands of React for the interactive parts (inbox, email viewer, timer). Static pages (blog, about, privacy, terms, contact) are fully static HTML generated at build time, which is great for SEO.

I went with Cloudflare Pages because the deploy is just a git push. No Docker, no CI pipeline to maintain. The site builds in under 3 seconds for 400+ pages.

20 languages without a translation team

The site works in 20 languages. I used i18n JSON files with a structure like translations/en.json, translations/pt-br.json, etc. Each file has the same keys, different values. The routing is /[lang]/page, so /pt-br/blog gives you the Portuguese blog.

Blog posts are in Astro content collections, one markdown file per language per post. A single post exists as 20 files: en/what-is-temp-mail.md, pt-br/what-is-temp-mail.md, etc. It’s a lot of files but the build handles it fine.

For the actual translations, I used AI to generate first drafts and then had native speakers review the top 5 languages by traffic. The long tail languages get fewer eyes but having them there at all helps with international SEO.

What I’d do differently

Redis pub/sub works but doesn’t persist messages if no one is listening. If the user opens the page after the email already arrived, they won’t see it unless it was also written to SQLite. I handle this by querying SQLite on page load and then switching to SSE for real-time updates. It works, but it means every email gets written twice (SQLite + Redis). In hindsight, I might use Redis Streams instead of pub/sub to get persistence and real-time in one place.

The site is trashbox.email if you want to try it. Source isn’t open but I’m happy to answer questions about the architecture.

Running a temp mail service in 20 languages without a translation team

I run temp-mail.you, a disposable email service. It works in 20 languages. I don’t have a translation team. It’s messier than you’d think.

The i18n setup

Every string the user sees lives in a JSON file. There’s one per language: translations/en.json, translations/pt-br.json, translations/ja.json, and so on. Same keys, different values. The app reads the right file based on the URL prefix.

Routes look like /pt-br/blog, /ja/contact, /de/about. English is the default at the root. Astro handles this with dynamic [lang] route parameters so I didn’t have to create separate page files for each language. One template, 20 outputs.

Blog posts are the hard part

The UI strings were straightforward. Blog content was not. I have 14 blog posts, each in 20 languages. That’s 280 markdown files. Each one is a separate file organized by language folder.

The English versions are the source of truth. I wrote those myself. For the other 19 languages, I generated first drafts with AI and then had the top 5 languages reviewed by native speakers. Portuguese, Spanish, German, French, and Japanese got human eyes on them. The rest I spot-checked but they’re mostly AI output with light editing.

Is that ideal? No. But having a Japanese version of “What Is Temp Mail?” that’s 90% accurate is better than not having one at all, especially for SEO. Google serves the right language version to the right users and I get traffic from countries I’d never reach with English only.

SEO gains from going multilingual

Before adding languages, the site had about 20 indexable pages. After: 400+. Each language version has its own URL, its own meta tags, its own hreflang tags pointing to every other version. Google treats each one as a separate page.

The keywords I’m targeting in English have massive competition. “Temp mail” in English is brutal. But “correo temporal” in Spanish? Way less competition. Some of these keywords have thousands of monthly searches and almost no one is targeting them with actual content in that language.

I also added native content for PT-BR and Spanish, not just translations. A guide written specifically for Brazilian users searching “gerador de email temporario” performs better than a translated version of an English guide because the search intent and examples are different.

The annoying parts

Right-to-left languages (Arabic) broke my layout in ways I didn’t expect. CSS logical properties helped but I still had to add specific RTL overrides for the email viewer and sidebar.

CJK languages (Chinese, Japanese, Korean) have different word length distributions which messed up my title tag optimization. A title that fits in 60 characters in English might be 20 characters in Japanese. I had to add per-language logic for meta title generation.

Some languages inflate the UI. German words are notoriously long. Buttons that look fine in English overflow in German. I ended up setting min-width on interactive elements and letting text wrap where needed.

The numbers after 3 months

The site gets traffic from 40+ countries now. Before the i18n push, it was 90% US/UK. The build still takes under 3 seconds for all 400 pages because Astro is fast and the pages are mostly static.

If you’re building a web tool and your only barrier to international traffic is translation, it’s worth doing even imperfectly. Ship 20 languages at 85% quality and fix the important ones over time.

temp-mail.you

Build a Smart Building Automation Controller Using NORVI X

Introduction

Modern buildings are packed with systems — HVAC, lighting, access control, and energy metering — but most of them run on separate proprietary controllers that don’t talk to each other. The result is a building that’s instrumented but not truly intelligent.

In this Instructable, you’ll learn how to build a flexible, open-source smart building automation controller using the NORVI X platform. It’s built on the ESP32-S3 microcontroller, supports industrial I/O, speaks Modbus RTU natively, and connects to the cloud over WiFi, Ethernet, or 4G — all at a fraction of the cost of traditional BMS hardware.

This guide covers a practical HVAC zone control system with remote monitoring, but the same hardware can handle lighting automation, energy metering, and access control.

Step 1: Understand the Problem With Traditional Building Automation

Before building anything, it’s worth knowing why this matters. Traditional Building Management Systems (BMS) come with some frustrating limitations:

  • Proprietary controllers that need vendor-certified technicians to configure
  • No interoperability between subsystems — each zone or floor operates in a silo
  • Little to no native cloud/API connectivity
  • High licensing costs that price out small-to-medium commercial buildings
  • Long lead times and expensive replacement cycles when hardware reaches end-of-life

The goal of this project is to replace that complexity with a single open platform that any engineer can program, configure, and scale.

Step 2: Gather Your Components

Hardware (per zone controller)

  • 1x NORVI CPU-ESPS3-X1 (ESP32-S3 with WiFi, Ethernet, RS-485)
  • 1x NORVI X-AI4 (4x analog inputs, 4–20 mA, 16-bit ADC)
  • 1x NORVI X-R4 (4x relay outputs)
  • 1x NORVI X-DI4 (fast digital inputs)

Sensors & Actuators

  • Temperature & humidity sensor (4–20 mA output)
  • CO2 sensor (for demand-controlled ventilation)
  • Fan coil unit (controlled via relay)
  • Door/window contact sensors

Tools & Software

  • Arduino IDE or ESP-IDF
  • MQTT broker (local or cloud — e.g., HiveMQ, Mosquitto, AWS IoT)
  • Optional: Node-RED or Grafana for dashboarding
    **
    Estimated cost per zone controller: ~$135–145 USD**

Step 3: Understand the System Architecture

Here’s how the layers of the system fit together:

Layer Component NORVI X Module
Sensing Temp, Humidity, CO2 sensors X-AI4 (4–20 mA)
Output Control HVAC relays, Fan Coil X-R4 relay module
Contact Monitoring Door/Window Sensors X-DI4 digital inputs
Communication Modbus RTU to BMS Built-in RS-485
Connectivity WiFi / Ethernet / 4G Built-in on CPU
Edge Logic Schedules, Alarms, Setpoints ESP32-S3 firmware
Module NORVI X Module Core processing and integration layer

Each CPU module connects to I/O expansion boards via a dedicated expansion bus. You can stack up to 200 I/O points across a single controller — no rewiring, no new CPU needed.

Step 4: Wire Up the Hardware

  1. Mount the NORVI CPU-ESPS3-X1 on a DIN rail in your control panel.
  2. Attach the X-AI4 expansion module — connect your 4–20 mA temperature, humidity, and CO2 sensors to the analog input terminals.
  3. Attach the X-R4 relay module — wire your fan coil unit switching circuits to the relay output terminals.
  4. Attach the X-DI4 — connect door/window contact dry contacts to the digital input terminals.
  5. Wire RS-485 A/B terminals to your upstream BMS or SCADA system (Modbus RTU).
  6. Connect Ethernet or configure WiFi for cloud/dashboard access.

Tip: For remote plant rooms or retrofit sites with no wired network, use the NORVI CPU-ESPS3-X2 or X3 variant, which adds 2G/4G cellular connectivity. No local network infrastructure needed.

Step 5: Program the Controller

All logic runs locally on the ESP32-S3. Use the Arduino framework or ESP-IDF — no proprietary IDE or vendor cloud dependency required. A basic control loop looks like this:

#include <ModbusMaster.h>
#include <PubSubClient.h>

float readTemperature() {
  int raw = analogRead(AI4_CH0_PIN);
  return map(raw, 0, 65535, 0, 100); // 4-20mA to temperature
}

void controlFanCoil(float temp, float setpoint) {
  if (temp > setpoint + 0.5) {
    digitalWrite(RELAY_CH1_PIN, HIGH); // Fan coil ON
  } else if (temp < setpoint - 0.5) {
    digitalWrite(RELAY_CH1_PIN, LOW);  // Fan coil OFF
  }
}

void loop() {
  float temperature = readTemperature();
  controlFanCoil(temperature, 22.5); // 22.5°C setpoint
  publishToMQTT("building/zone1/temp", temperature);
  delay(5000);
}

Define your schedules, setpoints, alarm thresholds, and failsafe behaviour in firmware. Optionally expose configuration parameters over MQTT or a lightweight REST API.

Step 6: Set Up Cloud / Remote Monitoring

One NORVI X CPU isn’t limited to HVAC. Add more expansion modules to cover:

  • Lighting automation — X-Q4/X-Q8 transistor outputs for 0–10 V dimming
  • Occupancy sensing — X-DI8/X-DI16 for PIR sensors and pushbutton overrides
  • Daylight harvesting — X-AV4 analog inputs for photocell sensors
  • Energy metering — RS-485 Modbus RTU to commercial energy meters

A single NORVI X controller can simultaneously manage HVAC zone control, occupancy-based lighting, and access logging — eliminating the need for separate controllers per subsystem.

For a 12-zone office building across three floors, deploy one NORVI X unit per zone and connect them all to a central data concentrator or SCADA system over Modbus RTU.

Step 8: Test and Deploy

Before deploying to site, run through this checklist:

  • Verify all analog input readings match expected sensor output ranges
  • Test relay outputs — confirm fan coil switching responds to setpoint logic
  • Verify Modbus RTU communication with your upstream BMS
  • Confirm MQTT telemetry is flowing to your dashboard
  • Simulate a network outage — confirm local failsafe logic holds
  • Test OTA update path before final installation

NORVI X vs Traditional Smart Building Hardware

Here’s how NORVI X compares against the alternatives engineers typically Evaluate:

Capability Traditional BMS Dev Boards IoT Gateways NORVI X
Industrial I/O (DI/DO/AI/AO) ✔✔✔ Limited ✖ ✔✔✔
Open Programming (C/C++) ✖ ✔✔✔ ✖ ✔✔✔
RS-485 / Modbus RTU ✔✔✔ Manual wiring ✔✔ Native built-in
4G / LTE Cellular Option Requires add-on DIY ✔✔ ✔✔✔
Modular I/O Expansion Vendor-locked ✖ ✖ Up to 200 I/O
Price Point High Very Low Medium $80–$150 USD

Conclusion

With NORVI X, you get industrial-grade I/O, open ESP32 programming, native Modbus RTU, and cloud connectivity — all in a modular platform that scales from a single zone to hundreds of I/O points

Estimated hardware cost per zone controller: ~$135–145 USD — a fraction of a traditional BMS controller, with full programming flexibility retained.

This approach is ideal for system integrators, OEM machine builders, smart building startups, and retrofit projects where legacy BMS hardware needs to be replaced with something open and scalable.

Note: NORVI X is not suitable for applications requiring strict IEC 61131-3 Ladder Logic compliance or certified safety functions (SIL/PLd). Those applications require dedicated safety PLCs.

More Resources

Why Most AI Products Are Built Wrong (From a System Design Perspective)

Introduction

Most conversations around AI today focus on:

  • better models
  • better prompts
  • better outputs

But after working on AI systems more closely, I’ve started to see a different problem.

Most AI products are not limited by the model.
They’re limited by how the product is designed around the model.

This becomes obvious when you move from one-time usage to repeated interaction.

The Default AI Architecture

Most AI applications follow a simple pipeline:

User Input → LLM → Response → End

Sometimes extended with:

  • short-term chat history
  • prompt templates
  • basic memory

But fundamentally, it’s still:

a stateless, response-driven system

This works well for:

  • content generation
  • Q&A systems
  • automation tasks

But starts failing in long-term usage.

Where This Model Breaks

When users interact with AI repeatedly, the expectations change.

Instead of:

  • “give me an answer”

It becomes:

  • “continue this”
  • “remember this”
  • “adapt to me”

But the system isn’t designed for that.

So you get:

  • repeated context setup
  • inconsistent tone
  • fragmented conversations

This is not a model problem.

It’s an architecture problem.

The Core Design Flaw

Most AI systems treat AI as:

  • a feature
  • a tool
  • a request-response engine

Instead of:

a persistent interaction system

This leads to a mismatch:

Real Usage Current Design
Ongoing interaction One-shot responses
Context evolution Static prompts
Behavioral consistency Output variability

Rethinking AI as a System

To support real usage, the architecture needs to shift.

From:

Input → Output

To:

Interaction → Memory → Behavior → Next Interaction

This introduces three key layers.

1. Memory Layer

Not just storing chat history, but structuring:

  • user intent patterns
  • recurring context
  • relevant past interactions

This allows:

  • continuity
  • reduced repetition
  • better follow-ups

2. Personality / Constraint Layer

Raw LLM output is inherently variable.

To stabilize interaction, you need:

  • consistent tone
  • response constraints
  • behavioral guidelines

Think of it as:

LLM Output → Constraint Layer → Final Output

3. Interaction Layer

This is the most overlooked part.

The system should adapt based on:

  • conversation type
  • user intent
  • interaction depth

Example:

  • direct question → concise response
  • exploration → open-ended response
  • reflection → conversational tone

This creates a dynamic system instead of static responses.

Simplified Architecture

User Input
   ↓
Context Builder (recent + stored memory)
   ↓
LLM Processing
   ↓
Constraint / Personality Layer
   ↓
Interaction Adjustment Layer
   ↓
Final Response
   ↓
Memory Update

This loop repeats.

Why This Matters

When users return to an AI system, they evaluate:

  • consistency
  • usability over time
  • interaction quality

Not just:

  • correctness
  • speed

Which means:

Stateless systems feel disposable
Stateful systems feel usable

Where Aaradhya Fits In

This is the direction I’ve been exploring with Aaradhya on CloYou.

Instead of building around responses, the focus is on:

  • interaction loops
  • memory-backed continuity
  • consistent behavior

It’s still evolving, but the goal is simple:

Design AI systems people can return to, not just use once.

Final Thought

We’re currently optimizing AI for:

  • better answers
  • faster responses

But the next shift won’t come from that alone.

It will come from:

better system design

If you’re building with AI, try rethinking your architecture:

Are you designing for responses…
or for interaction over time?

And if you’re curious about how this looks in practice, you can explore it here:

👉 https://cloyou.com/

Tailwind CSS vs Bootstrap – Why Developers Are Ditching Bootstrap and Never Looking Back 🎨

The CSS Framework War Nobody Asked For (But Everyone Has Opinions About)

Let’s be real — picking a CSS framework feels like choosing your starter Pokémon. You commit, you invest time, and then someone on Twitter tells you that you made the wrong choice.

For years, Bootstrap was the framework. You wanted a navbar? Bootstrap. A modal? Bootstrap. A button that looked like it belonged on a government website from 2013? Still Bootstrap. It worked, and it worked reliably. Nobody got fired for using Bootstrap.

But then Tailwind CSS showed up, and the frontend world quietly started having a crisis.

Suddenly, developers were writing className="flex items-center justify-between px-6 py-3 bg-orange-500 text-white rounded-lg" and enjoying it. No more custom CSS files. No more fighting specificity wars. No more “why does this div have margin I didn’t ask for?!”

So the real question is — is Tailwind actually better than Bootstrap, or is it just new and shiny? Buckle up, because we’re about to break this down like a senior dev explaining it over chai at the office. ☕

What Are Tailwind CSS and Bootstrap, Anyway?

Before we start throwing punches, let’s make sure we’re on the same page.

Bootstrap is a component-based CSS framework. Think of it like IKEA furniture — you get pre-built pieces (buttons, cards, navbars, modals), and you just assemble them. It ships with ready-made components, a 12-column grid system, and a bunch of JavaScript utilities baked in. Super convenient, especially when you want something up and running fast.

Tailwind CSS, on the other hand, is a utility-first CSS framework. Instead of giving you pre-built components, it gives you tiny, single-purpose CSS classes — utilities — that you combine directly in your HTML/JSX to build whatever you need. It’s like getting a full LEGO set instead of a pre-assembled kit. More effort upfront, but the end result is entirely yours.

Here’s a quick analogy: Bootstrap is ordering a pre-made burger from a fast food chain. Tailwind is getting all the fresh ingredients and building your perfect custom burger. One is faster; the other is exactly what you wanted.

Why This Topic Matters for Developers in 2025

Here’s something the tutorials don’t always tell you: your choice of CSS framework affects way more than just how your UI looks.

It affects your bundle size, your design flexibility, your team’s velocity, your codebase maintainability, and even how easy it is to hire developers who can hit the ground running on your project.

Bootstrap and Tailwind take fundamentally different philosophies toward styling, and understanding the tradeoffs is something every frontend developer needs in their toolkit — whether you’re a freelancer building client sites, a solo developer working on a SaaS product, or part of a large engineering team shipping a design system.

In 2025, most modern React and Next.js projects default to Tailwind. But that doesn’t mean Bootstrap is dead. It means you need to know why one fits better in certain scenarios — and that’s exactly what this post covers.

Benefits of Tailwind CSS with Real-Life Examples

Here’s where Tailwind starts pulling ahead. Let’s go through the actual wins:

1. 🔥 No Context Switching Between Files

With Bootstrap, you’d write your HTML, then open a separate CSS file to override the styles, then come back to HTML… it’s a ping-pong match that gets exhausting fast. With Tailwind, your styles live right next to your markup. You write, you see, you move on.

2. 📦 Tiny Production Bundle (Thanks to PurgeCSS / JIT)

Bootstrap’s CSS is heavy out of the box — around 180–200KB minified. Tailwind, with its JIT (Just-In-Time) compiler, only includes the classes you actually use. Most production Tailwind builds end up under 10KB. That’s a massive performance win for your users on slower connections.

3. 🎨 Total Design Freedom Without a Design System Fight

Ever tried to make a Bootstrap button look different from a Bootstrap button? You either override styles (specificity nightmare), modify the SCSS variables (setup overhead), or give up and just use the default blue button for the 500th time.

With Tailwind, you design what you want from scratch. Every color, spacing, border-radius — you control it using utility classes. No fighting the framework’s opinions.

4. ⚡ Faster Prototyping Once You Know the Classes

There’s a learning curve, sure. But once you internalize the class naming pattern — and it is very logical — building UIs becomes almost as fast as writing plain English. p-4 means padding: 1rem. text-xl means a larger font size. It clicks, and then it really clicks.

5. 🧩 Works Beautifully with Component-Based Frameworks

Tailwind was basically born for React, Next.js, and Vue. Since you’re already thinking in components, scoping styles to a component using Tailwind utilities feels completely natural. No CSS Modules setup, no Styled Components overhead — just classes in your JSX.

6. 🛠️ Highly Customizable Config

Tailwind’s tailwind.config.js lets you define your design tokens — your brand colors, fonts, spacing scale, breakpoints — all in one place. Consistent design system baked right in. It’s as easy as unlocking your phone once you know the password.

Tailwind CSS vs Bootstrap: A Straight-Up Comparison

Let’s put these two side by side. No fluff, just facts.

Feature Tailwind CSS Bootstrap
Approach Utility-first Component-based
Bundle Size (prod) ~5–15KB (JIT) ~180–200KB
Design Flexibility Very High Moderate
Learning Curve Moderate Low
Pre-built Components No (use Headless UI, shadcn/ui) Yes (many built-in)
Customization Excellent via config Requires SCSS override
JS Dependency None (pure CSS) jQuery / Popper (v4 drops jQuery)
Best For Custom UI, modern apps Rapid prototyping, legacy projects
Community & Ecosystem Growing fast Massive, established
React / Next.js fit ⭐⭐⭐⭐⭐ ⭐⭐⭐

The Honest Verdict

Bootstrap wins on raw speed for building generic interfaces. If you’re spinning up an admin dashboard with no custom design requirements, Bootstrap gets you there without thinking twice.

Tailwind wins on virtually everything else — performance, flexibility, maintainability, and modern developer experience. It’s the reason almost every new React/Next.js project you’ll see in 2025 is Tailwind-first.

Best Tips for Using Tailwind CSS 🧠

Do’s ✅

  • Do set up your tailwind.config.js with your brand colors and fonts from day one. Doing it later is a refactor headache.
  • Do use the @apply directive sparingly for repeated utility combinations (like a button style). But don’t overuse it — that defeats the purpose.
  • Do use a Tailwind IntelliSense extension in VS Code. It’s a game changer — autocomplete for every class.
  • Do learn the responsive prefixes early: sm:, md:, lg:, xl:. Mobile-first styling becomes dead simple.
  • Do pair Tailwind with a headless component library like shadcn/ui or Radix UI if you need accessible, unstyled components to dress up.

Don’ts ❌

  • Don’t write a thousand classes inline and call it a day. Extract repeated UI patterns into reusable React components.
  • Don’t mix Tailwind utilities with global CSS haphazardly. It gets messy quickly and causes unexpected overrides.
  • Don’t skip the JIT mode setup — you want that tiny bundle in production.
  • Don’t expect Tailwind to do everything Bootstrap does. It’s a different tool. Embrace that.

Common Mistakes Developers Make When Switching to Tailwind

If you’re coming from Bootstrap, here’s what trips people up — and what to watch out for.

1. Trying to recreate Bootstrap components in Tailwind manually
This is the wrong mindset. Tailwind isn’t Bootstrap with a different syntax. It’s a different philosophy. Stop looking for the Bootstrap equivalent and start thinking in utility-first terms.

2. Not configuring the theme upfront
Many developers start using Tailwind with default colors and spacing, then later realize their brand colors aren’t in the config. Set it up before writing a single component.

3. Overloading JSX with utility classes and not extracting components
Yes, className="flex items-center gap-4 px-6 py-3 bg-[#FF6F49] text-white font-semibold rounded-xl hover:bg-orange-500 transition-all duration-200" is valid Tailwind. But if you write that on every button, you’ve just created maintenance debt. Put it in a <Button /> component.

4. Ignoring the Tailwind docs
The Tailwind documentation is genuinely one of the best in the industry. Every class is documented with examples and live previews. There’s no excuse for guessing.

5. Assuming Tailwind is only for React
You can absolutely use Tailwind with plain HTML, Vue, Astro, SvelteKit, or any other modern web tech. It’s not framework-locked.

Conclusion: Should You Make the Switch? 🚀

Here’s the truth: Tailwind CSS isn’t just better than Bootstrap in a few areas — it’s a fundamentally more modern approach to styling that aligns with how we build UIs today. Component-based architecture, design systems, performance-first development — Tailwind fits right in.

Bootstrap is not dead. It still has a place in projects that need quick, opinionated scaffolding without a design team. But for anyone building production-grade React or Next.js apps with custom designs, Tailwind is the clear choice in 2025.

So — have you already made the switch to Tailwind, or are you still holding onto your Bootstrap container divs? Drop your thoughts in the comments. 👇

If you found this useful, you’ll love the other deep-dives on hamidrazadev.com — from Next.js performance tips to real-world frontend patterns. Go explore, bookmark what you love, and share this post with a developer friend who’s still on the fence about Tailwind. They’ll thank you later. 😄

🦀 Rust Weekly Log — Weekly progress snapshot

📡 RustPulse project
Explored Postgres persistence using sqlx and documented the workflow for schema initialization, ingestion pipeline, and verification strategy.
https://vinecksie.super.site/rustpulse

🔐 Sealed in Rust Crypto book
Started the Identity Systems domain chapter, covering real-world password storage with Argon2 and how cryptographic primitives compose into practical authentication architectures.
https://vinecksie.github.io/sealed-in-rust/03-domains/03-00-identity-systems.html

▶️ Fearless in Rust
Published a new video on ChaCha20: keystream structure, nonce safety considerations, and implementation details in Rust.
https://youtu.be/UUQj-9SiKKg

rust #rustlang #backend #cryptography #security #postgres #learninginpublic

How I Manage 15+ Repos with Claude Code (Without Losing My Mind)

Most Claude Code users work in one repo at a time. It’s fine until your system spans multiple repos — then you’re copy-pasting context between sessions, manually tracking which PR depends on which, and babysitting agents that can’t see the full picture.

I manage 15+ repos across Go, Rust, TypeScript, Python, and C++. 10 specialized Claude Code agents coordinate through Telegram. Here’s what I tried first, why it didn’t work, and what does.

What Doesn’t Work

All of these approaches try to solve “how do I give one session access to multiple repos.” But that’s the wrong framing. When you need cross-repo context, what you actually need is cross-repo read and explore. The write should always be focused on a single repo, a single PR. ttal handles this by separating the two: ttal ask reads and explores anything, workers write to one repo at a time.

Monorepo. I use Moon for monorepo management — I’m not anti-monorepo. But when your stack spans Go, Rust, TypeScript, Python, and C++, a single repo doesn’t cut it. Each language has its own build tooling, CI pipelines, and dependency management. Cramming them together creates more problems than it solves. Even with Moon handling the orchestration, past 3-4 languages, splitting is cleaner. This isn’t a theoretical concern — it’s a real gap in the CC ecosystem that people are hitting.

Submodules. Even if you link repos together with submodules, you still don’t get what you actually need: cross-repo coordination, shared context, parallel execution. Submodules give you a way to pin repo versions together — they don’t give you a way to plan across repos, route tasks, or run parallel agents. And they don’t compose well with worktrees, which are essential for parallel agent work. It’s solving the wrong problem.

CC’s native cross-repo workflow. Want to explore another repo? You manually /add-dir, then that session starts reading in that directory. It works, but it’s manual and the session context pays the cost. In ttal, all exploration is handled by ttal ask — a lightweight bash-only agent that collects info from any source (--web, --repo, --project) and returns a detailed report without polluting your main session’s context. And because ttal ask runs on a simple bash-based agent loop, you can use any fast, cheap model (MiniMax M2.7 HighSpeed) for exploration — so Opus stays focused on the thinking work that actually needs it.

Single-session cross-repo orchestration. This is the trap most people fall into. You give one CC session access to all your repos and ask it to plan and implement a cross-repo feature. The context window fills up fast, the agent loses track of which repo it’s in, and the quality of both planning and execution suffers. Don’t try to make one session do orchestration, planning, and execution across repos. Let manager agents hold the big picture. Once the plan is done, let workers handle execution detail — one repo, one task, one worktree.

What Works: A Coordination Layer

The answer wasn’t a better monorepo or a smarter IDE. It was a thin coordination layer on top of Claude Code.

ttal is a single binary. Install it, define your projects in a TOML file, and you have a system that routes tasks to the right repo, the right agent, at the right time.

Two planes:

Worker plane — ephemeral CC sessions that plan, review, and implement. Each gets its own git worktree, sandboxed environment, and tmux session. Spin up, do the work, merge, clean up. No babysitting.

Manager plane — persistent agents that live across sessions. They hold the big picture — what features they designed with you, which tasks are done or blocked, what shipped yesterday. The manager never touches code. The worker never worries about the big picture.

Message bridge — the glue between everything. Human ↔ agent via Telegram. Agent ↔ agent via ttal send. Manager ↔ worker via ttal alert (workers notify their spawner automatically). CI status, PR reviews, task updates — all routed through the same daemon. You talk to your agents like coworkers in a chat app.

I wrote about the philosophy in ttal — More Than a Harness Engineering Framework, the tooling in We Replaced Every Tool Claude Code Ships With, and the memory model in How We Manage Memory and Sessions.

Daily Workflow

I open Telegram. 10 agent chats in a folder.

A typical morning:

  1. Tell Yuki (orchestrator) what I want to build today
  2. She breaks it into tasks, routes them to the right pipeline
  3. ttal go advances each task — spawning planners, reviewers, coders in parallel
  4. Workers run in isolated worktrees across whichever repos need changes
  5. PR reviews happen automatically — parallel sub-reviewers check security, tests, types, edge cases separately
  6. I review verdicts, approve, ttal go merges and cleans up

Cross-repo features just work. A change that touches ttal-cli, temenos, and organon gets three parallel workers, each in their own worktree, each with context about why the change exists.

Under the Hood

  • Unix philosophy — task management via Taskwarrior, knowledge via FlickNote, editing via tree-sitter. Compose dedicated tools, don’t bundle into a platform.
  • Sandbox auto-config — specialized roles mean known paths. The multi-repo project registry means sandbox config writes itself. No manual permission prompts.
  • Pipeline-driven — tag-based pipelines borrowed from event sourcing. One command (ttal go) drives every transition. Human gates where they matter.
  • Built-in quality control — parallel sub-reviewers focus on different aspects (security, tests, silent failures, type design). By the time a PR reaches you, it’s been through plan review, code review, and CI.
  • Session forking — brainstorm figures out what and why, then the session forks. Each fork inherits the full conversation and writes a plan scoped to its target repo. No summarization, no lossy handoff. Plan forks figure out the how, workers carry all of it into implementation.

The Numbers

Last week: 190+ tasks completed across all repos. In ttal, each task is a PR — planned, reviewed, implemented, merged. One person, 10 agents.

Throughput scales because coordination is automated. I don’t track which session is doing what. I track tasks.

Getting Started

brew tap tta-lab/ttal
brew install ttal
ttal doctor --fix

Define your projects:

# ~/.config/ttal/projects.toml
[backend]
name = "Backend API"
path = "/code/backend"

[frontend]
name = "Frontend App"
path = "/code/frontend"

[infra]
name = "Infrastructure"
path = "/code/infra"

Route a task:

ttal task add --project backend "add rate limiting middleware" --tag feature && ttal go

The pipeline figures out the rest.

Multi-repo at scale with Claude Code isn’t about getting CC to understand all your repos at once. It’s about a coordination layer that routes work to the right repo, the right agent, at the right time.

ttal, organon, and temenos are open source at github.com/tta-lab.

How to Visualize Multiple Overlapping Routes on a MapLibre GL Map

Screenshot showing four routes from Paris landmarks to Arc de Triomphe, each route visually separated

When building route planners, logistics dashboards, or public transport maps, you often need to display multiple routes that share the same roads. Without proper handling, these routes stack on top of each other and only the last-drawn route is visible. Users cannot compare routes or even confirm that all paths are displayed.

In a previous tutorial, we explored three approaches to solve this problem in Leaflet. MapLibre GL offers a simpler solution: the built-in line-offset paint property. No plugins, no geometry manipulation – just a single property that visually shifts each route by a specified number of pixels.

This tutorial shows how to use line-offset to display multiple overlapping routes clearly on a MapLibre GL map.

Try the live demo:
View on CodePen

APIs used:

What you will learn:

  • When overlapping routes are a problem and how to solve it
  • How to use MapLibre GL’s line-offset property
  • Understanding the line layer paint parameters
  • Creating custom markers with the Map Marker Icon API

Table of Contents

  1. When Overlapping Routes Matter
  2. The Solution: line-offset Property
  3. Set Up Routes and Offsets
  4. Add Route Layers with Offset
  5. Understanding the Paint Parameters
  6. Add Custom Markers
  7. Explore the Demo
  8. Summary
  9. FAQ

When Overlapping Routes Matter

Multiple routes sharing the same roads is common in several scenarios:

Route Planners
When comparing alternative routes from A to B, the paths often overlap near the origin and destination. Users need to see all options to make informed decisions about travel time, distance, or toll roads.

Delivery and Logistics
Multiple drivers heading to the same warehouse or distribution center converge on the same streets. Dispatchers need to see all routes simultaneously to coordinate timing and identify bottlenecks.

Public Transport Maps
Bus or train lines frequently share corridors. A transit map that only shows one line per street fails to communicate the actual service coverage.

Multi-stop Route Comparison
When planning trips with multiple waypoints, users might want to compare different orderings. The routes will share many segments but differ in key sections.

In all these cases, stacked routes create confusion. The map appears to show fewer routes than expected, and users cannot visually compare the paths.

Screenshot showing four routes from Paris landmarks to Arc de Triomphe, each route visually separated

Four routes from different Paris landmarks to the Arc de Triomphe. Each route is visible even where they share the same streets.

The Solution: line-offset Property

MapLibre GL provides a built-in solution: the line-offset paint property. This property shifts the rendered line perpendicular to its direction by a specified number of pixels.

paint: {
    "line-color": "#E53935",
    "line-width": 4,
    "line-offset": -6  // Shift 6 pixels to the left
}

Key characteristics of line-offset:

  • Pixel-based: The offset is in screen pixels, not meters. Routes maintain consistent visual separation at all zoom levels.
  • Perpendicular shift: The line moves sideways relative to its direction, creating parallel paths.
  • No geometry modification: The underlying coordinates remain unchanged. Only the visual rendering is affected.
  • Positive/negative values: Positive values shift right (relative to line direction), negative values shift left.

For four routes, we use offsets like [-6, -2, 2, 6] to spread them evenly across a 12-pixel width.

This is much simpler than the Leaflet approaches we covered previously. In Leaflet, you need external libraries like Turf.js for geometric offset or plugins like leaflet-polylineoffset. MapLibre GL handles it natively.

Set Up Routes and Offsets

Define your routes with different colors and assign pixel offsets to each:

const DESTINATION = {
    lat: 48.8738,
    lon: 2.2950,
    name: "Arc de Triomphe"
};

const ROUTES = [
    {id: "r1", name: "From Eiffel Tower", color: "#E53935", origin: {lat: 48.8584, lon: 2.2945}},
    {id: "r2", name: "From Louvre", color: "#43A047", origin: {lat: 48.8606, lon: 2.3376}},
    {id: "r3", name: "From Notre-Dame", color: "#1E88E5", origin: {lat: 48.8530, lon: 2.3499}},
    {id: "r4", name: "From Sacré-Cœur", color: "#FB8C00", origin: {lat: 48.8867, lon: 2.3431}}
];

// Offset in pixels for parallel routes (visual offset only)
const OFFSETS = [-6, -2, 2, 6];

The offset values spread routes symmetrically: two routes shift left (-6, -2 pixels) and two shift right (2, 6 pixels). This keeps the routes centered on the actual road.

API Key: Sign up at geoapify.com to get your API key.

Add Route Layers with Offset

Each route needs two layers: an outline (darker, wider) and the main line (route color, narrower). The outline creates visual depth and helps routes stand out from the map.

function addRouteLayer(routeId) {
    const route = routeState[routeId];
    const offsetPixels = OFFSETS[route.index % OFFSETS.length];

    // Add source with original route geometry (no geometric offsetting)
    map.addSource(routeId, {
        type: "geojson",
        data: route.feature
    });

    // Add outline layer with pixel-based visual offset
    map.addLayer({
        id: `${routeId}-outline`,
        type: "line",
        source: routeId,
        layout: {
            "line-join": "round",
            "line-cap": "round"
        },
        paint: {
            "line-color": darkenColor(route.color, 30),
            "line-width": 7,
            "line-opacity": 0.9,
            "line-offset": offsetPixels
        }
    });

    // Add main line layer with same pixel-based visual offset
    map.addLayer({
        id: `${routeId}-line`,
        type: "line",
        source: routeId,
        layout: {
            "line-join": "round",
            "line-cap": "round"
        },
        paint: {
            "line-color": route.color,
            "line-width": 4,
            "line-opacity": 1,
            "line-offset": offsetPixels
        }
    });
}

Both layers use the same line-offset value so the outline and main line move together. The outline is 7 pixels wide, the main line is 4 pixels, creating a 1.5-pixel border on each side.

Understanding the Paint Parameters

The line layer paint properties control how routes appear on the map. Here’s what each parameter does:

line-offset

"line-offset": offsetPixels  // e.g., -6, -2, 2, or 6

The core parameter for this tutorial. It shifts the line perpendicular to its direction:

  • Negative values shift the line to the left (relative to the line direction)
  • Positive values shift the line to the right
  • Zero draws the line on the actual coordinates

For multiple routes, distribute offsets evenly. With 4 routes using [-6, -2, 2, 6], routes are spaced 4 pixels apart, creating a total width of 12 pixels.

line-width

"line-width": 7  // outline
"line-width": 4  // main line

The thickness of the line in pixels. The outline is wider than the main line to create a border effect. The difference (7 – 4 = 3 pixels) is split between both sides, giving a 1.5-pixel border.

line-color

"line-color": darkenColor(route.color, 30)  // outline - 30% darker
"line-color": route.color                    // main line - original color

The outline uses a darkened version of the route color. This creates depth and helps distinguish overlapping routes. The darkenColor helper reduces RGB values by a percentage.

line-opacity

"line-opacity": 0.9  // outline - slightly transparent
"line-opacity": 1    // main line - fully opaque

Opacity ranges from 0 (invisible) to 1 (fully opaque). The outline is slightly transparent (0.9) to soften the border effect.

Layout properties: line-join and line-cap

layout: {
    "line-join": "round",
    "line-cap": "round"
}
  • line-join: How line segments connect at corners. "round" creates smooth curves at turns. Other options: "miter" (sharp corners), "bevel" (flat corners).
  • line-cap: How line ends are drawn. "round" adds a semicircle at each end. Other options: "butt" (flat end), "square" (flat end extended by half the line width).

Using "round" for both creates smooth, professional-looking routes.

Close-up showing how the line-offset property creates parallel routes on a shared road segment

The line-offset property shifts each route perpendicular to its direction, creating clear visual separation.

Add Custom Markers

Use the Geoapify Map Marker Icon API to create markers for origins and the destination. Each origin marker matches its route color, making it easy to identify which path starts where.

function createMarker(lon, lat, color, iconName, name, label) {
    const el = document.createElement("div");
    el.style.width = "36px";
    el.style.height = "48px";
    el.style.backgroundImage = `url(https://api.geoapify.com/v2/icon?type=awesome&color=${color}&icon=${iconName}&iconType=awesome&size=48&scaleFactor=2&apiKey=${API_KEY})`;
    el.style.backgroundSize = "contain";
    el.style.backgroundRepeat = "no-repeat";
    el.style.cursor = "pointer";

    const popup = new maplibregl.Popup({offset: [0, -48]})
        .setHTML(`<strong>${name}</strong><br>${label}`);

    const marker = new maplibregl.Marker({element: el, anchor: "bottom"})
        .setLngLat([lon, lat])
        .setPopup(popup)
        .addTo(map);

    return marker;
}

Map Marker Icon API Parameters

The marker URL includes several parameters to customize the icon. See the full Marker Icon API documentation for all options.

Parameter Value Description
type awesome Marker style. Options: material (Material Design shape), awesome (Font Awesome marker shape), circle, plain (no background)
color %23E53935 Background color of the marker in hex (URL-encoded # as %23)
icon flag, circle Name of the inner icon. Depending on iconType, use Material Design icon names or Font Awesome v6 icon names
iconType awesome Icon library: material or awesome. Default depends on marker type
size 48 Icon image height in pixels
scaleFactor 2 Multiplier for retina displays (2x resolution)

The destination uses a flag icon, while origins use circle icons with colors matching their routes.

MapLibre GL Marker Options

new maplibregl.Marker({element: el, anchor: "bottom"})
  • element: Custom HTML element to use as the marker (instead of default pin)
  • anchor: Which part of the element sits on the coordinates. "bottom" places the pin tip on the location

The popup offset [0, -48] positions it above the marker (negative Y moves up).

Explore the Demo

The interactive demo shows four routes from Paris landmarks to the Arc de Triomphe. You can:

  • Toggle routes using checkboxes to show/hide individual paths
  • Click a route in the list to zoom and see details
  • Compare visually how routes overlap and diverge

Notice how the line-offset property keeps all routes visible even on shared road segments. Each color remains distinct without any complex geometry manipulation.

Summary

MapLibre GL’s line-offset property provides a simple, built-in solution for displaying multiple overlapping routes. Unlike Leaflet, which requires external libraries or plugins, MapLibre GL handles parallel route visualization natively.

Key points:

  1. line-offset is pixel-based – Routes maintain consistent visual separation at all zoom levels
  2. No geometry modification – The underlying coordinates stay unchanged; only rendering is affected
  3. Simple implementation – Just add "line-offset": pixelValue to your paint properties
  4. Works with outlines – Apply the same offset to both outline and main line layers

Recommended offset values:

  • For 2-3 routes: [-4, 0, 4] or [-3, 3]
  • For 4 routes: [-6, -2, 2, 6]
  • For more routes: Spread evenly, keeping total width under 20 pixels

When to use this approach:

  • Comparing alternative routes between the same points
  • Displaying multiple delivery or logistics routes
  • Building public transport maps with shared corridors
  • Any scenario where users need to see all routes simultaneously

Useful links:

FAQ

Q: How does line-offset compare to Turf.js lineOffset?
A: MapLibre GL’s line-offset is a visual/pixel-based offset applied during rendering. Turf.js lineOffset modifies the actual geometry in meters. The MapLibre approach is simpler and maintains consistent screen-space separation at all zoom levels. Turf.js creates truly parallel paths in geographic space, which may be better for precise geographic analysis.

Q: What happens at sharp turns or intersections?
A: The line-offset property handles corners reasonably well due to the line-join: round setting. At very sharp turns, you might see slight visual artifacts, but for typical road networks this is rarely noticeable.

Q: Can I change the offset dynamically?
A: Yes. Use map.setPaintProperty(layerId, 'line-offset', newValue) to update the offset without removing and re-adding layers. This is useful for interactive applications where users might adjust the separation.

Q: How many routes can I display before performance suffers?
A: MapLibre GL uses GPU rendering and handles dozens of routes efficiently. The line-offset calculation is done on the GPU, so it adds minimal overhead. For hundreds of routes, consider server-side simplification or clustering.

Q: Can I use different offsets at different zoom levels?
A: Yes. MapLibre GL expressions support zoom-based interpolation. For example:

"line-offset": [
    "interpolate", ["linear"], ["zoom"],
    10, 2,  // 2 pixels at zoom 10
    16, 8   // 8 pixels at zoom 16
]

Q: What marker icon types are available?
A: The Map Marker Icon API supports four types: material (Material Design shape), awesome (Font Awesome marker shape), circle (simple circle), and plain (no background shape). Each can include an icon from Font Awesome v6 or Material Design Icons libraries.

Try It Now

Open the Live Demo

Sign up at geoapify.com to get your API key and start building multi-route visualizations with MapLibre GL.

Your 2026 Mobile Stack Is Modern Everywhere Except Testing

I spent 6 months talking to mobile engineers about their tooling. Flutter or React Native on the frontend. Supabase or Firebase on the backend. GitHub Actions for CI/CD. Mixpanel for analytics. Sentry for crash reporting.

Every layer modern, maintained, actually pleasant to work with.
Then I’d ask about testing. The energy would shift.
Appium suites held together by brittle XPaths and Thread.sleep(). Espresso on Android, XCUITest on iOS same user flow, written and maintained twice. Flakiness rates sitting at 15-20%, sometimes spiking to 25% on real devices. One mobile lead estimated $200K/year in engineering time just on test maintenance not catching bugs, but fixing selectors that broke because someone changed an accessibility label or moved a component one level deeper in the hierarchy.

Some teams just stopped writing tests altogether. Fell back to manual QA for critical flows. Not because they wanted to because the testing experience was so painful that false failures every morning felt worse than no automation at all.

The numbers tell the same story. I audited the modern mobile stack across 8 layers using adoption data from Stack Overflow’s 2025 Developer Survey, Statista, and 40+ engineer conversations.

Here’s what stood out:

  • Flutter (46% market share) and React Native (35%) dominate frontend both shipped or had major architecture updates between 2017-2024.
  • Supabase hit $2B valuation and 1.7M+ developers. 40% of recent YC batches build on it.
  • GitHub Actions leads CI/CD for most teams. Bitrise reports 28% faster builds vs. GitHub Hosted Runners for mobile-specific workflows.
  • Sentry’s AI-powered root cause analysis hits 94.5% accuracy. Crashlytics remains free and solid.

All of this is 2019-2024 era tooling. Then there’s testing still running on frameworks built in 2011-2012. Appium was created the same year Instagram launched. Think about that for a second.

The core problem isn’t that Appium doesn’t work. It’s architectural. Selector-based testing couples your tests to implementation details. Your test doesn’t say “tap the login button” it says “find the element at //android.widget.Button[@resource-id=’com.app:id/login_btn’] and click it.”
Designer renames that ID? Test breaks. A promo banner shifts the layout? Timing error.
Need the same test on iOS? Rewrite it.

None of these failures mean your app is broken. They mean your
locator stopped matching. That’s busywork, not QA.

The architectural shift that’s closing this gap is Vision AI testing. Instead of querying the element tree, it looks at the rendered screen the same pixels your user sees. Tools like Drizz identify a “Login” button visually whether the underlying component is a Button, a TouchableOpacity, or a custom View with an onPress handler.
What that looks like in practice: a checkout flow that takes 30+ lines of Java with explicit waits and XPath selectors in Appium becomes 6 lines of plain English. Same coverage. Runs on both platforms without rewriting. And when the UI changes button moves, text updates, component gets refactored the test keeps passing because it’s not tied to the DOM.

The early numbers from teams running this approach: <5% flakiness vs. the 15-20% industry average. Test creation dropping from hours to minutes. And the part that surprised me most non-engineers (PMs, designers) actually contributing test cases because there’s no code to write.

I’m not saying rip out Appium tomorrow. If you’ve got a stable suite, deep device-level tests (biometrics, sensors, push notifications), or compliance requirements that mandate W3C WebDriver Appium is still the right tool. The full post gets into where each approach wins honestly.

But if you’re spending more sprint time fixing green-path tests than shipping features, the comparison is worth 10 minutes of your time.

👉 Read the full 8-layer stack audit with adoption stats, side by side code comparisons, and the ROI math on what test maintenance is actually costing your team

Your frontend is 2026. Your backend is 2026. Is your testing layer still stuck in 2012?