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:
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:
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
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 }
);
}
});
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
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)
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 } ` );
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 }
);
}
});
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