How I Built Factcovery in 2 Days with Claude — 0 Lines of Code Written by Me

Here’s what Factcovery actually looks like.

Website

Mobile App

App coming soon, pending store review

Lighthouse Report

Honestly, hitting a good Lighthouse score isn’t rocket science — but I’m pretty confident AI will nail it more consistently than most of us would on our own.

Factcovery is live at factcovery.com. The mobile app is coming soon — currently pending store review. Have you tried building this way? I’d love to hear how it went in the comments.

First things first — this is NOT vibe coding

There’s a way most people use AI that looks something like this: open Claude or ChatGPT, type “build me a todo app”, get code back, paste it in, pray it works. When it breaks, paste the error back in. Repeat forever.

That’s vibe coding. It works fine for small throwaway tasks. But for a real product — with a web app, a REST API, a mobile app, and CI/CD — it falls apart fast.

What I did was a little different. I used Claude as an AI Agent, which means before Claude writes a single line of code, we plan together first.

The core rule: Plan in plain English → Review → Then build.

Nothing fancy. But that one extra step changed everything about how the project turned out.

What is Factcovery?

Factcovery is an app that aggregates and generates interesting “facts” by topic — bite-sized, accurate, shareable pieces of information you’d want to send to a friend.

The final stack:

  • Web: NuxtJS
  • API: Django REST Framework
  • Mobile: Flutter
  • Infrastructure: GitHub Actions + VPS

Total time: ~2 days.

Part 1 — Web with NuxtJS: Plan first, code second

The first step is never code — it’s a conversation

Instead of saying “build me a web app about facts”, I started with an actual conversation:

My prompt:
“I want to build a web app called Factcovery — where users can discover interesting facts by topic. Target users are 20–35 year olds who like quick reads and mostly browse on mobile. Before writing any code, help me list all the screens we need, the main user flows, and suggest a folder structure for NuxtJS. No code yet — just the plan.”

Claude came back with a clear plan: a list of screens, a user journey from app open to sharing a fact, and a sensible folder structure. I read it, adjusted what didn’t feel right, and only then gave Claude the green light to start coding.

This is the key point: you make the decisions, Claude does the execution.

UI — Describe the feeling, not the pixels

I didn’t say “blue button, border-radius 8px”. I said:

My prompt:
“The UI of Factcovery should feel like a modern science magazine — clean, has depth, not too serious. Dark background with amber yellow as the accent color. Typography needs to be readable on mobile. Please propose a simple design system: color palette, font pairing, spacing scale. Then apply it to the homepage.”

The result? A consistent design system from day one — not something patched together screen by screen.

Multiple Languages — Plan it from the start

Retrofitting i18n into an existing codebase is painful. This time I asked Claude to plan the i18n strategy before the project structure was even finalized:

My prompt:
“This project needs to support English and Vietnamese from day one. Propose how to organize translation files in NuxtJS, a naming convention for keys, and a workflow for adding new languages later without having to refactor. No code yet — just the approach.”

Lighthouse — Build for performance, don’t fix it later

Instead of waiting until the app was done to find out the Lighthouse score was bad, I asked Claude upfront:

My prompt:
“For a NuxtJS app that displays a lot of text and images, what are the most common Lighthouse issues? Create a checklist of things to implement from the start to hit 90+ on both mobile and desktop.”

Claude returned a concrete checklist. I implemented it while building — not after.

Part 2 — Django Backend: Read the frontend, then write the API

A reverse workflow — that actually makes sense

Instead of designing the API first and making the frontend conform to it, I did the opposite: I used the frontend code itself as input for Claude to infer the data model.

My prompt:
“Here are my Nuxt components and pages [paste code]. From these, infer the Django models needed, the relationships between them, and the minimum API endpoints required for the frontend to work correctly. Output in this order: Models → Serializers → URLs.”

The result was a backend that was in sync with the frontend from the very beginning — no discovering missing fields after the fact.

Crawl & Generate Fact Pipeline

This was the most logic-heavy part. I spent the most time on the planning step before letting Claude write anything:

My prompt:
“I need a pipeline: crawl news sources → extract content → use AI to summarize into a short ‘fact’ → save to DB → expose via API. Design the architecture for this pipeline: what Django models are needed, what task queue to use, error handling strategy, and how to schedule it to run periodically. No code yet — just the architecture in text form.”

After reviewing and tweaking the architecture, I had Claude implement it piece by piece.

Part 3 — Mobile with Flutter: Know when to pivot

Tried React Native — and recognized it wasn’t the right fit

I started with React Native because I was comfortable in the JavaScript ecosystem. Claude scaffolded the project, I ran it, and the performance wasn’t where I wanted it — animations felt sluggish, and the structure was more complex than a relatively simple app needed.

Instead of trying to fix it, I made the call to pivot. This is where human judgment matters more than any prompt.

Switching to Flutter

With Flutter, I started fresh — the right way:

My prompt:
“I’m building the Factcovery mobile app with Flutter. The app has these screens: [list]. Please propose: project structure, a state management solution that fits this app, naming conventions, and how to organize the theme and styles. Explain the reasoning behind each choice.”

Claude didn’t just give me a structure — it explained why. I read it, understood the reasoning, and agreed before moving forward.

Push Notification System

My prompt:
“I need push notifications for the Flutter app: notify users when there are new facts on topics they follow. Plan the entire flow: from user subscribing to a topic → backend triggering the notification → Flutter receiving and displaying it. Include background handling. Plan first, code after.”

Part 4 — CI/CD: GitHub Actions deploying to VPS

This is the part that intimidates a lot of people — but with the “plan first” approach, it’s straightforward:

