Django vs Node.js: Which Wins for Data-Heavy Apps?

Metric Value
Redis operations per second with Django caching 100,000+
Daily data processed by Spotify’s Django analytics 600GB
NumPy speed advantage over JavaScript for matrices 10-100x

Django vs Node.js is the core decision for any data-heavy application: you either prioritize real-time concurrency (Node.js) or deep data processing (Django). A data-heavy application isn’t just one with a big database. It’s processing millions of records daily, running ETL pipelines that transform messy data into insights, and integrating machine learning models that need constant retraining. Instagram processes 95 million photos every single day. Spotify crunches through 600GB of user data to power its recommendation engine. These aren’t simple CRUD operations. they’re complex workflows that demand serious computational muscle. And here’s where the fundamental difference matters: Django runs on Python, the language that owns 49.28% of the data science market. Node.js runs on JavaScript, which barely registers at 3.17%.

That market share tells the real story. When you’re building with Django, you get pandas for data manipulation, NumPy for numerical computing, and scikit-learn for machine learning. all in the same language as your web framework. No context switching. No serialization overhead between services. We learned this firsthand at Horizon Dev when rebuilding VREF Aviation’s 30-year-old platform. Processing 11 million OCR records isn’t just about raw speed. it’s about having the right tools to clean, validate, and extract meaningful data from scanned documents. Python’s ecosystem made that possible.

Node.js isn’t slow. it actually destroys Django in raw throughput benchmarks. Techenable’s latest round shows Express handling 367,069 requests per second for JSON serialization while Django manages 12,142. But those numbers miss the point entirely. Your bottleneck in data-heavy applications isn’t serving JSON. It’s the data pipeline that transforms raw records into something useful, the statistical models that detect anomalies in financial data, or the neural network that classifies millions of images. Try implementing a random forest algorithm in JavaScript. Now try it in Python with scikit-learn. One takes a week, the other takes an afternoon.

Django’s ORM isn’t just another abstraction layer. With proper indexing, it handles over 1 million database records at 0.8-1.2ms per query, fast enough for real-time dashboards serving thousands of concurrent users. The magic is in how Django generates SQL. It’s smart about JOIN operations, prefetching related objects, and query optimization. We rebuilt a legacy aviation platform that processes 11 million aircraft maintenance records, and Django’s ORM handled complex queries across 47 related tables without breaking a sweat. Built-in connection pooling with pgBouncer scales to 15,000+ concurrent database connections.

The real advantage shows when you start processing data. Node.js hits a wall at 1.4GB of memory on 64-bit systems unless you manually adjust V8 flags. Python and Django? No artificial ceiling. Load a 50GB dataset into memory for machine learning preprocessing, Python handles it. This matters when you’re building data pipelines that transform millions of records. Eventbrite’s Django REST Framework setup serves 80 million users with 98.6% of requests completing under 50ms, including complex aggregations across event data, user preferences, and payment processing.

Python’s data science ecosystem integration changes everything. Read a 1GB CSV with pandas in 2.3 seconds, then pipe it directly into your Django models. NumPy gives you 10-100x faster matrix operations compared to vanilla JavaScript implementations. You’re not gluing together disparate tools, it’s one cohesive stack. When we build automated reporting systems for clients, Django handles the web layer while pandas and scikit-learn crunch the numbers in the same process. No message queues, no microservice overhead, just Python talking to Python.

Node.js is fast. Really fast. PayPal found out when they dropped Java and watched their servers handle 2 billion requests daily with half the hardware. The engineering team reported a 2x jump in requests per second. That kind of performance improvement makes CFOs smile and DevOps teams sleep better. But raw speed tells only part of the story when you’re crunching gigabytes of user behavior data or training recommendation models.

The V8 engine that powers Node.js has a dirty secret: it caps heap memory at 1.4GB by default. You can bump this limit with flags, sure, but then you’re fighting the runtime’s design. Worker threads help distribute CPU-intensive tasks, but you’re stuck with whatever cores your server has. typically 4 to 16. Django with Celery? I’ve scaled task queues to 1000+ concurrent workers without breaking a sweat. Netflix figured this out early. They use Node.js for their slick UI layer but rely on Python for the heavy lifting. analyzing viewing patterns, personalizing content, processing terabytes of user data.

Here’s what Node.js does brilliantly: I/O operations. Reading files, making API calls, handling WebSocket connections. Node.js crushes these tasks. Instagram serves 500 million daily active users and processes 95 million photos every single day. Their stack? Django. Not because Node.js couldn’t handle the traffic (it absolutely could), but because Python’s data ecosystem is unmatched. NumPy, Pandas, scikit-learn. these aren’t just libraries, they’re the foundation of modern data engineering. Node.js has… well, it has npm packages that wrap Python libraries. That should tell you everything.

Discord’s infrastructure team learned this lesson the hard way. After hitting 120 million daily messages, they migrated key data processing services from Node.js to Python. The culprit wasn’t Node’s speed, it was memory management and data transformation bottlenecks. When you’re processing CSV exports at scale, Python’s pandas library destroys JavaScript alternatives: 2.3 seconds for a 1GB file versus 8-12 seconds with the best JavaScript libraries. That’s not a small difference. It determines whether your data pipeline finishes before lunch or drags into the afternoon. Django wraps this performance in a battle-tested framework that connects to PostgreSQL with pgBouncer handling 12,000+ concurrent database connections. Node.js’s pg library? Defaults to just 100.

Instagram’s 500 million daily active users generate absurd amounts of data, all flowing through Django backends. Their engineering team isn’t choosing Django for nostalgia, they need Python’s ecosystem for computer vision, recommendation algorithms, and data pipelines. Same story at Spotify and Pinterest. When we rebuilt VREF Aviation’s platform at Horizon Dev, OCR extraction from 11+ million aviation records was non-negotiable. Node.js would have meant stitching together half-baked libraries or calling Python microservices anyway. Django gave us pytesseract, OpenCV, and pandas in one cohesive stack.

PayPal’s Node.js migration gets cited constantly as a success story. They doubled their requests per second moving from Java. But look closer, they’re processing payments, not training models or running complex analytics. Node.js excels when you need to move JSON between services at breakneck speed. Django’s real strength appears in the boring stuff: auto-generating admin interfaces for 100+ database models, built-in migration systems that handle schema changes across millions of rows, and ORMs that actually understand complex relationships. These features don’t sound exciting until you’re drowning in data models at 2 AM.

Django’s ORM beats Node.js alternatives when you’re working with complex queries and large datasets. I’ve migrated dozens of legacy systems where Sequelize fell apart on batch operations that Django handled fine. Take batch inserts: Django processes 10,000 records in 0.5 seconds. Sequelize? 2.8 seconds for the same thing. That’s a 5.6x difference. The gap gets worse with complex joins and aggregations. Django REST Framework at Eventbrite serves 98.6% of requests under 50ms while managing 80 million users, that’s production-scale reliability you can actually depend on.

