I’m an ECE Student Who Vibe Codes Hardware Projects — Here’s What Google I/O 2026 Actually Changed for Me

Submitted as part of the Google I/O 2026 Writing Challenge

Most Google I/O recaps look the same: benchmark tables, pricing comparisons, model spec sheets.

This one won’t.

I’m an Electronics & Communication Engineering student and the person behind BlinkNBuild, a YouTube channel where I build ESP32, Arduino, and Raspberry Pi projects. I also vibe code a lot: browser-based OLED pixel editors, circuit diagram tools, IoT dashboards — all shipped from a single HTML file to Netlify on zero budget.

When I watched the Google I/O 2026 developer keynote, I wasn’t evaluating these announcements as an ML researcher or a senior web developer. I was asking one question: does any of this actually change what I can build on a Tuesday night with no sleep and a free API tier?

For the first time in a while — yes. Genuinely.

The Shift That Actually Matters

Something Sundar Pichai said in the keynote stuck with me:

“We’ve transitioned from AI that simply assists you, to agents that can independently navigate complex tasks across your entire workflow.”

If you’ve spent time babysitting an LLM through a multi-step project — manually copy-pasting between steps, correcting it after every tool call, re-explaining context it forgot — you know exactly why this matters. The gap between “AI that helps” and “AI that owns a task” is enormous in practice. Google just made a serious move toward closing it.

Here’s what actually shipped, and why I think it matters for builders like us.

Gemini 3.5 Flash: The First Flash That Beats a Pro Model

This is the headline announcement, and it deserves more than a spec table.

The Flash series has always been the “fast and cheap” tier — you trade capability for speed and cost. Gemini 3.5 Flash breaks that tradeoff entirely. It outperforms Gemini 3.1 Pro on coding and agentic benchmarks:

  • 76.2% on Terminal-Bench 2.1 (coding) vs 70.3% for 3.1 Pro
  • 1656 Elo on GDPval-AA (real-world agentic tasks)
  • 83.6% on MCP Atlas (tool-use reliability)
  • 84.2% on CharXiv Reasoning (multimodal understanding)

Meanwhile it runs at 4× the speed of comparable frontier models with a price of $1.50/$9 per 1M tokens — less than half what GPT-5.5 costs for the same output.

For a student building hobby projects on a free or low-cost API plan, that last sentence is everything.

The context window is 1M tokens (1,048,576 to be exact), with dynamic thinking on by default — meaning the model auto-allocates extra compute when the problem is hard and skips it when it’s simple. You don’t tune this. It just works.

One honest caveat: there’s a regression on 128k long-context retrieval (MRCR v2 dropped 7.6 points versus the preview). If you’re doing RAG over massive multi-hundred-page documents, that’s worth knowing. For the typical IoT/maker project — sensor code, dashboard logic, firmware debugging — it’s not relevant. 3.5 Flash is still the best thing in the Flash tier by a wide margin.

I tested it immediately via AI Studio after the keynote. I gave it a prompt to generate Arduino sensor code with a web dashboard interface. The output was noticeably more structured and the code ran first-try in a way that older Flash models didn’t always manage.

For makers and side-project builders: if you’re still on 3.1 Pro for budget reasons, you can now get better results at lower cost. That’s a direct upgrade.

Google AI Studio: More Than a Key Generator

I’ll be honest — I’ve always thought of AI Studio as “the place you go to get an API key.” I didn’t think of it as a serious development environment.

I/O 2026 changed that framing entirely.

Native Android vibe coding is now in AI Studio. You can describe what you want, and it generates a Kotlin Android app — not a web wrapper, an actual native Android project. Combined with one-click deploy to Cloud Run and Firebase integration, you can go from idea to deployed app without ever leaving the browser.

There’s also an Export to Antigravity button — one click and your entire project state, including all context, moves to local Antigravity development. No re-explaining, no re-scaffolding.

And a new AI Studio mobile app is available for pre-registration this week, so you can capture ideas on the go and have a prototype waiting when you get back to your desk.

For someone who builds projects that need a companion app (sensor dashboards, remote control interfaces, IoT monitors), the setup overhead for native apps just collapsed.

Managed Agents: The Feature I’m Most Excited to Build With

This is the one that’s going to change my actual projects.

Google introduced Managed Agents in the Gemini API — a single API call that spins up a fully provisioned agent running inside an isolated Linux container. Files and state persist across follow-up calls. The agent can reason, use tools, execute code, and iterate without you manually orchestrating every step.

Why does this matter for makers?

Because most IoT projects need more than one-shot generation. You want to describe a problem — “I have a noisy ADC reading from my soil moisture sensor, help me clean it and write the data to a CSV” — and have something that can work through it. Run the code, see it failed, debug it, retry. Not just hand you a code block and wish you luck.

Managed Agents are designed for exactly that loop. One API call. Persistent state. Real execution environment. I’m planning to build a small agent that reads ESP32 serial output and iterates on firmware fixes autonomously. That workflow was genuinely not practical before this.

Antigravity 2.0: The Bigger Picture

Underneath all of this is Antigravity 2.0 — Google’s agent-first development platform, now a full three-component rebuild: a standalone desktop app, a new CLI (written in Go, replacing the old Gemini CLI), and an SDK for custom agents.

A few things worth noting if you’re evaluating it:

The desktop app orchestrates multiple agents in parallel, schedules background tasks, and has native voice command support. You describe a problem; specialized subagents divide the work.

The Antigravity CLI replaces the Gemini CLI entirely — Google is asking existing users to migrate. For makers, this is good news: you can now script around it, pipe results into your own tooling, and integrate it into terminal-based workflows naturally.

The Managed Agents in the Gemini API give you the Antigravity agent harness without the full local setup. One API call provisions a sandboxed Linux environment. This is the lowest-friction entry point for builders who don’t want to install a full desktop tool.

I Built Something With It — Here’s the Receipt

This isn’t just theory. I used Antigravity 2.0 to build and ship a real project: a free, browser-based PDF to Image converter that runs 100% offline, no server, no uploads, no account required.

The entire thing — from idea to live app on Netlify — took under an hour.

That’s not a marketing claim. That’s what it felt like to go from “I have a problem” to “this is deployed and usable by anyone.” The old workflow would’ve taken me an evening at minimum: scaffolding the PDF.js rendering pipeline, debugging canvas GPU memory exhaustion on large documents, wiring the File System Access API fallback for Firefox. Antigravity handled the architecture and iteration. I steered.

🔗 Live App → pdf-2-image.netlify.app
📦 GitHub → pdf-2-image source code

The use case matters too: most AI tools (including Gemini and ChatGPT) parse PDFs as raw text streams, which silently mangles math equations, circuit diagrams, and scanned notes. Converting each page to a high-res image first — before uploading to an AI tool — fixes that entirely. It’s a problem every ECE student hits.

If you want the full technical writeup (PDF.js rendering loop, canvas memory management, File System Access API), I’ve covered it in detail here.

What I’m Building Next

Based on what shipped at I/O 2026, here’s what I’m immediately trying:

  1. ESP32 firmware debug agent — using Managed Agents to read serial output, identify errors, and iterate on fixes autonomously, without me manually copy-pasting error messages back and forth
  2. Android companion app — using AI Studio’s Kotlin vibe coding to generate a native sensor dashboard, deployed with one click to Cloud Run
  3. Antigravity CLI pipeline — a terminal-scripted subagent workflow that takes a GitHub README, extracts a bill of materials, and auto-generates an Instructables-style writeup

None of these were practical to build before this week.

The Honest Take

Google I/O 2026 wasn’t one moonshot announcement. It was a lot of things getting meaningfully better at the same time — the model, the tooling, the agent infrastructure, the developer experience — in a way that adds up to a real shift in what a solo builder can accomplish.

The old Flash tradeoff (speed vs. capability) is gone. The old AI Studio limitation (great for prompts, not for building) is gone. The old agent friction (manually orchestrate everything yourself) is shrinking fast.

For ECE students, makers, and side-project builders on a tight budget: this is the best the Gemini stack has ever been, and it’s available right now on the free tier.

Go build something.

Useful Links

What’s the first thing you’d build with Managed Agents + Gemini 3.5 Flash? Drop it in the comments — I’m especially curious what the embedded and IoT folks here are thinking.

From Fragmented Pipelines to Coherent Intelligence — Why Gemma 4 Actually Changes How I Work

Two months ago, I was stuck in the same fragmented workflow most developers still accept: paying $50/month for OCR APIs, aggressively chunking logs for RAG, and stitching together multiple AI services just to get basic work done. Gemma 4 didn’t just replace parts of that stack — it made the entire fragmented approach feel obsolete.

This is my submission for the “Write about Gemma 4” track.

Two months ago, I was doing what most developers still do: maintaining complex RAG pipelines, managing brittle document transformations, and chopping files into tiny pieces because local models couldn’t handle real-world scale.

Today, I default to Gemma 4 running locally for most of my diagnostic and automation workflows. Not because it beats every closed cloud model on massive hyper-specific leaderboards, but because it finally makes coherent, private, and simple intelligence practical on consumer hardware.

The Two Problems That Defined My Old Workflow

I was fighting my development tools instead of solving actual code problems across two parallel engineering tasks:

  1. Document Intelligence: I was feeding messy contractor invoice photographs into a paid, cloud-based OCR + LLM pipeline. The results were inconsistent, multi-column tables frequently broke, network round-trips introduced lag, and sensitive accounting data had to leave my local machine.
  2. Temporal Reasoning: A critical background job was crashing every Tuesday at exactly 3:14 AM with a generic context deadline exceeded error. Twelve months of production logs (~115K tokens) were being sliced into arbitrary 30-day chunks to fit into standard local model context windows. The root cause remained entirely invisible because no single context window could see the continuous timeline.

Gemma 4 cleanly eliminated both bottlenecks.

Case Study 1: Killing the Paid OCR Pipeline

Gemma 4’s native visual footprint (especially the 26B MoE and 31B Dense variants) processes image matrices down at the weight layer. It doesn’t need a separate visual parser or an intermediate text conversion engine; it reasons over spatial pixel layouts directly.

