MoreRSS

site iconThe Practical DeveloperModify

A constructive and inclusive social network for software developers.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of The Practical Developer

We Thought Hosting on the Cloud Was Enough. It Wasn’t.

2026-02-06 22:55:58

When we first started building products for startups, we treated “on the cloud” and “cloud-native” like the same thing. Get it deployed, make sure it runs, move on.

Then real usage happened.

We saw products slow down under traffic spikes. Small failures would ripple into bigger issues. Scaling meant manual tweaks.

Deployments felt heavier than they should have. None of this was dramatic, just the kind of friction that slowly eats into a product’s reliability and a team’s time.

Over time, we learned that building for the cloud is different from just building on the cloud.
What changed for us wasn’t some big rewrite. It was getting better at the basics:

  • breaking products into smaller, independent services
  • letting infrastructure scale automatically instead of guessing capacity
  • using managed cloud services so teams can focus on product, not maintenance
  • designing systems that expect failures and recover quickly

This shift made launches calmer, updates smoother, and scaling far less stressful, for us and for the teams we work with.

We’ve been refining how we approach cloud-native builds and automation across SaaS and web projects, and it’s been a clear upgrade in how products behave in the real world.

Would love to hear how your team learned the difference between “hosted on the cloud” and “built for the cloud.”

How I Stopped Rewriting My Code Every Time I Switched LLM Providers

2026-02-06 22:42:42

You know that feeling when you've just finished migrating your entire codebase from GPT-4 to Claude — rewriting API calls, fixing response parsing, updating streaming logic — and then Google drops a new Gemini with benchmarks that make everything else look like a calculator from 1995?

Yeah. I've been there. Multiple times.

After the third migration in six months, I sat down and thought: why am I rewriting integration code when the only thing that changes is which HTTP endpoint I'm hitting? The request is JSON. The response is JSON. The models all take messages in, text out. Why does switching providers feel like changing the engine on a moving car?

That's how I ended up looking at LM-Proxy — and it changed the way I architect LLM-powered applications. Let me walk you through what it is, why it matters, and how to set it up in under 5 minutes.

The Problem (or: Why Your LLM Integration Code Is a Ticking Time Bomb)

Here's the typical evolution of an LLM-powered project:

Month 1: "We'll just use OpenAI. One provider, one SDK, simple."

Month 3: "Claude is actually better for our summarization task. Let's add Anthropic too." You now have two different SDKs, two authentication flows, two response formats, and an if/else that haunts your dreams.

Month 5: "Gemini Flash is way cheaper for simple queries. Let's route the easy stuff there." Your routing logic now lives in three different files, you have provider-specific error handling everywhere, and your new developer spent two days just understanding which API key goes where.

Month 7: Someone asks "can we add a local model for sensitive data?" and you start updating your LinkedIn.

The root cause is simple: every LLM provider decided to invent their own API format, even though they all do fundamentally the same thing. OpenAI's format became the de facto standard, but Anthropic, Google, and others each have their own quirks, authentication schemes, and streaming protocols.

What you actually need is a reverse proxy — a single endpoint that speaks OpenAI's API format on the outside, but can talk to any provider on the inside.

Enter LM-Proxy

LM-Proxy is a lightweight, OpenAI-compatible HTTP proxy/gateway built with Python and FastAPI. Here's the pitch in one sentence:

Your app talks to one API (OpenAI format). LM-Proxy talks to everyone else.

It supports OpenAI, Anthropic, Google (AI Studio and Vertex AI), local PyTorch models, and anything that speaks OpenAI's API format — all through a single /v1/chat/completions endpoint. Requires Python 3.11+.

But "another LLM gateway" isn't what makes it interesting. Here's what does:

1. It's Truly Lightweight

No Kubernetes. No Redis. No Kafka. No 47-microservice architecture. It's a single Python package:

pip install lm-proxy

One TOML config file. One command to start:

lm-proxy

That's it. You can have it running on a $5/month VPS, inside a Docker container, or even embedded directly into your Python application as a library (since it's built on FastAPI, you can import and mount it as a sub-app). The entire core is minimal by design — no bloat, no enterprise-grade complexity for a problem that doesn't need it.

2. Configuration That Doesn't Require a PhD

Here's a complete, working config that routes GPT requests to OpenAI, Claude to Anthropic, and Gemini to Google:

host = "0.0.0.0"
port = 8000

[connections.openai]
api_type = "open_ai"
api_base = "https://api.openai.com/v1/"
api_key = "env:OPENAI_API_KEY"

[connections.anthropic]
api_type = "anthropic"
api_key = "env:ANTHROPIC_API_KEY"

[connections.google]
api_type = "google_ai_studio"
api_key = "env:GOOGLE_API_KEY"