My prompt:
“I have a VPS running Ubuntu and my code is on GitHub. When I push to main, I want to automatically: SSH into the VPS, git pull, run docker-compose up to restart services, then run a health check. Plan this workflow step by step, then write the complete .github/workflows/deploy.yml file. Also suggest how to set up SSH keys securely for GitHub Actions.”

Claude wrote the full YAML config, the deploy script, and a guide for setting up secrets. I just read it, understood the flow, and followed the instructions.

What I learned

✅ What worked really well

1. Always ask for a plan before any code
This is rule number one. Any task more complex than “fix this specific bug” should start with a planning conversation. Spending 5 extra minutes on a plan saves 2 hours of debugging.

2. Use the output of one step as input for the next
Frontend code → input for backend design. Backend API spec → input for mobile app. Everything connects in a logical chain.

3. Ask Claude to explain its reasoning
Add “explain why” to your prompts. You don’t need to read every line of code, but understanding the reasoning lets you review properly and make real decisions.

4. Break tasks into small pieces — don’t prompt everything at once
One giant prompt = mediocre output across the board. One focused prompt per step = higher quality output that’s actually reviewable.

⚠️ Where human judgment is still essential

  • Deciding to pivot (like switching from React Native to Flutter): AI can’t sense that something “feels off” in the context of your specific project.
  • Reviewing architecture before implementation: Claude will suggest something reasonable, but you understand the business requirements better than it does.
  • Choosing trade-offs: When Claude presents multiple options, you’re the one who decides — not Claude.

The real cost of building with AI

Let’s talk about something people rarely mention in posts like this — how much it actually costs.

I’m on Claude Pro ($20/month). For a normal day of work, that’s more than enough. But building an entire product in 2 days — web, API, mobile, and CI/CD all at once — is not a normal day. I hit the usage limit. Multiple times.

When that happened, I switched over to minimax 2.5 to keep the momentum going – It’s free. It handled most tasks just fine, but there were moments where I could feel the difference — especially on the heavier architecture planning prompts where Sonnet tends to reason a bit deeper and stay more consistent across a long context.

So if you’re planning something like this: either budget for API usage on top of your Pro subscription, or be strategic about when you use the big prompts. Save the architecture sessions for when you have the strongest model available. Don’t waste it on boilerplate.

Total cost was still a fraction of what any freelancer would charge for the same scope. But it wasn’t free — just worth being upfront about.

The takeaway

Factcovery wasn’t built by a developer who’s great at writing code. It was built by someone who knows how to work with AI intentionally.

The difference isn’t in the prompts or the tools. The difference is in the mindset: you are the product manager of your own project, and Claude is the best engineer you’ve ever worked with — as long as you brief them clearly enough.

If you’re curious about anything — the project structure, the exact prompts, the mistakes I didn’t write about — just leave a comment. I’ll answer everything. Building in public means sharing the messy parts too.

React Server Components Have a Free Data Fetching Model — No More useEffect Waterfalls

useEffect → fetch → loading spinner → another useEffect → another fetch → another spinner. React Server Components end this waterfall permanently.

What Are React Server Components?

RSCs run exclusively on the server. They can directly access databases, file systems, and APIs — then send the rendered HTML to the client. No loading spinners. No client-side fetch calls.

Why RSCs Change Everything

1. Direct Database Access in Components

// This component runs on the server — never ships to the browser
async function UserProfile({ userId }: { userId: string }) {
  const user = await db.users.findUnique({ where: { id: userId } });
  const posts = await db.posts.findMany({ where: { authorId: userId } });

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
      <PostList posts={posts} />
    </div>
  );
}

No API route. No useEffect. No loading state. The HTML arrives ready.

2. Zero Client-Side JavaScript

// Server Component — 0 KB shipped to browser
async function ProductCatalog() {
  const products = await db.products.findMany({
    where: { active: true },
    orderBy: { createdAt: 'desc' },
  });

  return (
    <div className="grid grid-cols-3 gap-4">
      {products.map(p => (
        <div key={p.id}>
          <img src={p.imageUrl} alt={p.name} />
          <h3>{p.name}</h3>
          <p>${p.price}</p>
        </div>
      ))}
    </div>
  );
}

This component could render 10,000 products and ship 0 bytes of JavaScript.

3. Mix Server and Client Components

// Server Component (default in Next.js App Router)
async function Dashboard() {
  const stats = await getStats();  // Server-side data fetch

  return (
    <div>
      <h1>Dashboard</h1>
      <StatsDisplay stats={stats} />          {/* Server — no JS */}
      <InteractiveChart data={stats.chart} />  {/* Client — has JS */}
      <RecentActivity items={stats.recent} />  {/* Server — no JS */}
    </div>
  );
}
// Client Component (opt-in with "use client")
"use client";
import { useState } from 'react';

function InteractiveChart({ data }) {
  const [timeRange, setTimeRange] = useState('7d');
  // ... interactive chart logic
}

4. Streaming and Suspense

async function Page() {
  return (
    <div>
      <Header />  {/* Renders immediately */}

      <Suspense fallback={<Skeleton />}>
        <SlowDataSection />  {/* Streams in when ready */}
      </Suspense>

      <Suspense fallback={<Skeleton />}>
        <AnotherSlowSection />  {/* Streams in independently */}
      </Suspense>

      <Footer />  {/* Renders immediately */}
    </div>
  );
}

5. Server Actions (Mutations)

async function CreatePost() {
  async function createPost(formData: FormData) {
    "use server";
    const title = formData.get('title') as string;
    const content = formData.get('content') as string;
    await db.posts.create({ data: { title, content } });
    revalidatePath('/posts');
    redirect('/posts');
  }

  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <button type="submit">Publish</button>
    </form>
  );
}

RSC vs Traditional React