By leveraging the official Python SDK, I hooked a unified local processing pipeline straight into my environment:

import ollama

def extract_invoice_fields(image_path):
    # Ensure "gemma4:26b" matches your local alias from "ollama list"
    response = ollama.chat(
        model="gemma4:26b",
        messages=[{
            'role': 'user',
            'content': """You are a strict data extraction engine. Analyze this document image and return ONLY a valid JSON object matching this exact schema:
{
 "date": "YYYY-MM-DD",
 "amount": number_no_symbols,
 "description": "concise_summary"
}
If a field is completely unidentifiable, set its value to null. Do not include markdown wraps, conversational intros, or post-explanations."""
        }],
        images=[image_path],
        options={'temperature': 0.1}
    )
    return response['message']['content']

By adding a basic two-line adaptive contrast enhancement pass via opencv-python to clean up shadows and tilted angles before running inference, my extraction accuracy reached 94% on complex real-world receipts—matching the performance of my previous paid cloud API at zero marginal cost.

💻 Hardware Baseline: All tests were executed locally on an M1 MacBook Pro (16GB unified memory). The 26B MoE model variant maps comfortably inside ~12GB of active memory, completing localized visual inference in 2.3 seconds per page.

Case Study 2: The Return of Temporal Coherence

To solve the recurring system crash, I completely stopped chunking the log files. I passed the entire continuous 115K-token historical stream into Gemma 4’s native 128K context window using a strict forensic analyst system prompt.

Within roughly 70 seconds of local execution, the engine traced an explicit causal chain across distant chronological boundaries:

  • January 12 (Line 1,450): A quiet database configuration tweak reduced the active connection pool size from 50 to 20.
  • March 28 (Line 58,200): A separate DevOps commit flipped the network retry backoff strategy from exponential to linear.
  • June 3 (Line 112,400): A seasonal traffic spike hit the server. The restricted connection pool choked, and the linear retries instantly amplified the backlog into a recursive queue crash.

Neither event was an error on its own. They were two pieces of a shattered plate separated by months of high-volume logging noise.

FRAGMENTED APPROACH (Chunking / RAG)
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  JANUARY CHUNK  │     │   MARCH CHUNK   │     │   JUNE CHUNK    │
│ [Pool: 50→20]   │     │ [Linear Backoff]│     │ [System Crash!] │
└────────┬────────┘     └────────┬────────┘     └────────┬────────┘
         │                       │                       │
         └──── Severed Link ─────┴──── Severed Link ─────┘
              (Zero structural correlation across windows)

TEMPORAL COHERENCE APPROACH (Gemma 4 · 128K context)
┌──────────────────────────────────────────────────────────────────┐
│  JAN: Pool 50→20  ──►  MAR: Linear Backoff  ──►  JUN: Crash     │
└──────────────────────────────────────────────────────────────────┘
   └──── Native attention weights trace the full causal chain ────┘

Vector-based RAG pipelines fail here because a January pool adjustment and a June timeout share absolutely no semantic similarity or keyword vectors. You need temporal coherence—the ability of a model’s internal attention mechanisms to hold the unbroken timeline live in working memory to calculate long-range dependencies.

To prove the power of temporal coherence over traditional chunking patterns on analytical data, consider this sample snippet of a raw financial transaction ledger covering the same fiscal period:

Timestamp,TransactionID,Vendor,Amount,Category,AuthCode
2026-01-15,TX-9012,Office_Supply_Corp,9850.00,Operations,AUTH-882
2026-02-11,TX-9412,Consulting_Group_LLC,14500.00,Legal,AUTH-109
2026-05-18,TX-1044,Office_Supply_Global,9850.00,Operations,AUTH-882
2026-09-04,TX-1299,Freelance_Network,4200.00,Marketing,AUTH-455
2026-11-12,TX-1522,Office_Sup_Direct,9850.00,Operations,AUTH-901

Reviewed monthly or quarterly, these entries are clean, well-spaced, and unremarkable. However, when passed whole into the 128K context window, the model cross-references across all 12 months simultaneously and flags a red-alert pattern of structured threshold evasion that month-by-month chunking misses entirely:

{
  "audit_status": "CRITICAL_FLAG",
  "anomaly_detected": "Layered Threshold Evasion (Structuring)",
  "confidence_score": 0.96,
  "forensic_trail": {
    "nodes": [
      {"timestamp": "2026-01-15", "id": "TX-9012", "amount": 9850.00, "vendor": "Office_Supply_Corp"},
      {"timestamp": "2026-05-18", "id": "TX-1044", "amount": 9850.00, "vendor": "Office_Supply_Global"},
      {"timestamp": "2026-11-12", "id": "TX-1522", "amount": 9850.00, "vendor": "Office_Sup_Direct"}
    ],
    "analysis": "Identical transaction values of $9,850.00 executed at 10-month intervals. Target amounts sit precisely beneath the $10,000 corporate manager sign-off threshold. Vendor name variations suggest an intentional attempt to evade standard monthly pattern matchers."
  }
}

Why This Feels Different: Workflow Simplification

The true value of open-weight models of this caliber isn’t minor benchmark gains; it is the radical simplification of software architecture.

Old Fragile Stack New Gemma 4 Way Real Engineering Impact
Cloud OCR API ➔ Text Cleaner ➔ Cloud LLM Single localized multimodal inference call Eliminates third-party API dependencies and reduces formatting hallucinations.
Chunking Scripts ➔ Vector DB Embedding ➔ RAG Retrieval ➔ Target Window Context Single unbroken 128K context pass over raw historical source arrays Preserves chronological continuity and captures deep systemic drift.
Multiple proprietary paid subscription keys One unified open-weight model family deployed completely on-device Guarantees absolute data privacy and introduces predictable, flat infrastructure costs.
Complex multi-agent coordination frameworks Deep, single-pass reasoning loops via native weights Drastically reduces code scaffolding complexity and shortens prototyping cycles.

Practical Context Budget Guide

Running long-context inference locally requires intentional resource balancing. Here is the framework I use to match my task parameters to the correct model distribution:

Target Engineering Task Required Context Budget Optimal Model Variant Practical System Rationale
Deep forensic logs, transaction tracking, annual ledger audits 64K–128K Gemma 4 26B MoE Retains long-range causal links across multi-month chronologies.
Multi-file code repository dependency mapping & architecture audits 32K–64K Gemma 4 26B MoE Maps cross-module structural relationships without dropping core definitions.
Automated unit test generation & localized workspace refactoring 8K–16K Gemma 4 4B High-throughput execution speeds with minimal VRAM and memory footprints.
Real-time inline code documentation & terminal autocomplete loops 2K–4K Gemma 4 2B Near-instant token generation running seamlessly on lightweight edge devices.

💡 An Honest Operational Note: Don’t default to the 128K window for basic programming tasks. Processing over 100K tokens on consumer hardware takes 50 to 90 seconds. Treat it as a high-leverage analytical tool for deep diagnostic workloads, not an autocomplete engine.

Honest Technical Limitations

Open-weight architectures are powerful, but engineers should avoid treating them like magic:

  • Severely Degraded Inputs: If an invoice photograph is completely blurry or features highly complex cursive handwriting, specialized commercial cloud HTR pipelines still retain an accuracy edge.
  • Real-Time Thresholds: Processing massive document arrays introduces inescapable local hardware compute cycles. If your app requires sub-second streaming feedback, scale down to the ultra-fast 4B parameter models or leverage targeted API setups.
  • Knowledge Cutoffs: Long-context windows do not replace live knowledge. If you are tracking system updates or breaking news that occurred after the model’s training threshold, you still need a local web search or RAG pipeline to ground the output.

Try This in Your Workspace Today

The next time you face an intractable bug or a messy data extraction problem, try this layout experiment:

  1. Fire up your terminal and pull down the weights: ollama pull gemma4:26b
  2. Take your longest, unchunked log file or your messiest visual document data stream.
  3. Pass the entire timeline whole into the context window.
  4. Ask it a question that requires tracing an explicit chronological link between two events separated by weeks of lines.

You will likely experience the exact same paradigm shift that I did—moving away from the constant friction of fighting and managing your data processing tools, and returning to simply thinking about the engineering problem.

Final Thoughts

Gemma 4’s most significant contribution to the open-source community isn’t its raw leaderboard metrics. It is the fact that it makes coherent, private, and local intelligence completely practical for everyday developer workflows.

By delivering clean multi-modal vision and an expansive context footprint under a true commercial Apache 2.0 license, it allows independent developers to tear down complex infrastructure scaffolding. It transforms local-first design from an interesting, idealistic experiment into the most rational engineering default.

🔗 Core Developer Resources

🤖 AI Transparency Disclosure

In full compliance with the official challenge evaluation criteria:

  • Writing and Layout Assistance: I utilized AI workflows to restructure raw text blocks into scannable markdown comparison tables, verify code parameter keys inside script wrappers, and balance the thematic flow of the text segments. All system metrics, operational logs, pipeline designs, and technical viewpoints are entirely my own.
  • Hardware Validation: The software integration scripts, local open-weight runtime benchmarking passes, and document filter evaluations were verified and executed on my local development hardware.

Our AI Inference Bill Dropped 65% After We Stopped Treating Every Query the Same

  • Every query hitting our AI layer was going straight to the most powerful model we had. A user asking “what does HIPAA Section 164.312 say?” got the same compute budget as one asking “should we shut down the payment processor during this active incident?” That was expensive and stupid, and it took embarrassingly long to fix.

This is the story of how we built a routing layer called CascadeFlow into SentinelOps AI, an enterprise decision intelligence platform, and what actually happened when we turned it on.

The Problem With “One Model Fits All”

When you’re building an AI system for enterprise operations teams—people making real decisions about infrastructure, compliance posture, and incident response—you face a genuine tension. You need the model to be good when it matters. But “good” on a documentation lookup is a different thing from “good” on “we have a potential SOC2 violation, walk me through the remediation path.”