[routing]
"gpt*" = "openai.*"
"claude*" = "anthropic.*"
"gemini*" = "google.*"
"*" = "openai.gpt-4o-mini"        # fallback

[groups.default]
api_keys = ["my-team-api-key-1", "my-team-api-key-2"]

Read that config top to bottom. You understood it in 30 seconds, didn't you? No YAML indentation nightmares, no 200-line JSON blobs — just clean TOML with obvious semantics. (YAML, JSON, and Python config formats are also supported, by the way.)

The env: prefix pulls secrets from environment variables (or .env files), so your API keys never touch version control.

3. Pattern-Based Routing (The Killer Feature)

The [routing] section is where the magic happens. Keys are glob patterns that match against the model name your client sends. The .* suffix means "pass the model name as-is to the provider." So when your client asks for claude-sonnet-4-5-20250929, LM-Proxy forwards exactly that to Anthropic's API. No mapping tables, no model ID translation files — it just works.

You can also pin patterns to specific models:

[routing]
"custom*" = "local.llama-7b"        # Any "custom*" request → local Llama
"gpt-3.5*" = "openai.gpt-3.5-turbo" # Pin to a specific model
"*" = "openai.gpt-4o-mini"          # Everything else → cheap fallback

This means your client code never changes. Want to try a new model? Update one line in the config. Want to A/B test two providers? Add a routing rule. Want to deprecate a model? Redirect the pattern to something else. Zero code changes.

4. Virtual API Keys and Access Control

This is the feature that makes LM-Proxy production-ready rather than just a toy.

LM-Proxy maintains two layers of API keys:

  • Virtual (Client) API Keys — what your users/services use to authenticate with the proxy
  • Provider (Upstream) API Keys — the real API keys for OpenAI, Anthropic, etc., which stay hidden
# Premium users get everything
[groups.premium]
api_keys = ["premium-key-1", "premium-key-2"]
allowed_connections = "*"

# Free tier gets OpenAI only
[groups.free]
api_keys = ["free-key-1"]
allowed_connections = "openai"

# Internal tools get local models only
[groups.internal]
api_keys = ["internal-key-1"]
allowed_connections = "local"

Your upstream API keys are never exposed to clients. You can rotate them without updating any client configuration. You can create granular access tiers — premium users get Claude Opus, free users get GPT-4o-mini, internal tools use local models. All managed in one config file.

It even supports external authentication — you can validate virtual API keys against Keycloak, Auth0, or any OIDC provider:

[api_key_check]
class = "lm_proxy.api_key_check.CheckAPIKeyWithRequest"
method = "POST"
url = "http://keycloak:8080/realms/master/protocol/openid-connect/userinfo"
response_as_user_info = true
use_cache = true
cache_ttl = 60

[api_key_check.headers]
Authorization = "Bearer {api_key}"

Your existing OAuth tokens become LLM API keys automatically. And if the built-in validators don't fit, you can write a custom one — just a Python function that takes an API key string and returns a group name.

5. Full Streaming Support

SSE streaming works out of the box. Your clients get real-time token-by-token responses regardless of which provider is actually generating them. The proxy handles the format translation transparently — Anthropic's streaming format becomes OpenAI-compatible SSE events before they reach your client.

6. Use It as a Library, Not Just a Server

LM-Proxy isn't just a standalone service — it's also an importable Python package. Since it's built on FastAPI, you can mount it inside your existing application, use it in integration tests, or compose it with other ASGI middleware. No need to run a separate process if you don't want to.

Real-World Setup: 5 Minutes to Production

Let me walk through a concrete scenario. You're building a SaaS product that uses LLMs. You want GPT-4o for complex reasoning, Claude Sonnet for long-document processing, and Gemini Flash for cheap classification.

Step 1: Install

pip install lm-proxy

Step 2: Create .env

OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
GOOGLE_API_KEY=AI...

Step 3: Create config.toml

host = "0.0.0.0"
port = 8000

[connections.openai]
api_type = "open_ai"
api_base = "https://api.openai.com/v1/"
api_key = "env:OPENAI_API_KEY"

[connections.anthropic]
api_type = "anthropic"
api_key = "env:ANTHROPIC_API_KEY"

[connections.google]
api_type = "google_ai_studio"
api_key = "env:GOOGLE_API_KEY"

[routing]
"gpt*" = "openai.*"
"claude*" = "anthropic.*"
"gemini*" = "google.*"
"*" = "google.gemini-2.0-flash"

[groups.backend]
api_keys = ["backend-service-key"]
allowed_connections = "*"

[groups.frontend]
api_keys = ["frontend-widget-key"]
allowed_connections = "google"     # cheap models only