Server Components Client Components
Runs on Server only Browser
JS shipped 0 KB Full component code
Data access Direct DB/fs/API Via fetch/API
Interactivity None (static HTML) Full (state, events)
Use for Data display, layout Forms, charts, modals

Getting Started

RSCs are built into Next.js App Router (13.4+):

npx create-next-app@latest --app
# All components are Server Components by default
# Add "use client" only when you need interactivity

The Bottom Line

Server Components eliminate the client-server waterfall. Fetch data where it lives (the server), render HTML, send it to the browser. Less JavaScript, faster pages, simpler code.

Need data tools? I build scraping solutions. Check my Apify actors or email [email protected].

# I Created a Pagination Challenge… And AI Missed the Real Problem

📄

Pagination looks simple.

Page → limit → slice → done.

That’s what I thought.

So I created a small challenge on VibeCode Arena.

And things got interesting.

🚨 The Problem

The logic works perfectly… on static data.

But real systems are not static.

Data keeps changing.

Users keep adding and removing items.

🧠 What Goes Wrong?

In real-world scenarios:

  • New data gets inserted
  • Old data gets deleted
  • Users refresh pages

And suddenly:

👉 You see duplicate data

👉 Or you miss some records

This is a very common production bug.

🤯 What I Observed

When AI models tried this:

  • Most gave basic slice-based logic
  • Some didn’t consider dynamic data
  • Very few suggested cursor-based pagination

The code works.

But the system is unreliable.

🔥 Try It Yourself

I created this challenge to test real-world API thinking.

👉 Try it here:
https://vibecodearena.ai/duel/b772342a-30ee-4d38-838c-c2c888dfffa7

Can you:

  • Prevent duplicate records?
  • Handle dynamic data?
  • Design a scalable pagination system?

💡 Final Thought

Pagination is not about slicing data.

It’s about making sure users see the right data at the right time.

Would you trust AI to design API-level logic like this?

Let’s discuss 👇

Upstash Has a Free Serverless Redis and Kafka — Pay Only Per Request

Traditional Redis costs $15/month minimum even when idle. Upstash Redis is serverless — you pay per command. 10K commands/day free.

What is Upstash?

Upstash provides serverless Redis, Kafka, QStash (message queue), and Vector — all with per-request pricing. Perfect for serverless apps where you can’t maintain persistent connections.

Why Upstash for Serverless

1. HTTP-Based Redis (No TCP Connections)

import { Redis } from '@upstash/redis';

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL,
  token: process.env.UPSTASH_REDIS_REST_TOKEN,
});

// Works in Vercel Edge, Cloudflare Workers, Deno Deploy
await redis.set('user:1', { name: 'Alice', role: 'admin' });
const user = await redis.get('user:1');

HTTP-based — no connection pooling needed. Works in every serverless runtime.

2. Rate Limiting (One Line)

import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'),
});

// In your API handler
const { success, remaining } = await ratelimit.limit(userId);
if (!success) return new Response('Too many requests', { status: 429 });

3. QStash (Serverless Message Queue)

import { Client } from '@upstash/qstash';

const qstash = new Client({ token: process.env.QSTASH_TOKEN });

// Send message to your endpoint (with retries)
await qstash.publishJSON({
  url: 'https://myapp.com/api/process',
  body: { orderId: '123', action: 'fulfill' },
  retries: 3,
  delay: '10s',
});

// Schedule recurring tasks
await qstash.schedules.create({
  destination: 'https://myapp.com/api/cleanup',
  cron: '0 * * * *', // Hourly
});

4. Vector Database

import { Index } from '@upstash/vector';

const index = new Index({
  url: process.env.UPSTASH_VECTOR_REST_URL,
  token: process.env.UPSTASH_VECTOR_REST_TOKEN,
});

// Store embeddings
await index.upsert([
  { id: 'doc1', vector: embedding1, metadata: { title: 'React Guide' } },
  { id: 'doc2', vector: embedding2, metadata: { title: 'Vue Tutorial' } },
]);

// Semantic search
const results = await index.query({
  vector: queryEmbedding,
  topK: 5,
  includeMetadata: true,
});

5. Serverless Kafka

import { Kafka } from '@upstash/kafka';

const kafka = new Kafka({
  url: process.env.UPSTASH_KAFKA_REST_URL,
  username: process.env.UPSTASH_KAFKA_REST_USERNAME,
  password: process.env.UPSTASH_KAFKA_REST_PASSWORD,
});

const producer = kafka.producer();
await producer.produce('orders', JSON.stringify({ orderId: '123' }));

const consumer = kafka.consumer();
const messages = await consumer.consume({ consumerGroupId: 'group1', topic: 'orders' });

Free Tier

Service Free
Redis 10K commands/day
QStash 500 messages/day
Vector 10K queries/day
Kafka 10K messages/day

Upstash vs Traditional Redis/Kafka

Upstash Self-hosted Redis AWS ElastiCache
Connection HTTP (stateless) TCP (persistent) TCP (VPC only)
Pricing Per request Per hour Per hour
Cold cost $0 $15+/month $12+/month
Edge compatible Yes No No
Setup 30 seconds Manual 5+ minutes

Getting Started

npm install @upstash/redis @upstash/ratelimit @upstash/qstash

Sign up at upstash.com → create database → copy REST URL and token.

The Bottom Line

Upstash makes Redis, Kafka, and vector search serverless-native. HTTP-based, per-request pricing, and compatible with every edge runtime. If you’re building on Vercel or Cloudflare, Upstash is the missing piece.

Need data tools? I build scraping solutions. Check my Apify actors or email [email protected].

EdgeDB Has a Free Graph-Relational Database — SQL Power With Object Syntax

SQL is powerful but verbose. ORMs are convenient but limited. EdgeDB gives you a query language that feels like writing objects — with the power of PostgreSQL underneath.

What is EdgeDB?