Node.js ORMs feel unfinished next to Django. TypeORM and Sequelize don’t have Django’s proven migration system that tracks schema changes across hundreds of deployments. Connection pooling is another headache. Django with pgBouncer handles thousands of concurrent connections right away. Node.js? You’re stuck piecing together pool configurations that crash under load. We learned this the hard way at Horizon Dev when migrating VREF Aviation’s 30-year-old platform with 11 million OCR records.

Caching shows the real difference. Django’s built-in Redis integration hits 100,000+ operations per second without extra libraries or setup nightmares. Node.js makes you write manual cache invalidation logic that Django handles automatically through ORM signals. Netflix gets this, they use Node.js for their UI layer (cutting startup time by 70%) but keep Python for data processing and recommendation algorithms. The lesson? Pick the right tool. For data-heavy applications, that’s Django.

When raw request throughput matters, Node.js destroys Django. Express handles 367,069 requests per second for JSON serialization while Django manages 12,142 in Techenable’s Round 22 benchmarks. But here’s the thing: data-heavy applications rarely bottleneck on JSON serialization. They choke on ETL pipelines, batch processing, and complex analytics. Django pairs with Celery to spawn 1000+ workers that crunch through terabytes without breaking a sweat. I’ve watched teams try to replicate this with Node.js cluster module. They hit wall after wall.

Spotify processes 600GB of user data daily through Django-powered analytics pipelines. Their architecture runs thousands of Celery workers across hundreds of machines, each handling specific data transformation tasks. Node.js excels at streaming that same data with minimal memory overhead. processing 1GB files using just 50MB RAM through streams. But Spotify needs more than streaming. They need NumPy vectorization, Pandas groupby operations, and scikit-learn model training. Python owns 49.28% of the data science market while JavaScript sits at 3.17%. There’s a reason for that gap.

Django’s horizontal scaling patterns are boring. And that’s exactly what you want. Database read replicas, Redis caching layers, Celery worker pools. these patterns have worked for a decade. Teams at Horizon Dev have migrated legacy platforms handling millions of records using these exact strategies. Node.js microservices offer more architectural flexibility, sure. You can build event-driven pipelines that scale elastically. But complexity compounds fast when you’re juggling 20 services just to run a data pipeline that Django handles in a single codebase.

After running both frameworks in production for years, here’s the truth: Django wins for data-heavy applications. Not because it’s faster at serving requests. it isn’t. Node.js beats Django in raw throughput benchmarks every time. But when you’re processing datasets over 1GB regularly, Django’s mature ecosystem and Python’s data science libraries are hard to beat. The Django ORM handles 1 million+ database records with proper indexing, processing queries at 0.8-1.2ms each. That’s plenty fast. Plus you get battle-tested tools for migrations, admin interfaces, and complex queries without writing raw SQL.

Node.js runs into problems when memory becomes the constraint. The V8 engine caps out at 1.4GB of memory by default on 64-bit systems. Sure, you can increase it, but you’re fighting the runtime. Python and Django? No such limitation. We learned this firsthand at Horizon Dev when building OCR extraction systems for VREF Aviation’s 11 million aviation records. Started with Node.js for the API layer. Two weeks later we switched to Django after constant memory errors and garbage collection issues. The Python ecosystem gave us pandas for data manipulation, scikit-learn for classification, and Tesseract bindings that actually worked.

Node.js shines in specific scenarios: real-time data streaming where you’re processing small chunks continuously, not batch operations on massive datasets. Building a trading platform dashboard? IoT sensor network? Node.js is your answer. Its event loop architecture handles thousands of concurrent WebSocket connections beautifully. But for typical business applications. generating reports, running analytics, integrating machine learning models. Django offers a smoother path. Legacy migrations especially benefit from Django’s solid ORM and automatic admin interface. You’ll have a working CRUD interface in hours, not days.

Verdict

Which is faster for processing large datasets: Django or Node.js?

Node.js wins when you’re streaming data. Its event-driven design lets you process chunks without loading everything at once. You can handle a 1GB file with just 50MB of memory in Node.js. Django? It’ll eat the whole gigabyte. But speed isn’t the whole story. Django’s ORM paired with PostgreSQL queries 5 million records in 0.3 seconds if you index properly. For batch jobs, Django with Celery is more reliable than Node.js. Instagram processes 95 million photos daily on Django. clearly it scales. The real answer? It depends. Streaming sensor data in real-time? Go Node.js. Running complex queries across 20+ related tables? Django’s ORM will save you weeks. Most data-heavy apps actually need both. Use Node.js microservices to ingest data, Django for your business logic and reports.

How much memory does Django use compared to Node.js for data processing?

Django eats 3-5x more memory than Node.js for the same data tasks. Processing a 500MB CSV? Your Django worker needs 2GB RAM. Node.js does it with 400MB using streams. Why? Django loads entire querysets into memory by default. Fire up Django’s debug toolbar and watch memory spike 800MB when you serialize 100,000 records to JSON. Node.js stays flat at 150MB using cursor pagination. But that memory hunger has perks. Django’s aggressive caching makes repeat requests 10x faster. The Django admin generates CRUD interfaces for 100+ models in under a second because of this approach. Plan on 4GB RAM per Django worker in production, versus 1GB for Node.js. Spotify runs thousands of Django instances. they’ve decided the memory cost is worth the speed boost.

Can Django handle real-time data updates as well as Node.js?

No, Django isn’t great at real-time. You need Django Channels for WebSockets, which adds complexity and 30% infrastructure overhead. One Node.js server handles 10,000 WebSocket connections with Socket.io. Django Channels tops out around 3,000 before you’re scrambling to add Redis and scale workers. Makes sense when you think about it. Node.js was designed for real-time. Django was designed for request-response cycles. Uber tracks driver positions with Node.js. 15 million updates per minute. But Django shines elsewhere. You can build a complete analytics dashboard in 2 days that would take 2 weeks in Node.js. Want the best of both? Run Node.js for WebSockets and Django as your API. Discord does this. Node.js manages 120 million concurrent users while Django handles the actual user data and permissions.

What are the database performance differences between Django ORM and Node.js?

Django’s ORM beats Node.js ORMs hands down for complex queries. A 5-table join with aggregations? 12 lines in Django. 45+ in Sequelize or TypeORM. Django’s prefetch_related() and select_related() kill N+1 queries automatically. Reddit loads 500+ comments on their homepage with just 3 database hits. that’s Django at work. Node.js ORMs can’t match this. Prisma gets close but you’re still optimizing queries by hand. Raw SQL speed? Identical. Both hit 50,000 queries/second on PostgreSQL with decent hardware. The real difference is developer time. Django migrations handle schema changes across 200+ tables without breaking prod. Node.js tools like Knex need manual rollback scripts. Simple CRUD? Either works. Data warehouse with 50+ models and gnarly business logic? Django’s 19-year-old ORM is still king. Even Stripe uses Django for financial reporting while running Node.js microservices.

Should I rebuild my legacy data platform in Django or Node.js?