Step 4: Run

lm-proxy

Step 5: Use from your app

from openai import OpenAI

client = OpenAI(
    api_key="backend-service-key",
    base_url="http://localhost:8000/v1"
)

# Complex reasoning → GPT-4o
response = client.chat.completions.create(
    model="gpt-5.2",
    messages=[{"role": "user", "content": "Analyze this contract..."}]
)

# Long document → Claude
response = client.chat.completions.create(
    model="claude-opus-4-6",
    messages=[{"role": "user", "content": "Summarize this 100-page report..."}]
)

# Quick classification → Gemini Flash (cheap)
response = client.chat.completions.create(
    model="gemini-2.0-flash",
    messages=[{"role": "user", "content": "Is this email spam? ..."}]
)

One client. One base URL. Three providers. And tomorrow, when someone releases a model that's 50% cheaper, you change one line in config.toml and restart the server. Your application code stays the same.

How It Compares to LiteLLM

The obvious question: "How is this different from LiteLLM?"

Let's be honest — LiteLLM is the 800-pound gorilla in this space. With 33k+ GitHub stars, 2000+ supported LLMs, an AWS Marketplace listing, and features like spend tracking dashboards, guardrails, caching, rate limiting, SSO, and MCP integration, it's a full-blown enterprise platform. It also offers both a Python SDK and an HTTP proxy server, so architecturally it covers similar ground.

So why would you choose LM-Proxy instead?

The same reason you'd choose Flask over Django, or SQLite over PostgreSQL. Not everything needs the full enterprise stack.

LM-Proxy LiteLLM Proxy
Philosophy Minimal core, extend as needed Batteries included
Setup complexity pip install lm-proxy + one TOML file pip install 'litellm[proxy]' + YAML + optional DB
Dependencies FastAPI + MicroCore (lightweight) Heavier dependency tree
Config format TOML / YAML / JSON / Python YAML
Embeddable as library First-class use case Supported but discouraged by docs
Virtual keys + groups Built-in Built-in (more advanced)
OIDC / Keycloak auth Built-in Built-in (more advanced: JWT validation, role mapping, SSO UI)
Spend tracking, guardrails, caching Not built-in (extensible via add-ons) Built-in
Admin UI No Yes
Supported providers All major providers + local models / embedded inference All major providers

Choose LM-Proxy when: you want a lightweight, easy-to-embed proxy with a tiny footprint, Python-config extensibility, and you don't need 90% of the enterprise features. It's ideal for small teams, personal projects, or when you want a gateway you can fully understand by reading the source in an afternoon.

Choose LiteLLM when: you need enterprise-grade spend tracking, a management UI, dozens of integrations, guardrails, caching, and support for 100+ providers and integrations out of the box.

Other alternatives like Portkey lean even more enterprise. LM-Proxy intentionally occupies the "just enough gateway" niche — powerful enough for production, simple enough that your config file is the documentation.

What's Already There (and What I'd Love to See Next)

Credit where it's due — LM-Proxy already covers more ground than you might expect from a "lightweight" tool:

  • Structured logging — a pluggable logger system with JsonLogWriter and LogEntryTransformer (tracks tokens, duration, group, connection, remote address), plus the lm-proxy-db-connector add-on for writing logs to PostgreSQL, MySQL, SQLite, and other databases via SQLAlchemy
  • Load balancing — there's an example configuration that distributes requests randomly across multiple LLM servers using the Python config format
  • Request handlers — a middleware-like system for intercepting requests before they reach upstream providers, enabling cross-cutting concerns like auditing and header manipulation
  • Vertex AI support — Google Cloud's Vertex AI is supported alongside the simpler AI Studio API, with a dedicated config example

That said, the project is still evolving (latest release: v3.0.0), and a few things remain on my personal wishlist:

  • Usage analytics dashboard — the logging and DB infrastructure is solid, but a built-in UI for visualizing spend and usage would be the cherry on top
  • Wildcard model expansion — the expand_wildcards mode for /v1/models is planned but not yet implemented — for now you need to list models explicitly in the routing config
  • Automatic provider failover — if OpenAI returns 5xx, automatically reroute to Anthropic. Load balancing across instances of the same provider is there, but cross-provider failover would complete the picture

The extensibility-by-design philosophy means most of these can be added as add-ons without touching the core — and the DB connector already demonstrates this pattern nicely. The codebase is MIT licensed and small enough to read end-to-end in an afternoon.

TL;DR

If you're working with multiple LLM providers (or think you might in the future), stop writing provider-specific integration code. Set up LM-Proxy as a gateway, point all your services at it, and never think about API format differences again.

pip install lm-proxy

GitHub | PyPI

