Skip to main content
TrailBase includes a WebAssembly runtime powered by Wasmtime, enabling you to extend your backend with custom TypeScript/JavaScript endpoints and scheduled jobs without recompiling the server.

Overview

The WASM runtime provides:
  • Custom HTTP endpoints - Create API routes with TypeScript/JavaScript
  • Background jobs - Schedule periodic tasks (minutely, hourly, daily, weekly)
  • Database access - Query your database from WASM code
  • Hot reload - Update code without restarting TrailBase
  • Sandboxed execution - Secure isolation from the host system
  • Multiple isolates - Parallel request handling

Quick Start

1. Install WASM Component

TrailBase provides official TypeScript/JavaScript support:
# List available components
trail components list

# Install TypeScript runtime
trail components add trailbase-guest-ts

2. Create Your First Endpoint

Create traildepot/wasm/index.ts:
traildepot/wasm/index.ts
import { defineConfig } from "trailbase-wasm";
import { HttpHandler, HttpRequest, HttpResponse } from "trailbase-wasm/http";

export default defineConfig({
  httpHandlers: [
    HttpHandler.get("/hello", (req: HttpRequest) => {
      return "Hello from WASM!";
    }),
    
    HttpHandler.get("/greet/:name", (req: HttpRequest) => {
      const name = req.getPathParam("name");
      return `Hello, ${name}!`;
    }),
    
    HttpHandler.post("/json", (req: HttpRequest) => {
      const data = req.json();
      return HttpResponse.json({
        received: data,
        timestamp: Date.now(),
      });
    }),
  ],
});

3. Build and Deploy

Compile your TypeScript to WASM:
# Build WASM component
cd traildepot/wasm
npm install
npm run build

# Output: dist/component.wasm

4. Test Your Endpoint

Restart TrailBase to load the WASM component:
trail run
Test your endpoint:
curl http://localhost:4000/hello
# Output: Hello from WASM!

curl http://localhost:4000/greet/Alice
# Output: Hello, Alice!

curl -X POST http://localhost:4000/json -d '{"message":"test"}'
# Output: {"received":{"message":"test"},"timestamp":1234567890}

HTTP Handlers

Request Methods

import { defineConfig } from "trailbase-wasm";
import { HttpHandler, HttpRequest } from "trailbase-wasm/http";

export default defineConfig({
  httpHandlers: [
    HttpHandler.get("/users", listUsers),
    HttpHandler.post("/users", createUser),
    HttpHandler.put("/users/:id", updateUser),
    HttpHandler.patch("/users/:id", patchUser),
    HttpHandler.delete("/users/:id", deleteUser),
  ],
});

function listUsers(req: HttpRequest) {
  return HttpResponse.json({ users: [] });
}

function createUser(req: HttpRequest) {
  const data = req.json();
  return HttpResponse.json({ id: 1, ...data });
}

function updateUser(req: HttpRequest) {
  const id = req.getPathParam("id");
  const data = req.json();
  return HttpResponse.json({ id, ...data });
}

function patchUser(req: HttpRequest) {
  const id = req.getPathParam("id");
  const data = req.json();
  return HttpResponse.json({ id, updated: data });
}

function deleteUser(req: HttpRequest) {
  const id = req.getPathParam("id");
  return HttpResponse.json({ deleted: id });
}

Request API

Access request data:
HttpHandler.get("/api/example", (req: HttpRequest) => {
  // Query parameters
  const page = req.getQueryParam("page") ?? "1";
  const limit = req.getQueryParam("limit") ?? "10";
  
  // Path parameters
  const id = req.getPathParam("id");
  
  // Request body as JSON
  const body = req.json();
  
  // Raw request body
  const raw = req.body();
  
  return HttpResponse.json({
    page,
    limit,
    id,
    body,
  });
});

Response API

import { HttpResponse } from "trailbase-wasm/http";

// Return plain text (default)
HttpHandler.get("/text", () => {
  return "Hello, World!";
});