Django’s your safer bet for legacy systems with complex data. You get admin panels, user auth, and migrations out of the box. Node.js? You’re building these yourself. add 2-3 months to your timeline. We rebuilt a platform processing 11 million aviation records. Django handled it without breaking a sweat. The admin interface alone saved us 400 hours on the VREF Aviation project. Legacy systems often need OCR and automated reporting. Django has battle-tested libraries like django-q and Celery. Node.js options aren’t as solid. Your team matters too. Python pros? Django will be smooth sailing. JavaScript shop? Maybe Node.js is worth the extra hassle. At Horizon Dev, we use both. But for data-heavy legacy rebuilds, Django gets you there faster with fewer headaches. The Microsoft Flipgrid migration worked because Django handled 1 million users without any architectural rewrites.

Originally published at horizon.dev

7 Headless CMSs in 2026: Stop Picking the Wrong Headless CMS — A No-BS Guide

In this guide, we’ll walk through how to pick a headless CMS that fits real‑world enterprise, marketing, and content‑ops needs — not just your current stack.
When you choose a CMS for your next project, that decision shapes:

  1. How your content operations run.
  2. How editors create and update content.
  3. How your stack scales over time.

47% of organizations cite a skills gap and 44% report integration complexity as their top headless CMS challenges Global Growth Insights — both symptoms of picking the wrong platform for your team.

We’ve shipped headless projects in production using Contentful, Sanity, Storyblok, Payload CMS, Hygraph, Directus, and Strapi — no vendor partnerships, no hidden agenda, just real‑world experience picking the right tool for the job.

Let’s get into it — platform by platform, no vendor bias.

The headless CMS landscape (the short list)

Here are the most relevant options we see clients seriously considering:

  1. Contentful
  2. Storyblok
  3. Sanity
  4. Payload CMS
  5. Hygraph CMS
  6. Directus
  7. Strapi

TL;DR – CMS “defaults” in 60 seconds

Contentful CMS – Enterprise default. Mature, compliant, expensive.

Pay 40–60% more than the headline price once you hit hidden limits.

Worth it if you’re doing sophisticated personalization at scale; otherwise, you’re just carrying costs.

Storyblok CMS – Marketing‑site‑first CMS with great self‑serve UX.

Visual editor is excellent, but the architecture can push back once your content model outgrows typical marketing patterns.

Sanity CMS – Content operations platform for dev‑led teams.

Schema‑as‑code, GROQ, real‑time collaboration.

Overkill for a simple marketing site; rewards teams who invest in building a proper Studio.

Payload CMS – MIT‑licensed, Next.js‑native, self‑hostable CMS.

No licensing ceiling, full ownership.

You’re betting on your dev team: upfront investment is real, but the ceiling is yours.

Hygraph – GraphQL federation for complex backends.

Only worth it if you genuinely need GraphQL federation across multiple systems.

Directus – Open‑source path. Use Directus when you need to wrap an existing SQL database.

Strapi – Use Strapi when building fresh with a new DB.

Match Your Project to the Right CMS

Best default CMS Situational need
Contentful Enterprise-scale, large budget, advanced personalization
Storyblok Small–mid marketing site, strong editor independence
Sanity Experienced dev team, complex content ops, or Shopify-like complexity
Payload CMS Flexible, scalable platform beyond standard CMS limits
Hygraph Genuine need for GraphQL federation across backends
Strapi New project, open-source, JavaScript/TypeScript stack
Directus Existing database, internal tools, legacy data modernization

Headless CMS Overview: Directorial listing

1. Contentful – The enterprise standard
Contentful on GitHub

Think of Contentful CMS as the “IBM of headless CMS.” It’s the safe, nobody‑gets‑fired‑for‑this choice. It’s mature, stable, and has solid compliance and uptime — which is why ~30% of the Fortune 500 use it.

Pros

  • Structured content, strong localization, and growing AI‑assisted features.
  • Enterprise‑grade availability and governance.
  • Huge ecosystem and referenceable case studies for procurement teams.

Cons

  • Pricing is designed to extract maximum value. You start on an “affordable” plan, then hit content‑type limits, usage overages, and “enterprise‑only” features.
  • The content model is rigid compared to more flexible options like Sanity or Payload.
  • Marketers get a functional but uninspiring UI and pay enterprise rates for it.
  • Many teams end up paying for 80% of features they never use.

👍🏻 When to use Contentful

You need enterprise‑grade personalization (Contentful Studio, A/B testing, AI content gen).

You’re running sophisticated campaigns and want compliance, SLAs, and integrations in one package.

👎🏻 When to avoid it

  • Cost‑conscious projects.
  • Small teams.
  • Any situation where you pay the enterprise tax without using the enterprise‑level features.

2. Storyblok – The marketer’s platform
Storyblok on Github

Headless CMS has a classic problem: it liberates developers but often imprisons marketers. A lot of projects focus on dev flexibility while leaving editors with ticket‑based workflows.

Storyblok is built on the opposite idea: editors come first.

Storyblok Pros

  • Drag‑and‑drop visual editor, live previews, and self‑serve publishing.
  • Component‑based architecture that makes sense for marketing sites: heroes, features, testimonials, CTAs.
  • Huge win for marketing sites, landing pages, and campaign content.

Storyblok Cons

  • The component‑based model gives you Storyblok’s constraints, not yours.
  • As your content model grows, page/component counts rise, and multi‑domain or multi‑channel content becomes harder.
  • If your “marketing site” evolves into an application‑like product (commerce, workflows, custom logic), you hit architectural walls.

👍🏻 When to use Storyblok

  • Small–mid marketing sites.
  • Agencies whose clients need to self‑serve.
  • Organizations where marketing autonomy is a real business requirement.

👎🏻 When to avoid Storyblok

  • Complex content models.
  • Unusual data relationships.
  • Projects likely to grow beyond typical marketing‑site patterns.

3. Sanity – The content operations platform
Sanity on GitHub

Sanity is the headless CMS for devs who typically hate CMSs.

Sanity Pros

  • Schema‑as‑code in version control.
  • Real‑time collaboration that actually works (Google Docs‑style).
  • GROQ: fetch exactly the data you need and transform it in a single query.
  • Dev‑led teams with strong front‑end capabilities.

Organizations running complex content operations (media archives, e‑commerce catalogs, content‑heavy SaaS). Cases where content structure itself is a competitive advantage.

Sanity Cons

  • Visual editing is weak. Present (preview) is helpful, but marketers will still need dev help.
  • Free tier is generous for small projects, but enterprise pricing scales aggressively.
  • Overkill for standard corporate marketing sites — you pay for capabilities you don’t use and get a worse editor experience than simpler options.

👍🏻 When to use Sanity

  • Large content operations.
  • Teams willing to invest in building a custom Studio.
  • Organizations where content modeling is strategic.

👎🏻 When to avoid Sanity

  • Tight budgets.
  • Standard marketing sites.
  • Teams that need marketing independence.

4. Payload CMS – Build your ideal CMS
Payload on GitHub

Payload is the newer, MIT‑licensed, Next.js‑native CMS that’s gaining traction in the TypeScript ecosystem.

Pros

  • Deep integration with Next.js (front‑end + admin).
  • Self‑hostable, scalable, and fully owned.
  • MIT‑style licensing: no awkward commercial‑use restrictions.
  • A flexible foundation, not a finished product.

