Table of Contents
- Databases & Tenant Isolation
Databases & Tenant Isolation
VSKI provides built-in multi-tenancy support, allowing you to manage multiple isolated databases within a single VSKI instance. This is perfect for SaaS applications, multi-environment setups, and client data isolation.
Note
Schemas are append-only. You cannot rename columns in a database trough the interface. Dropping and renaming columns is a manual task.
Note
Tables and columns are always soft-deleted when using VSKI API. Data migration and breaking changes is an admin task that has to be done carefully and with cool head.
Understanding Multi-Tenancy
Each VSKI instance can host multiple databases, each stored as a separate file. This ensures complete data isolation between tenants while sharing the same application code and server infrastructure.
Database Directory Structure
data/
├── default.db # Default database
├── stats.db # Statistics database (reserved)
├── workflows.db # Workflows database (reserved)
├── client1.db # Client 1 database
├── client2.db # Client 2 database
└── client3.db # Client 3 database
Creating Databases
Create a New Database
Using the SDK
import { VskiClient } from "@vski/sdk";
const client = new VskiClient("http://localhost:3000");
// Create a new database
const database = await client.settings.databases.create({
name: "client1",
});
console.log(database.name); // "client1"
This creates a new client1.db file in the data directory.
List All Databases
const databases = await client.settings.databases.list();
console.log(databases);
// [
// { name: "default" },
// { name: "stats" },
// { name: "workflows" },
// { name: "client1" },
// { name: "client2" },
// ]
Accessing Specific Databases
Note: The setDb() method is deprecated. The recommended approach is to use
client.db(name) to access a specific database.
Using the SDK
// Access a specific database
const client1Db = client.db("client1");
// All operations now use client1.db
const posts = await client1Db.collection("posts").getList(1, 20);
// Access another database
const client2Db = client.db("client2");
const posts2 = await client2Db.collection("posts").getList(1, 20);
Using HTTP Headers
When making direct HTTP requests, use the x-dbname header:
const headers = {
"Authorization": "Bearer your-token",
"x-dbname": "client1",
};
const response = await fetch(
"http://localhost:3000/api/collections/posts/records",
{
headers,
},
);
Database Operations
Database Isolation
Each database is completely isolated:
// Access client1 database
const client1Db = client.db("client1");
// Create a post in client1
await client1Db.collection("posts").create({
title: "Post in Client 1",
});
// Access client2 database
const client2Db = client.db("client2");
// This post is in client2 (different from client1)
await client2Db.collection("posts").create({
title: "Post in Client 2",
});
// Switch back to client1
const posts = await client1Db.collection("posts").getList(1, 20);
console.log(posts.items.length); // 1 (only the post created in client1)
Deleting Databases
// Delete a database
const success = await client.settings.databases.delete("client1");
console.log(success); // true
Warning: Deleting a database permanently removes all its data.
Reserved Databases
VSKI reserves three database names:
Default Database (default.db)
The default database is used when no specific database is specified:
// No database specified - uses default
const posts = await client.collection("posts").getList(1, 20);
Stats Database (stats.db)
The stats database stores system statistics and logs:
// Access stats database
const statsDb = client.db("stats");
const stats = await statsDb.collection("_stats").getList(1, 20);
Do not store user data in the stats database.
Workflows Database (workflows.db)
The workflows database stores all workflow-related data:
- Workflow runs and their status
- Workflow events and signals
- Job queues for distributed workers
- Hooks for external integrations
- Wait states for sleep/signal operations
// Access workflows database
const workflowsDb = client.db("workflows");
const runs = await workflowsDb.collection("workflow_runs").getList(1, 20);
Do not store user data in the workflows database. This database is
automatically omitted from replication.
SaaS Application Pattern
Tenant Registration
// When a new tenant registers
async function registerTenant(tenantData) {
// Create tenant record in default database
const tenant = await client.db("default").collection("tenants")
.create({
name: tenantData.name,
subdomain: tenantData.subdomain,
status: "active",
});
// Create isolated database for tenant
await client.settings.databases.create({
name: tenant.id,
});
// Initialize tenant's collections
await initializeTenantDatabase(tenant.id);
return tenant;
}
Tenant Database Initialization
async function initializeTenantDatabase(tenantId) {
// Note: Collections schemas are created globally and apply to all databases.
// Records created in a database use these global schemas but are stored
// separately in each database file.
await client.settings.collections.create({
name: "users",
type: "auth",
fields: [
{ name: "email", type: "email", required: true },
{ name: "password", type: "text", required: true },
{ name: "name", type: "text" },
],
});
await client.settings.collections.create({
name: "posts",
type: "base",
fields: [
{ name: "title", type: "text", required: true },
{ name: "content", type: "text" },
],
});
}
Tenant Access
// When a tenant user logs in
async function loginTenantUser(tenantId, email, password) {
// Access tenant's database
const tenantDb = client.db(tenantId);
// Login
const result = await tenantDb.auth.login(email, password);
return result;
}
Multi-Environment Setup
Development, Staging, Production
// Environment-based database selection
const env = process.env.NODE_ENV || "development";
const databases = {
development: "dev",
staging: "staging",
production: "prod",
};
const envDb = client.db(databases[env]);
Environment Isolation
// Create environment databases
await client.settings.databases.create({ name: "dev" });
await client.settings.databases.create({ name: "staging" });
await client.settings.databases.create({ name: "prod" });
// Each environment has isolated data
const devDb = client.db("dev");
// All operations here are isolated from staging and prod
Database Configuration
Custom Data Directory
Set a custom directory for database files:
export DATA_DIR="/path/to/custom/data"
Database File Location
// Default location: ./data/
// Custom location via DATA_DIR environment variable
// Database files are:
// - default.db
// - client1.db
// - etc.
Database Permissions
Database-Level Rules
You can create rules that apply to entire databases:
await client.settings.rules.create({
name: "Tenant Access",
collection: "*",
rule: "@request.auth.tenantId = @request.data.dbname",
});
Cross-Database Operations
// Access data from another database (with proper permissions)
const otherDbData = await client.collection("shared_data").getList(1, 20, {
headers: {
"x-dbname": "shared",
},
});
Monitoring
Database Statistics
// Get statistics for a specific database
const client1Db = client.db("client1");
const stats = await client1Db.collection("_stats").getList(1, 20);
console.log(stats.items);
Storage Usage
// Check database file sizes
import { readdir, stat } from "fs/promises";
const dataDir = process.env.DATA_DIR || "./data";
const files = await readdir(dataDir);
for (const file of files) {
if (file.endsWith(".db")) {
const stats = await stat(`${dataDir}/${file}`);
console.log(`${file}: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
}
}
Backup and Restore
Backup Database
import { copyFile } from "fs/promises";
async function backupDatabase(dbName, backupPath) {
const dataDir = process.env.DATA_DIR || "./data";
const source = `${dataDir}/${dbName}.db`;
const destination = `${backupPath}/${dbName}.db`;
await copyFile(source, destination);
}
// Usage
await backupDatabase("client1", "./backups");
Restore Database
async function restoreDatabase(dbName, backupPath) {
const dataDir = process.env.DATA_DIR || "./data";
const source = `${backupPath}/${dbName}.db`;
const destination = `${dataDir}/${dbName}.db`;
await copyFile(source, destination);
}
// Usage
await restoreDatabase("client1", "./backups");
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/databases |
List all databases |
POST |
/api/databases |
Create new database |
DELETE |
/api/databases/:name |
Delete database |
Best Practices
- Use descriptive names - Make database names clear and meaningful
- Isolate tenants completely - Each tenant gets their own database
- Document database structure - Keep track of which databases exist and their purpose
- Implement proper backup - Regular backup of all databases
- Monitor storage - Track database file sizes
- Use reserved names carefully - Only use
default,stats, andworkflowsfor their intended purposes - Set proper permissions - Ensure users can only access their own databases
- Test multi-tenancy - Verify data isolation in your application