// Return JSON
HttpHandler.get("/json", () => {
  return HttpResponse.json({ status: "ok" });
});

// Custom status code
HttpHandler.get("/not-found", () => {
  return HttpResponse.json(
    { error: "Not found" },
    { status: 404 }
  );
});

// Custom headers
HttpHandler.get("/custom", () => {
  return HttpResponse.json(
    { data: "test" },
    { 
      headers: {
        "X-Custom-Header": "value",
        "Cache-Control": "max-age=3600",
      }
    }
  );
});

Database Access

Query your database from WASM:
import { defineConfig } from "trailbase-wasm";
import { HttpHandler } from "trailbase-wasm/http";
import { query } from "trailbase-wasm/db";

export default defineConfig({
  httpHandlers: [
    HttpHandler.get("/stats/todos", async () => {
      // Execute SQL query
      const rows = await query(
        "SELECT COUNT(*) as total, SUM(completed) as done FROM todos",
        []
      );
      
      return HttpResponse.json({
        total: rows[0][0],
        done: rows[0][1],
        pending: rows[0][0] - rows[0][1],
      });
    }),
    
    HttpHandler.get("/todos/search", async (req) => {
      const q = req.getQueryParam("q") ?? "";
      
      // Parameterized queries prevent SQL injection
      const rows = await query(
        "SELECT * FROM todos WHERE text LIKE ? LIMIT 10",
        [`%${q}%`]
      );
      
      return HttpResponse.json({ results: rows });
    }),
  ],
});
Always use parameterized queries to prevent SQL injection:Bad:
const rows = await query(`SELECT * FROM todos WHERE id = ${id}`);
Good:
const rows = await query("SELECT * FROM todos WHERE id = ?", [id]);

Complex Queries

HttpHandler.get("/reports/user-activity", async () => {
  const rows = await query(`
    SELECT 
      u.email,
      COUNT(t.id) as todo_count,
      SUM(t.completed) as completed_count
    FROM _user u
    LEFT JOIN todos t ON t.user = u.id
    GROUP BY u.id
    ORDER BY todo_count DESC
    LIMIT 10
  `, []);
  
  return HttpResponse.json({ topUsers: rows });
});

Background Jobs

Schedule periodic tasks:
import { defineConfig } from "trailbase-wasm";
import { JobHandler } from "trailbase-wasm/job";
import { query } from "trailbase-wasm/db";

export default defineConfig({
  jobHandlers: [
    // Run every minute
    JobHandler.minutely("cleanup-old-todos", async () => {
      const weekAgo = Math.floor(Date.now() / 1000) - (7 * 24 * 60 * 60);
      
      await query(
        "DELETE FROM todos WHERE completed = 1 AND updated_at < ?",
        [weekAgo]
      );
      
      console.log("Cleaned up old completed todos");
    }),
    
    // Run every hour
    JobHandler.hourly("send-reminders", async () => {
      const rows = await query(`
        SELECT t.*, u.email 
        FROM todos t
        JOIN _user u ON t.user = u.id
        WHERE t.completed = 0 AND t.due_date < ?
      `, [Math.floor(Date.now() / 1000)]);
      
      for (const row of rows) {
        console.log(`Reminder: ${row[2]} is overdue for ${row[4]}`);
        // Send email notification
      }
    }),
    
    // Run daily at midnight
    JobHandler.daily("daily-report", async () => {
      const rows = await query(
        "SELECT COUNT(*) FROM todos WHERE created_at > ?",
        [Math.floor(Date.now() / 1000) - (24 * 60 * 60)]
      );
      
      console.log(`Daily report: ${rows[0][0]} todos created today`);
    }),
    
    // Run weekly (Sunday at midnight)
    JobHandler.weekly("weekly-cleanup", async () => {
      console.log("Weekly cleanup started");
      // Perform weekly maintenance tasks
    }),
  ],
});

Async Operations

Timers

import { addPeriodicCallback } from "trailbase-wasm";