Cons

  • You build the CMS experience, so upfront dev time is real.
  • Ecosystem is growing but younger than Strapi or Sanity.

👍🏻 When to use Payload

  • Teams that live in Next.js and TypeScript.
  • Organizations that want to own their stack completely.
  • Projects that won’t fit neatly into standard CMS patterns.

👎🏻 When to avoid Payload

  • Non‑Next.js teams.
  • “Turnkey” expectations.
  • Cases where time‑to‑market matters more than long‑term flexibility.

5. Hygraph CMS – The GraphQL specialist
Hygraph on Github

Think of Hygraph as the purpose-built CMS choice when your architecture genuinely requires unifying content across multiple backends, systems, and data sources into a single GraphQL API layer.

Pros

  • Native GraphQL federation: combine editorial content, product data, and third-party APIs into one unified graph.
  • Content federation without replication — query remote data directly rather than duplicating it.
  • Clean developer experience for teams already living in GraphQL.

Cons

  • Easy to overbuy: most projects don’t actually need federation, and Hygraph’s core strength goes unused.
  • Smaller community and ecosystem compared to Contentful, Sanity, or Strapi.
  • Visual editing is basic — not a platform marketers will enjoy independently.

👍🏻 When to use GraphQL CMS

  • Strong fit for complex, multi-source architectures where content isn’t siloed.

👎🏻 When to avoid GraphQL

  • Project budget is a concern
    Hygraph sits in the $10K–60K/year range and delivers value only in specific architectural scenarios.

  • You’re building a standard marketing site or blog
    This is overkill by definition.

6. Strapi – The open‑source path (new DB)
Strapi on GitHub

Strapi dominates the open‑source, JavaScript headless CMS space:

  • Visual content‑type builder.
  • Plugin marketplace and self‑hosting + Strapi Cloud options.

👍🏻 When to use Strapi

  • You’re building a new project with a separate database.

  • You want a large community and pre‑built plugins (auth, media, SEO fields).

👎🏻 When to avoid Strapi

  • Core is free, but “must‑have” plugins often require per‑seat licensing.
  • You need NoSQL — MongoDB users are explicitly excluded
    You expect zero ops overhead — self-hosting means your team owns uptime, backups, and upgrades
  • You’re mid-project and need to upgrade major versions — the “quick and dirty” reputation makes it harder to position as an enterprise solution to stakeholders

7. Directus – Wrap an existing database
Directus on GitHub

Directus takes a different approach: instead of imposing its own storage, it wraps your existing SQL database (PostgreSQL, MySQL, SQLite) and auto‑generates REST + GraphQL APIs plus an admin UI.

👍🏻 When to use Directus

  • Existing databases you don’t want to migrate.
  • Internal tools and legacy‑system modernization.
  • Exposing legacy data with an API and admin UI.

👎🏻 When to avoid Directus

  • You’re starting fresh with no existing database —
    Strapi is a better fit.
  • Your editors need a polished, marketer-friendly UI —
    the interface is clean but SQL-oriented and can
    confuse non-technical users.
  • You need deep UI customization — extensions require
    Vue.js knowledge and are sandboxed to the SDK.
  • Your org is over the BSL revenue threshold —
    commercial licensing costs need to be factored in
    before you commit.
  • You have no DevOps capacity — self-hosting means
    you own backups, scaling, and security.

Make Your Choice Based on CMS Costs

1. Contentful

The enterprise default: stable, compliant, and expensive once you start scaling.

Cost: $20K–150K+ / year

** 2. Storyblok**

The marketer-friendly choice: excellent for visual editing, as long as your content model stays fairly standard.

Cost: $5K–50K / year

3. Sanity

The content operations platform: powerful for structured content, but it expects real investment in the editor experience.
Cost: $5K–100K / year

** 4. Payload**

The builder’s CMS: flexible, modern, and self-hostable, with no licensing ceiling — but it asks more from your developers upfront.

Cost: $0 + dev time

** 5. Hygraph**

The GraphQL specialist: great when you truly need federation across systems, but easy to overbuy for simpler setups.

Cost: $10K–60K / year

** 6. Strapi**

The open-source path for new builds: familiar and flexible, with a strong community, but operational overhead still applies.

Cost: license + dev time

** 7. Directus**

The database-first CMS: ideal when you already have an existing SQL database and want to expose it cleanly.

Cost: license + dev time

Complexity grows.

Editors hit the limits of their UI.

Renewals hit your budget at scale.

Common Mistakes While Choosing CMS

  • Overpaying for Contentful without using personalization, A/B testing, or Studio properly.

  • Treating Sanity like a magic editor UX — it’s excellent only when you invest in the Studio; otherwise, marketers suffer.

  • Picking Storyblok for a project that will grow into something app‑like, then facing big rework or migration.

  • Reaching for Hygraph’s federation when they don’t actually have a multi‑backend data problem.

Wrap-Up 💬

The best headless CMS is the one your team can implement well. Budget for the real costs — licenses, dev time, and ops — and focus on what you build on top of it, because that’s where the real competitive edge lives.

If you need help choosing or setting up a CMS, feel free to reach out to Focus Reactive CMS Agency.

React Native Android plugin not found: what I would check first

If a React Native Android build fails with a message that a plugin was not found, it can look very specific at first.

But in practice, “plugin not found” errors can come from several different categories of problems:

  • React Native Gradle plugin setup
  • settings.gradle misconfiguration
  • plugin management issues
  • included build resolution problems
  • version mismatches after upgrades
  • autolinking-related setup problems
  • mixed Expo / bare React Native Android config

So even though the error message sounds narrow, the debugging still needs structure.

This is the checklist I would go through first.

Why this error wastes time

A “plugin not found” error often pushes people toward one of two bad reactions:

  • randomly changing Gradle files until something works
  • assuming the plugin name in the message is the only thing that matters

Both can waste time.

Usually, the real question is not just:

“Why can’t Gradle find this plugin?”

It is:

“Why is this Android build looking for that plugin in the wrong way or wrong place?”

That difference matters.

What this error usually points to

When I see a React Native Android plugin-not-found error, I usually think about these buckets first:

Bucket 1: Build structure

  • settings.gradle
  • pluginManagement
  • includeBuild
  • root build.gradle

Bucket 2: Version alignment

  • React Native version
  • Gradle wrapper
  • Android Gradle Plugin
  • Kotlin version

Bucket 3: Project migration / upgrade issues

  • partial upgrade
  • copied config from another project
  • outdated setup mixed with newer RN expectations

Bucket 4: Native ecosystem mismatch

  • Expo modules mixed into bare RN config
  • autolinking assumptions
  • package install changed project structure

That mental model usually saves more time than chasing the plugin name alone.

What I would check first

1. Go straight to android/settings.gradle

This is one of the first files I would inspect.

A lot of plugin-not-found errors come from how the build is wired together, not from the plugin itself.

I would check whether:

  • the React Native Gradle plugin is included correctly
  • includeBuild(...) is pointing where I expect
  • plugin management looks consistent with the project’s React Native version
  • there are no half-old / half-new setup patterns mixed together