Have you tried LM-Proxy or a similar LLM gateway? What's your approach to multi-provider LLM integration? Share your experience in the comments.

How Analysts Turn Chaos Into Clarity: My Real Journey Through Power BI

2026-02-06 22:39:31

Introduction

When I first started learning Power BI, I thought data analysis was mostly about building charts and dashboards.
I was wrong.
What I quickly realized is that most of the work happens before the visuals. Real-world data is messy, inconsistent and often confusing and Power BI is only powerful when you know how to translate that mess into something meaningful.

This article explains how analysts (especially beginners like me) use Power BI to move from messy data, through DAX and finally into dashboards that actually drive action.

The Reality Check Nobody Prepared Me For

You open that CSV file or connect to that database feeling optimistic. You're ready to build some dashboards, create insights, change the world. Then you see it: dates formatted as text, customer names spelled seventeen different ways, empty cells scattered around like landmines, numbers mixed with currency symbols, and columns labeled "Field_47" with zero documentation.

My first real dataset had all of this plus duplicate records of the same transactions. I remember staring at it thinking, "Did I get bad data? Should I ask for a cleaner version?"

Then I learned the truth: this is normal. This is the job. Messy data isn't bad data,it's just raw. And if you can't translate raw data into something usable, none of the fancy dashboard skills matter.

What You're Actually Looking For (And Why It Matters)

When I first crack open a messy dataset, I'm hunting for specific problems. Here's what trips up most analyses before they even start:

Inconsistent Formatting

This drives me nuts but it's everywhere. You'll have one column with "New York," "NY," "new york," and "N.Y." all meaning the same thing. Power BI doesn't know they're the same, so your visuals will treat them as four separate categories. Your boss asks why sales are split across four different locations and you look like you don't know what you're doing.

Missing Values

These are trickier than they seem. Sometimes a blank means "we don't know." Sometimes it means "zero." Sometimes it means "this doesn't apply." You have to figure out which one it is, because filling in blanks with zeros when they actually mean "unknown" will completely screw up your averages and totals.

I learned this the hard way when my average order value calculation was totally wrong because I'd replaced nulls with zeros.

Data Types That Make No Sense

This is a constant headache. Dates stored as text, numbers stored as text, percentages stored as whole numbers (so 85% shows up as 85.00 instead of 0.85). Power Query's type detection helps, but you can't just trust it blindly.

I once built an entire time series analysis before realizing my dates were still text and none of my date hierarchies worked.

Duplicate Records

These happen more than you'd think. Maybe the same transaction got logged twice. Maybe someone entered the same customer with slightly different info. You need to spot these and decide whether to keep both records, merge them or delete the duplicates entirely.

This step completely changed how I viewed data analysis. Instead of rushing to build visuals, I learned to slow down and ask: Can this data be trusted? If the answer is no, everything that follows becomes unreliable.

Power Query: Where the Translation Actually Happens

In Power BI, the translation process starts in Power Query. This is where analysts prepare data so Power BI can analyze it correctly.

Here's my actual workflow when I'm cleaning data:

First, I use Power Query to get a feel for what I'm working with. I turn on the column quality, column distribution and column profile options. These show you at a glance how many nulls you have, how many distinct values, whether you have outliers. It's like getting an X-ray of your data before you operate on it.

Then I start fixing things systematically:

Standardizing text - I use TRIM and CLEAN to get rid of weird spacing. I use Replace Values for common inconsistencies. For the "New York" problem I mentioned earlier, I create a conditional column or use Replace Values to make everything consistent.

Fixing data types - I manually set every single column type because auto-detect has burned me too many times. Date/Time for dates, Decimal Number for currency, Whole Number for counts, Text for IDs (even if they look like numbers—trust me on this one).

Handling missing data intentionally - Sometimes I filter out incomplete rows. Sometimes I replace nulls with "Unknown" or "Not Provided" for categorical data. For numbers, it depends on context—sometimes zero makes sense, sometimes you need to leave it null, sometimes you calculate an average to fill it in.

Removing junk - Duplicate records go away. Irrelevant columns get deleted. Rows that are clearly data entry errors get filtered out (like that one time I found a customer age of 247).

Splitting and combining - I split columns so each one represents a single piece of information. A "Full Name" column becomes "First Name" and "Last Name." A messy address becomes separate fields for street, city, state, zip.

Renaming everything - I rename columns to clearly describe what they contain. "Col_17" becomes "Customer_Acquisition_Date." Future me always thanks past me for this.

This is where you earn your money as an analyst. Nobody sees this work, but it's the foundation of everything.

Data Modeling: Teaching Power BI How Things Connect