Before routing, every query went to our primary reasoning model (Llama 3.3 70B via Groq). The latency was fine. The quality was fine. The cost was not fine. At scale, routing simple factual queries through a 70B parameter model is just burning money.

The naive fix is to have engineers triage queries manually, which doesn’t scale. The correct fix is a classifier that does it automatically.

CascadeFlow: A Lightweight Routing Engine

We integrated @cascadeflow/core as our routing middleware. The idea is straightforward: before a query hits the expensive model, a cheap, fast classifier decides which tier it belongs to.

Our routing logic looks roughly like this:

import { CascadeFlow } from '@cascadeflow/core';

const cascade = new CascadeFlow({
  classifier: {
    model: 'llama-3.1-8b-instant', // fast, cheap
    provider: 'groq',
  },
  tiers: [
    {
      name: 'simple',
      model: 'llama-3.1-8b-instant',
      triggers: ['documentation', 'lookup', 'definition', 'what is'],
    },
    {
      name: 'complex',
      model: 'llama-3.3-70b-versatile',
      triggers: ['incident', 'compliance', 'risk', 'critical', 'breach'],
    },
  ],
});

The classifier runs first—it’s an 8B model, so it’s fast and cheap—and classifies the incoming query into a complexity tier. Simple queries (policy lookups, definition requests, status checks) stay on the 8B model. Complex queries (active incidents, compliance risk assessments, multi-system decisions) escalate to the 70B.

From our LLM service layer, the routing call is transparent:

async function routeAndExecute(query, context) {
  const tier = await cascade.classify(query);
  const model = tier === 'complex'
    ? 'llama-3.3-70b-versatile'
    : 'llama-3.1-8b-instant';

  return groq.chat.completions.create({
    model,
    messages: buildMessages(query, context),
    response_format: { type: 'json_object' },
  });
}

That response_format: json_object constraint is important—we’ll come back to it.

What Routing Actually Costs You

There’s a hidden cost to routing that nobody talks about: the classifier itself can be wrong.

In our early testing, the 8B classifier was misrouting about 12% of complex queries down to the cheap tier. A question like “is our current encryption at rest sufficient for PHI storage?” looks superficially like a documentation query. The classifier saw “encryption” and “PHI” as lookup-adjacent terms and routed it to the cheap model, which gave a technically accurate but shallow answer that lacked the risk-weighted framing an auditor would need.

We fixed this in two ways:

  1. Conservative misclassification bias. When the classifier’s confidence is below a threshold, escalate to the expensive tier. False positives (routing simple queries high) cost money. False negatives (routing complex queries low) cost credibility. In an enterprise governance context, credibility is more expensive.

  2. Domain keyword pre-checks. Before the classifier even runs, we scan for a hardcoded list of high-stakes terms. If a query contains words like breach, PHI, incident, remediation, or SOC2, it goes to the 70B model unconditionally.

const HIGH_STAKES_KEYWORDS = [
  'breach', 'incident', 'PHI', 'PII', 'SOC2', 'HIPAA',
  'remediation', 'critical', 'violation', 'audit', 'penalty'
];

function requiresComplexModel(query) {
  const lower = query.toLowerCase();
  return HIGH_STAKES_KEYWORDS.some(kw => lower.includes(kw));
}

This is not elegant, but it’s safe. The performance overhead is a single .includes() check per query.

The Numbers

After deploying CascadeFlow routing against a realistic mix of enterprise queries, roughly 68% of queries fell into the “simple” tier. The remaining 32% were genuinely complex—incident-related, compliance-heavy, or multi-system risk assessments that benefited from the more capable model.

That routing split—combined with the price difference between an 8B and 70B parameter model—accounts for most of the cost reduction. The exact figure depends on your query distribution and your provider’s pricing, but 60-65% is a reasonable estimate for an enterprise operational workload where most interactions are informational rather than analytical.

Forcing Structure Out of Both Models

One consequence of routing to two different models is that you now have two sources of unstructured text to deal with. We solved this by enforcing a strict JSON response schema at the prompt level, regardless of which model is running.

Every response from SentinelOps AI conforms to this shape:

{
  "summary": "One-sentence decision summary",
  "risk_level": "LOW | MEDIUM | HIGH | CRITICAL",
  "confidence": 0.87,
  "recommendation": "Specific, actionable recommendation",
  "tradeoffs": ["Tradeoff A", "Tradeoff B"],
  "governance_flags": [],
  "citations": []
}

The frontend renders this as a Decision Card—not a chat bubble. Risk level gets a color-coded badge. Confidence is displayed as a progress bar. Tradeoffs are rendered as a checklist. Governance flags trigger a separate UI element that routes to the compliance dashboard.

When you force both the cheap and expensive model into the same output schema, the quality difference between tiers becomes measurable. You can compare confidence scores, count governance_flags, and track whether the 8B model’s recommendations match the 70B model’s on borderline queries. This becomes a feedback loop for improving your routing thresholds over time.

Lessons

1. Start with keyword gating, not just ML classification. A simple list of high-stakes terms as a pre-filter saved us from the worst misrouting failures. ML classifiers are probabilistic. Safety-critical routing decisions shouldn’t be.

2. Misrouting in the wrong direction is asymmetric. Routing a simple query to a powerful model costs you money. Routing a complex query to a weak model costs you trust. Size your misclassification bias accordingly.

3. A common output schema across tiers is essential. Without it, you’re comparing apples and oranges and your frontend needs to handle two different response shapes. Force the schema at the prompt level.

4. Routing is a product decision, not just an infrastructure one. The thresholds you set for escalation reflect your platform’s risk tolerance. In a governance context, we erred conservative. A developer tool might err aggressive. Know which direction your users would rather you fail.

You can read more about how CascadeFlow handles multi-tier routing in the cascadeflow docs. The cost savings are real, but the more important outcome is that complex queries now get the compute they actually need instead of competing on the same tier as “what does this acronym stand for.”

Why P95 Latency Is the Only Metric That Matters at 3 AM

If your checkout endpoint serves 10,000 requests per minute, a 5% latency spike means 500 users are having a bad experience every minute.

Averages compress that pain into a single comfortable number.
P95 latency — the latency at the 95th percentile — tells you what your slowest users are actually experiencing.

It’s the metric that catches the spike average hides.
This is why I track P95 as the primary health signal, not averages.

*How Latency Spikes Actually Propagate
*

A latency spike rarely starts in your application.It usually starts somewhere else and cascades inward.

The typical pattern looks like this:

Slow upstream dependency
↓
Connection pool saturation
↓
Request queue growth
↓
Latency spike propagation
↓
Timeouts and failures

The Cascade Pattern
An upstream dependency (database, payment gateway, third-party API) slows down
Your FastAPI app keeps accepting requests while waiting for responses.
Your connection pool fills up – new requests queue behind existing ones.
Queue depth grows, memory pressure builds
Response times climb across all endpoints, not just the affected one. Eventually requests start timing out or failing entirely

By stage 3, you have a problem. By stage 5, your customers know about it before you do.
The cascade failure pattern is particularly nasty.A slow database query holds a connection.

That held connection blocks another request.That blocked request ties up execution capacity.Multiply that by concurrent users and you get full service degradation from a single slow dependency.

Under async workloads, the failure mode becomes especially deceptive because the application continues accepting requests while upstream awaits accumulation in the background.

High Traffic Spikes Make This Worse. Under normal load, a slow upstream dependency is annoying.
Under a traffic spike, it’s catastrophic.

Here’s why:

Connection pool saturation happens faster. If you have 20 database connections and traffic doubles, you hit the ceiling twice as fast.
Queue depth explodes. Requests piling up behind a slow dependency compound each other’s wait time.
Memory pressure builds. Each queued request holds state. Enough of them and you drift toward OOM territory.
Recovery is non-linear. Once a connection pool is saturated, it often stays saturated even after the upstream issue resolves — because the backlog keeps it full.

The cruel irony is that traffic spikes happen when your service matters most.

A flash sale. A viral moment. A major announcement.
Exactly the wrong time to be debugging latency from a dashboard.

What Didn’t Work For Me

Monitoring sounds easy in theory. In practice, most setups failed me in one of four ways.

Prometheus + Grafana. Powerful, but operationally heavy.

Setting up exporters, configuring dashboards, maintaining the stack — all before writing a single alert rule.

And when the alert fires at 3am, one still has to log in and interpret charts under pressure.

Simple Health Checks

GET /health → 200 OK tells you the service is alive.
It doesn’t tell you it’s running at 8x normal latency while technically responding.

Average Latency Monitoring

Averages mask the spikes that actually hurt users.

In one case, a payment provider slowdown pushed P95 latency from roughly 180 ms to over 2 seconds within minutes — while average latency still looked acceptable.

By the time averages reflected the issue, checkout failures had already started.

Alert Fatigue

I added more monitors to catch more things. Which meant more alerts. Most of them were noise. When everything is urgent, nothing is. Monitoring systems usually optimise for data collection.

Operators actually need decision compression.

What I Built Instead

I wanted something that:
Tracked P95, not averages
Produced a single health score instead of 15 metrics to interpret
Caught degradation trends early, before full failure
Required zero config to add to an existing FastAPI app

The result is a FastAPI middleware that continuously computes degradation signals directly from live request traffic.

from fastapi import FastAPI
from fastapi_alertengine import instrument

app = FastAPI()
instrument(app)

The middleware exposes a structured /health/alerts endpoint:

{
“status”: “warning”,
“health_score”: {
“score”: 61,
“trend”: “degrading”
},
“metrics”: {
“overall_p95_ms”: 1847.3,
“error_rate”: 0.08,
“anomaly_score”: 0.9
}
}

One status.One score.One trend direction.No dashboards to configure.No agents to run.No Prometheus exporters.

The Human-in-the-Loop Layer

Once I had a reliable health signal, the next question was:
What do I do with it?

I built a managed orchestration layer that polls /health/alerts every 5 seconds.When the score drops below threshold, it:

Runs Claude AI diagnosis on the metric context
Sends a WhatsApp or Telegram message with a plain-English summary
Generates a single-use recovery link

