Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/trailbaseio/trailbase/llms.txt

Use this file to discover all available pages before exploring further.

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