HttpHandler.get("/delayed-response", async () => {
  // Sleep
  await new Promise(resolve => setTimeout(resolve, 1000));
  return "Delayed by 1 second";
});

HttpHandler.get("/periodic", () => {
  let count = 0;
  
  // Schedule periodic callback
  addPeriodicCallback(250, (cancel) => {
    console.log(`Callback #${count}`);
    count++;
    
    if (count >= 10) {
      cancel(); // Stop after 10 iterations
    }
  });
  
  return "Started periodic task";
});

Parallel Queries

HttpHandler.get("/dashboard", async () => {
  // Execute queries in parallel
  const [users, todos, articles] = await Promise.all([
    query("SELECT COUNT(*) FROM _user", []),
    query("SELECT COUNT(*) FROM todos", []),
    query("SELECT COUNT(*) FROM articles", []),
  ]);
  
  return HttpResponse.json({
    users: users[0][0],
    todos: todos[0][0],
    articles: articles[0][0],
  });
});

Real-World Examples

Vector Search Endpoint

From the Coffee Vector Search example:
import { defineConfig } from "trailbase-wasm";
import { HttpHandler, HttpRequest, HttpResponse } from "trailbase-wasm/http";
import { query } from "trailbase-wasm/db";

export default defineConfig({
  httpHandlers: [
    HttpHandler.get("/api/search", async (req: HttpRequest) => {
      const q = req.getQueryParam("q");
      const limit = parseInt(req.getQueryParam("limit") ?? "10");
      
      if (!q) {
        return HttpResponse.json({ error: "Missing query" }, { status: 400 });
      }
      
      // Perform vector similarity search
      const results = await query(`
        SELECT 
          c.name,
          c.description,
          c.flavor_notes,
          vec_distance_cosine(c.embedding, vec_f32(?)) as distance
        FROM coffees c
        ORDER BY distance ASC
        LIMIT ?
      `, [await embed(q), limit]);
      
      return HttpResponse.json({ results });
    }),
  ],
});

// Mock embedding function
async function embed(text: string): Promise<Float32Array> {
  // In production, call an embedding API
  return new Float32Array(384); // Example: 384-dimensional embedding
}

API Rate Limiting

const rateLimits = new Map<string, number[]>();

function rateLimit(ip: string, maxRequests: number, windowMs: number): boolean {
  const now = Date.now();
  const requests = rateLimits.get(ip) ?? [];
  
  // Remove old requests outside the window
  const recentRequests = requests.filter(time => now - time < windowMs);
  
  if (recentRequests.length >= maxRequests) {
    return false; // Rate limit exceeded
  }
  
  recentRequests.push(now);
  rateLimits.set(ip, recentRequests);
  return true;
}

HttpHandler.get("/api/expensive", (req) => {
  const ip = req.getHeader("x-forwarded-for") ?? "unknown";
  
  // Allow 10 requests per minute
  if (!rateLimit(ip, 10, 60000)) {
    return HttpResponse.json(
      { error: "Rate limit exceeded" },
      { status: 429 }
    );
  }
  
  // Handle request...
  return "OK";
});

Data Aggregation

HttpHandler.get("/api/analytics/overview", async () => {
  const [totalUsers, activeUsers, totalTodos, completionRate] = await Promise.all([
    query("SELECT COUNT(*) FROM _user", []),
    query("SELECT COUNT(DISTINCT user) FROM todos WHERE updated_at > ?", [
      Math.floor(Date.now() / 1000) - (7 * 24 * 60 * 60),
    ]),
    query("SELECT COUNT(*) FROM todos", []),
    query("SELECT AVG(completed) FROM todos", []),
  ]);
  
  return HttpResponse.json({
    users: {
      total: totalUsers[0][0],
      active: activeUsers[0][0],
    },
    todos: {
      total: totalTodos[0][0],
      completionRate: (completionRate[0][0] * 100).toFixed(1) + "%",
    },
  });
});