Most AI incident tooling jumps straight to autonomous remediation.I intentionally didn’t.

Production systems deserve human authorization before recovery actions execute.I read the diagnosis, preview the recovery action, and tap approve – from my phone.

Nothing executes automatically.Every action is logged immutably.

I built the mobile-first delivery because I work in Zimbabwe, where engineers aren’t always at laptops when things break.

WhatsApp is the operational control plane here.

That constraint produced something better than I expected:

Alerts that find you, rather than dashboards you have to find.

The Open Source Core
The telemetry middleware is free and MIT licensed.
pip install fastapi-alertengine

The managed orchestration layer (AI diagnosis, WhatsApp/Telegram alerts, human-authorized recovery) is a commercial service.

GitHub: https://github.com/Tandem-Media/fastapi-alertengine
Docs: https://tandem-media.github.io/fastapi-alertengine/

Most monitoring stacks are good at detecting incidents.
Very few are good at reducing operator uncertainty during one.
How are you handling that gap today?

Recycling made easy: a Polish recycling assistant powered by Gemma 4

Photo of Polish color-coded recycling bins

This is a submission for the Gemma 4 Challenge: Build with Gemma 4

I clearly remember this scene: my mom and I are dyeing our hair together. I’m holding a small plastic bottle that still has some dye inside, and I’m asking myself: where the hell do I throw this? I ask my mom – she has no idea either. Plastic bin, even though the bottle is contaminated with dye? Or is it mixed waste now?

Sorting waste in Poland has improved a lot over the last few years. The system is based on six colourful bins that you’ll see in any backyard:

Bin Color What goes in
PLASTIC/METAL 🟡 (yellow) plastic bottles, cans
PAPER 🔵 (blue) paper, cardboard
GLASS 🟢 (green) glass jars, bottles
BIO 🟤 (brown) food scraps, garden waste
MIXED ⚫ (black/grey) mixed waste: contaminated or composite items
TEXTILE 🟣 (violet) clothes, shoes (separate collection mandatory since Jan 2026)

Photo of Polish color-coded recycling bins

Obvious stuff like cardboard or an old pair of socks is easy. But there are traps – you can’t throw an old mug or a broken mirror into the glass bin, even though it seems to be the right choice. And then there’s the hair dye bottle with dye still inside. The pizza box with grease on the bottom. The blister pack from your pills.

Real debates are happening on the Polish internet about this stuff. Type “gdzie wyrzucić…” into Google and you’ll see how much people struggle. I once watched two friends argue about where a used coffee cup goes — one swore it was paper, the other said mixed waste because of the plastic lining.

Confusing? Hell yeah, it is! 😩

What I Built

That’s why I built a web app called Gdzie to wyrzucić? [ENG: Where to throw it?]. It’s a Polish recycling assistant – you take a photo of an item with your camera and send it to a Gemma 4-backed AI assistant. As a response, it tells you which bin the waste goes in.

The photos are not stored anywhere – they are processed in-memory by the API route, sent to Gemma 4 for analysis, and discarded immediately after the response is returned.

The recycling rules live in a ~200-line system prompt. What you see now is v4 – there were a lot of iterations and a lot of live testing along the way. At this point, my entire phone gallery has turned into one big collection of trash photos 🙈 And my neighbours probably think I’ve gone nuts, because I keep photographing “interesting” items from the backyard bin to see how Gemma will handle them 😅

The app is Polish-only by design – Polish recycling rules apply to people in Poland, and the system prompt is built around Polish categories. I’m planning to add English and Ukrainian translations, though, because there are minorities living in Poland who could really use this, too. Until then, I hope the app is intuitive enough for a non-Polish speaker.

Demo

To test the project, go to Gdzie to wyrzucić?. The walkthrough is as follows:

  1. Take a photo of the wasted item or choose one from your gallery. Press the Aparat or Galeria button, respectively.
  2. Click Analizuj and wait ~15-30 seconds to get the sorting result. Alternatively, click Zrób ponownie if you are not satisfied with the photo and want to retake it.
  3. The app returns the answer to which bin you should throw your waste, together with the explanation and some additional remarks (if any).
  4. If you want to try another waste item, press Sprawdź inny przedmiot.

For the English-speaking dev.to users, I prepared a walkthrough video with some explanation:

Code

All the code lives in the recycling-app repository on GitHub.

At this point, let me be honest with you – I vibe-coded this. I am a Technical Writer, not a Developer, and even though I have experience with git and done some small code updates in the past, this is the first bigger project I’ve built myself.

It was a multi-agent work. I used Claude Code for the boilerplate code and for turning business logic into reality. I used Gemini’s deep search to dig up current recycling rules from Polish government sources and eco-experts. I was the brain behind everything else – the idea, the business logic, the testing, the improvements, leading each next step, holding it all together. And Gemma is the heart of the project 💛

How I Used Gemma 4

The app sends the photo to Gemma 4 with a detailed system prompt that encodes the current Polish recycling rules. Gemma looks at the image and returns a structured JSON: which bin, how to prepare the item, an explanation, and an extra note if relevant. The frontend turns that into a result card in the right bin colour.

I went with Gemma 4 26B Mixture-of-Experts (gemma-4-26b-a4b-it) via Google AI Studio’s free tier. The MoE architecture only activates around 4B parameters per token out of ~26B total. It’s efficient enough to run on the free tier and smart enough to handle the actually hard cases.

Gemma 4 26B A4B model listing on Google AI Studio

The free tier means the demo runs 24/7 without costs. And because Gemma is open-weights, the same app could one day ship with Gemma 4 E2B or E4B running directly on the user’s device. That’s a path I’d like to explore.

Known limitations

The test phase revealed that some photos trigger an immediate error before Gemma is even reached. The failing photos share the same characteristics: detailed scenes, multiple objects, busy compositions, and possibly larger file sizes than simple isolated-item photos. I think that the issue is about image size or encoding, but I need to confirm it yet.

Also, analysis takes 10–15 seconds. Free-tier cloud inference is slow. It is acceptable for the initial phase and the challenge, but I’d need to think about some other options in the future.

What’s Next

The roadmap is in the issue tracker. A few highlights:

  • Multilingual support for English and Ukrainian – many Ukrainians have moved to Poland in recent years, and there are also exchange students and expats living in Poland who would find an English translation handy.
  • Interactive clarification – when Gemma isn’t sure what’s in the photo, let it ask the user a yes/no question instead of guessing.
  • Suggesting reuse before recycling for books, clean clothes, working electronics, old furniture, and so on. Recycling is good. Reusing is better.

Closing sentence

If you’re in Poland, try out Gdzie to wyrzucić?. If you spot a misclassification or a rule the app gets wrong, please open an issue — I’d love to keep improving it.

Thanks to the dev.to challenge for the deadline, to my colleagues and friends who tested the app, and to Gemma 4 for actually making it all happen 💛

The Complete Guide to Running a Midnight Node: Setup, Sync & Monitoring

If you’ve been building on Midnight or want to participate in the network without trusting third-party infrastructure, run your own full node. This tutorial takes you from a bare Ubuntu/Debian machine to a healthy, synced node, including common failure points such as dropped peers and getting stuck on block 1.

Target audience: Developers and node operators who are comfortable with the Linux command line and Docker.

Prerequisites:

  • A Ubuntu 22.04 or Debian 12 server (bare metal or VPS)
  • Docker Engine installed (covered below)
  • Git installed
  • A running Cardano-db-sync + PostgreSQL instance (covered in step 1)
  • Open firewall on port 30333 (P2P)

What you’ll have by the end: A full node synced to Midnight’s Testnet, with monitoring commands and troubleshooting steps for common issues.

Understanding the node types

Before you spin anything up, it helps to know what you’re actually running. Midnight has four node types:

  • Full node: syncs the chain, validates transactions, and serves real-time state queries. It prunes historical state older than 256 blocks by default. This is what most developers and node operators need.
  • Archive node: the same as a full node but retains the entire chain history. Required for block explorers or deep historical queries. Uses more disk space.
  • RPC node: exposes an HTTP/WebSocket API for DApps to interact with the chain programmatically.
  • Boot node: helps new nodes discover peers. You don’t need to run one unless you’re contributing to network bootstrapping.

This tutorial focuses on the full node. Where the archive node diverges, this guide calls out the one flag you need to change.

Step 1: Set up Cardano-db-sync and PostgreSQL

Midnight operates as a Cardano Partnerchain. That means your Midnight node needs a live connection to a Cardano-db-sync instance to track relevant scripts on the Cardano blockchain. This is required. Your node will not function without it.

Clone the midnight-node-docker repository

The midnight-node-docker repository contains the official Docker Compose files for the entire stack.

git clone https://github.com/midnightntwrk/midnight-node-docker
cd midnight-node-docker

Install Docker Engine

If Docker isn’t already installed:

# Add Docker's official GPG key
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg 
  -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository
echo 
  "deb [arch=$(dpkg --print-architecture) 
  signed-by=/etc/apt/keyrings/docker.asc] 
  https://download.docker.com/linux/ubuntu 
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | 
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io 
  docker-buildx-plugin docker-compose-plugin

Verify the installation:

docker --version
sudo systemctl status docker

You should see Active: active (running) in the status output.

Configure the Cardano-db-sync service

Open compose-partner-chains.yml in an editor:

nano compose-partner-chains.yml

Make the following changes:

  1. Replace all default usernames and passwords with values only you know. Do not leave postgres / password123 in a production or publicly reachable environment.
  2. Comment out the Ogmios service unless you are a block producer who needs it. Find the ogmios: block and add # to the start of each line.
  3. Save the file.

Start Cardano-db-sync

docker compose -f compose-partner-chains.yml up -d

This starts Cardano-db-sync and PostgreSQL as background services.

Verify Cardano-db-sync is syncing

Install the PostgreSQL client if you don’t have it:

sudo apt-get install postgresql-client

Connect and run the sync progress query:

psql -h localhost -U your_postgres_user -d cexplorer -p 5432

Inside the PostgreSQL shell:

