← Back to all posts

25 March 2026 · 4 min read · AI Agents, Next.js, Supabase, Architecture, LangGraph

The AI Agent Stack Isn't Ready, But We're Building It Anyway

Moving AI chatbots from demos to production requires a robust architecture. Here is the stack I am betting on for stateful agents, memory, and reliable tool execution.


Everyone is shipping a chatbot wrapper. I get it. The barrier to entry is non-existent. You spin up a Next.js app, drop in the Vercel AI SDK, hook it up to GPT-4, and call it a day.

But the moment you try to move that demo into a real production environment—especially one where the agent needs to actually do something—the architecture falls apart. The stateless nature of HTTP requests fights against the conversational flow, context windows get expensive, and reliability becomes a nightmare.

I have spent the last few months at Thea Tech Solutions trying to bridge the gap between a cool demo and a reliable production agent. Here is the stack I am currently betting on to make AI agents actually functional.

The Statelessness Problem

The biggest issue with modern LLM apps is that the web is stateless, but agents are stateful. A user might say, "Book that meeting," referring to a request ten messages ago. Standard REST APIs or even simple Server Actions in Next.js struggle with this because every request is a fresh start.

You need a coordination layer. You need an orchestrator that understands the history of the conversation without you having to jam the entire chat history into the prompt every single time (token costs get out of hand fast).

The Orchestration Layer: LangGraph or Custom

I am not a huge fan of heavy abstraction layers, but for agents, you need a way to manage cycles. The agent needs to think, act, observe, and repeat. This is a graph, not a linear function.

Right now, I am leaning towards LangGraph. It allows you to define a stateful graph as code. You can define nodes (your tools) and edges (the logic flow). This is critical because it lets you persist the state of the conversation at every step.

If LangGraph feels too heavy, a custom cycle using LangChain's expression language is a lighter alternative, but you end up writing more boilerplate for state management. For production, I want the graph structure. It makes debugging easier when an agent goes off the rails.

Memory: Supabase + Postgres

Do not rely on the LLM provider's memory. It is opaque, often slow, and you lose control.

I treat memory like I treat any other user data. I use Supabase (Postgres) to store conversation history and session data. The architecture looks like this:

  • User sends message.
  • Next.js route handler fetches recent history from Supabase.
  • We summarize old history (using a smaller, cheaper model like Llama 3 8b via an OpenAI-compatible endpoint) to save tokens, keeping the last few messages verbatim.
  • Pass the condensed context to the Agent.
  • This gives me full control over the RAG (Retrieval-Augmented Generation) pipeline. I can query the database for specific user permissions or past interactions before the agent even decides what to do.

    Tool Execution: The Danger Zone

    This is where most demos fail. An agent wants to call a function, say delete_all_records(). If you just blindly execute that, you are going to have a bad time.

    I use a Tool Registry pattern. Every "tool" the agent can access is a TypeScript function that includes:

    * Input validation: Zod schemas are non-negotiable here. If the agent hallucinates a parameter type, the function throws an error immediately.

    * Permission checks: The tool checks the user's session ID against Supabase RLS policies before executing anything.

    import { z } from "zod";
    
    const deleteRecordSchema = z.object({
      recordId: z.string().uuid(),
    });
    
    async function deleteRecordTool(input: z.infer<typeof deleteRecordSchema>) {
      // 1. Validate input
      const params = deleteRecordSchema.parse(input);
      
      // 2. Execute business logic (Supabase client)
      const { data, error } = await supabase
        .from('records')
        .delete()
        .eq('id', params.recordId);
        
      if (error) throw new Error("Failed to delete");
      
      return { success: true };
    }
    

    This keeps the agent powerful but sandboxed.

    The Frontend: Stream Everything

    The user experience needs to feel snappy. I use Next.js with the Vercel AI SDK on the frontend. It handles the streaming response generation beautifully.

    However, I do not let the frontend talk directly to the LLM. The frontend calls my backend API route. That route:

  • Loads memory from Supabase.
  • Calls the Agent (LangGraph).
  • Streams the response back to the frontend.
  • This adds a tiny bit of latency, but it secures my API keys and allows me to inject middleware logic (like rate limiting) without the client knowing.

    Where We Are Heading

    The stack is stabilizing, but it is not plug-and-play yet. You have to build your own "Agent Runtime" around the database and the model.

    If you are starting today, do not try to build a fully autonomous agent from scratch. Build a Copilot. Build an agent that assists a user who is in the loop. The moment you try to go fully autonomous with the current tooling, you will spend 90% of your time building safety rails instead of features.

    The Takeaway:

    Stop thinking of AI apps as simple prompts. Start thinking of them as stateful applications. Use a graph-based approach (LangGraph) for control flow, use Postgres (Supabase) for memory, and wrap every single tool in strict validation logic.


    AI Agents Next.js Supabase Architecture LangGraph
    ← All posts