Once the data is clean, the next challenge is structuring it properly. Power BI doesn't just display tables—it uses relationships between them to filter and calculate data correctly.

As a beginner, the biggest concept I learned was the difference between fact tables and dimension tables:

Fact tables hold the transactions, the events, the things that happened. Sales records. Website visits. Support tickets. These tables tend to be long—lots of rows.

Dimension tables describe the context around those facts. Customer information. Product catalogs. Date tables. Geography. These tables tend to be wide—lots of attributes about each thing.

By connecting these tables using clear one-to-many relationships (one customer can have many sales, one product can appear in many transactions), Power BI can filter visuals correctly, respond properly to slicers, and produce accurate aggregations.

I'll be honest,I didn't understand why this mattered until I built a dashboard without proper relationships. When someone clicked a slicer to filter by region, only some of the visuals updated. Others showed the wrong numbers. It was a mess.

A good data model doesn't look exciting, but it makes dashboards behave the way users expect. Without it, even clean data produces confusing results.

DAX: Defining What Numbers Actually Mean

DAX was the part I found most intimidating at first. All those functions and syntax rules felt like learning a new programming language.

But over time, I realized that DAX isn't just about formulas—it's about defining meaning.

Instead of asking "How do I write this formula?", analysts ask:

  • What exactly does "total sales" mean for our business?
  • Should it include returns? Discounts? Tax?
  • How should this number change when someone filters by date or region?
  • How do we compare current results to last year's performance?

Using DAX measures, Power BI allows calculations to adapt dynamically to filters, dates and user interaction. This is what transforms static numbers into insights.

A simple example: when I create a measure for Total Revenue, it's not just SUM(Sales[Amount]). I have to think about whether cancelled orders should be excluded. Whether I need to account for currency conversion. Whether partial refunds affect the number.

DAX is the bridge between raw numbers and real business questions. It's where you translate data into the language your stakeholders actually care about.

The Stuff That's Hard to Learn From Tutorials

Here's what trips people up: knowing when to clean in Power Query versus when to handle it in DAX measures.

Generally, if it's structural (fixing data types, removing duplicates, standardizing categories), do it in Power Query. If it's calculation-based (like handling division by zero in a ratio, or computing year-over-year growth), do it in DAX.

Why? Power Query transformations happen once when you refresh your data. DAX calculations happen every time someone interacts with your report. If you try to do heavy transformations in DAX, your dashboards will be slow and painful to use.

Another thing—documentation is not optional. When I create a Power BI file, I keep notes about what I cleaned and why. Three months later when someone asks why you excluded certain records or how you defined "active customer," you need to have an answer. I use the Description field in Power Query steps and add comments to complex DAX measures.

Also, always keep a copy of your original raw data. Never overwrite source files. I learned this the hard way when I spent a day cleaning a dataset, saved over the original, then realized I'd filtered out records I actually needed.

Dashboards: When Translation Finally Becomes Visible

Only after data is cleaned, modeled and measured does the dashboard truly matter.

Here's what I learned about dashboards: they're not meant to show everything—they're meant to guide attention. Early on, I tried cramming every possible metric onto one page. It looked impressive to me but confused everyone else.

Effective Power BI dashboards do a few things really well:

They highlight key metrics

Using KPI cards or scorecards. The three or four numbers that actually matter get prominent placement. Everything else is secondary.

They show trends instead of isolated values

A single number like "450 sales this month" means nothing without context. But a line chart showing whether that's up or down from last month? That tells a story.

They allow exploration through slicers and filters

Users can slice by date, region, product category, whatever makes sense. This turns a static report into an interactive tool.

They keep layouts simple and focused

White space is your friend. Clear labels matter more than fancy visuals. If someone has to squint or ask what they're looking at, you've failed.

When dashboards are designed well, decision-makers don't need to ask for new reports. They can explore the data themselves and act immediately. That's when analysis turns into action.

What Success Actually Looks Like

You know you've translated your messy data successfully when you can build a visual and it just works. You don't have to keep going back to Power Query to fix "one more thing." Your slicers show the categories you expect. Your date hierarchies make sense. Your totals actually total correctly.

The goal isn't perfection—there's always another tweak you could make. The goal is reliable data that answers business questions without creating new confusion.

That's what this whole job is about, really. Taking information that's scattered and inconsistent and broken, and turning it into something someone can actually use to make a decision.

The dashboards and visualizations are just the final 10% that everyone sees. The real work happens in Power Query, in those transformation steps nobody ever asks about.

From Learning Power BI to Actually Thinking Like an Analyst

Learning Power BI taught me something important: data analysis is not about tools—it's about thinking.

Power BI simply provides the environment where analysts:

  • Translate messy data using Power Query
  • Structure it with proper modeling
  • Define meaning using DAX
  • Communicate insights through dashboards