SELECT 100 * (
    EXTRACT(EPOCH FROM (MAX(time) AT TIME ZONE 'UTC')) -
    EXTRACT(EPOCH FROM (MIN(time) AT TIME ZONE 'UTC'))
) / (
    EXTRACT(EPOCH FROM (NOW() AT TIME ZONE 'UTC')) -
    EXTRACT(EPOCH FROM (MIN(time) AT TIME ZONE 'UTC'))
) AS sync_percent
FROM block;

You’ll see a number between 0 and 100. Cardano-db-sync must reach 100% before your Midnight node can operate correctly. On the Preview Testnet this typically takes several hours depending on your hardware and disk IOPS. Grab a coffee.

Tip: You can keep checking sync progress without entering the shell every time:

docker exec -it db-sync-postgres psql -U your_postgres_user 
  -d cexplorer -c 
  "SELECT 100 * (EXTRACT(EPOCH FROM (MAX(time) AT TIME ZONE 'UTC')) - 
  EXTRACT(EPOCH FROM (MIN(time) AT TIME ZONE 'UTC'))) / 
  (EXTRACT(EPOCH FROM (NOW() AT TIME ZONE 'UTC')) - 
  EXTRACT(EPOCH FROM (MIN(time) AT TIME ZONE 'UTC'))) 
  AS sync_percent FROM block;"

Step 2: Resource requirements

Before you start the Midnight node itself, make sure your machine meets the minimum specs. Running an undersized node is the fastest way to end up stuck on block 1 with peers constantly dropping.

Resource Full node (Preview Testnet) Archive node (Preview Testnet)
CPU 4 cores 4 cores
Memory 16 GB RAM 16 GB RAM
Storage 40 GB SSD (for Cardano-db-sync) + 20 GB for node data 150 GB+ SSD
Disk IOPS 30,000+ 30,000+
Network Stable connection, port 30333 open Stable connection, port 30333 open

Storage note: Low-IOPS disks, including spinning HDDs and cheap cloud volumes, often cause sync stalls and peer disconnections. If your node keeps falling behind the chain tip, check your disk I/O before anything else:

iostat -x 1 5

Look at the %util column. If it stays above 80%, your disk is the bottleneck.

Step 3: Configure and start the full node

Check the current node version

Always use the version listed in the compatibility matrix. At the time of writing, the current node version is 0.22.3 on Preview and Preprod.

Create the Docker Compose file

Create a file called compose-midnight-node.yml in your working directory:

nano compose-midnight-node.yml

Paste the following:

version: "3.8"

services:
  midnight-full-node:
    image: midnightnetwork/midnight-node:0.22.3
    container_name: midnight-full-node
    platform: linux/amd64
    restart: unless-stopped
    ports:
      - "30333:30333"
    volumes:
      - midnight-data:/node
    environment:
      MIDNIGHT_NODE_IMAGE: "midnightnetwork/midnight-node:0.22.3"
      POSTGRES_HOST: "your_postgres_host"
      POSTGRES_PORT: "5432"
      POSTGRES_USER: "your_postgres_user"
      POSTGRES_PASSWORD: "your_postgres_password"
      POSTGRES_DB: "cexplorer"
      DB_SYNC_POSTGRES_CONNECTION_STRING: "psql://your_postgres_user:your_postgres_password@your_postgres_host:5432/cexplorer"
      BASE_PATH: "./node/chain/"
      CFG_PRESET: "testnet-02"
    command: >
      --chain=/res/testnet-02/testnetRaw.json
      --no-private-ip

volumes:
  midnight-data:

Replace all placeholder values (your_postgres_host, your_postgres_user, your_postgres_password) with the credentials you set in step 1.

If you want an archive node instead, add --pruning archive to the command block:

    command: >
      --chain=/res/testnet-02/testnetRaw.json
      --pruning archive
      --no-private-ip

Start the node

docker compose -f compose-midnight-node.yml up -d

The node starts in the background. Give it 30–60 seconds to initialize, then check that it’s running:

docker ps

You should see midnight-full-node with status Up.

Step 4: Monitor the initial sync

Stream logs in real time

docker logs -f midnight-full-node

During a healthy sync, you’ll see lines like:

2024-05-04 12:00:01 ⚙️  Syncing 45.2 bps, target=#187432 (12 peers), best: #8492 (0x1a2b…c3d4), finalized #8448 (0x5e6f…7a8b)

Key things to watch:

  • bps: blocks per second being synced. A healthy node syncs at several dozen bps during initial sync.
  • target=#XXXXXX: the current chain tip.
  • best: #XXXXXX: your local best block. This should climb steadily.
  • finalized #XXXXXX: the last finalized block. This should trail your best block by a small number.
  • (N peers): the number of connected peers. You want to see 5 or more.

Monitor block height

To check your current block height at any point without reading the full log stream:

docker logs midnight-full-node 2>&1 | grep "best:" | tail -1

Or query the node’s RPC endpoint directly (if you’ve also started an RPC node):

curl -s -H "Content-Type: application/json" 
  -X POST 
  -d '{"jsonrpc":"2.0","id":1,"method":"chain_getHeader","params":[]}' 
  http://127.0.0.1:9944 | python3 -m json.tool

The number field in the response is the current best block in hex. Convert it:

printf "%dn" 0x2DB20   # example output: 187168

Check peer count

docker logs midnight-full-node 2>&1 | grep "peers" | tail -5

A healthy node shows 5–50 peers. If you’re consistently seeing 0–2 peers, see the troubleshooting section below.

Step 5: Verify your node is synced and healthy

Once you believe your node has caught up, confirm it’s genuinely at the chain tip rather than just stopped at a stale block.

Check the sync gap

docker logs midnight-full-node 2>&1 | grep "Syncing|Idle" | tail -5

When fully synced, the log line changes from Syncing to Idle:

2024-05-04 14:22:01 💤 Idle (14 peers), best: #187432 (0x1a2b…c3d4), finalized #187428 (0x5e6f…7a8b)

Idle means your node is at the tip and waiting for new blocks. The best block number should match (or be within a few blocks of) the Midnight block explorer.

Compare with the network tip

You can check the current network block height against the Midnight block explorer. If your node’s best block is within 5–10 blocks of the explorer, you’re synced.

Verify P2P port is reachable

From another machine or using a port checking tool:

nc -zv your_server_ip 30333

If the port is closed, your node can’t accept inbound peer connections, which limits your peer count and sync speed. Open it in your firewall:

sudo ufw allow 30333/tcp
sudo ufw reload

Step 6: Troubleshooting common issues

Node is stuck on block 1 (or a very low block number)

This is the most common issue reported in the community. There are three usual causes:

1. Cardano-db-sync hasn’t finished syncing

Your Midnight node requires a fully synced Cardano-db-sync instance. If sync_percent in PostgreSQL is below 100, your node stalls. Check it with the query from step 1, and wait until it reaches 100%.

2. The PostgreSQL connection string is wrong

Double-check your DB_SYNC_POSTGRES_CONNECTION_STRING environment variable. A common mistake is using localhost as the host when PostgreSQL is running in a different Docker container. Use the actual container name or host IP instead.

Test the connection directly:

psql "psql://your_postgres_user:your_postgres_password@your_postgres_host:5432/cexplorer" 
  -c "SELECT count(*) FROM block;"

If this fails, fix your connection string before restarting the node.

3. Node image version mismatch

Running an outdated node image against the current Testnet causes immediate sync failure. Always verify against the compatibility matrix and pull the latest image:

docker pull midnightnetwork/midnight-node:0.22.3
docker compose -f compose-midnight-node.yml up -d --force-recreate

Peers keep disconnecting

If peer counts keep climbing to 5 and then dropping back to 0, check these items:

1. Port 30333 is not publicly reachable

This is the most common cause. Even if Docker is forwarding the port locally, your firewall or cloud security group might be blocking inbound connections. Peers connect to you on port 30333; if they can’t reach it, they time out and disconnect.

Check your firewall:

sudo ufw status

If 30333 isn’t listed as ALLOW, add it:

sudo ufw allow 30333/tcp
sudo ufw reload

If you’re on a cloud VM (AWS, GCP, DigitalOcean), also check your provider’s inbound security group or firewall rules in the web console.

2. The --no-private-ip flag is missing

Without --no-private-ip, your node advertises its private/internal IP address to peers. External peers try to connect to an address they can never reach, fail, and disconnect. Make sure this flag is in your command block.

3. NAT or double-NAT

If your server is behind NAT (common with home servers or some VPS setups), peers can’t initiate connections to you. You either need to set up port forwarding at the router level, or use a VPS with a public IP.

4. Disk I/O bottleneck

High disk utilisation causes the node to fall behind processing blocks, which causes peers to consider it unresponsive and drop the connection. Check with:

iostat -x 1 10

Upgrade to an SSD with 30,000+ IOPS if %util is consistently high.

Node crashes or restarts frequently

Check the logs for the last error before restart:

docker logs midnight-full-node 2>&1 | grep -E "ERROR|WARN|panic" | tail -20

Common causes:

  • Out of memory: check with free -h. If your machine has less than 16 GB RAM, the node may OOM. Add swap as a temporary workaround:
  sudo fallocate -l 8G /swapfile
  sudo chmod 600 /swapfile
  sudo mkswap /swapfile
  sudo swapon /swapfile
  • Docker volume corruption: rare, but possible after a hard shutdown. Recreate the volume:
  docker compose -f compose-midnight-node.yml down
  docker volume rm midnight-data
  docker compose -f compose-midnight-node.yml up -d

Note: this wipes your sync progress and starts from block 0.

Checking connectivity manually

To confirm your node can actually talk to the network:

# Check if P2P port is listening
ss -tlnp | grep 30333

# Test reachability from outside (run on a different machine)
nc -zv your_server_ip 30333

# Check how many peer connections are currently open
docker exec midnight-full-node ss -tn | grep 30333 | wc -l

Keeping your node up to date

Midnight is in active Testnet development. Node versions update frequently, and running an outdated version will cause your node to stop syncing or get rejected by peers.

Subscribe to the Midnight Discord channel on Discord and watch the release notes for new node versions.