Hot Reload

During development, enable hot reload to update code without restarting:
traildepot/wasm/hot-reload.ts
import { watch } from "fs";
import { exec } from "child_process";

watch("./src", { recursive: true }, (eventType, filename) => {
  console.log(`File changed: ${filename}`);
  
  // Rebuild WASM component
  exec("npm run build", (error, stdout, stderr) => {
    if (error) {
      console.error(`Build failed: ${stderr}`);
      return;
    }
    
    console.log("WASM component rebuilt successfully");
  });
});
Run during development:
cd traildepot/wasm
npm run dev
TrailBase automatically reloads WASM components when the file changes.

Configuration

Runtime Settings

Configure the WASM runtime in traildepot/config.textproto:
server {
  # Number of JavaScript isolates (default: number of CPUs)
  runtime_threads: 4
}

Filesystem Access

Grant sandboxed filesystem access:
# Start with filesystem root
trail run --runtime-root-fs /path/to/data
Access files from WASM:
HttpHandler.get("/files/read", async () => {
  // Read from sandboxed root
  const content = await readFile("/data/config.json");
  return content;
});

Component Management

List Available Components

trail components list
Output:
Available first-party components:
- trailbase-guest-ts (TypeScript/JavaScript runtime)
- trailbase-guest-rust (Rust runtime)

Install Component

# Install from registry
trail components add trailbase-guest-ts

# Install from file
trail components add ./component.wasm

# Install from URL
trail components add https://example.com/component.wasm

List Installed Components

trail components installed

Remove Component

trail components remove trailbase-guest-ts

Update Components

# Update all installed first-party components
trail components update

Debugging

Console Logging

console.log("Info message");
console.warn("Warning message");
console.error("Error message");
Logs appear in TrailBase’s output:
[WASM] Info message
[WASM] Warning message
[WASM] Error message

Error Handling

HttpHandler.get("/api/risky", async (req) => {
  try {
    const data = await query("SELECT * FROM nonexistent_table", []);
    return HttpResponse.json(data);
  } catch (error) {
    console.error("Query failed:", error);
    return HttpResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
});

Performance Monitoring

HttpHandler.get("/api/slow", async () => {
  const start = Date.now();
  
  const result = await query("SELECT * FROM large_table", []);
  
  const duration = Date.now() - start;
  console.log(`Query took ${duration}ms`);
  
  return HttpResponse.json({ duration, rows: result.length });
});

Best Practices

1

Keep Handlers Lightweight

WASM has overhead for each invocation. For simple operations, use Record APIs directly.Use WASM for:
  • Complex business logic
  • Custom data transformations
  • Third-party API integrations
  • Scheduled background tasks
Avoid WASM for:
  • Simple CRUD operations (use Record APIs)
  • Static content (use --public-dir)
2

Use Parameterized Queries

Always use parameterized queries to prevent SQL injection:
// Good
await query("SELECT * FROM todos WHERE user = ?", [userId]);

// Bad
await query(`SELECT * FROM todos WHERE user = ${userId}`);
3

Handle Errors Gracefully

Catch errors and return meaningful HTTP responses:
HttpHandler.post("/api/resource", async (req) => {
  try {
    const data = req.json();
    // Process...
    return HttpResponse.json({ success: true });
  } catch (error) {
    console.error("Error:", error);
    return HttpResponse.json(
      { error: String(error) },
      { status: 400 }
    );
  }
});
4

Optimize Database Queries

Use indexes and limit result sets:
// Create index in migration
// CREATE INDEX _todos__user_index ON todos(user);

// Use LIMIT in queries
await query(
  "SELECT * FROM todos WHERE user = ? ORDER BY created_at DESC LIMIT 100",
  [userId]
);

Next Steps

First App

Build your first application

Database Setup

Design efficient schemas

Realtime Subscriptions

Combine WASM with live updates

CLI Usage

Manage WASM components

Examples

Build docs developers (and LLMs) love