Virke DB

Full SQL at the edge. Virke DB gives you a SQLite database running on Fastly Compute@Edge via rusqlite, with auto-tiered storage that scales from zero with no hard size limit.

How It Works

  1. On each request, the SQLite database is loaded from KV Store (or Object Storage for larger databases)
  2. Your SQL query executes against the in-memory database using rusqlite
  3. For writes, the updated database is serialized back to storage

Auto-tiered storage

Tier Size Backend Read Latency Write Latency
KV Store 0–20 MB Fastly KV Store 9–120ms 430–2,070ms
Object Storage 20 MB+ Fastly Object Storage + CDN 2–12ms (cached) Similar

Create a Database

Via CLI

virke db create my-db

Via virke.toml

[project]
name = "my-app"

[database]
name = "my-app-db"

The database is created automatically on first deploy.

CLI Usage

Run a read query

virke db query "SELECT * FROM users WHERE active = 1"
id  name    email              active
--  ------  -----------------  ------
1   Alice   alice@example.com  1
3   Carol   carol@example.com  1

Run a write statement

virke db execute "INSERT INTO users (name, email, active) VALUES ('Dave', 'dave@example.com', 1)"

View schema

virke db schema

Import a SQL dump

virke db import backup.sql

Database status

virke db status

Performance settings

virke db settings            # view/update settings
virke db flush               # force sync pending writes

Promote to higher tier

virke db promote

Promotes a database from KV Store to Object Storage for larger datasets.

SDK Usage (from Virke Run)

With Hono (recommended)

import { Hono } from "hono";
import { virke } from "@virke/runtime/hono";

const { fire, middleware, Bindings } = virke({ db: "my-app-db" });
const app = new Hono<{ Bindings: typeof Bindings }>();

app.use("*", middleware);

app.get("/users", async (c) => {
  const result = await c.env.db.query("SELECT * FROM users");
  return c.json(result.rows);
});

app.post("/users", async (c) => {
  const { name, email } = await c.req.json();
  const affected = await c.env.db.execute(
    "INSERT INTO users (name, email) VALUES (?, ?)",
    [name, email]
  );
  return c.json({ created: affected });
});

fire(app);

DB binding API

interface VirkeDB {
  query(sql: string, params?: unknown[]): Promise<QueryResult>;
  execute(sql: string, params?: unknown[]): Promise<number>;
}

interface QueryResult {
  columns: string[];
  rows: unknown[][];
  rows_affected: number;
}

Parameterized Queries

Always use parameterized queries to prevent SQL injection:

// Safe — parameterized
const result = await env.db.query(
  "SELECT * FROM users WHERE email = ?",
  [userInput]
);

// Unsafe — never do this
// const result = await env.db.query(`SELECT * FROM users WHERE email = '${userInput}'`);

Performance

Operation Latency
Simple SELECT (single row) 9–15ms
SELECT with JOIN (100 rows) 25–50ms
Complex aggregation 50–120ms
Single INSERT 430–600ms
Batch INSERT (100 rows) 800–2,070ms

Tips for best performance

  • Batch writes — combine multiple writes into a single transaction
  • Index frequently queried columns — SQLite indexes work as expected
  • Keep databases small — the entire database loads on each request
  • Read-heavy patterns — optimized for workloads where writes are infrequent

Concurrency

Virke DB uses optimistic concurrency control (OCC) via Fastly KV Store's if_generation_match. No lost writes, no locks, and conflicts are detected and surfaced.

Migrations

Place SQL migration files in a migrations/ directory:

migrations/
  001_create_users.sql
  002_add_posts_table.sql
  003_add_user_avatar.sql
-- migrations/001_create_users.sql
CREATE TABLE IF NOT EXISTS users (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  email TEXT UNIQUE,
  created_at TEXT DEFAULT (datetime('now'))
);
virke db migrate

Limitations

  • Single writer per database — only one write can succeed at a time (OCC ensures correctness)
  • Eventual consistency — reads from different POPs may briefly see old data after a write
  • No hard size limit — Object Storage tier has no size cap. Practical ceiling is ~100 MB for the serialize/deserialize model (deserialization scales at ~8ms/MB).
  • No wall clockdatetime('now') returns the request timestamp

Further Reading

  • Virke Run — Build compute functions that use Virke DB
  • CLI Commands — Full virke db command reference