Once I understood this workflow, Power BI stopped feeling overwhelming and started making sense. The tool just facilitates the process. The real skill is knowing what questions to ask at each stage.

If you're just starting out, focus less on perfect visuals and more on understanding this process. Master the unglamorous parts first—the data cleaning, the relationships, the proper data types. Everything else becomes easier once the foundation is right.

And honestly? Once you get good at this part, the actual analysis becomes almost fun. Because you're working with data you trust, data you understand, data that's finally ready to tell you something true.

If you're learning Power BI, mastering how data flows from raw to actionable insight will save you months of confusion and make your dashboards far more valuable than any fancy visual ever could.

What's been your biggest challenge learning Power BI? Drop a comment below,I'd love to hear what trips people up or what clicked for you.

Could Your Serverless Database Be Costing You Too Much? Meet Coldbase.

2026-02-06 22:26:00

Building serverless apps is great, right? Fast to build, scales easily. But sometimes, those database bills can be a surprise. Popular serverless databases often come with costs that grow quickly.

What if there was a different way? A simpler database designed for serverless that helps you manage costs better?

That's where Coldbase comes in.

Coldbase is a small, serverless-focused database. It uses affordable cloud storage, like AWS S3 or Azure Blob, to keep your data. It's a different approach to storing data in serverless environments. It is currently in Beta, and we welcome your feedback as we continue to refine it.

Why Database Costs Can Add Up

Serverless databases are great for many things, but their pricing can sometimes be high. They often charge based on how many reads and writes you do. As your app gets more users, these costs can increase quickly.

Coldbase's Simple Idea: Cloud Storage as Your Database

Coldbase takes a different path. It uses standard cloud storage (like S3 or Azure Blob) as its foundation. These services are known for being very affordable, especially for storing lots of data and handling many operations.

By building on this low-cost storage, Coldbase aims to give you significant savings on your database bill. We've seen it be much cheaper than some traditional options. For a more detailed look at the cost difference, you can check our comparison here.

Coldbase Features: Simple Tools for Serverless

Coldbase offers features to help you build serverless applications more efficiently:

  • Works with Serverless: Designed for serverless functions (like AWS Lambda). It doesn't hold data in memory between calls, which fits the serverless way.
  • Affordable Storage: Uses cloud storage you already know (AWS S3, Azure Blob, or local files) to keep costs low.
  • Vector Search: If you're working with AI, it can store and search vector embeddings. This can be useful for semantic search or RAG applications.
  • Automated Housekeeping: It can automatically tidy up your data files (compaction and vacuuming) in the background, without you needing to manage it.
  • Easy Data Access: Find your data with simple queries, filtering, and pagination.
  • Batch Operations & TTL: Write many items at once or set data to expire automatically.
  • HTTP API Option: You can quickly add a REST API using Hono, complete with basic security and documentation.

Quick Start: Try Coldbase with AWS S3

Want to see Coldbase in action with a cloud storage backend? Here’s a simple example using AWS S3.

import { Db, S3Driver } from 'coldbase'
import { S3Client } from '@aws-sdk/client-s3'

interface User {
  id: string
  name: string
  email: string
  role: string
}

// Configure your S3 client
const s3Client = new S3Client({ region: 'us-east-1' })

// Instantiate S3Driver with your bucket name and S3 client
const db = new Db(new S3Driver('my-coldbase-bucket', s3Client))
const users = db.collection<User>('users')

// Add a new user
await users.put({ id: 'u1', name: 'Alice', email: '[email protected]', role: 'admin' })

// Find a user
const user = await users.get('u1')
console.log('Found user:', user)

// Get all admins
const admins = await users.find({ where: { role: 'admin' } })
console.log('Admins:', admins)

// Remove a user
await users.delete('u1')
console.log('User u1 deleted.')

Coldbase is a practical way to manage data in serverless applications, with a focus on cost-efficiency. If you're looking for an alternative to high database bills, consider giving Coldbase a try.

Repo here

Automated Video Generation with n8n and Remotion: Server Setup Guide

2026-02-06 22:20:43

I built a video generation workflow by combining n8n (a business automation tool) and Remotion (a React-based video framework).

In this post—the "Server Setup Edition"—I will share how to build an HTTP API server (Express) on a VPS to render Remotion videos, along with the specific pitfalls I encountered (such as memory shortages and bundling times).

Architecture

The setup involves sending a POST request from n8n's "HTTP Request" node. The server receives parameters (like text and colors), renders the video on the server side, and returns the file path.

  1. n8n: Sends text and parameters via POST
  2. Express Server (VPS): Receives the request and executes Remotion
  3. Remotion: Renders React components into an MP4 file
  4. Response: Returns the path of the generated video