If the structure is wrong here, the whole build can fail before anything else matters.

2. Check whether the project was partially upgraded

This is a very common cause.

For example:

  • React Native version was upgraded
  • but Android build files still reflect older setup expectations
  • or one Gradle-related file was updated while another stayed behind

That can create exactly the kind of plugin resolution error that feels mysterious until you compare the project structure against the RN version you are actually on.

When I see this kind of issue, I always ask:

Was this project recently upgraded or migrated?

If yes, that is a very strong clue.

3. Check version compatibility across RN / Gradle / AGP / Kotlin

A plugin-not-found error is not always purely about file paths.

Sometimes the project is structurally correct, but the version combination is not.

I would verify whether these actually make sense together:

  • React Native version
  • Gradle wrapper version
  • Android Gradle Plugin version
  • Kotlin version
  • JDK version

If one part of the toolchain is mismatched, plugin loading and project configuration can break before the build even gets to app code.

4. Check if Expo/native config is mixed in a fragile way

This is especially worth checking if the project uses:

  • Expo modules
  • bare React Native Android config
  • custom Gradle/plugin setup
  • autolinking expectations from more than one system

Sometimes the problem is not that one system is wrong by itself.

It is that two systems are being mixed in a way the build no longer understands cleanly.

If the project recently changed between Expo-managed, Expo modules, or bare-native assumptions, I would inspect that carefully.

5. Look at the exact plugin name, but don’t stop there

Yes, the plugin name matters.

But I would use it as a clue, not as the whole diagnosis.

For example, I would ask:

  • is this a React Native plugin?
  • an Android Gradle plugin?
  • a settings plugin?
  • a plugin expected from a package that is missing or not linked correctly?
  • something that changed in newer React Native versions?

The name helps place the issue into the right category, but it does not replace structural debugging.

6. Check what changed right before the error started

This question is often more useful than staring at logs for too long:

What changed last?

Examples:

  • React Native upgrade
  • new library added
  • Android files modified
  • Firebase/native setup changed
  • Expo modules added or removed
  • Gradle/Kotlin/JDK changed

Plugin-not-found errors often appear right after a structural change.

That is why the shortest path is often to review the last meaningful change first.

7. Treat cache cleanup as secondary unless the error supports it

Sometimes people jump straight to:

  • deleting .gradle
  • cleaning builds
  • reinstalling node modules
  • clearing everything possible

That can help, but I would not start there unless the error also suggests stale/corrupted build state.

A plugin-not-found error is more often structural or version-related than a pure cache issue.

So I would first inspect:

  • project structure
  • versions
  • upgrade/migration status
  • plugin resolution setup

Then do cleanup if needed.

8. Compare against a known-good structure for your RN version

If the project looks suspicious, one of the most useful sanity checks is:

Does this Android setup actually resemble what this React Native version expects?

That does not mean blindly overwriting files.

It means checking whether your current:

  • settings.gradle
  • plugin inclusion
  • Android root config

still matches the era/version of React Native you are on.

This is especially helpful when a project has been patched repeatedly over time.

A practical order of checks

If I hit a React Native Android “plugin not found” error, this is the order I would usually follow:

  1. inspect android/settings.gradle
  2. identify whether the plugin is part of RN, Android Gradle, or another native dependency
  3. check whether the project was partially upgraded
  4. verify RN / Gradle / AGP / Kotlin / JDK compatibility
  5. review the most recent structural change
  6. check Expo/native/autolinking interactions if relevant
  7. only then do broader cleanup if the error still points that way

That order usually reduces wasted motion.

What I would not do first

I would not immediately:

  • change multiple Gradle files randomly
  • downgrade libraries blindly
  • assume the plugin name alone explains the issue
  • reinstall everything on the machine
  • treat it like a cache problem by default

That usually increases confusion.

I built a small helper for this kind of issue

While working through React Native / Firebase / Android debugging problems, I built a small free error helper here:

fixmyrnerror.onhercules.app

It is meant to help narrow down likely causes faster.

Final takeaway

A React Native Android “plugin not found” error usually means the build structure, version alignment, or migration state needs to be checked carefully.

The plugin name is only the entry point.

The real fix usually comes from identifying which category you are actually in:

  • build structure
  • version mismatch
  • project upgrade drift
  • native ecosystem mismatch

That is where I would start every time.

Migrating From Laravel Forge to Deploynix: A Step-by-Step Guide

Switching server management platforms is one of those tasks that sounds risky but, with proper planning, is remarkably straightforward. If you are currently using Laravel Forge and considering a move to Deploynix, this guide will walk you through the entire migration process. We will cover how to document your existing configuration, provision equivalent infrastructure on Deploynix, migrate your data, and cut over DNS with minimal downtime.

The key principle throughout this migration is simple: run both platforms in parallel until you are confident the new setup works perfectly, then switch.

Why Migrate?

Before diving into the how, let us briefly address the why. Developers move between server management platforms for various reasons: pricing, features, support, or simply wanting a fresh approach. While Forge has added features like zero-downtime deployments and server monitoring in its recent overhaul, Deploynix offers a different set of strengths: built-in load balancing with multiple balancing methods (Round Robin, Least Connections, IP Hash), seven specialized server types for granular architecture control, scheduled deployments via API, Valkey as a truly open-source Redis alternative, support for modern frontend frameworks (Next.js, Nuxt, SvelteKit) alongside Laravel, and a free tier with vanity domains on deploynix.cloud to get started without a credit card.

But ultimately, the decision to migrate is yours. This guide assumes you have already made that decision and need a clear path from A to B.

Phase 1: Document Your Forge Configuration

The first step is to create a complete inventory of everything running on your Forge-managed servers. This is your migration blueprint.

Server Inventory

For each server on Forge, document:

  • Provider and region (e.g., DigitalOcean NYC3, Hetzner Falkenstein)
  • Server size (RAM, CPU, disk)
  • PHP version (8.1, 8.2, 8.3, 8.4)
  • Database type and version (MySQL 8, PostgreSQL 15, etc.)
  • Operating system (Ubuntu 22.04, 24.04)
  • Any custom packages installed via recipes or SSH

Site Configuration

For each site, document:

  • Domain name(s) and any aliases
  • Repository URL and branch
  • Web directory (usually /public)
  • Environment variables — Export or copy your entire .env file
  • SSL certificate type (Let’s Encrypt, custom)
  • Nginx configuration — Copy any custom directives
  • Deployment script — If you have customized the default deploy script, copy it

Background Services

Document all:

  • Queue workers — Command, number of processes, connection, queue names
  • Daemons — Any custom long-running processes
  • Cron jobs / Scheduled tasks — What is in your scheduler and what cron entries Forge manages
  • Firewall rules — Any custom rules beyond the defaults

Database Details

For each database:

  • Database name
  • Database users and their permissions
  • Database size (so you can plan server sizing appropriately)
  • Whether you use database backups and where they are stored

DNS Configuration

Document your current DNS records:

  • A records pointing to your Forge server IPs
  • CNAME records for any subdomains
  • MX records (these typically will not change, but document them)
  • TXT records (SPF, DKIM, DMARC)