When a new version drops, update like this:

# Pull the new image
docker pull midnightnetwork/midnight-node:NEW_VERSION

# Update the version in your compose file
nano compose-midnight-node.yml
# Change 0.22.3 to NEW_VERSION in both the image: and MIDNIGHT_NODE_IMAGE: lines

# Restart with the new image
docker compose -f compose-midnight-node.yml up -d --force-recreate

What to do next

You now have a full node running, synced, and healthy. From here:

  • Set up an RPC node: if you want your DApp to talk directly to your own infrastructure instead of a public endpoint, follow the RPC node setup guide.
  • Explore the DApp quickstart: start building a privacy-preserving DApp on top of the network you’re now running: Getting started.
  • Join the community: the Midnight developer forum and Discord are the best places to ask questions, report issues, and follow network updates.
  • Share your work: if you write about your node setup or share what you built on top of it, tag @midnightntwrk on X and use #MidnightforDevs.

This tutorial reflects Midnight node v0.22.3 on the Preview/Preprod Testnet. Always check the compatibility matrix before updating your setup.

De CSRF a RCE: una visita web cuesta una shell en OpenYak

OpenYak en funcionamiento

OpenYak v1.0.8 arranca un servidor local en 127.0.0.1:19141 sin autenticación, sin validación de Origin y sin protección CSRF. Una sola visita a una web maliciosa basta para que un atacante remoto ejecute comandos arbitrarios en la máquina de la víctima a través del agente bash de la propia aplicación. Reportado al mantenedor, parcheado en v1.1.3.

0x01 — Por qué OpenYak

Llevaba tiempo dándole vueltas a sentarme un fin de semana con un objetivo claro: encontrar una vulnerabilidad real en una aplicación real. Nada de CTF, nada de laboratorios. Algo que la gente instala, ejecuta y deja corriendo en su máquina mientras navega.

El criterio de selección fue corto:

  1. Aplicación de escritorio que exponga una API local. Esa superficie está históricamente mal cuidada.
  2. Manejo de LLMs — un dominio que conozco bien, donde sé qué buscar.
  3. Base de usuarios suficiente para que la investigación tenga impacto real.

OpenYak cumplía las tres. Aplicación open-source, expone una API REST en 127.0.0.1:19141, gestiona chats con modelos de varios proveedores, y — la cereza del pastel — incluye un agente capaz de ejecutar comandos bash. El tipo de aplicación donde, si las cosas no están bien atadas, el premio es gordo.

OpenYak en funcionamiento

0x02 — Reconocimiento

Lo primero, lo de siempre: instalar, levantar Burp y mapear endpoints. Cuando una app local expone HTTP, el reconocimiento se vuelve trivial — todo el tráfico pasa por tu proxy.

Lo que llamó mi atención casi de inmediato fue la ausencia total de cualquier mecanismo de autenticación: nada de Authorization, nada de cookies, nada de API keys, nada de tokens en headers. Cada request era un POST o GET limpio contra localhost.

Tráfico interceptado mostrando ausencia de autenticación

Bonus: el servidor exponía Swagger en producción.

