Eliza Simplified: A Modern Bun-based Architecture Guide
March 18th, 2025

It took me longer than I expected to write another blog post as I got extremely busy participating in all the hackathons possible. The good thing is that I got to deepen my skills using Eliza and I've now figured out how to bootstrap a simple project with it.

The recommended way from Eliza is to use their starter repo but I still found that repo extremely confusing and bloated for nothing. I believe that’s a recuring issue with Eliza, the team seem to want to have the project be as turnkey as possible to the detriment of cleaner, simpler code, but I disgress.

For this blog post, refer to my Github project to get the basics of the architecture. This project is Eliza stripped down to what’s really needed (in my opinion) and structured in a way that makes sense (again, in my opinion).

Note that the repo only has a dev method, I’ll make a post to deploy it.

Bun monorepo

First of all, as you can see, I’ve used bun monorepo to split the backend/frontend logic. That is much different than the Eliza Starter repo or even the Eliza Core repo where they actually define the frontend as a client within the Eliza project. In my opinion, having the client in your Eliza project is not a good separation of concern. Eliza is simply a backend and should be treated like so. If we wanted to make the architecture bigger, we could have a project for types so that we can share it within the projects.

Simplified architecture
Simplified architecture

Now on to Eliza

Everything starts with the index.ts file in backend/eliza . The file is relatively simple but does a few different things. The entry point is the startAgents function, which will begin by adding clients. Clients in Eliza are basically entry points to your app. Typically, it would be an API, you can also add a Chat feature if you want, but I got rid of it as it felt useless since it was just a wrapper to make API calls. Now the API, Eliza comes with a DirectClient which is essentially an API to connect to your agent. I didn’t want to use the built-in one because it has tons of routes I don’t need and I also wanted to have the liberty to add my own routes and manage them the way I wanted. Using the DirectClient and then extending it wasn’t a solution I liked, so I completely rewrote it and while at it, I chose Elysia over the built-in DirectClient for several reasons:

  • Better performance (it's built on top of Bun)

  • More flexible routing system

  • Easier to extend with custom endpoints

  • Better TypeScript support

Understanding the Agent Lifecycle

One crucial aspect of Eliza that deserves a deeper explanation is the agent lifecycle. While it might seem complex at first, it follows a straightforward pattern:

  1. Database Initialization

    • The first step is setting up the database connection. I opted for Postgres (via Supabase) instead of SQLite:
    const db = new PostgresDatabaseAdapter({
      connectionString: process.env.POSTGRES_URL,
      parseInputs: true
    });
    await db.init();
    
  2. Agent Runtime Creation

    • Each agent gets its own runtime, which is the core of Eliza's architecture

    • The runtime manages the agent's state, handles message processing, and coordinates between different components

    const runtime = new AgentRuntime({
      databaseAdapter: db,
      token,          // API token for the model provider
      modelProvider,   // Which LLM to use
      character,      // Agent's personality and behavior
      plugins: [],    // Optional plugins for extended functionality
      cacheManager    // Handles caching of conversations and state
    });
    
  3. Client Registration

    • After initialization, the agent needs ways to communicate

    • In my implementation, I use a custom Elysia-based API client instead of Eliza's built-in DirectClient

    • The API client gets registered with the runtime and can then handle incoming requests

    runtime.clients = await initializeClients(character, runtime);
    directClient.registerAgent(runtime);
    
  4. Graceful Shutdown

    • Often overlooked but crucial for production deployments

    • My implementation handles SIGINT and SIGTERM signals

    • Ensures proper cleanup of database connections and server shutdown

    const shutdown = async () => {
      await directClient.server.close();
      await db.close();
    };
    

The backend

As you can see, the backend resides in the api.ts file and really just sets up the routes for our app. In this case, the only app we have is the message handler. I then split the logic of handling the request in a controller (message-controller.ts), the controller then uses a service to execute the logic (message-service.ts). This is a pretty standard API logic architecture and gives us the flexibility we need. Going through this simple refactoring also allowed me to understand exactly what is happening when a message is sent. I will cover this in a later blog post (spoiler, it’s easier than you think), but you can see all the code if you want here. The only thing that is important to follow here is the start and stop function for the client, rest is not used within the framework.

Database/cache

Once the API client is started, it will be distributed and added to every specific agent. One important part of this is the Database and cache manager. For the cache manager, you can simply use the CacheManager from Eliza. I haven’t looked into it much, but it did what I wanted it to do. Perhaps there are optimizations to be done there, but didn’t run into limitations. For Database, Eliza recommends SQLite for testing and Postgres for prod. I ran into issues using SQLite with Bun so I just resorted to using Postgres. I use Supabase, even though Eliza has a plugin for it, I use the Postgres adapter from core as the plugin was not working properly, and I didn’t want to figure out why.

Tieing it all together

As you can see, the project in its current form is extremely simple and that’s what I wanted it to be. I wanted to understand exactly what everything does and why, and I felt like the examples or the starter repo were completely bloated. Building this simpler version also allowed me to understand exactly how the framework works.

Simply put, every agent will start its own AgentRuntime based on the character definition file.

Character Configuration

Characters in Eliza define the personality and behavior of your agent. Here's a simplified example:

// eliza/character.ts
export const character: Character = {
  name: 'MyAgent',
  modelProvider: 'openai',
  personality: {
    traits: ['helpful', 'friendly'],
    background: 'An AI assistant focused on...'
  },
  // Other configuration options
};

In our case, we kept it very simple with a single character with no plugin or client (will cover that in another post), but the important thing to note here is that you can run multiple agents at the same time, they will have their own AgentRuntime which allows every agent to do its own thing. They will typically all share a client to contact them (our API), but can also have their own clients (think Twitter, Telegram, etc.). They will all have their own plugins and actions too.

Resource Sharing and Configuration

One important aspect to note is how resources are shared between agents. While each agent has its own runtime:

  • Database connections are pooled and shared

  • The API server handles requests for all agents (it basically reaches the proper agent based on the request)

  • Cache can be configured to be either shared or isolated

  • Plugins and other clients are on a per-agent basis (defined in the Character file)

Wrapping it up

I think going through a refactoring and cleaning of the code from the Eliza Core allowed me to fully wrap my head around the architecture and how it works. Even though the Eliza documentation is quite extensive, it feels like a lot of the key elements are barely explained and the starter project felt so bloated it was a bit hard to understand the different moving parts. I also feel like the team is not fully exploiting the power of Typescript for some parts of the project and could really benefit from this.

As usual, feel free to reach out if you have any questions!

Subscribe to Gabey
Receive the latest updates directly to your inbox.
Nft graphic
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.
More from Gabey

Skeleton

Skeleton

Skeleton