Welcome to the fourth chapter of the cc-switch tutorial!
In the previous chapter, Provider Adaptation Layer, we built a universal translator so our proxy could talk to any AI provider.
But we have a loose end. If you configure your API keys, set up your routing rules, and then restart the application... poof! Everything is gone.
In this chapter, we will give cc-switch a permanent memory. We are moving away from fragile JSON configuration files to a robust SQLite Database.
Many beginner projects save settings in a config.json file. This works fine for simple tools, but it has major issues for a desktop app like ours:
We use SQLite. It allows us to store data in a single file on your disk (cc-switch.db), but interact with it using structured rules.
Think of it this way:
A database needs a blueprint. You can't just throw data in; you have to define Tables first.
providers).api_key, url).Instead of the Frontend having one state and the Backend having another, the Database is the ultimate authority. If it's not in the database, it didn't happen.
This is the most critical concept in this chapter. When you update the cc-switch app from v1.0 to v2.0, the database structure might need to change (e.g., adding a "Cost per Token" column). Migrations are scripts that automatically upgrade the database structure without deleting your existing data.
When cc-switch launches, it doesn't just blindly open the database. It performs a "Health Check" and an "Upgrade" if necessary.
create_tables)
In src-tauri/src/database/schema.rs, we define what our "Filing Cabinet" looks like. We use SQL (Structured Query Language) to create tables if they don't exist.
Here is how we define the providers table:
// src-tauri/src/database/schema.rs
// "conn" is our active connection to the database file
conn.execute(
"CREATE TABLE IF NOT EXISTS providers (
id TEXT NOT NULL,
app_type TEXT NOT NULL,
name TEXT NOT NULL,
settings_config TEXT NOT NULL,
PRIMARY KEY (id, app_type)
)",
[], // No parameters needed for creation
)?;
Explanation: This SQL command tells SQLite: "Create a drawer labeled 'providers'. It must have columns for ID, App Type, Name, and Config. If this drawer already exists, do nothing."
This is where the magic happens. We assign a version number to our database structure.
When the app starts, it runs apply_schema_migrations. It loops through versions until the database catches up to the code.
// src-tauri/src/database/schema.rs
// Get the version number stored inside the DB file
let mut version = Self::get_user_version(conn)?;
// Loop until the DB version matches the App version
while version < SCHEMA_VERSION {
match version {
1 => {
// If DB is v1, run the upgrade logic to make it v2
Self::migrate_v1_to_v2(conn)?;
Self::set_user_version(conn, 2)?;
}
2 => {
// If DB is v2, upgrade to v3
Self::migrate_v2_to_v3(conn)?;
Self::set_user_version(conn, 3)?;
}
// ... handle other versions
}
// Refresh version variable for the next loop iteration
version = Self::get_user_version(conn)?;
}
What does a migration look like? It's usually just adding a missing column.
Imagine we released a new feature to track costs. We need to add a cost_multiplier column to the providers table, but we don't want to lose the providers the user already saved.
// src-tauri/src/database/schema.rs
fn migrate_v1_to_v2(conn: &Connection) -> Result<(), AppError> {
// Helper function that adds a column only if it's missing
Self::add_column_if_missing(
conn,
"providers", // Table Name
"cost_multiplier", // New Column Name
"TEXT NOT NULL DEFAULT '1.0'" // Data Type & Default Value
)?;
// We can also create entirely new tables here
conn.execute("CREATE TABLE IF NOT EXISTS proxy_request_logs ...", [])?;
Ok(())
}
Beginner Note: This safely "renovates" the house while people are living in it. It adds a new room (column) without knocking down the walls (deleting the table).
By implementing this schema system:
cc-switch app can crash, restart, or update, and your data remains safe.Provider can have many Logs. This relationship is hard to manage in JSON but native to SQLite.In this chapter, we gave our application a robust long-term memory:
Now that we have a secure place to store configuration, we can build complex features on top of it. One of the most powerful features in our roadmap involves managing external tools directly.
In the next chapter, we will learn how to manage the "Model Context Protocol" (MCP) servers.
Next Chapter: Unified MCP Management
Generated by Code IQ