Environment

Since Remotion rendering uses a headless browser, it consumes significant resources, especially memory.

  • VPS OS: Ubuntu 22.04
  • Node.js: 20.x
  • Remotion: 4.0.x
  • Framework: Express 4.x
  • Memory: 4GB or more recommended (2GB will likely cause crashes)

Project Structure

The structure is simple. The api directory contains the server logic, and src contains the React components for the video.

remotion-server/
├── src/
│   ├── Video.tsx          # Video content (React component)
│   └── index.ts           # Remotion entry point
├── api/
│   └── server.ts          # Express server entry point
├── out/                   # Destination for rendered videos
├── package.json
└── tsconfig.json

Implementation

1. package.json

The key point here is the start script. Since the Remotion rendering process is heavy, I relaxed the Node.js heap memory limit.

{
  "name": "remotion-server",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node --max-old-space-size=3072 api/server.js"
  },
  "dependencies": {
    "@remotion/bundler": "^4.0.0",
    "@remotion/renderer": "^4.0.0",
    "express": "^4.18.2",
    "react": "^18.2.0",
    "remotion": "^4.0.0"
  },
  "devDependencies": {
    "@types/express": "^4.17.17",
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0"
  }
}

2. Video Component (src/Video.tsx)

This is a simple video component that changes text and background color based on the received props.

import { AbsoluteFill, useCurrentFrame, interpolate } from 'remotion';
import React from 'react';

interface VideoProps {
  text: string;
  color: string;
}

export const MyVideo: React.FC<VideoProps> = ({ text, color }) => {
  const frame = useCurrentFrame();

  // Fade in between frames 0 and 30
  const opacity = interpolate(frame, ,, {[1]
    extrapolateRight: 'clamp',
  });

  return (
    <AbsoluteFill
      style={{
        backgroundColor: color,
        justifyContent: 'center',
        alignItems: 'center',
      }}
    >
      <h1 style={{ fontSize: 100, color: 'white', opacity }}>
        {text}
      </h1>
    </AbsoluteFill>
  );
};

3. API Server (api/server.ts)

This is the core implementation. To improve performance, I designed it to execute the Webpack bundle only once at server startup and cache the result for reuse.

import express from 'express';
import { bundle } from '@remotion/bundler';
import { renderMedia, selectComposition } from '@remotion/renderer';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';

const app = express();
app.use(express.json());

// Path resolution for ES Modules environment
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Keep bundle result in memory
let cachedBundleLocation: string | null = null;

// Create bundle at server startup
async function initializeBundle() {
  console.log('Creating Webpack bundle...');
  const entryPoint = path.resolve(__dirname, '../src/index.ts');

  cachedBundleLocation = await bundle({
    entryPoint,
    webpackOverride: (config) => ({
      ...config,
      cache: { type: 'filesystem' }, // Speed up build
    }),
  });
  console.log('Bundle created:', cachedBundleLocation);
}

// Video generation endpoint
app.post('/render', async (req, res) => {
  try {
    const { text = 'Hello', color = '#000000' } = req.body;

    if (!cachedBundleLocation) {
      return res.status(500).json({ error: 'Bundle not initialized' });
    }

    // Select composition (video settings)
    const composition = await selectComposition({
      serveUrl: cachedBundleLocation,
      id: 'MyVideo',
      inputProps: { text, color },
    });

    // Generate output file path
    const timestamp = Date.now();
    const outputLocation = path.resolve(__dirname, `../out/video-${timestamp}.mp4`);

    // Create directory
    const outputDir = path.dirname(outputLocation);
    if (!fs.existsSync(outputDir)) {
      fs.mkdirSync(outputDir, { recursive: true });
    }

    console.log(`Rendering started: ${outputLocation}`);

    // Execute rendering
    await renderMedia({
      composition,
      serveUrl: cachedBundleLocation,
      codec: 'h264',
      outputLocation,
      inputProps: { text, color },
    });

    console.log('Rendering completed.');

    res.json({
      success: true,
      path: outputLocation,
      filename: path.basename(outputLocation),
    });

  } catch (error) {
    console.error('Render error:', error);
    res.status(500).json({
      error: 'Rendering failed',
      message: error instanceof Error ? error.message : 'Unknown error',
    });
  }
});

// Startup process
const PORT = process.env.PORT || 3000;
initializeBundle()
  .then(() => {
    app.listen(PORT, () => {
      console.log(`Server running on port ${PORT}`);
    });
  })
  .catch((e) => {
    console.error('Failed to start server:', e);
    process.exit(1);
  });

Usage

Startup