Take your time with this inventory. Missing a cron job or environment variable can cause subtle issues that are hard to debug after migration.

Phase 2: Set Up Deploynix

Create Your Account and Organization

Sign up at deploynix.io and create your organization. If you have team members, you can invite them with appropriate roles: Owner, Admin, Manager, Developer, or Viewer. Each role has different permissions, so assign them according to your team’s needs.

Connect Your Cloud Provider

Navigate to your organization’s settings and connect the same cloud provider you are using with Forge. Deploynix supports DigitalOcean, Vultr, Hetzner, Linode, AWS, and custom servers.

Using the same cloud provider means your new servers will be in the same network, which makes database migration faster and simplifies the transition.

Connect Your Git Provider

Connect GitHub, GitLab, Bitbucket, or your custom Git provider. Deploynix needs access to the same repositories that Forge is currently deploying from.

Phase 3: Provision Servers on Deploynix

Now, provision servers on Deploynix that mirror your Forge infrastructure. For each Forge server, create an equivalent Deploynix server.

Choose the same region as your existing server. This keeps latency consistent for your users and allows for easy data migration between old and new servers.

Match the server size. If your Forge server is a 2GB/2vCPU instance, provision the same on Deploynix. You can always resize later.

Select the right server type. Deploynix offers specialized server types: App, Web, Database, Cache, Worker, Meilisearch, and Load Balancer. For a standard Forge server that runs everything, an App Server is the equivalent.

Match the database. If your Forge server runs MySQL 8, select MySQL during Deploynix server provisioning.

Match the PHP version. Deploynix supports PHP 8.0 through 8.4, so you can match your current Forge PHP version exactly. If your Forge server runs an older version, this is also a good opportunity to upgrade, but only if your application supports it.

Wait for provisioning to complete. Deploynix will configure Nginx, PHP-FPM, your database, Valkey, and Supervisor automatically.

Phase 4: Create Sites and Configure

Create Sites

For each site on your Forge server, create a corresponding site on your Deploynix server. Enter the domain name, select the repository, choose the branch, and set the web directory.

Important: Do not change your DNS yet. The new sites will not be receiving traffic at this point. You are building the new environment in parallel.

Configure Environment Variables

Copy your .env from Forge and paste it into the environment editor on Deploynix. Update the following values:

  • DB_HOST — Should be 127.0.0.1 or localhost for an App Server
  • DB_DATABASE, DB_USERNAME, DB_PASSWORD — Use the credentials Deploynix generated, or create a database matching your existing names
  • CACHE_STORE — Set to redis to use Valkey
  • SESSION_DRIVER — Set to redis
  • QUEUE_CONNECTION — Set to redis if you use queues

If you have external service credentials (mail, payment, etc.), those stay the same.

Configure Custom Nginx Rules

If you had custom Nginx configuration on Forge, apply the same rules in Deploynix. Navigate to the Nginx configuration for your site and add your custom directives. Deploynix validates the configuration before applying, so you will catch syntax errors immediately.

Set Up Queue Workers

Recreate your queue workers as Daemons in Deploynix. For each queue worker on Forge, create a corresponding daemon with the same command, number of processes, and configuration.

Set Up Cron Jobs

Add your scheduled tasks through the Deploynix Cron Jobs interface. At minimum, ensure the Laravel scheduler cron entry is configured: php artisan schedule:run running every minute.

Set Up Firewall Rules

Review your Forge firewall rules and recreate any custom rules in Deploynix. The default rules (SSH, HTTP, HTTPS) are created automatically.

Phase 5: Migrate Your Database

This is the most critical step. You need to move your data from the old server to the new one.

Export from Forge Server

SSH into your Forge server and create a database dump:

mysqldump -u forge -p --single-transaction --routines --triggers your_database > /tmp/database_backup.sql

For PostgreSQL:

pg_dump -U forge -h localhost your_database > /tmp/database_backup.sql

Transfer to Deploynix Server

Copy the dump file to your new Deploynix server:

scp /tmp/database_backup.sql deploynix@your-new-server-ip:/tmp/

Import on Deploynix Server

SSH into your Deploynix server (or use the web terminal) and import:

mysql -u your_db_user -p your_database < /tmp/database_backup.sql

For PostgreSQL:

psql -U your_db_user -h localhost your_database < /tmp/database_backup.sql

Verify the Import

Run a few quick checks: count records in key tables, verify that recent data is present, and test a few queries. Make sure the import is complete before proceeding.

Phase 6: Deploy and Test

Initial Deployment

Trigger a deployment for each site on Deploynix. Watch the deployment log to ensure everything completes successfully: code cloned, dependencies installed, migrations run (they should report nothing to migrate since you imported an existing database), assets built.

Test Without DNS

You can test your new sites without changing DNS by modifying your local /etc/hosts file:

your-new-server-ip    yourdomain.com

This makes your browser connect to the new server when visiting your domain, while everyone else still hits the old server. Browse through your application, test critical flows (login, signup, payments, API endpoints), and verify that everything works correctly.

Verify Background Services

Check that your queue workers are processing jobs, scheduled tasks are running, and any daemons are functioning correctly. The Deploynix dashboard shows the status of all managed processes.

Phase 7: DNS Cutover

Once you are confident the new setup works perfectly, it is time to switch DNS.

Reduce TTL (Optional but Recommended)

If your DNS records have a high TTL (Time to Live), consider reducing it to 300 seconds (5 minutes) a few hours before the cutover. This ensures that after you update DNS records, the change propagates quickly.

Update DNS Records

Update your A records to point to the IP address of your Deploynix server. If you use CNAME records for subdomains, update those as well.

Provision SSL Certificates

Once DNS points to your new server, provision SSL certificates through Deploynix. If you are using Let’s Encrypt, Deploynix handles the entire ACME challenge process. If you are using a DNS provider supported by Deploynix (Cloudflare, DigitalOcean, AWS Route 53, Vultr), you can provision wildcard certificates using DNS validation.

Monitor

After the DNS switch, monitor your application closely for the first few hours. Check error logs, response times, and user reports. The Deploynix dashboard provides real-time monitoring and health alerts to help you catch issues quickly.

Phase 8: Cleanup

Keep Forge Running Temporarily

Do not cancel your Forge subscription or destroy your old servers immediately. Keep them running for at least a week after the migration. This gives you a safety net: if something goes wrong, you can revert DNS back to the old servers instantly.

Final Database Sync (If Needed)

If your application continued receiving writes on the old server during DNS propagation, you may need to do a final data sync. This depends on your application’s tolerance for brief data gaps and whether you can put the old site in maintenance mode during the cutover.

Verify Backups

Configure automated database backups on Deploynix before decommissioning your old servers. Deploynix supports backups to AWS S3, DigitalOcean Spaces, Wasabi, and any S3-compatible storage. Verify that at least one backup completes successfully.

Decommission Old Servers

Once you are satisfied that everything is working on Deploynix and you have verified backups, you can safely destroy your Forge servers and cancel your Forge subscription.

Common Gotchas

