Logo
Kythia
Logo
Kythia
Login
Login
Database & Caching Layer
Minimized (Click to Restore)

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();
Redis is the primary cache (shared across all shards). If Redis is unavailable, the system transparently falls back to an in-process LRU Map — no code changes needed.

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!)
Migrations are tracked in batches. --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;
Kythia Documentation Sign in →