# Install dependencies
npm install

# TypeScript compilation
npx tsc

# Start server (with heap memory expansion)
npm start

Request Example

Send a request from n8n or Curl as follows:

curl -X POST http://localhost:3000/render \
  -H "Content-Type: application/json" \
  -d '{"text":"Hello Zenn","color":"#3ea8ff"}'

Response:

{
  "success": true,
  "path": "/path/to/remotion-server/out/video-1738465200000.mp4",
  "filename": "video-1738465200000.mp4"
}

Technical Challenges & Gotchas

Here are three major issues I faced during implementation and their solutions.

1. Bundling was too heavy and caused timeouts

Initially, I executed the bundle() function for every request. This meant Webpack ran every time a video was generated, resulting in a wait time of over 5 minutes before rendering even started.

Solution:
I changed the logic to run bundling only once at server startup (initializeBundle) and cache the generated bundleLocation in a variable. This allows rendering to start immediately for all subsequent requests.

// NG: Executed inside every request
// app.post('/render', async (req, res) => {
//   const bundleLocation = await bundle({ ... }); // Too heavy
// });

// OK: Executed once at startup and cached
let cachedBundleLocation: string | null = null;
await initializeBundle(); 

2. Process crashed due to memory shortage

When running on a VPS (2GB memory plan), the process was frequently killed (Killed) during rendering. Remotion uses Chromium in headless mode, which consumes a lot of memory.

Solution:
I added the --max-old-space-size option to increase the Node.js heap size limit. I also highly recommend using a VPS with at least 4GB of RAM.

"scripts": {
  "start": "node --max-old-space-size=3072 api/server.js"
}

3. Path resolution differences (__dirname vs process.cwd)

I encountered issues where file paths drifted between the development environment and the production environment (when started with PM2, etc.).

  • process.cwd(): The directory where the command was executed (depends on launch location)
  • __dirname: The directory where the script file is located (depends on file location)

Solution:
I unified the path resolution by using __dirname (generated from import.meta.url since this is ESM) for specifying the entry point, resolving relative paths from there. This ensures stability regardless of where the command is run.

// NG: Path changes depending on where you launch
// const entryPoint = path.resolve(process.cwd(), 'src/index.ts');

// OK: Reliable as it's based on server.ts location
const entryPoint = path.resolve(__dirname, '../src/index.ts');

Conclusion

I have now completed an API server that can be called from n8n to generate videos at any time.

In this example, I simply saved the MP4 locally and returned the path. However, in a production environment, adding a process to upload the file to AWS S3 and return a signed URL would make it more practical.

Automated video generation can be applied to various use cases, such as automating SNS posts or creating personalized videos.

References

Welcome to The Foundation: Preserving Developer Knowledge in Public

2026-02-06 22:18:55

We're building public alternatives to private AI knowledge.

Why We Exist

Stack Overflow's traffic dropped 78% in two years. Wikipedia gets buried by Google's AI summaries. Developers solve problems in private AI chats that leave no public trace.

The result: Junior developers have no Stack Overflow to learn from. The knowledge commons that taught verification, architecture, and skepticism is dying.

Our mission: Preserve and build public knowledge infrastructure.

What We're Building

Right now (Bridge solution):

  • Curate valuable discussions
  • Document reasoning paths publicly
  • Share verification techniques
  • Mentor explicitly in public

Soon (Federated infrastructure):

  • ActivityPub-based Q&A platform
  • Community-owned (no corporate control)
  • Federated (multiple instances, no single point of failure)
  • Open source from day one

How This Started

From a series of articles:

  1. My Chrome Tabs Tell a Story We Haven't Processed Yet
  2. We're Creating a Knowledge Collapse and No One's Talking About It
  3. Above the API: What Developers Contribute When AI Can Code

The comments proved something: developers want to build alternatives, not just complain.

Current Members

  • @dannwaneri - Founder, Cloudflare Workers specialist
  • @richardpascoe - Fediverse advocate, builder
  • @nandofm - Self-hosting specialist, months of thinking on knowledge entropy

How to Contribute

If you're interested in:

  • Writing about verification techniques in the AI era
  • Documenting architecture decisions publicly
  • Building federated infrastructure
  • Preserving developer knowledge

You're welcome here.

Comment below or message us to get involved.

The Bridge to Building

We're using this organization as a bridge:

  1. Test what developers actually need (on existing platform)
  2. Document what works and what doesn't
  3. Build federated version with real user feedback

We don't wait for perfect infrastructure. We start with imperfect platforms, learn fast, build better.

Join Us

If you believe knowledge should compound publicly, you belong here.

Let's build what comes after Stack Overflow.

Questions? Comments? Want to help? Drop a comment below.