EdgeDB is a database built on top of PostgreSQL that replaces SQL with EdgeQL — a modern query language that handles relationships, aggregations, and nested data naturally. It also auto-generates TypeScript types.

Why EdgeDB

1. EdgeQL vs SQL

-- SQL: Get users with their posts and comments count
SELECT u.id, u.name,
  (SELECT json_agg(json_build_object(
    'title', p.title,
    'comment_count', (SELECT COUNT(*) FROM comments c WHERE c.post_id = p.id)
  )) FROM posts p WHERE p.author_id = u.id) as posts
FROM users u WHERE u.name ILIKE '%alice%';
# EdgeQL: Same query
SELECT User {
  name,
  posts: {
    title,
    comment_count := count(.comments)
  }
} FILTER .name ILIKE '%alice%';

2. Schema as Code

# dbschema/default.esdl
module default {
  type User {
    required name: str;
    required email: str {
      constraint exclusive;
    };
    multi posts := .<author[IS Post];
    created_at: datetime {
      default := datetime_current();
    };
  }

  type Post {
    required title: str;
    required content: str;
    required author: User;
    multi comments: Comment;
    multi tags: str;
    published: bool {
      default := false;
    };
  }

  type Comment {
    required body: str;
    required author: User;
    created_at: datetime {
      default := datetime_current();
    };
  }
}

3. Auto-Generated TypeScript Client

npx @edgedb/generate edgeql-js
import e from './dbschema/edgeql-js';
import { createClient } from 'edgedb';

const client = createClient();

// Fully typed queries
const users = await e.select(e.User, (user) => ({
  name: true,
  email: true,
  posts: (post) => ({
    title: true,
    comment_count: e.count(post.comments),
    filter: e.op(post.published, '=', true),
  }),
  filter: e.op(user.name, 'ilike', '%alice%'),
})).run(client);

// users is typed as Array<{ name: string, email: string, posts: Array<{ title: string, comment_count: number }> }>

4. Built-in Auth

using extension auth;

module default {
  type User {
    required name: str;
    required identity: ext::auth::Identity;
  }
}

Email/password, OAuth, and magic links — built into the database.

5. Migrations

# Generate migration from schema changes
edgedb migration create

# Apply
edgedb migrate

# Squash migrations
edgedb migration squash

6. Built-in UI

edgedb ui
# Opens browser with:
# - Schema viewer
# - Data browser
# - Query editor with autocomplete
# - Migration history

EdgeDB vs PostgreSQL vs MongoDB

EdgeDB PostgreSQL MongoDB
Query language EdgeQL SQL MQL
Relationships First-class JOINs $lookup
Schema SDL (strict) DDL (strict) Schemaless
TypeScript types Auto-generated Manual / Prisma Manual
Nested queries Natural Subqueries/CTEs Aggregation pipeline
Auth Built-in Extension Atlas only
Migrations Auto-generated Manual N/A

Free Tier (EdgeDB Cloud)

Feature Free
Instances 1
Compute Shared
Storage 1GB
Bandwidth 5GB/month

Getting Started

# Install
curl --proto '=https' --tlsv1.2 -sSf https://sh.edgedb.com | sh

# Create project
edgedb project init

# Start UI
edgedb ui

The Bottom Line

EdgeDB makes PostgreSQL feel modern. EdgeQL is SQL without the pain, auto-generated TypeScript types eliminate manual mapping, and built-in auth saves weeks of development.

Need data tools? I build scraping solutions. Check my Apify actors or email [email protected].

The Simple System I Use to Never Miss a Deadline

Here's a simple system I've developed over the years that helps me never miss a deadline, even when juggling multiple projects. It's a practical approach tailored for developers and freelancers, combining a few powerful tools with some solid time management techniques.

## Prioritization: The Foundation of Success

Starting off, prioritize your tasks effectively. The Eisenhower Box is an excellent method to categorize tasks based on their urgency and importance. By doing so, you ensure that you tackle high-priority tasks promptly without getting overwhelmed by less pressing matters.

## Breaking Down Large Projects

Large projects can often seem intimidating, but breaking them down into smaller, manageable chunks makes them easier to handle. This technique is commonly known as "divide and conquer." For example, if you're working on a complex feature for an application:

bash
Feature Development
├── Design Research
│ ├── Competitor Analysis
│ └── User Interviews
├── Prototyping
│ ├── Low-fidelity wireframes
│ └── High-fidelity mockups
├── Implementation
│ ├── Frontend development
│ └── Backend development
├── Testing
│ ├── Unit tests
│ └── Integration tests
└── Deployment
├── Staging environment setup
└── Production deployment


Breaking a project down in this way allows you to focus on one task at a time, making it more achievable and less overwhelming.

## Time Blocking: Mastering Your Calendar

Time blocking is an effective strategy for managing your time efficiently. Assign specific time slots to tasks throughout the day, ensuring that you dedicate concentrated blocks of time to each task without interruptions. For example, I block out two hours every morning for focused coding and save email checking for designated breaks.

## Using Tools to Stay Organized

Staying organized is crucial in meeting deadlines. Here are some popular tools that can help you manage your workload:

1. **Trello**: A flexible project management tool that allows you to organize tasks into boards, lists, and cards. Great for tracking the progress of multiple projects at once.

2. **Jira**: Popular among Agile teams for managing software development tasks, Jira offers features like customizable workflows, issue tracking, and reporting.

3. **Notion**: A powerful all-in-one workspace that includes note-taking, task management, wikis, databases, and more. It's perfect for freelancers who need an organized hub for their projects.

## Establishing a Buffer Zone

Always build a buffer zone into your schedule to accommodate unexpected issues that might arise. This ensures you stay on track even when obstacles present themselves. I usually allocate an extra day or two for each project, which gives me room to maneuver if necessary.

