Best headless CMS for Next.js in 2026: Sanity vs Contentful vs Payload vs Storyblok

Choosing the best headless CMS for a Next.js project in 2026 is not a one-size-fits-all decision. I have shipped production projects on all four platforms covered here — Sanity, Contentful, Payload, and Storyblok — and each one has a clear home and a clear set of pain points. This post breaks them down honestly.

What I evaluated and why it matters

I judged each CMS on six criteria that actually affect delivery time and client satisfaction:

  1. Query language / API — how much round-trip overhead you take on in a Next.js App Router data fetch.
  2. Schema DX — how fast you can iterate on content models without breaking existing queries.
  3. Next.js App Router support — native RSC patterns, draft mode, on-demand ISR.
  4. Pricing — where the walls are as your client scales.
  5. Studio / editor experience — how quickly a non-technical editor becomes self-sufficient.
  6. Self-hosted vs cloud — who owns the data and what that costs operationally.

Sanity

Query language / API: GROQ is the best query language available in any CMS for Next.js. You pull exactly the fields you need, dereference references inline, and handle conditionals without multiple fetches. The @sanity/client with useCdn and tag-based revalidation slots cleanly into route handlers and RSC fetch calls.

Schema DX: TypeScript-first schema files checked into your repo. Sanity TypeGen generates inferred types from your GROQ queries, so a schema change surfaces type errors before you deploy. Iteration speed is high.

App Router support: First-class. draftMode(), revalidateTag(), and the Presentation tool for visual editing all work without adapter shims. The next-sanity package handles the plumbing.

Pricing: Free tier is generous (two users, 500k API requests/month). The first wall hits at team size — the Growth plan at $15/user/month is fine for agencies billing it to clients, but it can surprise a startup that suddenly has five internal editors.

Studio / editor experience: Sanity Studio v3 is the most customisable editor on this list. Portable Text, image hot-spot, and the structure builder give editors a focused interface. The flip side: you build what you need, which means initial setup time.

Self-hosted vs cloud: Studio is open source and can be embedded in your Next.js app or hosted separately. The content lake is cloud-only unless you are on an enterprise contract.

Contentful

Query language / API: REST and GraphQL. The GraphQL API is well-typed and predictable, but deeply nested content models generate expensive queries. There is no equivalent of GROQ’s single-query dereferencing — you often end up with multiple requests or overfetching.

Schema DX: The web UI content model editor is approachable but not code-first. There is no built-in way to version schema alongside your codebase. Contentful Merge (formerly Content Type Merger) helps in CI but adds tooling overhead.

App Router support: Works, but relies on Contentful’s Content Preview API for draft mode. The setup is documented but not as seamless as Sanity’s Presentation tool. No official App Router SDK as of May 2026 — you wire it yourself.

Pricing: The free tier is limited (5 users, 25k records). Paid plans start at $300/month for teams, which is a real threshold for small startups. Enterprise pricing is opaque. This is where Contentful loses most small to mid-size projects.

Studio / editor experience: The Contentful web app is polished and familiar to non-technical teams. Editors who have used WordPress or Notion get comfortable quickly. Rich text is less flexible than Portable Text but causes fewer surprises.

Self-hosted vs cloud: Cloud only. No self-hosting path at any tier.

Payload CMS

Query language / API: REST and GraphQL auto-generated from your collection schemas. The local API (direct MongoDB/Postgres queries when running Payload in the same Node process) is genuinely fast and removes the HTTP layer entirely for SSR — a significant advantage.

Schema DX: TypeScript config files, fully code-first. Collections and globals are defined in payload.config.ts, and types are generated automatically. This is the tightest schema-to-type loop of any CMS here.

// payload.config.ts (simplified)
import { buildConfig } from 'payload/config'
import { Posts } from './collections/Posts'

export default buildConfig({
  collections: [Posts],
  db: postgresAdapter({ pool: { connectionString: process.env.DATABASE_URI } }),
  editor: lexicalEditor({}),
})

App Router support: Payload 3.x runs inside a Next.js App Router project as a plugin — the admin UI and your front-end share the same next process. This is architecturally clean. Draft mode and ISR revalidation are handled with standard Next.js APIs because Payload is Next.js.

Pricing: Open source, MIT licensed. You pay for your own hosting (Vercel, Railway, Render, VPS). At volume, self-managed infra costs less than SaaS CMS plans, but operational burden is yours.

Studio / editor experience: The Payload admin is functional and customisable but noticeably less polished than Sanity Studio or Contentful. Lexical-based rich text is solid. Non-technical editors need more onboarding.

Self-hosted vs cloud: Fully self-hosted. Payload Cloud exists as a managed option but is not widely adopted yet. This is the major differentiator — you own the database.

Storyblok

Query language / API: REST API with a CDN delivery layer. The Content Delivery API returns nested JSON matching your component tree — convenient for visual editors but verbose for headless consumption. There is no query language; you get the full document and filter client-side or in your fetch logic.

Schema DX: Components (analogous to schemas) are defined in the Storyblok UI, not in code. A storyblok-generate-ts tool can produce types, but the schema lives in the cloud, not your repo. This is the weakest DX for developer-heavy teams.

App Router support: The @storyblok/react SDK has been updated for RSC but still leans on client components for live preview. Visual editing (the main reason to choose Storyblok) requires a client-side bridge, which adds hydration cost.

// app/[slug]/page.tsx — Storyblok RSC fetch pattern
import { getStoryblokApi } from '@storyblok/react/rsc'

export default async function Page({ params }: { params: { slug: string } }) {
  const client = getStoryblokApi()
  const { data } = await client.get(`cdn/stories/${params.slug}`, {
    version: 'published',
  })
  // data.story.content is the full component tree
  return <StoryblokStory story={data.story} />
}

Pricing: Free tier is very limited (1 user, 1 space). The entry paid plan starts around $99/month. The pricing model is per-space and per-user, which scales awkwardly for agencies managing many small client sites.

Studio / editor experience: Storyblok’s visual editor is the best on this list for marketing teams that want to build pages without developer involvement. The block-based composability is genuinely powerful for landing page workflows.

Self-hosted vs cloud: Cloud only.

Side-by-side summary