Método Endpoint Descripción Impacto inmediato
POST /shutdown Apaga el servidor 🔴 DoS
POST /api/chat/prompt Envía prompt al agente con permisos configurables 🔴 RCE
GET /api/sessions Lista sesiones activas 🟠 Info disclosure
GET /api/stream/{id} Recupera el output del stream de respuesta 🟠 Exfiltración
* /api/files/* CRUD completo sobre el filesystem 🔴 Lectura/escritura arbitraria

El que más me interesó fue /api/chat/prompt. El payload aceptaba un campo permission_presets donde el cliente — no el servidor — declaraba qué permisos tenía esa interacción. Entre ellos, bash: true. Es decir: el cliente le dice al servidor qué puede hacer. Esto, combinado con la ausencia de autenticación, ya no es un bug puntual: es un agujero de diseño.

A primera vista, el problema parecía contenido. La API bindea a 127.0.0.1, así que un atacante remoto no puede llegar a ella directamente. Pero esa frase contiene la trampa de siempre:

“No puedo llegar desde fuera, ¿pero qué pasa si consigo que el navegador del usuario llegue por mí?“

0x03 — Cuatro grietas que se convierten en una

Esto no es un bug aislado. Es una cadena de decisiones de diseño que individualmente serían malas prácticas pero que, juntas, forman una superficie de ataque crítica.

Grieta #1 — Sin autenticación (CWE-306)

El servidor no implementa ningún tipo de autenticación. Cualquier proceso o página web que pueda alcanzar 127.0.0.1:19141 tiene acceso completo a todos los endpoints, con los privilegios del usuario que ejecuta la aplicación.

Si existiera cualquier token de sesión, aunque fuera el más simple, los siguientes tres flaws serían inocuos. Esta es la raíz.

Grieta #2 — Origin no se valida (CWE-346)

El servidor no inspecciona el header Origin de las requests entrantes. Una request originada desde https://evil.attacker.com se procesa de forma idéntica a una originada desde la propia UI de OpenYak.

Grieta #3 — CORS ausente (CWE-942)

No hay headers CORS configurados. El detalle clave que mucha gente pasa por alto: que el navegador bloquee la lectura de la respuesta no implica que la request no se procese. Para vectores como POST /shutdown no necesito leer la respuesta — el daño está hecho en el momento en que el servidor recibe y atiende la petición. Y, como veremos en breve, lo mismo aplica al RCE.

Grieta #4 — Content-Type permisivo (CWE-352)

El servidor acepta cualquier Content-Type: application/json, text/plain, valores absurdos. Y aquí es donde la cadena se cierra: con text/plain permitido, el camino al CSRF “clásico” via <form> HTML sin preflight CORS queda abierto. (Spoiler: en este caso concreto este punto no terminó siendo el vector definitivo, pero ya volveremos a esto.)

El cuadro completo

Víctima abre evil.attacker.com en el navegador
           │
           ▼
  JS / form hace request a 127.0.0.1:19141
           │
           ▼
  API la procesa — no auth, no Origin check
           │
           ▼
  Agente AI ejecuta bash → RCE

0x04 — El obstáculo: Private Network Access

Aquí es donde la cosa se pone interesante.

Chrome introdujo en 2021–2022 una mitigación llamada Private Network Access (PNA) pensada precisamente para escenarios como este. La idea es simple: una página pública no debería poder hacer requests directas a 127.0.0.1 o rangos privados sin autorización explícita del servidor de destino.

PNA funciona con un preflight OPTIONS:

OPTIONS / HTTP/1.1
Host: 127.0.0.1:19141
Origin: https://evil.attacker.com
Access-Control-Request-Private-Network: true

Si el servidor local no responde con:

Access-Control-Allow-Private-Network: true
Access-Control-Allow-Origin: https://evil.attacker.com

…el navegador bloquea la request real antes incluso de enviarla.

OpenYak, evidentemente, no envía ese header. Lo cual, en teoría, debería protegerlo del ataque desde Chrome.

En teoría.

0x05 — El camino al exploit (incluyendo lo que no funcionó)

Intento 1 — <form> con enctype="text/plain"

El primer intento fue el truco clásico: un formulario HTML con enctype="text/plain", partiendo el JSON entre el atributo name y value del input para construir el body válido sin que el navegador lance preflight CORS.

<form action="http://127.0.0.1:19141/api/chat/prompt"
      method="POST"
      enctype="text/plain">
  <input type="hidden"
         name='{"session_id":"pwn","text":"whoami","agent":"build","permission_presets":{"bash":true},"x":'
         value='1}'>
</form>
<script>document.forms[0].submit();</script>

Resultado: falla. Aunque el Content-Type no estaba validado, el parser del lado servidor terminaba fallando, y la concatenación name=value introducía caracteres extra creo que rompían el parseo de JSON.

Probablemente exista un bypass para este vector concreto (la combinatoria de cómo se interpretan name/value en text/plain da bastante margen), pero a estas alturas ya tenía un camino más limpio en mente.

Intento 2 — DNS rebinding

El siguiente movimiento natural fue probar DNS rebinding. Configuré un dominio (loopback.creathem.one) que resuelve alternativamente a una IP pública y a 127.0.0.1. Este tipo de truco históricamente ha funcionado para evadir restricciones basadas en origin y host.

Resultado: falla de nuevo, esta vez por una razón distinta. PNA no decide en función del nombre del host, sino del target IP space tras la resolución DNS. Chrome detecta que la respuesta DNS apunta a un rango privado y dispara el preflight PNA antes de la petición real. El servidor responde sin el header esperado y Chrome corta la conexión.

Gracias a esta investigacion, he abierto otra para encontrar bypasses del PNA.

El movimiento final — Firefox

A esta altura tocaba parar y replantear. ¿Qué supuestos había estado asumiendo?

“El target es Chrome.“

Pero Firefox no implementa PNA. Y Firefox no es exactamente un navegador minoritario. Para confirmar la cadena completa con un PoC limpio basta con que la víctima visite la página maliciosa desde Firefox — y a partir de ahí todo el flujo funciona sin obstáculos.

Vale la pena dejar tres puntos claros aquí:

  1. PNA tiene un historial de bypasses desde su introducción. No es una mitigación que sustituya a las protecciones reales (auth + Origin validation). Es una defensa en profundidad, y como tal, no debería ser el único muro.
  2. POST /shutdown se puede disparar desde cualquier navegador, incluido Chrome, simplemente porque no requiere leer la respuesta y se dispara como <form> simple sin preflight especial. El DoS es universal.
  3. Cualquier otra vulnerabilidad SSRF en el sistema (en otra aplicación instalada en la máquina, por ejemplo) que permita disparar peticiones HTTP arbitrarias termina enlazando contra OpenYak sin esfuerzo, saltándose PNA por completo.

0x06 — PoC: visita una web, ejecuta un comando

Con todas las piezas en su sitio, la PoC se reduce a esto:

// CSRF → RCE en OpenYak v1.0.8
// La víctima abre esta página en Firefox y el comando se ejecuta en su máquina.
// Nota: no necesitamos leer la respuesta — el daño está hecho en el momento
// en que el servidor procesa la request. CORS no nos molesta.

fetch("http://127.0.0.1:19141/api/chat/prompt", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    session_id: "pwned-session",
    text: "Ejecuta el siguiente comando: <CUALQUIER COMANDO>",
    model: "openai/gpt-4.1-mini",
    provider_id: "openrouter",
    agent: "build",
    attachments: [],
    permission_presets: { bash: true }   // 🚩 el cliente dicta los permisos
  })
});

Tres detalles importantes:

  • No leemos la respuesta. No nos hace falta. En el momento en que el servidor recibe el prompt y el agente decide ejecutarlo, el RCE ya ocurrió. CORS y mode: "no-cors" son irrelevantes para este vector.
  • No hay ninguna interacción adicional de la víctima. Basta con que abra la página.
  • El permission_presets lo dicta el atacante. El servidor honra alegremente lo que le diga el cliente.

RCE confirmado — el agente ejecuta el comando inyectado

Output del comando llegando desde la máquina víctima

¿Y las API keys?

Por fortuna (porque viendo el resto del diseño dudo seriamente que sea por elección consciente), el endpoint que lista los providers devuelve las API keys enmascaradas con asteriscos. Eso cierra una vía de exfiltración directa.

Pero /api/files/* sigue abierto. Listar, leer y escribir ficheros del sistema, todo sin autenticación. En una explotación realista, nada impide leer los ficheros de configuración donde las claves están almacenadas en disco — o, para no complicarse, dropear un binario y persistir.

Endpoints de /api/files/* expuestos sin autenticación

0x07 — Impacto

Esta vulnerabilidad afecta a cualquier usuario de OpenYak v1.0.8 o anterior que tenga la aplicación corriendo mientras navega.

Vector Severidad
RCE completo via agente bash 🔴 Critical
DoS instantáneo via POST /shutdown 🔴 High
Lectura/escritura arbitraria de ficheros 🔴 Critical
Exfiltración de historial de chats y sesiones 🟠 High
Escalada a persistencia / movimiento lateral 🔴 Critical

En términos de complejidad, esto es nivel easy de HackTheBox. No hay nada exótico — no hay heap, no hay race conditions, no hay primitivos extraños. Lo que lo hace crítico es el impacto, no la dificultad.

0x08 — El fix

Reporté la vulnerabilidad al mantenedor con writeup técnico completo y PoC. La respuesta fue rápida y profesional, y el fix llegó en v1.1.3 (commit 1c54ae3).

La mitigación correcta:

Validar Origin a nivel de middleware ataca la grieta #2, que era la raíz explotable. La autenticación tambien se introdujo según el mantenedor.

0x09 — Timeline de disclosure

El desarrollador lo resolvio casi instantaneamente.
No obstante dejo bastante que desear a la hora de publicarlo y asignar CVE, tardo unas 5 semanas desde la publicación del parche.

0x0A — Reflexión final

Este fue el primer repositorio al que me senté a auditar con esta metodología. Encontrar la vulnerabilidad no me llevó nada — y eso, francamente, me preocupa más que el bug en sí.

La aplicación está cuidada en muchos aspectos: la UI funciona, el código compila, los tests pasan, la integración con providers de LLM está bien resuelta. Pero el modelo de amenaza del servidor local no parece haberse considerado seriamente. Y eso me lleva a la pregunta inevitable:

¿Cuántas aplicaciones modernas, especialmente las que están naciendo al calor de la ola de IA, exponen servidores locales con esta misma combinación exacta de errores?

La respuesta empírica, después de un par de tardes mirando alrededor, es: muchas. La superficie de ataque “servidor en localhost que asume que localhost es seguro” se está multiplicando, y el navegador es cada vez peor compañero de viaje para esa asunción.

Validar Origin es trivial. Implementar un token de sesión que el servidor pasa al frontend al arrancar y exige en cada request es trivial. Y sin embargo seguimos viendo el mismo patrón.

El mantenedor de OpenYak hizo lo correcto: respondió rápido, reconoció el bug, lo arregló bien y abrió el camino al CVE. Ojalá fuera la norma.

Esta investigación se realizó bajo responsible disclosure coordinado con el mantenedor del proyecto. La vulnerabilidad fue reportada antes de cualquier publicación pública y el fix estaba disponible antes de este writeup. No se realizaron pruebas sobre instalaciones de terceros sin consentimiento.

arturo0x90 · Independent Security Researcher · CVEs · Responsible Disclosure

Why We Built a Faster Wiki

Why We Built a Faster Wiki

I first tried to build a wiki in 2010. Working prototype, decent idea, abandoned prematurely. Sixteen years later, the market has barely moved. The tools got heavier, the page loads got slower, and everyone just… accepted it.

I didn’t accept it. So I built Raccoon Page.

The problem nobody fixed

Here’s what happens in most companies: someone writes documentation. It goes into the wiki. A week later, a new hire needs that documentation. They open the wiki. They wait. The page loads in three, four, five seconds. They try search. The results are irrelevant. They close the tab and walk over to a coworker’s desk.

The wiki is now a graveyard. The knowledge is there, technically, but nobody uses it because the tool is slower than asking Dave.

This isn’t a people problem. It’s a tools problem. And it affects more than just software teams — every industry that relies on documentation feels this. Financial analysts looking up compliance docs during live calls. Operations managers referencing SOPs in a warehouse on a mediocre tablet. Support agents pulling up knowledge articles while a customer waits in silence.

The wiki market has been stagnant for fifteen years. The dominant tools are either slow and enterprise-bloated, or flexible-but-sluggish all-in-one workspaces that treat the wiki as an afterthought. Nobody shipped a wiki that’s both fast and pleasant to use.

What Raccoon Page is

Raccoon Page is a wiki. Not a workspace, not a database, not a project management tool that also has pages. A wiki. It does what your current wiki does — pages, hierarchy, search, comments, version history, permissions — except it doesn’t waste your time doing it.

Pages load near-instantly. There are no spinners on basic navigation. Search is fast and typo-tolerant. If you’re a keyboard person, there’s a command palette, shortcuts for everything, and slash commands in the editor. If you’re not a keyboard person, it’s still just a clean, fast wiki — no learning curve required.

The speed isn’t an accident. It’s the first architectural decision I made and the one that shaped everything after it. PostgreSQL with careful query design instead of an ORM that generates page-long joins. Server-side rendering of the critical path. No client-side data fetching waterfalls on page load. Every technical choice was filtered through: “Does this keep the page load fast?”

Why import matters more than features

I’ve watched enough SaaS launches to know the pattern: team evaluates new tool, likes it, starts a trial, realizes migrating their existing content would take weeks, abandons the trial. The switching cost kills adoption, not the product quality.

So I built import before I built templates, before threaded comments, before half the features on my roadmap. Raccoon Page imports from Confluence (XML export), Notion (Markdown/CSV export), and Obsidian (vault zip). The whole process takes about ten minutes for a typical team wiki. You also export to Obsidian-compatible Markdown, so you’re never locked in.

This was originally a month-two feature. I moved it to launch because I realized the import story changes the entire sales pitch. It’s not “try our wiki and rebuild your content.” It’s “bring your content and see it load faster.”

What we built, what we deferred

Real-time co-editing shipped at launch. Multiple people can work on the same page simultaneously — cursors, selections, and edits all sync live. This was originally deferred, but we pulled it forward because every competitor has it and it’s table-stakes for team adoption. No whiteboards yet — that’s still on the roadmap.

Authentication supports both email/password and OAuth (Google, GitHub), so your team can sign in the way they already work. Dark theme is the default, with a light mode toggle for those who prefer it.

There’s no AI search yet. I deliberately deferred it. AI-powered search sounds impressive in a feature matrix, but it costs real money per query — money that eats margin at $8/user/month. Raccoon Page ships with PostgreSQL full-text search that’s fast and actually works. AI synthesis will come later as a premium feature, once there’s revenue to fund it.

There are no Slack integrations, no API, no SAML SSO. All planned. None necessary for the first ten teams to get value.

Every feature we shipped or deferred was a deliberate trade. The filter was simple: does it reduce churn, drive conversion, or remove a barrier to adoption? If not, it can wait.

The honest competitive picture

Confluence is the market leader and it deserves respect. Deep Jira integration, enterprise compliance, decades of trust. If your organization runs on Jira and has a formal procurement process, Confluence is probably staying. I’m not here for that fight.

Notion is beautiful and flexible. If you need a workspace that does everything — databases, projects, docs, wikis — Notion is genuinely good at being everything. It’s less good at being a fast wiki specifically, and the performance at scale is a known pain point, but it earns its 100 million users.

Outline is the closest thing to what I’m building — open-source, fast, clean. If you want to self-host your wiki and have the infrastructure skills to do it, Outline is a solid choice. Raccoon Page is for teams who want the speed without the operational overhead — managed SaaS, one-click import, ready to go.

I’m not building the wiki for everyone. I’m building it for teams where the documentation is supposed to help but the tool gets in the way instead.

The boring technical details (for those who care)

The backend is C# on .NET, which surprises people who expect a wiki startup to be written in TypeScript or Go. I have eight years of Go experience and chose C# anyway — better AI code generation quality, a richer type system for the kind of dynamic content a wiki handles, and a more stable package ecosystem. The right tool for the job isn’t always the familiar one.

The editor is TipTap, built on ProseMirror. Headless, extensible, battle-tested. Headings, lists, code blocks with syntax highlighting, callouts, tables, images, slash commands. It’s not trying to be Google Docs — it’s trying to be a good wiki editor that stays out of the way.

The database is PostgreSQL. Not because it’s trendy, but because 80% of a wiki’s data model is relational (users, permissions, spaces, memberships) and the remaining 20% (page content) fits perfectly in JSONB columns. MongoDB would have meant reinventing relational integrity for the majority of the schema.

The whole thing runs on DigitalOcean for about $100/month. No Kubernetes cluster, no microservices, no distributed systems. One process, one database, one server. It’s enough for thousands of users and it’s simple enough for one person to operate at 3 AM when something breaks.

A small team, one bet

We’re two co-founders. No funding, no investors. The code is roughly 80-90% generated by AI (Claude Code, specifically), which is the only reason a tiny team can ship a full-featured wiki in a few months. I write the architecture and make the technical decisions. My co-founder handles testing, and increasingly the legal and marketing side. The AI writes the bulk of the implementation. It’s a force multiplier that didn’t exist when I first tried this in 2010.

This is a bootstrapped bet with a 12-18 month competitive window. AI-native wiki tools are coming — probably dozens of them. The question is whether a fast, focused, well-imported wiki can establish a foothold before the market gets crowded.

We think it can. Not because the technology is revolutionary, but because the problem is so obvious and so neglected that even a competent solution wins. Your wiki should not be slower than asking a coworker. That’s it. That’s the whole pitch.

Try it

Raccoon Page is live at raccoon.page. Free tier: 3 users, 1 space, 100 pages. Enough to decide if speed matters to you.

If you’re on Confluence, Notion, or Obsidian, bring your content. A typical team wiki imports in about ten minutes. See your own pages load fast, not a demo with cherry-picked content.

If it’s not for you, the export button is right there. No lock-in, no tricks.

Building a Browser-Based Inkarnate Alternative for D&D Battle Maps

Building a Browser-Based Inkarnate Alternative for D&D Battle Maps

When people search for an Inkarnate alternative, they are usually not asking for one single thing.

Some want a beautiful fantasy world map.

Some want a regional map for a novel.

Some want a city map.

But a lot of Dungeon Masters want something much more practical:

“I need a playable D&D battle map for this week’s session, and I do not want to spend the whole evening fighting a heavy tool.”

That is the problem I am building around with RPGMapEditor.com.

It is a browser-based RPG map editor focused on encounter-scale battle maps for D&D, TTRPGs, Roll20, Foundry VTT, and printable tabletop sessions.

Not a full fantasy illustration suite.

Not a worldbuilding atlas.

Not a replacement for every cartography tool.

A faster way to create readable, playable battle maps in the browser.

Quick answer: is RPGMapEditor.com an Inkarnate alternative?

Yes, but only for a specific workflow.

RPGMapEditor.com is an Inkarnate alternative if your main goal is to create grid-based battle maps for D&D or other tabletop RPG sessions.

It is not trying to replace Inkarnate for polished world maps, regional fantasy maps, or large illustrated map galleries.

The difference is simple:

Use case Better fit
Fantasy world map Inkarnate or a broader fantasy map tool
Regional campaign map Inkarnate-style tools
Fast D&D battle map RPGMapEditor.com
Browser-based encounter prep RPGMapEditor.com
PNG export for Roll20 / Foundry background use RPGMapEditor.com
Highly polished fantasy illustration Broader art-first map tools

If your weekly pain is encounter production, not fantasy illustration, a narrower tool can be better.

Why another RPG map editor?

Most DMs do not need a perfect map.

They need a map that is:

  • readable
  • fast to make
  • aligned to a grid
  • easy to revise
  • exportable to a VTT
  • good enough for actual play

That sounds obvious, but many map tools slowly become asset browsers, art suites, marketplace platforms, or full VTT automation systems.

Those can be powerful.

They can also be slow when the party unexpectedly goes into a cave, tavern, forest road, sewer, bridge, or ruined chapel you did not prepare.

The bet behind RPGMapEditor.com is that a focused editor can win by reducing decisions.

Open the browser.

Start from a blank canvas or demo map.

Paint terrain.

Place props.

Check the grid.

Export PNG.

Use it in Roll20, Foundry VTT, or at the table.

That workflow matters more than having every possible feature on day one.

What RPGMapEditor.com supports today

The current shipped workflow focuses on practical battle map creation:

  • browser-based map editing
  • terrain painting
  • props and stamps
  • grid-based battle maps
  • demo projects
  • PNG export
  • free accounts with limited saved maps
  • optional Studio plan for more saved maps and sharing features

That means the tool is already useful for a common tabletop workflow:

  1. Create a tactical map.
  2. Export it as a PNG.
  3. Upload it into Roll20 or Foundry VTT.
  4. Align the grid inside your VTT.
  5. Run the encounter.

This is intentionally simple.

PNG export is not glamorous, but it is one of the most universal handoff formats for virtual tabletops.

What RPGMapEditor.com does not claim yet

This matters because fake comparison articles are useless.

RPGMapEditor.com does not currently claim to export:

  • native Foundry VTT scenes
  • Roll20 dynamic lighting
  • automatic walls
  • automatic doors
  • full VTT automation data

For now, the product focuses on the visual battle map.

Your VTT still handles tokens, walls, lighting, fog, automation, initiative, sheets, and live gameplay.

That is not a weakness if the user understands the scope.

It becomes a weakness only if the marketing lies.

Why browser-based map editing matters

Desktop map tools can be powerful, but they create friction:

  • installation
  • updates
  • local files
  • asset folders
  • OS compatibility
  • device switching
  • heavier onboarding

A browser-based RPG map editor removes some of that friction.

For many DMs, the best tool is not the one with the longest feature list.

The best tool is the one they actually open on a weeknight when the session is tomorrow.

That is where browser-based editing has a real advantage.

You can open a demo project, test the editor, and understand the workflow before committing to an account or paid plan.

The real comparison: art suite vs encounter tool

Inkarnate is known for fantasy map creation across multiple styles.

That is a strength.

But broad tools often serve broad use cases:

  • world maps
  • regional maps
  • city maps
  • fantasy art scenes
  • map galleries
  • cloneable maps
  • large asset libraries

RPGMapEditor.com is narrower.

It is aimed at:

  • D&D battle maps
  • grid-based tactical scenes
  • encounter prep
  • Roll20 PNG workflows
  • Foundry VTT background map workflows
  • browser-first editing
  • quick iteration

That narrower positioning is the product strategy.

Not “more features than Inkarnate.”

Not “better at everything.”

Just faster battle map prep for the use case where speed matters.

A simple example workflow

Imagine you need a forest ambush map for tomorrow’s session.

You do not need a masterpiece.

You need:

  • a road
  • trees
  • cover
  • difficult terrain
  • readable grid spacing
  • maybe a ruined cart
  • maybe rocks or elevation hints
  • a clean export

In RPGMapEditor.com, the intended workflow is:

  1. Start from a blank map or demo.
  2. Paint the main playable terrain.
  3. Add props that affect tactics.
  4. Keep the grid readable.
  5. Export a PNG.
  6. Import it into Roll20 or Foundry VTT.
  7. Add tokens, walls, lighting, and gameplay details inside the VTT.

That is the practical DM workflow.

The map does not need to win an art contest.

It needs to make combat understandable.

Who should try RPGMapEditor.com?

RPGMapEditor.com is worth testing if you are:

  • a Dungeon Master making regular battle maps
  • a GM running grid-based TTRPG encounters
  • preparing maps for Roll20
  • preparing scene backgrounds for Foundry VTT
  • tired of installing desktop tools for simple maps
  • looking for a lightweight browser-based RPG map editor
  • comparing Inkarnate alternatives specifically for battle maps

It is probably not the right first choice if you mainly want:

  • a giant world map
  • a polished regional fantasy map
  • advanced illustration tools
  • a huge established asset marketplace
  • automatic VTT walls and doors today

That distinction is important.

A focused tool should be judged by the job it is designed to do.

Why I am building it this way

The product philosophy is simple:

A battle map editor should help DMs reach a playable map faster.

That means the important questions are not only technical.

They are product questions:

  • Can a new user understand the editor quickly?
  • Can they make a usable map in one sitting?
  • Can they export without confusion?
  • Can they reopen and revise maps later?
  • Can the grid stay readable?
  • Can the map survive real VTT usage?
  • Does the workflow reduce prep stress?

Those questions matter more than adding another shiny feature.

RPGMapEditor.com vs Inkarnate: the honest version

Here is the honest comparison.

Use Inkarnate or a broader fantasy map tool when you want a polished fantasy illustration workflow, world maps, regional maps, city maps, or access to a mature asset ecosystem.

Use RPGMapEditor.com when you want a focused browser-based battle map editor for encounter prep, terrain painting, props, grid maps, saved projects, and PNG export for VTT use.

That is the lane.

And for many DMs, that lane is enough.

Try it

You can try the browser-based editor here:

RPGMapEditor.com

Useful pages:

If you are a DM, the best test is simple:

**Create one encounter map.

Export it.

Use it in your actual VTT.

If it saves prep time, the product is doing its job.**

Apache Kafka

What is Apache Kafka

Main Concepts

Event

  • A record that something actually happened, i.e., a message
    When you read/write to Kafka, you do this in the form of events

Producers
Use the producer api to produce and send events

Topics

  • Events are organized and durably organized into topics
  • Topics are categorized into which the events are published that trigger other actions

Consumers
Those who subscribe (read and process) the events sent by producers
Real time processing(Streams)
Partitions for Scalability and Performance
Consumer Groups for scalable Message Consumption

Kafka Brokers

  • A server that stores data(messages), topics, and manages message distribution to consumers
  • Fault tolerance since topic partitions are distributed across multiple brokers
  • Each partition has a leader broker and multiple replicas
    Apache Kafka – stores messages on disk for a certain configurable retention period
    Benefits of data retention
  • Real-time processing of data
  • consumers can read multiple times they want
  • replay of messages, debugging of historical data
    Apache Kafka focuses on event streaming and long-term data retention
    Optimized for high throughput

Zookeper
Cluster Management
maintains a registry of all brokers in the cluster

Leader Election
Each Kafka partition has a leader broker
Metadata Configuration
And traditionally, Kfaka used an external tool Zookeeper for managing metadata and coordination of tasks in a distributed system
External dependency for Kafka
The newer versions of Kafka from v3 now use KRaft
Kafka version 3.0 and above

  • removed the dependency for zookeeper and introduced KRaft
  • And metadata is now being managed natively within Kafka brokers
  • Raft consensus Algorithm for leader election