4 Databases
x edited this page 2026-02-27 09:45:37 +00:00

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

img

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

  1. Use descriptive names - Make database names clear and meaningful
  2. Isolate tenants completely - Each tenant gets their own database
  3. Document database structure - Keep track of which databases exist and their purpose
  4. Implement proper backup - Regular backup of all databases
  5. Monitor storage - Track database file sizes
  6. Use reserved names carefully - Only use default, stats, and workflows for their intended purposes
  7. Set proper permissions - Ensure users can only access their own databases
  8. Test multi-tenancy - Verify data isolation in your application