Database & Caching Layer
Deep dive into the KythiaModel hybrid cache, Laravel-style migrations, and the ModelLoader — powered by kythia-core.
Kythia's data management is built for speed and reliability. We use Sequelize as our ORM, wrapped in a custom base class called KythiaModel that adds a sophisticated two-tier caching layer and batch-tracked migrations.
KythiaModel
Every table in your bot's database should be represented by a class extending KythiaModel.
// addons/leveling/database/models/UserLevel.js
const { KythiaModel } = require('kythia-core');
const { DataTypes } = require('sequelize');
class UserLevel extends KythiaModel {
static tableName = 'user_levels';
static init(sequelize) {
return super.init({
userId: {
type: DataTypes.STRING,
unique: true,
},
guildId: DataTypes.STRING,
level: {
type: DataTypes.INTEGER,
defaultValue: 1,
},
xp: {
type: DataTypes.INTEGER,
defaultValue: 0,
},
}, {
sequelize,
modelName: 'UserLevel',
tableName: this.tableName,
});
}
}
module.exports = UserLevel;
Available Cache Methods
| Method | Description |
|---|---|
Model.getCache(query) |
findOne with caching |
Model.getAllCache(query) |
findAll with caching |
Model.findOrCreateWithCache(options) |
findOrCreate with cache |
Model.countWithCache(options) |
count with cache |
Model.aggregateWithCache(options) |
Aggregate with cache |
Model.invalidateCache() |
Manually bust this model's cache |
instance.saveAndUpdateCache(key) |
Save and update the cache for the changed key |
Hybrid Caching (Redis + LRU)
KythiaModel transparently manages two cache tiers. Your code stays the same regardless of which tier is active:
flowchart LR
CODE["Your code\nUserLevel.getCache(query)"] --> KM
subgraph KM["KythiaModel Cache Layer"]
direction TB
GEN["Generate cache key\nSHA256 hash of query"] --> CHECK
CHECK{"Cache hit?"}
end
CHECK -- "Redis Hit" --> REDIS["Redis\nPrimary Cache"]
CHECK -- "Redis Miss / Down" --> LRU["LRU Map\nFallback Cache"]
CHECK -- "Both Miss" --> DB["Sequelize Database\n(SQLite / MySQL / PG)"]
DB --> STORE["Store result in cache\nwith tags:\n'UserLevel'\n'UserLevel:ID:1'\n'UserLevel:query:hash'"]
STORE --> REDIS
REDIS --> RETURN["Return result"]
LRU --> RETURN
DB2["afterSave / afterDestroy hooks"] --> INVAL["Invalidate cache\nby tag prefix 'UserLevel*'"]
INVAL --> REDIS
INVAL --> LRU
Cache Invalidation
Cache is automatically invalidated via Sequelize afterSave and afterDestroy hooks. You never manually manage cache when updating records through the ORM:
// This automatically clears the cache for this record
await user.update({ level: 25 });
// Manual cache bust if needed
await UserLevel.invalidateCache();
Migration System
Kythia uses a Laravel-inspired migration system powered by umzug. Each migration file has an up (apply) and down (rollback) method.
flowchart TD
A([KythiaMigrator called during boot]) --> B[Scan addons/*/database/migrations\nskip disabled addons]
B --> C[Sort files by timestamp prefix\n20250128120000_create_users_table.js]
C --> D[Compare with migrations table\nvia umzug storage adapter]
D --> E{Any pending?}
E -- No --> DONE([Nothing to do])
E -- Yes --> F[Run pending migrations up\nin timestamp order]
F --> G[Record batch number\nfor rollback support]
G --> DONE2([Done])
style DONE fill:#27ae60,color:#fff
style DONE2 fill:#27ae60,color:#fff
Creating a Migration
npx kythia make:migration --name create_user_levels_table --addon leveling
# Creates: addons/leveling/database/migrations/20250128120000_create_user_levels_table.js
Generated file template:
module.exports = {
up: async (queryInterface, DataTypes) => {
await queryInterface.createTable('user_levels', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
userId: {
type: DataTypes.STRING,
unique: true,
},
level: {
type: DataTypes.INTEGER,
defaultValue: 1,
},
xp: {
type: DataTypes.INTEGER,
defaultValue: 0,
},
createdAt: DataTypes.DATE,
updatedAt: DataTypes.DATE,
});
},
down: async (queryInterface) => {
await queryInterface.dropTable('user_levels');
}
};
Running Migrations
npx kythia migrate # Run all pending migrations
npx kythia migrate --rollback # Undo last batch
npx kythia migrate --fresh # Drop all + re-run (dev only!)
--rollback only undoes the last batch, not all migrations. This matches Laravel's behavior.ModelLoader (Auto-Discovery)
ModelLoader automatically discovers and boots all addon models during startup. You never manually register a model anywhere:
Boot sequence:
1. Scan addons/*/database/models/
2. Skip disabled addons
3. require() each model file
4. Call Model.autoBoot(sequelize) — introspects DB schema
5. Register in container.models
6. Execute dbReadyHooks (define associations)
Once booted, every model is available in commands via container.models:
async execute(interaction, container) {
const { UserLevel } = container.models;
const user = await UserLevel.getCache({
userId: interaction.user.id,
guildId: interaction.guild.id,
});
}
Database Seeders
Seeders populate your database with initial or test data.
npx kythia make:seeder PetSeeder --addon pet
npx kythia db:seed # Run all seeders
npx kythia db:seed --class PetSeeder # Run specific seeder
npx kythia db:seed --addon pet # Run all seeders in an addon
// addons/pet/database/seeders/PetSeeder.js
const { Seeder } = require('kythia-core');
class PetSeeder extends Seeder {
async run() {
const { Pet } = this.container.models;
await Pet.bulkCreate([
{ name: 'Fluffy', rarity: 'common', icon: '🐱' },
{ name: 'Blaze', rarity: 'epic', icon: '🔥' },
{ name: 'Shadow', rarity: 'legendary', icon: '🐉' },
]);
}
}
module.exports = PetSeeder;