## The Power of Focus

Avoid distractions during focused work sessions by turning off notifications, using browser extensions like Freedom or Cold Turkey, and practicing mindfulness techniques to maintain your focus. Remember, uninterrupted work allows you to complete tasks more quickly and with fewer errors.

## Setting Realistic Deadlines

When estimating project timelines, err on the side of caution. It's better to under-promise and over-deliver than the other way around. By setting realistic deadlines, you reduce the chances of missing them or compromising quality.

## Takeaway

Developing a system that prevents missed deadlines is crucial for any developer or freelancer. Prioritize tasks effectively, break down large projects, time block your calendar, use organizational tools like Trello, Jira, and Notion, establish a buffer zone, stay focused, and set realistic deadlines. With these tips in hand, you're well on your way to mastering deadline management and delivering top-notch work consistently.

Further Reading

OpenTelemetry Has a Free Observability Standard — Traces, Metrics, and Logs Without Vendor Lock-in

Datadog costs $23/host/month. New Relic’s free tier ends fast. OpenTelemetry sends your data to ANY backend — switch providers without changing your code.

What is OpenTelemetry?

OpenTelemetry (OTel) is a vendor-neutral observability standard. It provides APIs, SDKs, and a collector for traces, metrics, and logs. Your instrumentation stays the same — only the backend changes.

Why OpenTelemetry Is the Standard

1. Instrument Once, Export Anywhere

import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: 'http://localhost:4318/v1/traces',
    // Switch backends by changing this URL:
    // Grafana Tempo, Jaeger, Zipkin, Datadog, Honeycomb, etc.
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

2. Auto-Instrumentation

npm install @opentelemetry/auto-instrumentations-node

This ONE package automatically instruments:

  • HTTP/HTTPS requests
  • Express/Fastify/Koa routes
  • PostgreSQL/MySQL queries
  • Redis commands
  • MongoDB operations
  • gRPC calls
  • AWS SDK calls

Zero code changes in your application.

3. Custom Spans

import { trace } from '@opentelemetry/api';

const tracer = trace.getTracer('my-service');

async function processOrder(orderId: string) {
  return tracer.startActiveSpan('processOrder', async (span) => {
    span.setAttribute('order.id', orderId);

    const items = await tracer.startActiveSpan('fetchItems', async (childSpan) => {
      const result = await db.query('SELECT * FROM items WHERE order_id = $1', [orderId]);
      childSpan.setAttribute('items.count', result.length);
      childSpan.end();
      return result;
    });

    await tracer.startActiveSpan('chargePayment', async (childSpan) => {
      await stripe.charges.create({ amount: calculateTotal(items) });
      childSpan.end();
    });

    span.setStatus({ code: SpanStatusCode.OK });
    span.end();
  });
}

4. Metrics

import { metrics } from '@opentelemetry/api';

const meter = metrics.getMeter('my-service');

const requestCounter = meter.createCounter('http_requests_total', {
  description: 'Total HTTP requests',
});

const latencyHistogram = meter.createHistogram('http_request_duration_ms', {
  description: 'HTTP request latency',
});

app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    requestCounter.add(1, { method: req.method, path: req.path, status: res.statusCode });
    latencyHistogram.record(Date.now() - start, { method: req.method });
  });
  next();
});

5. The Collector (Route Data Anywhere)

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 5s

exporters:
  otlp/jaeger:
    endpoint: jaeger:4317
  prometheus:
    endpoint: 0.0.0.0:8889

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp/jaeger]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheus]

Free Backends

Backend Type Free Tier
Jaeger Traces Self-hosted (free)
Grafana Tempo Traces 50GB/month
Zipkin Traces Self-hosted (free)
Prometheus Metrics Self-hosted (free)
Grafana Loki Logs 50GB/month

Getting Started

npm install @opentelemetry/sdk-node 
  @opentelemetry/auto-instrumentations-node 
  @opentelemetry/exporter-trace-otlp-http

The Bottom Line

OpenTelemetry is the CNCF standard for observability. Instrument once, switch backends freely. No vendor lock-in, no $23/host/month surprise bills.

Need data tools? I build scraping solutions. Check my Apify actors or email [email protected].

The Real Stack Behind AI Agents in Production — MCP, Kubernetes, and What Nobody Tells You

Every team I talk to is building AI agents. They’ve got LangChain running, a vector database humming, and a demo that impresses the CEO.

Then someone asks: “Cool. How do we run this for 10,000 users?”

That’s where things get quiet.

I’ve spent the last year deploying agentic AI systems on Kubernetes — the kind that actually serve real traffic, not just notebook demos. And I’ve learned that the gap between “it works on my laptop” and “it runs in production” is enormous.

This post is about the real stack behind production AI agents in 2026. Not the LLM part — everyone covers that. I’m talking about the infrastructure layer that nobody writes about but everyone desperately needs.

The Problem Nobody Talks About

Here’s a typical conversation I have at least once a week:

Engineer: “We built an AI agent that can query our database, search documents, and draft reports.”

Me: “How does it connect to those systems?”

Engineer: “Custom Python scripts. Different ones for each tool.”

Me: “How do you deploy it?”

Engineer: “Docker container on a single VM.”

Me: “What happens when you need to add a new tool?”

Engineer: silence

The fundamental issue is that most AI agent architectures look like this:

LLM ──→ Hardcoded Tool A (custom REST client)
    ──→ Hardcoded Tool B (custom gRPC wrapper)
    ──→ Hardcoded Tool C (custom DB connector)
    ──→ Hardcoded Tool D (custom file reader)

Every tool integration is bespoke. Every deployment is fragile. Every new capability means rewriting half the agent. This doesn’t scale.