SSH keys: If your application needs to pull from private repositories or connect to other servers via SSH, make sure the necessary SSH keys are configured on your Deploynix server.

File storage: If your application stores user uploads on the local filesystem (not S3), you need to copy the storage directory from the old server to the new one. Consider migrating to S3-compatible storage during this transition.

Environment-specific settings: Double-check any settings that reference the old server’s IP address, such as trusted proxies, CORS origins, or webhook URLs.

Email configuration: If your old server ran a local mail server, you will need to configure an external mail service on the new setup or ensure the same mail configuration works.

Cron timing: After migration, verify that your scheduled tasks are running at the expected times. Check the timezone configuration in both your server and your Laravel application.

Conclusion

Migrating from Forge to Deploynix is a methodical process, not a risky one. By running both platforms in parallel, testing thoroughly before DNS cutover, and keeping the old infrastructure as a fallback, you eliminate virtually all risk.

The entire migration can typically be completed in an afternoon for simple setups, or over a weekend for more complex multi-server environments. Once complete, you gain access to Deploynix’s full feature set: real-time monitoring, health alerts, web terminal, scheduled deployments, load balancing, and more.

Start your migration today at deploynix.io. Your first server is free.

Gaziantep'te TÜBİTAK 2204-B Yarışması Ödül Töreni düzenlendi

Gaziantep Üniversitesi ev sahipliğinde düzenlenen TÜBİTAK 20. 2204-B Ortaokul Öğrencileri Araştırma Projeleri Yarışması Adana Bölge Sergisi’nde ödül töreni gerçekleştirildi. Dereceye giren projeler ödüllendirildi ve birinci olan öğrenciler Türkiye finalinde yarışma hakkı kazandı.

I Built a Zero-Dependency Supply-Chain Security Scanner for Node.js — 18 Checks, One Command

The npm install trust problem

When you run npm install, you’re trusting hundreds of strangers to not run malicious code on your machine. And that trust has been broken — repeatedly.

Packages like event-stream, ua-parser-js, and coa were hijacked to steal credentials, install backdoors, and exfiltrate data through postinstall scripts. By the time npm install finishes, the damage is already done.

The traditional answer? npm audit. But that only catches known CVEs. It completely misses:

  • Typosquatted packages hiding in your lockfile
  • Dropper packages that curl | sh in postinstall
  • Tampered package.json files swapped after install
  • C2 domains hardcoded in dependencies
  • Stolen npm tokens used to publish without CI/CD provenance

I needed something better. So I built it.

Meet sec-check — one command, 18 security checks, zero dependencies

npx @sathyendra/security-checker

That’s the entire setup. No config files, no API keys, no build step. It scans your project and tells you what’s wrong:

──────────────────────────────────────────────────────────────────────
  @sathyendra/security-checker — Diagnostic Report
──────────────────────────────────────────────────────────────────────
  🚨 CRITICAL: plain-crypto-js detected in node_modules       [FIXABLE]
  🚨 LOCKFILE: flatmap-stream found in dependency tree         [FIXABLE]
  🚨 DROPPER: suspicious-pkg has postinstall with curl | sh   [MANUAL]
  ⚠️  PROVENANCE: "[email protected]" — Manual Publish Detected      [MANUAL]
  ⚠️  OUTDATED: [email protected] — 2 major versions behind        [MANUAL]
──────────────────────────────────────────────────────────────────────
  5 threat(s) found | 2 fixable | 3 require manual review
  Run with --fix to auto-remediate fixable threats.
──────────────────────────────────────────────────────────────────────

Why zero dependencies?

A security tool that pulls in 200 transitive dependencies is an oxymoron. Every dependency is attack surface. sec-check uses only Node.js built-ins (fs, path, crypto, child_process, os, https) — nothing from npm. The tool that checks your supply chain has no supply chain of its own.

What does it actually check?

Here’s the full list of 18 detection modules:

# Check What it catches
1 Malicious packages Known bad packages (e.g. plain-crypto-js) in node_modules
2 npm audit High/critical CVEs
3 Deep lockfile audit Malicious packages buried in the full dependency tree
4 Dropper detection Packages with postinstall scripts but no real code — the “install-and-run” pattern
5 Integrity checksums Post-install tampering (hash mismatch vs lockfile and registry)
6 Decoy swap detection package.md, .bak artifacts — the exact trick used in the Axios attack
7 TeamPCP / WAVESHAPER RAT artifacts, persistence mechanisms across Windows/macOS/Linux
8 C2 domains Known command-and-control domains in your hosts file
9 Cross-ecosystem (PyPI) Malicious PyPI packages from the same campaigns
10 Python stager detection Suspicious .py files with backdoor patterns in Node.js projects
11 Malicious .pth files Python’s “importless execution” technique in site-packages
12 Provenance verification Popular packages published without CI/CD provenance (stolen token indicator)
13 Shadow execution LD_PRELOAD, DYLD_INSERT_LIBRARIES, NODE_OPTIONS --require hijacking
14 Outdated dependencies Major version drift = no security patches
15 Registry configuration Dependency Confusion via non-official or HTTP registries
16 Lifecycle script injection `curl
17 Dependency script sandboxing Same analysis on every dependency’s lifecycle scripts
18 Secrets detection {% raw %}.env files, hardcoded NPM_TOKEN, AWS keys, GitHub tokens, PEM keys

Plus bonus checks: npm doctor, lockfile enforcement, SSRF/C2 blocklist scan, environment variable audit, and Lockfile Sentinel (hash comparison against known-compromised database).

The killer feature: Zero Trust Shield

Instead of the traditional “install and hope” workflow, Shield gives you defense-in-depth:

sec-check --shield
  ▸ Stage 1: Pre-flight — scan lockfile & config BEFORE downloading anything
  ▸ Stage 2: Isolated Install — npm install --ignore-scripts (no code execution)
  ▸ Stage 3: Post-vetting — full integrity & security scan on downloaded files

This is the workflow that would have caught every major npm supply-chain attack. The malicious postinstall never runs because Stage 2 uses --ignore-scripts, and Stage 3 catches any artifacts before you run your code.

OWASP Top 10 mapping

Three Shield features map directly to OWASP risk categories, so you can reference them in compliance conversations:

OWASP Risk Feature What it does
A03: Injection Script Blocker Flags dangerous commands in lifecycle hooks before execution
A05: Misconfiguration Registry Guard Rejects non-official or HTTP registries across .npmrc, env, and lockfile
A08: Integrity Failures Lockfile Sentinel Compares lockfile hashes against known-compromised database

CI/CD integration in 30 seconds

Add one line to your GitHub Actions workflow:

- name: Security scan
  run: npx @sathyendra/security-checker

Exit code 1 = threats found = pipeline fails. That’s it.

For structured output (dashboards, artifact collection):

sec-check --json > scan-results.json

For compliance artifacts:

sec-check --vex-out   # CycloneDX VEX document
sec-check --sbom      # CycloneDX SBOM

Auto-fix what can be fixed

Not into manual cleanup? The --fix flag handles the boring stuff:

sec-check --fix
Threat What --fix does
Malicious packages npm uninstall <package>
Audit vulnerabilities npm audit fix
Integrity mismatches npm ci (clean reinstall)
Swap artifacts Deletes the artifact file

Threats that need human judgment (provenance issues, C2 indicators, system artifacts) stay as [MANUAL] — the tool never makes destructive decisions for you.

Quick start

# One-shot scan (no install needed)
npx @sathyendra/security-checker

# Install as dev dependency
npm install --save-dev @sathyendra/security-checker

# Auto-fix fixable threats
sec-check --fix

# Protect every future install
sec-check --init

# Full Zero Trust workflow
sec-check --shield

Try it now

The best way to evaluate it is to point it at your own project:

cd your-project
npx @sathyendra/security-checker

If it finds something, that’s a good thing — you just caught it before an attacker exploited it.

Links:

If this is useful, a ⭐ on GitHub or an npm i helps more than you’d think. Questions, feature requests, or “it flagged something weird” — open an issue, I read all of them.

Adding custom fonts to your Ember app with Fontsource

If you’re running a modern Ember app with Embroider and Vite, adding custom fonts is easy. Fontsource is a good way to do it: fonts are packaged as npm dependencies, so you get versioned, self-hosted fonts with zero config on the bundler side.

Here’s how it works in practice.

Install the font packages

Pick the fonts you need from fontsource.org and install them:

pnpm add @fontsource/geist @fontsource/geist-mono

Import the CSS

In your app/app.css (or wherever your main stylesheet lives), import the weights you actually use:

@import "@fontsource/geist/400.css";
@import "@fontsource/geist/500.css";
@import "@fontsource/geist/600.css";
@import "@fontsource/geist/700.css";
@import "@fontsource/geist-mono/400.css";
@import "@fontsource/geist-mono/500.css";

Each of these CSS files contains the @font-face declarations for that specific weight. The font files (woff2) ship as part of the npm package and Vite resolves them automatically.

Only import the weights you use. Every weight you add is another font file your users have to download.

Use the fonts in CSS

Reference the font family names in your styles:

body {
  font-family: "Geist", sans-serif;
}

code, pre {
  font-family: "Geist Mono", monospace;
}

If you’re using Tailwind v4, you can set them as theme variables instead:

@theme {
  --font-sans: "Geist", sans-serif;
  --font-mono: "Geist Mono", monospace;
}

Tailwind will then use these as the default font-sans and font-mono utilities.

Preloading fonts to avoid FOUT

The setup above works, but you’ll probably see a flash of unstyled text (FOUT) on first load. The browser discovers the font files only after parsing the CSS, which is late.

You can fix this by preloading the woff2 files. In an Embroider app with Vite, you can use the ?url import suffix to get the resolved URL for a font file, then inject <link rel="preload"> tags early.

Create a file like app/lib/preload-fonts.ts:

import geist400 from "@fontsource/geist/files/geist-latin-400-normal.woff2?url";
import geist500 from "@fontsource/geist/files/geist-latin-500-normal.woff2?url";
import geist600 from "@fontsource/geist/files/geist-latin-600-normal.woff2?url";

for (const href of [geist400, geist500, geist600]) {
  const link = document.createElement("link");
  link.rel = "preload";
  link.as = "font";
  link.type = "font/woff2";
  link.href = href;
  link.crossOrigin = "anonymous";
  document.head.appendChild(link);
}

Then import it from your app/app.ts entrypoint:

import './lib/preload-fonts';

The ?url suffix is a Vite feature. It makes the import return the final, hashed URL of the asset. In production, this will be something like /assets/geist-latin-400-normal-abc123.woff2. This means your preload links always point to the correct file, even after builds with content hashing.

You only need to preload fonts that are used above the fold. If you have a monospace font that only shows up in specific views, skip preloading it.

Variable fonts

If the font supports it, Fontsource also ships variable font versions. Instead of importing individual weights, you import a single file that covers all weights:

@import "@fontsource-variable/geist";
@import "@fontsource-variable/geist-mono";

The package name changes from @fontsource/ to @fontsource-variable/:

pnpm add @fontsource-variable/geist @fontsource-variable/geist-mono

One file per font, all weights included. The browser only downloads a single woff2 file and interpolates between weights. This is usually smaller than loading 4+ static weight files.

Why Fontsource over Google Fonts?

  • Fonts are self-hosted. No third-party requests.
  • You control exactly which weights and subsets are included.
  • Fonts are versioned alongside your app. No CDN surprises.
  • Works with any bundler that can resolve node_modules CSS imports (Vite, Webpack, etc).

That’s it

Three steps: install the package, import the CSS weights, and reference the font family. Preloading is optional but helps with perceived performance. The whole thing works because Embroider + Vite resolves npm CSS imports and static assets the same way any Vite app would.

The Rubber Duck Prompt: Debug AI Output by Making It Explain Every Decision

You know the trick — explain your code to a rubber duck and the bug reveals itself. Turns out it works on AI too, except you make the AI be the duck.

The Problem

Your AI assistant returns code that looks right. It runs. But something about it feels off — maybe it picked a weird data structure, ignored an edge case, or used a pattern you didn’t expect. You can’t tell if it’s wrong or just different.

Most people either accept it or start over. There’s a better move.

The Rubber Duck Prompt

After the AI generates code, hit it with this:

Before I review this, walk me through your decisions:
1. Why did you choose this data structure?
2. What alternatives did you consider and reject?
3. What edge cases did you think about?
4. What assumptions are you making about the input?
5. What would break first if requirements change?

That’s it. Five questions. The AI is forced to justify every choice.

Why This Works

When an LLM generates code, it’s pattern-matching from training data. It doesn’t know why it picked Map over Object — but when you ask, it reconstructs the reasoning. And in that reconstruction, gaps surface:

  • “I assumed the input is always sorted” → it wasn’t
  • “I chose a flat array for simplicity” → but you need O(1) lookups
  • “I didn’t consider empty input” → obvious bug

The explanation becomes a free code review.

Real Example

I asked Claude to write a rate limiter. It returned a token bucket implementation. Looked fine. Then I ran the Rubber Duck Prompt:

“I chose a token bucket because it handles bursts well. I assumed requests are single-threaded…”

Stop. My service is multi-threaded. That assumption would have caused a race condition in production. Caught in 30 seconds, not 3 hours of debugging.

When to Use It

  • After any generated code longer than ~30 lines
  • When the output “looks right” but you didn’t specify the approach
  • Before merging AI-generated PRs
  • When onboarding to unfamiliar code the AI wrote previously

When to Skip It

  • Trivial code (formatting, simple CRUD)
  • You specified the exact approach in your prompt
  • You’re prototyping and correctness doesn’t matter yet

Template

Save this as your post-generation step:

## Review Gate: Rubber Duck Check

Explain your implementation decisions:
- Data structures chosen and why
- Alternatives considered
- Edge cases handled (and deliberately skipped)
- Assumptions about input/environment
- Fragility points if requirements change

The Takeaway

Don’t trust AI code that you can’t explain. But you don’t have to explain it yourself — make the AI explain it to you. The bugs are hiding in the assumptions, and assumptions only surface when you ask.