Welcome to the fourth chapter of our journey into building airi.
So far, we have built:
But we have a problem. Right now, if you talk to your AI on your desktop computer, close it, and then open it on your phone... the AI on your phone has no idea who you are. It has amnesia.
In this chapter, we will build the Central Data & Identity Server. This is the long-term memory and the "Passport Office" of the ecosystem.
Let's look at our central use case: Continuity.
Imagine you are playing a game with your AI character on your PC. You unlock a special ability: "Fireball." Later, you go out for a walk and open the mobile app. You want that same character, with the same "Fireball" ability, to be there.
Without a central server:
With a central server:
This server acts as the Single Source of Truth for everything: chat history, user accounts, and character personalities.
To understand the server, think of a Public Library.
This is where the actual information lives. We use PostgreSQL, a powerful database that stores data in tables (like Excel sheets).
You don't talk to the Filing Cabinet directly; it speaks a complex language called SQL. Instead, we use a "Librarian" called Drizzle ORM. You tell the Librarian: "Get me the character named Airi," and the Librarian translates that into SQL and fetches the data for you.
This is the front door. We use a framework called Hono to listen for requests.
In this system, "using" the server mostly means defining Schemas. A schema is a blueprint. It tells the database what a "Character" actually looks like.
Let's look at how we define a Character in apps/server/src/schemas/characters.ts.
import { pgTable, text, integer } from 'drizzle-orm/pg-core'
// We define a table called 'characters'
export const character = pgTable('characters', {
// Every character has a unique ID
id: text('id').primaryKey(),
// They have a creator (the user who made them)
creatorId: text('creator_id').notNull(),
// They have stats like 'likes'
likesCount: integer('likes_count').default(0),
})
Explanation: This code creates a rule. It says: "Every character in our world MUST have an ID, a Creator, and a Like Count." If you try to save a character without a Creator, the Librarian (Drizzle) will stop you.
Characters are more than just names; they have skills. We store these as JSON data (flexible text objects).
export const characterCapabilities = pgTable('character_capabilities', {
id: text('id').primaryKey(),
// Link this skill to a specific character
characterId: text('character_id').notNull(),
// What kind of skill is it? (e.g., 'coding', 'painting')
type: text('type').notNull(),
// The details of the skill
config: jsonb('config').notNull(),
})
Explanation:
This allows our Cognitive Brain to look up what a character can do. If the Brain sees a capability of type coding, it knows it can write code.
What happens when your phone app tries to sync chat history?
The server is an application that runs 24/7. It starts in apps/server/src/app.ts.
First, the application wakes up and connects to the storage.
// Inside createApp() function
const db = injeca.provide('services:db', {
build: async ({ dependsOn }) => {
// Connect to the database URL
const dbInstance = createDrizzle(process.env.DATABASE_URL)
// Ensure the tables exist
await migrateDatabase(dbInstance)
return dbInstance
},
})
Explanation:
This code ensures that before the server starts listening for users, the Filing Cabinet is open and organized (migrateDatabase).
The server needs to know where to send different requests. We use Routes to organize this.
function buildApp({ chatService }) {
return new Hono()
// If the request starts with /api/chats...
.route('/api/chats', createChatRoutes(chatService))
// If the request is for health check...
.get('/health', c => c.json({ status: 'ok' }))
}
Explanation: This acts like a signpost.
Chat Service.health check.
Let's look at apps/server/src/routes/chats.ts to see how a chat sync request is actually handled.
export function createChatRoutes(chatService: ChatService) {
return new Hono()
// Protect this route: Only logged-in users allowed!
.use('*', authGuard)
.post('/sync', async (c) => {
// 1. Get the user from the session
const user = c.get('user')
// 2. Read the data sent by the App
const body = await c.req.json()
// 3. Ask the Chat Service to do the heavy lifting
const result = await chatService.syncChat(user.id, body)
// 4. Send the result back
return c.json(result)
})
}
Explanation:
authGuard): We check the ID card first. Strangers cannot read your chats.chatService. The route itself doesn't know how to save to the database; it just delegates the task.You might notice we use a lot of Abstraction Layers:
While this seems like extra work, it makes the code much safer. If we change the database from PostgreSQL to MySQL in the future, we only have to retrain the "Librarian" (Drizzle), and the rest of the app doesn't need to change at all.
The Central Data & Identity Server is the backbone of the airi ecosystem.
Now that we have a secure place to store our AI's memories, we can start giving it more advanced senses. It's time to let our AI hear the world.
Next Chapter: Sensory Audio Processing (Hearing)
Generated by Code IQ