Sanity Contentful Payload Storyblok
Query language GROQ (excellent) GraphQL/REST (good) Local API / REST (good) REST (basic)
Schema in code ✓ ✗ ✓ ✗
App Router native ✓ Partial ✓ (it is Next.js) Partial
Free tier usable ✓ Limited ✓ (self-host) ✗
Self-host option Studio only ✗ ✓ fully ✗
Editor UX High High Medium Very high (visual)

Recommendation matrix by team type

Solo developer / freelancer building client sites: Sanity. The free tier covers most client projects, Studio embeds in your Next.js repo, and GROQ keeps data fetching clean. The schema-in-code workflow means you can hand off a Git repo with no external CMS config to chase down.

Agency running 10+ client sites: Sanity for content-heavy sites. Storyblok if the client insists on drag-and-drop page building and has a marketing team that needs to self-serve layouts. Avoid Contentful at this tier — the per-user pricing becomes punishing across multiple client accounts.

Startup (1–15 person team, VC-backed or bootstrapped): Payload if you have a backend engineer who can manage a Postgres database and want zero vendor lock-in. Sanity if you want to move fast and the content lake cost is acceptable. Contentful only if the team is non-technical and has budget — the $300/month entry point is real.

Enterprise / large product team: Contentful or Sanity Enterprise. Contentful has a longer track record in enterprise procurement conversations. Sanity’s enterprise tier includes SSO, audit logs, and private datasets. Payload is viable at enterprise scale if you have devops capacity and need data residency control — several regulated-industry teams are moving this direction in 2026.

The call I actually make

For a new Next.js project in 2026, I default to Sanity unless the client has a specific reason to go elsewhere. GROQ with TypeGen produces the cleanest App Router data layer I have worked with. Payload is the right call when self-hosting is a hard requirement or when the project is a full-stack app rather than a content site. Storyblok earns its place when the deliverable is a page-building tool for marketers, not a content API for developers. Contentful is hard to recommend for new projects started today — the pricing curve and the lack of code-first schemas make it the least competitive option in the 2026 landscape.

Temporal vs Make for API-First Workflows

What You’ll Need

  • n8n Cloud or self-hosted n8n instance
  • Hetzner VPS or Contabo VPS for hosting Temporal locally
  • DigitalOcean as an alternative for cloud deployment
  • Namecheap if you need a domain
  • API keys for your target services (Stripe, GitHub, Slack, etc.)
  • Docker and Docker Compose (free, open-source)
  • Node.js 18+ and npm

Table of Contents

  • What’s the Real Difference Between Temporal and Make?
  • Temporal: The Orchestration Engine Approach
  • Make: The Visual No-Code Platform
  • Building Your First Temporal Workflow
  • Building the Same Workflow in Make
  • Cost and Infrastructure Breakdown
  • Getting Started

What’s the Real Difference Between Temporal and Make?

I’ve spent the last three years building API-first workflows for SaaS companies, and this question comes up constantly. The short answer? Temporal and Make.com solve the same business problem—automating API calls across services—but they approach it like night and day.

Temporal is a workflow orchestration platform. It’s open-source, runs on your infrastructure, and treats workflows like distributed systems. You write code. You version control it. You deploy it like a regular application.

Make is a visual, cloud-hosted workflow builder. You drag and drop modules, connect APIs with a GUI, and hit publish. No code required.

The choice between them depends on your tolerance for infrastructure complexity, your budget constraints, and whether you want the flexibility of code or the simplicity of no-code. I’ve helped teams switch from expensive SaaS solutions like Zapier to self-hosted alternatives to reduce their SaaS bill, and this comparison is foundational to that conversation.

If you’re building workflows that require complex branching logic, error handling, retry strategies, or need to integrate deeply with your existing codebase, Temporal wins. If you want to ship something in hours without touching a terminal, Make is faster. Let me show you how both work in practice.

Temporal: The Orchestration Engine Approach

Temporal was created by engineers from Uber and Doordash who needed a system that could handle reliability at scale. It’s not a SaaS platform—it’s a framework. You run the Temporal server yourself, write workflows in TypeScript or Python, and execute them against that server.

Here’s why developers love it:

Automatic retries and error handling – If an API call fails, Temporal retries it with exponential backoff without you writing retry logic.

Durable execution – If your worker crashes mid-workflow, Temporal replays it from the last checkpoint. No data loss.

Visibility – You get a full history of every workflow execution with timestamps, inputs, outputs, and failures.

Cost efficiency – No per-execution fees. You pay only for the infrastructure running your server.

This is especially important if you’re handling high-volume workflows. Imagine running 10,000 API calls per day through Zapier. You’d hit their usage limits fast. With Temporal, those 10,000 calls cost you only the server resources needed to run them.

Setting Up Temporal Locally

Let’s get a Temporal server running on your Hetzner VPS or local Docker setup.

mkdir temporal-workflow && cd temporal-workflow
curl -O https://raw.githubusercontent.com/temporalio/docker-compose/main/docker-compose.yml
docker-compose up -d

Once the server is running on localhost:7233, create a package.json:

{
  "name": "temporal-api-workflow",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "worker": "ts-node src/worker.ts",
    "workflow": "ts-node src/run-workflow.ts"
  },
  "dependencies": {
    "@temporalio/client": "^1.11.0",
    "@temporalio/worker": "^1.11.0",
    "@temporalio/workflow": "^1.11.0",
    "axios": "^1.6.0",
    "ts-node": "^10.9.0",
    "typescript": "^5.0.0"
  }
}

Install dependencies:

npm install

Now create your workflow definition in src/workflows.ts:

import { proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';

const { fetchUserFromStripe, sendSlackNotification, logToDatabase } = proxyActivities<typeof activities>({
  startToCloseTimeout: '10 seconds',
  retry: {
    initialInterval: '1 second',
    maximumInterval: '1 minute',
    maximumAttempts: 5,
  },
});

export async function processStripeWebhook(customerId: string) {
  const user = await fetchUserFromStripe(customerId);

  if (user.status === 'active') {
    await sendSlackNotification({
      channel: '#billing',
      message: `User ${user.email} is active and has ${user.subscriptions.length} subscriptions`,
    });
  }

  const logResult = await logToDatabase({
    customerId,
    status: user.status,
    timestamp: new Date().toISOString(),
  });

  return {
    processedAt: new Date(),
    userId: user.id,
    notificationSent: user.status === 'active',
    logId: logResult.id,
  };
}

Now define the activities that do the actual work in src/activities.ts:

import axios from 'axios';

interface StripeUser {
  id: string;
  email: string;
  status: string;
  subscriptions: Array<{ id: string; status: string }>;
}

interface SlackPayload {
  channel: string;
  message: string;
}

interface DatabaseLog {
  customerId: string;
  status: string;
  timestamp: string;
}

interface LogResult {
  id: string;
}

export async function fetchUserFromStripe(customerId: string): Promise<StripeUser> {
  const response = await axios.get(`https://api.stripe.com/v1/customers/${customerId}`, {
    headers: {
      Authorization: `Bearer ${process.env.STRIPE_API_KEY}`,
    },
  });

  const subscriptions = await axios.get(
    `https://api.stripe.com/v1/customers/${customerId}/subscriptions`,
    {
      headers: {
        Authorization: `Bearer ${process.env.STRIPE_API_KEY}`,
      },
    }
  );

  return {
    id: response.data.id,
    email: response.data.email,
    status: response.data.description || 'unknown',
    subscriptions: subscriptions.data.data,
  };
}

export async function sendSlackNotification(payload: SlackPayload): Promise<void> {
  await axios.post(process.env.SLACK_WEBHOOK_URL, {
    channel: payload.channel,
    text: payload.message,
    ts: Math.floor(Date.now() / 1000),
  });
}

export async function logToDatabase(log: DatabaseLog): Promise<LogResult> {
  const response = await axios.post(process.env.DATABASE_WEBHOOK_URL, {
    customerId: log.customerId,
    status: log.status,
    timestamp: log.timestamp,
  });

  return { id: response.data.insertedId };
}

Create a worker in src/worker.ts:

import { Worker } from '@temporalio/worker';
import * as workflows from './workflows';
import * as activities from './activities';

async function runWorker() {
  const worker = await Worker.create({
    workflowsPath: require.resolve('./workflows'),
    activitiesPath: require.resolve('./activities'),
    taskQueue: 'stripe-webhook-queue',
    namespace: 'default',
    workflowFailureExceptionTypes: [Error],
  });

  console.log('Temporal Worker started, listening on queue: stripe-webhook-queue');
  await worker.run();
}

runWorker().catch(console.error);

Finally, trigger a workflow execution in src/run-workflow.ts:

import { Connection, Client } from '@temporalio/client';
import { processStripeWebhook } from './workflows';

async function runWorkflow() {
  const connection = await Connection.connect({ address: 'localhost:7233' });
  const client = new Client({ connection });

  const workflowId = `stripe-webhook-${Date.now()}`;

  const result = await client.workflow.execute(processStripeWebhook, {
    args: ['cus_abc123xyz'],
    taskQueue: 'stripe-webhook-queue',
    workflowId,
  });

  console.log('Workflow completed:', result);
}

runWorkflow().catch(console.error);

Start your worker:

npm run worker

In another terminal, run the workflow:

npm run workflow

You’ll see the workflow execute across all three activities: fetch from Stripe, send a Slack message, and log to your database. If any step fails, Temporal retries it automatically. That’s the power of durable execution.

Make: The Visual No-Code Platform

Now let’s build the exact same workflow in Make.com—the visual way.

Make is cloud-hosted, which means zero infrastructure overhead. You log in, drag modules onto a canvas, and connect them. Each module represents an API call or data transformation. No code required, though you can add JavaScript when needed.

The tradeoff? You pay per operation. Make charges based on the number of “operations” (API calls and data transformations) your workflows perform. If you’re running 10,000 Stripe API calls per month, costs add up quickly.

Here’s how to replicate the Temporal workflow in Make:

  1. Log into Make and create a new scenario.

  2. Set up the trigger. Add a Webhook module as your trigger. This gives you a webhook URL that Stripe can POST to when events occur.

Make → Webhook → Listen on this URL for Stripe webhook events
  1. Parse the webhook. Add a JSON → Parse JSON module to extract the customerId:
{
  "customerId": "{{trigger.body.data.object.customer}}"
}
  1. Fetch from Stripe. Add a Stripe module (or use HTTP Request):

Module: HTTP → Make a request

  • URL: https://api.stripe.com/v1/customers/{{customerId}}
  • Method: GET
  • Headers: Authorization: Bearer {{env.STRIPE_API_KEY}}
  1. Conditional routing. Add a Router to check if status == 'active':
Router → if status = "active"
  → YES: Send Slack notification
  → NO: Skip notification
  1. Send to Slack. In the YES path, add a Slack module:
  • Message: User {{firstName}} {{lastName}} ({{email}}) is active with {{subscriptionCount}} subscriptions
  • Channel: #billing
  1. Log to database. Add an HTTP Request module:
  • URL: {{env.DATABASE_WEBHOOK_URL}}
  • Method: POST
  • Body:
{
  "customerId": "{{customerId}}",
  "status": "{{stripeResponse.status}}",
  "timestamp": "{{now()}}"

Originally published on Automation Insider.

The Antigravity 2.0 Forced Update: How to Fix the Broken Editor Loop

The Disruption: When Your Workspace Becomes a Ghost Town

I woke up ready to write code, but Google had other plans.

If you use Antigravity, you probably experienced the exact same sudden roadblock I did following yesterday’s Google I/O keynote. Without warning, a forced, silent auto-update on launch completely broke my working environment.

The transition from a highly productive workspace to absolute exhaustion happened in a matter of seconds. When the app loaded, my familiar VS Code-forked text editor, sidebar, terminal, and file explorer were entirely gone. In their place sat a stark, empty “Agent Manager” view.

Google decided to completely pivot Antigravity 2.0 into a standalone “agent-first” platform. To do this, they split the ecosystem into two separate apps: the core Antigravity 2.0 app (built strictly for high-level multi-agent orchestration without a raw code editor) and the standalone Antigravity IDE (which keeps the traditional layout).

The Panic and the Lost Hours

Assuming it was a minor glitch, I did what anyone would do—I turned to the internet. I spent hours scrolling through chaotic Reddit threads and developer forums, trying every single suggestion out there.

Even downloading the fresh, dedicated Antigravity IDE installer changed absolutely nothing. The app kept hijacking the launch script, booting straight back into the exact same blank Agent Manager view with zero editor.

The problem comes down to a basic file conflict. The new 2.0 update dumps a master archive file called app.asar directly into the shared application folder, hijacking the launch script. No matter which desktop icon you click, the app defaults straight back to the blank Agent view. Worse, because the IDE application treats itself as a brand-new installation, it initializes empty user paths—making your local project history, custom extensions, and past chats look completely wiped.

How I Finally Solved It (The Fast Folder Patch)

Here are the steps I finally took that don’t require uninstalling and wasting more time.

Step 1: Fix the Launcher Conflict

Close the application completely and ensure it isn’t running in your system taskbar.

For Windows:

  1. Press Win + R, paste this path, and hit Enter:
%USERPROFILE%AppDataLocalProgramsAntigravity

  1. Open the resources folder.
  2. Find the file named app.asar and rename it to app.asar.bak.

For Linux:

  1. Open your terminal and run this command to isolate the archive:
   sudo mv /opt/Antigravity/resources/app.asar /opt/Antigravity/resources/app.asar.bak

Renaming this file forces the IDE to skip the broken 2.0 Agent code and load your traditional text editor layout immediately.

Step 2: Get Your History & Extensions Back

If your editor opens up but looks completely blank, copy your old configurations into the new folders the update created.

On Windows:

  1. Press Win + R, type %APPDATA%, and press Enter. Copy the User folder from Antigravity and paste it into Antigravity IDE.
  2. Press Win + R, type %USERPROFILE%, and copy the contents of .antigravityextensions over to .antigravity-ideextensions.

On Linux:

  1. Run these commands in your terminal to move your profile cache and extensions over:
   cp -r ~/.config/Antigravity/User ~/.config/"Antigravity IDE"/
   cp -r ~/.antigravity/extensions/* ~/.antigravity-ide/extensions/

Other Methods Found Online

If the manual folder tweak isn’t ideal for your setup, these are the alternative workarounds currently floating around the developer community:

Method A: The Full Clean Reinstall

Many users on Reddit reported success by completely wiping both installations from the system, rebooting the machine to clear the cached registries, and installing only the standalone IDE version from the bottom of the download page.

Method B: Downgrading to v1.x

If the 2.0 ecosystem is completely unusable for your specific workflow, developers are bypassing it entirely by downloading the previous stable release (v1.23.2).
You can find the older installers by going to antigravity.google/releases or scrolling to the bottom of the main download page and clicking “View Previous Releases”.

Forcing a breaking, unprompted layout change on launch day is an absolute mess for developer workflows. Until an official hotfix separates these directories cleanly, stick to the manual folder rename, keep your environment stable, and get back to building.

10 Ways To Reduce Your LLM API Costs

The Connect providers modal in Manifest — toggle Anthropic, GitHub Copilot, MiniMax, Ollama Cloud, OpenAI and other subscriptions on for routing

So you made it, you have your AI app on prod, you are onboarding your users and they like what you’ve done, cheers! Now comes the hard-to-swallow part, the AI bill.

Serving those users consumes AI inference and it’s literally eating all your margins. Let’s see 10 ways to reduce that AI bill of yours.

Table of contents

  1. Choose a well-fitted AI model
  2. Use your Pro subscriptions
  3. Reduce output tokens to cut your LLM bill
  4. Use prompt caching when you can
  5. Use Batch API for nightly workflows
  6. Be FLEX-ible and accept slow tiers
  7. Don’t use AI
  8. Use free models and free tiers
  9. Don’t miss those Cloud provider credits
  10. Observe your AI costs and take back control

1. Choose a well-fitted AI model

Choosing the perfectly fitted model is not that easy. While realizing that your model is not “smart enough” for your requirements is the easy part, the other side is trickier: you may use an overkill model and you are overspending without noticing it.

Potential reductions are huge here, for example switching from GPT-5.5 to GPT-5.4 reduces costs by 50%. Use GPT-5.4 Mini instead? That’s 85% cheaper.

However nothing is magical here: the cheaper models produce lower-quality outputs. Spending time to benchmark models on your particular use cases is the key to evaluate the impact of the quality loss. The closer you are to real production data, the better. Here are some ideas to put you on the right track:

  • Downshift within your model’s family: flagship -> mini -> nano
  • Look for other providers’ models: For example Chinese providers tend to be cheaper for an equivalent quality
  • Go for the previous generation: Opus 4.7 -> Opus 4.6 -> Opus 4.5

In some cases, the query is variable and you can’t anticipate it. Using LLM routers that evaluate your query on-the-fly can be a good choice.

2. Use your Pro subscriptions

Are you paying for a ChatGPT, MiniMax or other pro subscription? Make the most of it and plug it into your app. With Manifest, you can plug several pro subscriptions into your apps, while keeping your code and favorite SDK as it is. As of today Manifest allows you to plug Anthropic, GitHub Copilot, MiniMax, Ollama Cloud, OpenAI, OpenCode Go and Z.ai pro subscription directly into your AI app. Make sure it’s ok with the terms of service of your providers.

The Connect providers modal in Manifest — toggle Anthropic, GitHub Copilot, MiniMax, Ollama Cloud, OpenAI and other subscriptions on for routing

The thing to watch here is the rate limit. Those subscriptions are often way cheaper than their API key alternative, but they are limited in usage. You need to set up a fallback model in case you hit the API rate limit. See how MyTrainer did exactly this in production.

3. Reduce output tokens to cut your LLM bill

Did you notice that you don’t actually pay directly for the inference of the model you call? You pay for what goes in and for what comes out of it, not the “amount of thinking” itself. What if we can tweak the system to produce the same value with less?

The output tokens cost on average 5 times more than the input tokens. It’s totally worth it to spend more on the input tokens to reduce the output ones.

The easiest way is to simply tell the LLM explicitly to “be concise” and even specify the format that you are expecting. Ask for structured JSON or CSV instead of prose. Prose tends to be long and adds verbosity to the outputs. If you need prose, Caveman (60k+ GH stars) reduces output tokens by 75% by… speaking like a caveman. Caveman tool real. Make agent talk short. 🪨

By the way most providers allow a “max_tokens” that truncate outputs. While this doesn’t make the model concise — it just truncates — it can still prevent long outputs.

4. Use prompt caching when you can

Caching has been used for decades to reduce compute and thus latency. For example when a website shows you a list of items, like the latest news for example, it will load from the database only for the first user, and then store it somewhere. Next users will receive that list from the cache directly, and have it served almost instantaneously.

It works the same with LLM prompts: Models do heavy work on every token you send to them. If part of it doesn’t change between requests, they do less work and you pay less. Generally cached input costs between 50% and 90% less.

The #1 rule here is that your static content (the one that doesn’t change) should come before the dynamic content (the volatile one). The first changed character and you’re paying full price for following tokens. Structure your call correctly so that system prompts and knowledge bases come first:

messages = [
    {"role": "system", "content": SYSTEM_PROMPT + KNOWLEDGE_BASE},  # Static
    {"role": "user", "content": f"{user_question}"},  # Dynamic
]

resp = client.chat.completions.create(
    model="gpt-5.4-mini",
    messages=messages,
)

5. Use Batch API for nightly workflows

Batch API is a straight 50% discount on inference if you accept to receive the response within 24 hours.

You’re not going to implement that on a chat or on any actions that need real-time answers, however any background process is a good candidate. It requires a bit of change in your code but it’s totally worth it if you have a significant amount of inference in scheduled tasks, nightly workflows or routines.

Here is the link to the providers’ docs that support batch API:

6. Be FLEX-ible and accept slow tiers

Some providers even have a “flex” tier: a synchronous tier explicitly slower than the standard. Unlike Batch API, here you do get the real-time response, but it is significantly slower. However the discount is usually the same: 50% off.

Now it’s up to you to decide if you can afford that extra latency on some calls. Here is a list of providers that offer that trade-off:

7. Don’t use AI

This one can sound a bit provocative but the cheapest inference will always be the one you don’t use. Are those AI features genuinely needed? Or are they just decorative nice-to-haves?

Limiting LLM calls and using algorithms (aka good old software) when possible is the biggest win on this list. Of course it’s easier said than done, but many operations can be done programmatically: validation, regex, rules, heuristics…

Pro tip: go hybrid. Sometimes algorithms cannot handle all cases, but work well on some of them. Use conditions in your code to use them when possible, and fall back on LLMs.

8. Use free models and free tiers

If you are just hacking around, maybe you don’t need to pay at all! Some providers offer inference for free on some models. Of course the rate limit is quite low but if your app has a low AI inference consumption, it may work. If it doesn’t, you can still use model fallback.

Awesome Free LLM APIs — a curated list of LLM providers with a permanent free tier

And you know what? We prepared a very cool list of Awesome Free LLM APIs just for you 😎

9. Don’t miss those Cloud provider credits

Building a startup? All major cloud providers can give you up to $300k in cloud credits, most of them can be used in inference. The terms vary from one provider to another, and you probably need to fill applications and even meet them.

The concept here is that the faster you can burn those tokens, the more they’ll offer you. It makes sense as they are looking for new potential big customers that will stick with their models.

You’ll probably never get the $300k on the first shot, it goes incrementally. One important thing: incubators, accelerators and startup programs tend to have agreements on credit packages with providers. Reach out to your program manager if you’re into one of those!

10. Observe your AI costs and take back control

Last but not least, you can’t optimize what you can’t see. Maybe one single recurrent LLM call is burning all your budget and you haven’t identified it yet.

30-day messages and usage chart from the Manifest dashboard — track your AI inference volume and cost over time

There are many tools (open source or proprietary) like Manifest that let you visualize your costs, analyse them, and even set some budget limits. Some of them are simple and easy to understand, others more complicated and let you go into details. Up to you to find your fit!

Conclusion

Reducing cloud AI inference costs is key for an app’s profitability. Consider the few lines of each point in this post as a starting point, an idea to dig into. Not all of them are necessarily applicable in your case, but they are worth knowing and understanding.

At Manifest, we think that AI is an incredible technology, and that it deserves to be affordable. Our platform is open source and gives total control to our users. Check out our website and give us a star on GitHub to support us! Or simply share this post, as it helps others pay less for AI.

Happy hacking!

mcp-probe v1.0.0: A CI readiness gate for MCP servers

mcp-probe started as a small CLI for checking whether an MCP server starts and exposes tools.

That was useful, but after feedback from developers running real MCP servers in agent workflows, the gap became obvious:

A server can start, pass tools/list, and still fail every real tool call because OAuth, browser auth, or downstream permissions are broken.

So I shipped mcp-probe v1.0.0 as a CI-ready readiness gate for MCP servers.

Install

npx @k08200/mcp-probe@latest <server>

Example:

npx @k08200/mcp-probe@latest @modelcontextprotocol/server-memory

What it checks

  • MCP protocol handshake
  • tools/list
  • optional resources and prompts discovery
  • tool schema shape
  • actual tool-call dry-runs
  • stderr classification
  • latency
  • batch/fleet CI status

Tool-call dry-runs

npx @k08200/mcp-probe@latest <server> --probe-tools

This closes the gap between “the server registered tools” and “those tools actually work in an agent loop.”

Sidecar inputs

Auto-generated inputs are fallback only. For real CI, v1 supports sidecar files:

{
  "tools": {
    "logs_query": {
      "input": {
        "query": "service:web status:error",
        "timeframe": "1h"
      },
      "expect": {
        "not_error_code": [401, 403]
      }
    }
  }
}

Run:

npx @k08200/mcp-probe@latest datadog-mcp --probe-tools --tools-file .mcp-probe.json

This lets CI validate meaningful tool calls instead of just schema-minimum empty strings.

Batch checks

npx @k08200/mcp-probe@latest --config mcp-probe.config.json

Useful when a team runs multiple MCP servers and wants one readiness gate.

GitHub Actions output

npx @k08200/mcp-probe@latest --config mcp-probe.config.json --github-summary

v1 writes GitHub step summaries, emits annotations, and can generate a shields-compatible badge JSON file.

HTTP and SSE

mcp-probe now supports stdio, Streamable HTTP, and legacy SSE:

npx @k08200/mcp-probe@latest https://example.com/mcp --header "Authorization: Bearer TOKEN"

Stderr classification

Some servers print harmless startup warnings; others print fatal init errors. v1 adds explicit rules:

npx @k08200/mcp-probe@latest <server> 
  --stderr-allow "deprecated" 
  --stderr-fatal "missing required api key"

Recipes

The repo includes starter recipes for Datadog, Supabase, Gmail, single-server GitHub Actions checks, fleet checks, and remote HTTP checks.

GitHub: https://github.com/k08200/mcp-probe

Release: https://github.com/k08200/mcp-probe/releases/tag/v1.0.0

npm:

npm install -g @k08200/mcp-probe

Building ValoVault: The Per-Agent Skin Loadouts Riot Never Shipped

The Missing Feature

Pick Jett. You want your clean, sharp Jett skins. Pick Killjoy next game. Now you want your cyberpunk-y Killjoy skins. Pick Reyna. Different vibe again.

Every Valorant player who owns more than a handful of skins has wished for the same thing: skins that automatically change based on the agent you pick. Your Jett loadout, your Killjoy loadout, your Reyna loadout — each one waiting for you the moment you lock in.

The community has been asking for per-agent skin presets for years. Browse the Valorant subreddit and you’ll find dozens of threads requesting exactly this. Unfortunately, Riot has stated they don’t currently plan to add it.

So I decided to build it myself.

Prior Art

Before writing a single line of code, I did some research to see what already existed. The most prominent project in this space is valorant-inventory-manager by colinhartigan. It’s a great project and does a lot of useful things — but it doesn’t do the one thing I actually wanted: automatically swap your skin loadout based on the agent you pick.

That meant I’d have to figure out the technical side myself.

The Key Discovery

Digging through the code of existing tools and poking at Valorant’s local client APIs, I discovered something interesting: you can technically change skins during agent selection. The client accepts loadout modifications at that point in the match lifecycle, which is exactly the window you need to make per-agent skins feel native.

That was the unlock. If I could:

  1. Detect when the player enters agent select
  2. Detect which agent they locked in
  3. Push the loadout tied to that agent to the client

…then I’d have agent-bound skins — the feature Riot never shipped.

Prototype #1: Python

The first prototype was a quick-and-dirty Python app: valorant_loadouts_python. It proved the concept worked — when I picked an agent, my skins changed to match. It actually felt magical the first time it triggered automatically.

But it was a terrible UI, no easy way to assign skins to agents visually, no good install story (I uploaded the installer to the repository itself lol). It worked and when I announced it on Reddit, people actually liked and used it but it was just a terrible user experience without a way to change existing presets and so much more.

Prototype #2: ValoVault

So I rebuilt it properly. Meet ValoVault.

The headline feature is exactly what the community has been begging for: bind a skin loadout to each agent, and ValoVault swaps them in automatically during agent select. Lock in Jett → Jett skins. Lock in Reyna → Reyna skins. No clicking, no menus, no thinking about it.

As a bonus, you can also save multiple named presets and switch between them manually — handy for “tryhard vs. for fun” setups, or for sharing builds with friends — but the per-agent assignment is the star of the show.

Architecture

The architecture ended up being a bit more interesting than a typical desktop app:

  • A Go API wrapper I wrote from scratch for the Valorant local client APIs
  • A Go sidecar backend that runs alongside the UI, watches the game state, and pushes loadouts at the right moment
  • A Next.js frontend for the actual interface — skin grids, agent assignment, preset management
  • Tauri wrapping the whole thing into a lightweight native desktop app

Why this stack?

  • Go for the backend — concurrency primitives are perfect for polling the game client and reacting to agent select events. A single static binary is easy to ship as a sidecar.
  • Next.js for the UI — I wanted a real component-driven UI with proper state management, not a glorified HTML page. The skin grid alone has thousands of images to handle gracefully.
  • Tauri instead of Electron — way smaller bundle, way less RAM, and the Rust shell plays nicely with the Go sidecar pattern.

The Next.js app talks to the Go backend over HTTP on localhost, and the Go backend talks to the Valorant client. Clean separation, easy to develop each piece independently.

What ValoVault Can Do

  • Assign a skin loadout to every agent and have it applied automatically when you lock in
  • Save additional named loadout presets for manual swapping
  • Browse your entire skin inventory with levels, chromas, and gun buddies
  • Pick variants and buddies per-weapon, per-preset

It’s open source, Apache-2.0 licensed, and works on Windows.

What’s Next

If you want to try it, grab a release from the GitHub repo. Contributions, bug reports, and feature requests are all welcome.

And if you’re a Riot dev reading this — please, just ship per-agent loadouts natively. I’d happily archive the repo. 🙂

Most CMS Platforms Aren’t Built for Full Lifecycle Ownership

For a long time, I thought a CMS was primarily about managing content.

Pages.
Posts.
Media.
Users.

And to be fair, that is the core responsibility of a CMS.

But after working deeper in infrastructure, deployment, operations, and business systems, I started realizing something:

Content management is only a small part of what businesses actually need.

Because the moment a website becomes important to a business, the conversation changes quickly.

Suddenly it’s not just:

“Can we publish content?”

Now it becomes:

  • How do we deploy safely?
  • How do we scale?
  • How do we separate environments?
  • How do we manage integrations?
  • How do we migrate providers?
  • How do we handle permissions?
  • How do we avoid vendor lock-in?
  • How do we manage infrastructure?
  • How do we maintain this long term?
  • How do we support multiple teams?
  • How do we extend functionality safely?

That’s no longer just “content management.”

That’s operational architecture.

And I think most CMS platforms were never truly designed around full lifecycle ownership.

The CMS Usually Stops at the Admin Panel

Most CMS platforms are excellent at:

  • editing content
  • managing media
  • handling themes
  • publishing pages
  • supporting plugins

But once you move beyond the content layer, many businesses end up stitching together:

  • hosting providers
  • deployment platforms
  • CDN services
  • caching solutions
  • backup systems
  • CI/CD pipelines
  • analytics services
  • authentication providers
  • API layers
  • monitoring tools
  • external databases
  • third-party integrations

Over time, the actual operational architecture becomes scattered across dozens of disconnected systems.

The CMS becomes just one piece of a much larger puzzle.

Businesses Need Ownership, Not Just Publishing

One thing I’ve been thinking about a lot lately is the idea of ownership.

Not just ownership of content.

Ownership of:

  • infrastructure
  • deployment
  • portability
  • architecture
  • workflows
  • operational behavior

Because businesses eventually outgrow “just having a website.”

A restaurant may need:

  • online ordering
  • inventory systems
  • customer accounts
  • loyalty systems
  • delivery tracking
  • analytics
  • staff management
  • marketing automation

A creator may need:

  • subscriptions
  • storefronts
  • courses
  • media delivery
  • memberships
  • CRM functionality
  • community systems

A church may need:

  • events
  • streaming
  • donations
  • member systems
  • volunteer coordination
  • communication workflows

At that point, the CMS is no longer just a publishing tool.

It becomes part of the operational backbone of the business.

This Is Why I Became Interested in Engines

This realization heavily influenced how I think about systems like KiwiPress and WebEngine.

I stopped asking:

“How do we build another CMS?”

And started asking:

“How do we build a content-aware operational runtime?”

That’s a very different question.

Because now you’re thinking about:

  • deployment
  • infrastructure
  • portability
  • scalability
  • integrations
  • operational workflows
  • modular systems
  • lifecycle management

Not just content editing.

The Traditional CMS Model Has Friction

One issue I keep noticing is that many CMS ecosystems evolved during a different era of the web.

An era where:

  • monolithic servers were normal
  • infrastructure complexity was lower
  • integrations were simpler
  • deployment expectations were smaller
  • frontend/backend separation was uncommon

Modern businesses often operate very differently now.

We have:

  • headless architectures
  • distributed systems
  • APIs everywhere
  • containerized infrastructure
  • edge deployments
  • multi-service ecosystems
  • hybrid cloud environments
  • AI integrations
  • real-time systems

But many CMS workflows still assume a more traditional operational model.

That creates friction.

Headless Helped — But Didn’t Fully Solve It

I actually think headless CMS architecture was a major step forward.

Separating:

  • content
  • presentation
  • frontend runtimes

was important.

But even headless setups often stop at:

“Here’s your API.”

Businesses still need:

  • deployment strategy
  • orchestration
  • permissions
  • observability
  • environment management
  • infrastructure scaling
  • operational tooling
  • workflow management

Headless solved presentation flexibility.

But operational ownership is a much bigger problem space.

Full Lifecycle Thinking Changes the Architecture

The more I think about it, the more I believe systems should be designed around the entire lifecycle.

Not just:

  • building
  • publishing

But also:

  • deploying
  • scaling
  • maintaining
  • evolving
  • integrating
  • migrating
  • observing
  • securing

Because real businesses live in those operational realities every day.

And architecture decisions made early eventually affect all of them.

Why Portability Matters So Much

One of the biggest things I care about now is portability.

I think businesses should be able to:

  • move providers
  • change infrastructure
  • evolve architecture
  • adapt workflows
  • retain ownership of systems

without rebuilding everything from scratch.

That’s one reason I’ve become increasingly interested in:

  • contracts
  • adapters
  • declarative configuration
  • modular runtimes
  • infrastructure abstraction
  • blueprint systems

Not because abstraction is trendy.

Because businesses change over time.

And systems that can’t evolve become liabilities.

The Future Probably Looks More Operational

I don’t think the future of CMS platforms is just:

“better editing experiences.”

I think the future is operational awareness.

Systems that understand:

  • infrastructure
  • workflows
  • scaling
  • deployment
  • modularity
  • portability
  • lifecycle management

alongside content itself.

Because content is rarely the entire business anymore.

It’s usually one layer of a much larger operational ecosystem.

Final Thoughts

I still think CMS platforms are incredibly valuable.

Some of the most important parts of the modern web were built on them.

But I also think businesses increasingly need more than publishing systems.

They need operational systems that can evolve with them.

Systems that help them:

  • own their infrastructure
  • manage complexity
  • scale intentionally
  • remain portable
  • operate sustainably

In other words:

The future probably isn’t just content management.

It’s lifecycle management.

45 MB of Claude Code Sessions You Don’t See

A documented user investigation posted in late April 2026 reported, for one Windows machine, the following two numbers: 715 Claude Code sessions on disk, and 69 sessions visible in the Claude Code desktop application’s sidebar. Roughly ten percent. The other six hundred and forty-six sessions, totalling about 48 megabytes of local_*.json files, were on the disk, were intact, and were entirely absent from the only program that can natively open them.

I want to take that ten-percent number seriously rather than file it under “user error” or “edge case,” because the structural reason it happens is the same reason it will happen to anyone with more than one Anthropic account, and there are now a fair number of people in that category. Anthropic introduced weekly rate limits on its Pro and Max plans on August 28, 2025, which means the response of any sufficiently committed Claude Code user is, eventually, to maintain more than one account and rotate when one of them hits its cap. This is not an exotic workflow. It is the normal response of a tool’s power users to a quota policy. The desktop application was not built around it.

Why anyone runs more than one account

The current weekly-limit structure is documented across Anthropic’s pricing pages and a year of TechCrunch / Northflank / Portkey coverage. The $200/month Max plan offers, by Anthropic’s own published guidance, somewhere in the 240–480 hours of Sonnet and 24–40 hours of Opus per week. The $100 plan is roughly half that. The Pro plan is well below either. For someone using Claude Code as a daily driver on multiple substantial projects, the cap is not theoretical — power users hit it within days of each weekly reset window.

The standard response, visible across half a dozen GitHub-issue threads and a year of community posts, is to maintain two or three accounts and switch when the active one runs out. Anthropic does not document this, does not condone this, and does not make it convenient — but the alternative is that the tool stops working partway through the week, and the calculus, for paying customers who depend on it, is straightforward.

The desktop application’s storage layer was designed for one account at a time. The collision between that design and the rotation pattern is the source of every symptom that follows.

Where the sessions actually live

Anthropic does not document on-disk session paths in the official Claude Desktop documentation. The CLI version’s ~/.claude/ directory layout is documented; the desktop app’s is not. The path users have reverse-engineered, and that the user investigation cited above located at line 771 of the bundled .vite/build/index.js as the constant claude-code-sessions, is:

  • macOS: ~/Library/Application Support/Claude/claude-code-sessions/<accountId>/<orgId>/local_*.json
  • Windows: %APPDATA%Claudeclaude-code-sessions<accountId><orgId>local_*.json

Each session is one JSON file. Each account gets its own <accountId> directory. Each organisation under that account gets its own <orgId> subdirectory. On a single user’s Windows machine, the count of accountId directories was six — the user expected four, but two old accounts were still on disk and forgotten, including the largest one (44 megabytes, 330 sessions, dormant since early April).

The desktop application reads from one of those six directories at any given time — the active account’s. The other five are inert, on the same disk, in the same parent folder, holding sessions in the same JSON format, and not displayed. There is no UI affordance to view them, no setting to merge them, no documented way to migrate a session out of one and into another. Sessions do not become invisible because they are corrupted or deleted. They become invisible because the application does not look in their folder.

The GitHub-issue trail

Documentation gaps in a developer tool are usually accompanied by a GitHub paper trail, and Claude Code’s is unusually long for a product still under heavy development.

Issue #26452, opened February 18, 2026, by a Max subscriber on macOS reporting that sessions disappeared after logout and reappeared only on the disk. Bug label, Open status, dozens of community comments, no MEMBER/OWNER replies as of late April. Issue #48511, opened April 15, 2026: same symptom, fresh report, again open with no engineering response. Issue #29373 — closed — is where the community located the path constant in the bundled JavaScript, by literally diffing build artefacts. Issue #48362 reports that Microsoft Store / MSIX builds break entirely, because the atomic-rename step from local_<sid>.json.tmp to local_<sid>.json fails with EXDEV inside the MSIX virtual file system, treating source and target as different devices. Issue #18645 — closed as a feature request — is where one user hypothesises that version 2.1.9 introduced a stricter check that prevents copying sessions between machines, on the basis of regression testing rather than any documented Anthropic announcement. Issue #54428, opened April 28, 2026, reports that on Claude Desktop 1.4758.0 for some macOS users, an entirely different storage format has begun rolling out: ~/Library/Application Support/Claude/vm_bundles/claudevm.bundle/, containing rootfs.img, sessiondata.img, and efivars.fd. A disk image, not a directory of JSONs.

The combination paints a clear picture. The session-storage layer is in active churn. Users are filing precise, well-instrumented bug reports and, on the most active thread, getting no MEMBER or OWNER reply at all. The community has been building its own discovery and migration tooling for at least three months, against a target that is currently moving.

The format keeps changing

Three storage formats in twelve months is the pattern.

The first format, used through 2025, was local-agent-mode-sessions/. It was renamed to claude-code-sessions/ in early 2026 — issue #29373, closed without comment, is the only public trace of the rename. Some user-built tools still parse for the old name as a fallback. The second format, the current claude-code-sessions/ layout, is the one all the user reverse-engineering above maps. The third format, the in-flight VM-bundle architecture from issue #54428, replaces the directory of JSON files with a single mounted disk image. Anything that reads local_*.json will stop working when the bundle migration completes.

There are good reasons a developer tool might consolidate session storage into a sandboxed image — Anthropic’s published positioning around agent isolation suggests one motive — but the consequence for users running custom tooling is that any tool which targets the on-disk format has, by construction, a short life. A migration script written against the current paths becomes a dead asset the moment the next storage format reaches general availability. Users who have built workflows around the JSON files are pre-emptively building the next workaround for the next migration.

CLI versus desktop

The same product family ships in two flavours, and only one of them has this problem.

The CLI version of Claude Code stores sessions in ~/.claude/projects/<slug>/, keyed by project rather than by account. Switching accounts on the CLI updates .credentials.json and nothing else; the project’s session history is shared across whatever account is currently authenticated. The CLI’s storage layout survives the multi-account rotation pattern without any of the desktop application’s pathology — sessions remain where they were, accessible to any account that opens the same project folder.

The desktop application’s choice to scope sessions by <accountId>/<orgId>/ is the structural source of the invisible-sessions symptom. If the storage path were project-keyed instead, a session opened under one account would still be visible to the next account on the same project. The desktop app’s design treats the account as the primary key for the data; the rotation workflow treats the project as the primary key. The difference is invisible until you look at where the JSON files end up.

This is not a bug that will be fixed by adding a “show sessions from other accounts” toggle to the sidebar. It is a schema choice that has not been reconciled with how the tool gets used.

State in your storage, not theirs

Chasing the on-disk format is, on the evidence of the last twelve months, a losing game. Three storage formats in twelve months, no documentation, no engineering response on the open issues, a fourth format already shipping. Any user-built migration tool is a stopgap with a measurable shelf life. The right architectural answer is to stop relying on the application’s storage for state that needs to survive a format change.

The pattern that does survive is per-project handoff files. At the end of each long Claude Code session, the agent writes a short markdown file into the project’s own repository, in something like .claude/handoffs/<date>_<session-id>.md. The file is short and structured: the session’s goal in one sentence; what was actually done, with file names; what didn’t work — the most valuable section, the hypotheses checked and discarded, with reasons; the current state of the work; one specific next step; and a small read-only check-list of things the next session should verify before resuming. The next session opens in the same project folder, reads the most recent handoff, and picks up the thread, regardless of which account is logged in, regardless of which storage format Anthropic shipped this morning.

The handoff file lives in your repo. Anthropic cannot reformat it. Switching machines does not lose it. Switching accounts does not hide it. The session-storage layer Anthropic owns is allowed to churn — it will keep churning, on the evidence — and the project state that the developer actually cares about lives somewhere churn cannot reach.

This is not a Claude-specific architectural pattern. It is the standard advice for any tool you do not own: the persistent state of work needs to live in storage you control. A migration tool that reads someone else’s storage is, by definition, a temporary measure. A handoff file written by the agent into your own repo is the permanent measure.

What the ten-percent number is actually showing

The 90% invisible-sessions number is not a bug in Claude Code. It is the predictable consequence of a per-account storage schema meeting a multi-account usage pattern that exists because of weekly rate limits the storage schema was not designed around. The weekly limits are a real product constraint. The multi-account rotation is a rational user response. The per-account <accountId>/<orgId>/ directory layout is a rational design decision. Each piece is locally sensible; the combination produces a state where most of one user’s session history is unreachable from the application that wrote it.

The fix the user community is converging on is not to chase the storage layer, because the storage layer is not stable enough to chase. The fix is to keep the project state outside the application entirely. That move has the additional property of making the user’s work portable across versions, across machines, across accounts, and across the storage formats Anthropic has not yet shipped. The session files Anthropic owns are going to get reformatted. The handoff file in your repo is not. That is the difference, and it is the part the ten-percent number is really showing — that the visible part of any external tool’s storage is the part you can lose, and the only state worth depending on is the state you wrote yourself.

JetBrains Launches Course Creators Program Bringing Education into the IDE

JetBrains has officially announced the launch of its Course Creators Program, a new initiative designed to integrate online programming education directly into professional development environments. This program aims to address a persistent “major gap” in the industry: students often learn through videos and browser-based exercises but lack exposure to the actual tools used in professional … continue reading

The post JetBrains Launches Course Creators Program Bringing Education into the IDE appeared first on SD Times.