Enter MCP: The USB-C Port for AI Agents

The Model Context Protocol (MCP) changes this equation entirely. Think of it as USB-C for AI — one standardized interface that connects any model to any tool.

Instead of custom integrations for each tool, MCP provides:

LLM ──→ MCP Client ──→ MCP Server: Database
                   ──→ MCP Server: Search
                   ──→ MCP Server: Kubernetes
                   ──→ MCP Server: Slack
                   ──→ MCP Server: (anything)

Here’s a minimal MCP server definition that exposes a Kubernetes health check as a tool:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import * as k8s from "@kubernetes/client-node";

const server = new McpServer({
  name: "k8s-health",
  version: "1.0.0",
});

server.tool(
  "check_pod_status",
  "Check the health status of pods in a namespace",
  { namespace: { type: "string", description: "Kubernetes namespace" } },
  async ({ namespace }) => {
    const kc = new k8s.KubeConfig();
    kc.loadFromDefault();
    const k8sApi = kc.makeApiClient(k8s.CoreV1Api);

    const res = await k8sApi.listNamespacedPod({ namespace });
    const unhealthy = res.items.filter(
      (p) => p.status?.phase !== "Running" && p.status?.phase !== "Succeeded"
    );

    return {
      content: [
        {
          type: "text",
          text: unhealthy.length === 0
            ? `All pods in ${namespace} are healthy.`
            : `Found ${unhealthy.length} unhealthy pods:n` +
              unhealthy
                .map((p) => `- ${p.metadata?.name}: ${p.status?.phase}`)
                .join("n"),
        },
      ],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

The agent doesn’t need to know how Kubernetes works. It just calls check_pod_status through MCP, and the server handles the rest.

Now here’s where it gets interesting: the MCP ecosystem has exploded. As of March 2026, there are over 10,000 public MCP servers, 97M+ monthly SDK downloads, and adoption by Claude, ChatGPT, Gemini, VS Code, and Cursor. OpenAI, Google, and Microsoft all support it. It’s not a bet anymore — it’s infrastructure.

The Part Everyone Skips: Running This on Kubernetes

So you’ve got MCP wiring up your agent to tools. Great. But where do these MCP servers actually run? How do you manage GPU resources for your LLM? What happens when traffic spikes?

This is where Kubernetes becomes the AI infrastructure OS.

Architecture: Agent Stack on Kubernetes

Here’s the production architecture I deploy:

┌──────────────────────────────────────────────────────┐
│              Kubernetes Cluster                       │
│                                                      │
│  ┌──────────────────────────────────────────────┐    │
│  │       Ingress / Gateway API                  │    │
│  │  Inference Gateway · model-aware routing     │    │
│  └───────────────────┬──────────────────────────┘    │
│                      │                               │
│  ┌─────────────┐ ┌───┴──────────┐ ┌──────────────┐  │
│  │ Agent API   │─│MCP Server    │ │ Key Metrics  │  │
│  │ FastAPI +   │ │Pool          │ │ TTFT <500ms  │  │
│  │ LangGraph   │ │ mcp-postgres │ │ GPU >70%     │  │
│  └──────┬──────┘ │ mcp-search   │ │ Tool >99.5%  │  │
│         │        │ mcp-k8s      │ │ Cost -39%    │  │
│         │        └──────┬───────┘ └──────────────┘  │
│  ┌──────┴──────┐ ┌──────┴───────┐                    │
│  │ LLM Server  │ │ Vector DB   │                    │
│  │ vLLM + DRA  │ │ Weaviate    │                    │
│  │ Llama-70B   │ │ Hybrid Srch │                    │
│  └─────────────┘ └─────────────┘                    │
│                                                      │
│  ┌──────────────────────────────────────────────┐    │
│  │ Observability: OTel → Prometheus → Grafana   │    │
│  └──────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────┘

Let me walk through each layer.

Layer 1: GPU Scheduling with Dynamic Resource Allocation

The biggest pain point in AI infrastructure is GPU management. Before DRA (Dynamic Resource Allocation), you were stuck with device plugins — crude, all-or-nothing GPU assignments.

DRA reached GA in Kubernetes 1.34 and changes the game. Here’s a ResourceClaim for an inference workload:

apiVersion: resource.k8s.io/v1beta2
kind: ResourceClaim
metadata:
  name: llm-inference-gpu
  namespace: ai-agents
spec:
  devices:
    requests:
    - name: gpu
      deviceClassName: gpu.nvidia.com
      selectors:
      - cel:
          expression: >
            device.attributes["gpu.nvidia.com"].memory >= 24000 &&
            device.attributes["gpu.nvidia.com"].productName.startsWith("A")
    constraints:
    - requests: ["gpu"]
      matchAttribute: "gpu.nvidia.com/numa-node"

What this does:

  • CEL-based filtering: Selects GPUs with 24GB+ VRAM and A-series (A100/A10G)
  • Topology-aware: NUMA node matching ensures the GPU is on the same physical socket as the CPU — this alone cuts inference latency by 15-20%
  • Declarative: No more shell scripts checking nvidia-smi

And here’s the Deployment that uses it:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-server
  namespace: ai-agents
spec:
  replicas: 2
  selector:
    matchLabels:
      app: vllm-server
  template:
    metadata:
      labels:
        app: vllm-server
    spec:
      containers:
      - name: vllm
        image: vllm/vllm-openai:v0.7.3
        args:
        - "--model=meta-llama/Llama-3.3-70B-Instruct"
        - "--tensor-parallel-size=1"
        - "--max-model-len=8192"
        - "--enforce-eager"
        ports:
        - containerPort: 8000
        resources:
          claims:
          - name: llm-gpu
      resourceClaims:
      - name: llm-gpu
        resourceClaimTemplateName: llm-inference-gpu

Early adopters report 25% better GPU utilization with DRA compared to static device plugins. That’s not a marginal gain — at cloud GPU prices, that’s potentially tens of thousands of dollars saved per month.

Layer 2: Inference Gateway for Traffic Routing

You don’t point users directly at vLLM instances. The Inference Gateway (now GA) routes inference traffic based on model names, LoRA adapters, and endpoint health:

apiVersion: inference.networking.x-k8s.io/v1alpha2
kind: InferencePool
metadata:
  name: llm-pool
  namespace: ai-agents
spec:
  targetPortNumber: 8000
  selector:
    matchLabels:
      app: vllm-server
---
apiVersion: inference.networking.x-k8s.io/v1alpha2
kind: InferenceModel
metadata:
  name: agent-model
  namespace: ai-agents
spec:
  modelName: "agent-llama-70b"
  targetModels:
  - name: "meta-llama/Llama-3.3-70B-Instruct"
    weight: 100
  poolRef:
    name: llm-pool

This gives you:

  • Model-aware routing: Request for agent-llama-70b automatically goes to the right pool
  • Health-based failover: Unhealthy endpoints are removed from rotation
  • Multi-tenant serving: Different teams share the same GPU pool without stepping on each other

Layer 3: MCP Servers as Kubernetes Deployments

Each MCP server runs as a standard Kubernetes deployment. Here’s the pattern I use:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mcp-postgres
  namespace: ai-agents
  labels:
    mcp-server: "true"
    mcp-tool: "database"
spec:
  replicas: 2
  selector:
    matchLabels:
      app: mcp-postgres
  template:
    metadata:
      labels:
        app: mcp-postgres
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9090"
    spec:
      serviceAccountName: mcp-postgres-sa
      containers:
      - name: mcp-server
        image: registry.internal/mcp-postgres:1.2.0
        env:
        - name: DB_HOST
          valueFrom:
            secretKeyRef:
              name: postgres-credentials
              key: host
        - name: MCP_TRANSPORT
          value: "streamable-http"
        ports:
        - containerPort: 3000  # MCP
        - containerPort: 9090  # Metrics
        resources:
          requests:
            cpu: 100m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 3000
          periodSeconds: 5
      volumes:
      - name: tls-certs
        secret:
          secretName: mcp-tls

Key decisions:

  • Separate deployments per MCP server: Isolates failures. A broken Slack MCP server doesn’t take down your database MCP server.
  • mTLS between agent and MCP servers: Non-negotiable for production. Agent ↔ MCP traffic carries credentials and sensitive data.
  • Horizontal scaling: MCP servers that hit external APIs (Slack, GitHub) might need more replicas during peak hours.

Layer 4: Observability — The AI-Specific Metrics

Standard CPU/memory metrics aren’t enough. For AI agent workloads, you need:

# OpenTelemetry Collector config for AI metrics
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  batch:
    timeout: 5s

exporters:
  prometheus:
    endpoint: 0.0.0.0:8889
    namespace: ai_agent

service:
  pipelines:
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheus]

The metrics that matter:

Metric Target Why It Matters
Time to First Token (TTFT) < 500ms User-perceived responsiveness
Tokens/second > 30 tok/s Throughput capacity
MCP tool call latency < 200ms p99 Agent step execution speed
GPU utilization > 70% Cost efficiency
Queue depth < 10 Capacity signal
Tool call success rate > 99.5% Reliability indicator
Agent task completion rate > 95% End-to-end quality

Here’s a Grafana dashboard query that’s saved me multiple times:

# Alert: Agent tool calls failing above threshold
(
  sum(rate(mcp_tool_call_errors_total[5m])) by (tool_name)
  /
  sum(rate(mcp_tool_call_total[5m])) by (tool_name)
) > 0.05

If any MCP tool starts failing more than 5% of calls, you want to know immediately — before users notice the agent giving wrong answers.

What I Got Wrong (So You Don’t Have To)

A few hard lessons from production deployments:

1. Don’t run MCP servers as sidecars

My first instinct was to run MCP servers as sidecar containers alongside the agent. Bad idea. When the MCP server for Postgres needs patching, you don’t want to restart your entire agent pod. Separate deployments. Always.

2. GPU over-provisioning is worse than under-provisioning

With DRA, it’s tempting to request the biggest GPU available. Don’t. A Llama-3.3-8B model running inference doesn’t need an A100 80GB. Start with the smallest GPU that meets your latency target, then scale up only with data.

3. Agents need circuit breakers on tool calls

An agent stuck in a retry loop on a failing MCP tool will burn through tokens (and money) shockingly fast. Implement circuit breakers:

from circuitbreaker import circuit

@circuit(failure_threshold=3, recovery_timeout=30)
async def call_mcp_tool(server: str, tool: str, params: dict):
    async with mcp_client.connect(server) as client:
        result = await client.call_tool(tool, params)
        return result

After 3 consecutive failures, the circuit opens and the agent gets an immediate “tool unavailable” response instead of waiting for timeouts.

4. Observability isn’t optional — it’s day one

I once spent 6 hours debugging why an agent was giving nonsensical answers. The root cause? The vector database MCP server was returning stale embeddings because a background indexing job had silently failed 2 days earlier. With proper embedding freshness metrics, I would have caught it in minutes.

The Numbers

Here’s what this stack delivers in practice:

Metric Before (Ad-hoc) After (This Stack)
Agent response time (p95) 12s 3.2s
GPU utilization ~45% ~72%
Tool integration time 2-3 weeks 2-3 days (MCP)
Incident detection Hours Minutes (OTel)
Monthly GPU cost (same workload) $18K $11K

The GPU cost savings alone paid for the engineering effort in the first month.

Getting Started

If you’re running AI agents (or about to), here’s my recommended path:

  1. Start with MCP — Wrap your existing tool integrations as MCP servers. Even before Kubernetes, this standardization pays off immediately.

  2. Deploy on Kubernetes with DRA — If you’re on K8s 1.34+, enable DRA for your GPU nodes. The topology-aware scheduling alone is worth it.

  3. Add the Inference Gateway — Don’t build custom load balancing for model serving. The Gateway API Inference Extension handles model-aware routing natively.

  4. Instrument from day one — OpenTelemetry + Prometheus + Grafana. Track TTFT, tool call latency, and GPU utilization as your golden signals.

  5. Run MCP servers as independent deployments — Not sidecars. Each MCP server gets its own lifecycle, scaling, and health checks.

What’s Next

This is Part 1 of a series on AI Infrastructure in Production. Coming next:

  • Part 2: Deep dive into GPU scheduling with DRA — benchmarks, gotchas, and advanced patterns
  • Part 3: Observability for LLM workloads — building dashboards that actually tell you what’s wrong

If you found this useful, give it a heart and follow me — I write about the infrastructure side of AI that doesn’t get enough attention.

This article was researched and written with AI assistance. Read more cloud-native engineering insights on the ManoIT Tech Blog.

Kubernetes Ingress NGINX Retirement and Gateway API Migration Guide

Kubernetes Ingress NGINX Retirement and Gateway API Migration

The NGINX Ingress Controller, one of Kubernetes’ most widely-used components, is transitioning from active development to maintenance mode. The Kubernetes community is now emphasizing the new Gateway API standard. For teams running NGINX Ingress in production, understanding this migration path is essential.

NGINX Ingress → Gateway API: Why the shift?

NGINX Ingress was designed as a standalone solution. However, as Kubernetes matured, native service networking evolved. Gateway API provides unified control across different implementations and better integrates with Kubernetes’ evolving network model.

Migration Strategy: NGINX to Gateway API

Phase 1: Assessment

# Check current NGINX Ingress version
kubectl get deployment -n ingress-nginx ingress-nginx-controller -o jsonpath='{.spec.template.spec.containers[0].image}'

# List all current Ingress resources
kubectl get ingress --all-namespaces
kubectl describe ingress <name> -n <namespace>

Phase 2: Deploy Gateway API

# Install Gateway API CRDs
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: my-gateway
spec:
  gatewayClassName: nginx
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    allowedRoutes:
      namespaces:
        from: All
  - name: https
    protocol: HTTPS
    port: 443
    tls:
      mode: Terminate
      certificateRefs:
      - name: tls-secret

Phase 3: Migrate Routes

# Old Ingress approach
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: example
spec:
  rules:
  - host: example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: web
            port:
              number: 8080

# New HTTPRoute (Gateway API)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: example
spec:
  parentRefs:
  - name: my-gateway
  hostnames:
  - "example.com"
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /
    backendRefs:
    - name: web
      port: 8080

Timeline and Support

NGINX Ingress security updates continue through 2026. Start migration planning now, complete migration by end of 2026.

FAQ

Q: Must I migrate immediately?

No. NGINX Ingress receives security updates through 2026. Plan migration strategically.

Q: What about performance?

Gateway API shows similar performance with better flexibility and future-proofing.

Q: Are there compatibility layers?

Yes, some distributions provide compatibility adapters during transition.

This article was originally published on ManoIT Tech Blog.

Trivy Docker Hub Supply Chain Attack Analysis and CI/CD Pipeline Security

Trivy Docker Hub Supply Chain Attack Analysis and CI/CD Pipeline Security

Trivy, the popular open-source vulnerability scanner from Aqua Security, discovered and disclosed a supply chain attack vector targeting Docker Hub and container registries. Understanding this attack pattern and implementing defensive measures is essential for secure DevOps practices.

Attack Vector Overview

The attack involved compromised container images in public registries containing backdoors and credential stealers. Vulnerable organizations pulled these images without verification, unknowingly deploying compromised workloads.

Detection Strategies

Trivy Vulnerability Scanning

# Scan local image
trivy image myrepo/myimage:latest

# Scan with severity filter
trivy image --severity HIGH,CRITICAL myrepo/myimage:latest

# Generate JSON report
trivy image --format json -o report.json myrepo/myimage:latest

SBOM Generation and Analysis

# Generate SBOM with Syft
syft myrepo/myimage:latest -o spdx > sbom.json

# Check against known vulnerabilities
trivy sbom sbom.json

Prevention in CI/CD

Pre-Build Scanning

Scan base images before using them:

# GitHub Actions example
- name: Scan base image
  run: |
    trivy image --severity HIGH ubuntu:22.04
    if [ $? -ne 0 ]; then
      echo "Vulnerable base image detected"
      exit 1
    fi

Post-Build Image Scanning

- name: Build and scan
  run: |
    docker build -t myapp:${{ github.sha }} .
    trivy image myapp:${{ github.sha }}

Registry Mirroring Strategy

Use private registries to mirror and verify images:

# Pull, scan, and push to private registry
docker pull ubuntu:22.04
trivy image ubuntu:22.04
docker tag ubuntu:22.04 private-registry.com/ubuntu:22.04
docker push private-registry.com/ubuntu:22.04

Best Practices

  1. Enable image signing and verification
  2. Implement admission controllers in Kubernetes
  3. Use private registries for sensitive applications
  4. Scan all images regularly, not just at deployment
  5. Monitor base image updates and vulnerabilities
  6. Implement runtime monitoring for suspicious behavior

FAQ

Q: How often should I scan?

Continuously in CI/CD pipelines, plus scheduled rescans of running images.

Q: What if my image is vulnerable?

Rebuild with patched base image or updated dependencies.

This article was originally published on ManoIT Tech Blog.