Tutorials
9 project-based tutorials. Click any row to expand.
Blog with ISRBeginnerMarkdown blog with incremental static regeneration.20 min
ISR lets you serve static HTML but re-generate pages in the background when content changes. Create a posts/ directory with .md files and expose them through a dynamic route.
// bext.config.ts
export default {
isr: {
revalidate: 60, // seconds
paths: ["/blog/*"],
},
};The page component reads markdown at build time. On the first request after the revalidate window, bext serves the stale page instantly and regenerates in the background.
// src/app/blog/[slug]/page.tsx
import { readFile } from "fs/promises";
import { marked } from "marked";
export async function generateStaticParams() {
const files = await readdir("posts");
return files.map((f) => ({ slug: f.replace(".md", "") }));
}
export default async function Post({ params }: { params: { slug: string } }) {
const md = await readFile(`posts/${params.slug}.md`, "utf-8");
return <article dangerouslySetInnerHTML={{ __html: marked(md) }} />;
}Deploy and request any post. The first hit triggers a build, subsequent requests serve the cached page. After 60 seconds the next visitor triggers a background regeneration.
JWT AuthIntermediateLogin flow with refresh tokens and protected routes.25 min
bext middleware runs before the route handler, making it the right place for auth checks. Start by creating a login endpoint that issues a short-lived access token and a long-lived refresh token.
// src/app/api/auth/login/route.ts
import { sign } from "jsonwebtoken";
export async function POST(req: Request) {
const { email, password } = await req.json();
const user = await db.verifyCredentials(email, password);
if (!user) return new Response("Unauthorized", { status: 401 });
const access = sign({ sub: user.id }, process.env.JWT_SECRET!, { expiresIn: "15m" });
const refresh = sign({ sub: user.id, type: "refresh" }, process.env.JWT_SECRET!, {
expiresIn: "7d",
});
return Response.json({ access, refresh });
}Create middleware that verifies the token on every request to protected routes. Expired access tokens return 401 so the client can call /api/auth/refresh.
// src/middleware.ts
import { verify } from "jsonwebtoken";
export default function middleware(req: Request) {
if (!req.url.includes("/api/protected")) return;
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) return new Response("Missing token", { status: 401 });
try {
verify(token, process.env.JWT_SECRET!);
} catch {
return new Response("Invalid token", { status: 401 });
}
}The refresh endpoint issues a new access token when the client sends a valid refresh token. Store revoked refresh tokens in Redis for instant invalidation.
Realtime DashboardIntermediateLive metrics via Server-Sent Events.30 min
bext has built-in SSE support through its realtime feature flag. Create a route that streams metric snapshots to connected clients every second.
// bext.config.ts
export default {
features: ["realtime"],
};// src/app/api/metrics/stream/route.ts
export function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
const interval = setInterval(async () => {
const metrics = {
cpu: os.loadavg()[0],
mem: process.memoryUsage().heapUsed,
rps: await redis.get("rps:current"),
timestamp: Date.now(),
};
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(metrics)}\n\n`)
);
}, 1000);
// Clean up when client disconnects
return () => clearInterval(interval);
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}On the client, connect with EventSource and update your chart on each message. bext handles connection keep-alive and automatic reconnection at the infrastructure level.
// src/app/dashboard/page.tsx
"use client";
import { useEffect, useState } from "react";
export default function Dashboard() {
const [metrics, setMetrics] = useState<any>(null);
useEffect(() => {
const es = new EventSource("/api/metrics/stream");
es.onmessage = (e) => setMetrics(JSON.parse(e.data));
return () => es.close();
}, []);
if (!metrics) return <div>Connecting...</div>;
return (
<div>
<span>CPU: {metrics.cpu.toFixed(2)}</span>
<span>Heap: {(metrics.mem / 1e6).toFixed(0)} MB</span>
<span>RPS: {metrics.rps}</span>
</div>
);
}WASM PluginAdvancedImage processor in Rust compiled to WASM.35 min
bext loads .wasm files from the plugins/ directory and exposes them as callable functions. Write a Rust crate that resizes images, then compile it to wasm32-wasi.
// plugins/image-resize/src/lib.rs
use image::{load_from_memory, ImageFormat};
use std::io::Cursor;
#[no_mangle]
pub extern "C" fn resize(input: *const u8, len: usize, width: u32, height: u32) -> *const u8 {
let bytes = unsafe { std::slice::from_raw_parts(input, len) };
let img = load_from_memory(bytes).unwrap();
let resized = img.resize_exact(width, height, image::imageops::Lanczos3);
let mut buf = Cursor::new(Vec::new());
resized.write_to(&mut buf, ImageFormat::WebP).unwrap();
let result = buf.into_inner();
let ptr = result.as_ptr();
std::mem::forget(result);
ptr
}Build with cargo build --target wasm32-wasi --release and copy the .wasm file into your plugins directory. Register it in your bext config.
// bext.config.ts
export default {
plugins: [
{
name: "image-resize",
runtime: "wasm",
path: "./plugins/image-resize/target/wasm32-wasi/release/image_resize.wasm",
},
],
};Call the plugin from any route handler. bext instantiates the WASM module once and reuses it across requests with near-native performance.
// src/app/api/resize/route.ts
import { callPlugin } from "bext/plugins";
export async function POST(req: Request) {
const body = await req.arrayBuffer();
const width = Number(req.headers.get("X-Width") || 800);
const height = Number(req.headers.get("X-Height") || 600);
const resized = await callPlugin("image-resize", "resize", {
input: new Uint8Array(body),
width,
height,
});
return new Response(resized, {
headers: { "Content-Type": "image/webp" },
});
}Next.js MigrationIntermediateMove an e-commerce storefront to bext.25 min
bext supports Next.js conventions out of the box when you enable compat mode. Start by updating your config to point at your existing source directory.
// bext.config.ts
export default {
compat: "full", // enable Next.js layout/page conventions
srcDir: "./src",
env: {
NEXT_PUBLIC_API_URL: process.env.API_URL,
},
};Replace next/image with standard <img> tags or bext's built-in image optimization. Replace next/link with plain <a> tags — bext handles client-side navigation automatically for internal links.
// Before (Next.js)
import Image from "next/image";
import Link from "next/link";
export default function ProductCard({ product }) {
return (
<Link href={`/products/${product.slug}`}>
<Image src={product.image} width={400} height={300} alt={product.name} />
<h3>{product.name}</h3>
</Link>
);
}
// After (bext)
export default function ProductCard({ product }) {
return (
<a href={`/products/${product.slug}`}>
<img src={product.image} width={400} height={300} alt={product.name} />
<h3>{product.name}</h3>
</a>
);
}Remove next.config.js and next from your dependencies. Run bext dev — your app should work with the existing file structure. API routes, layouts, and dynamic segments carry over without changes.
Multi-tenant RoutingAdvancedSubdomain routing with per-tenant databases.20 min
Use bext middleware to extract the tenant from the hostname and attach it to the request context. Each tenant gets its own database connection string stored in a central lookup table.
// src/middleware.ts
export default async function middleware(req: Request) {
const host = req.headers.get("host") || "";
const tenant = host.split(".")[0]; // acme.example.com -> acme
const config = await redis.hget("tenants", tenant);
if (!config) return new Response("Unknown tenant", { status: 404 });
const parsed = JSON.parse(config);
// Attach tenant context via headers for downstream handlers
const headers = new Headers(req.headers);
headers.set("X-Tenant-Id", parsed.id);
headers.set("X-Tenant-DB", parsed.databaseUrl);
return new Request(req.url, { headers, method: req.method, body: req.body });
}In your route handlers, read the tenant context from headers and connect to the correct database. Use connection pooling per tenant to avoid exhausting connections.
// src/lib/db.ts
import { Pool } from "pg";
const pools = new Map<string, Pool>();
export function getPool(req: Request): Pool {
const dbUrl = req.headers.get("X-Tenant-DB")!;
if (!pools.has(dbUrl)) {
pools.set(dbUrl, new Pool({ connectionString: dbUrl, max: 10 }));
}
return pools.get(dbUrl)!;
}
// src/app/api/products/route.ts
import { getPool } from "@/lib/db";
export async function GET(req: Request) {
const pool = getPool(req);
const { rows } = await pool.query("SELECT * FROM products ORDER BY created_at DESC");
return Response.json(rows);
}Register new tenants by adding entries to the Redis hash. bext routes requests to the correct handler with no restarts needed — wildcard DNS and auto-TLS handle the rest.
Auto TLSBeginnerHTTPS certificates with zero config.15 min
bext provisions TLS certificates automatically on first request using ACME (Let's Encrypt). The default config has TLS enabled — you only need to point your DNS and bext handles the rest.
// bext.config.ts
export default {
tls: {
auto: true, // default: true
redirectHttp: true, // 301 redirect HTTP -> HTTPS
domains: [
"example.com",
"www.example.com",
],
},
};For custom domains (e.g. in a SaaS), add them dynamically. bext provisions certificates on demand and caches them to disk.
// src/app/api/domains/route.ts
import { tls } from "bext/runtime";
export async function POST(req: Request) {
const { domain } = await req.json();
// Validate domain ownership (e.g. check DNS CNAME)
const cname = await dns.resolveCname(domain);
if (!cname.includes("app.example.com")) {
return new Response("CNAME not configured", { status: 400 });
}
// Tell bext to provision a cert for this domain
await tls.addDomain(domain);
return Response.json({ status: "provisioning", domain });
}Certificates renew automatically 30 days before expiry. Check certificate status at /_bext/tls in development mode. In production, bext logs renewal events to stdout.
Built-in Package ManagerBeginnerReplace npm/yarn/pnpm with bext's built-in Rust-native package manager.10 min
bext ships with a full npm-compatible package manager powered by the utoo toolchain. No Node.js or npm install required — everything runs from the bext binary.
# First, download the package manager companion bext download ut # Install dependencies from an existing project bext install # Add a new dependency bext install lodash # Add a dev dependency bext install -D typescript # Run scripts from package.json bext ut run dev # Execute a package binary (like npx) bext x prettier --write .
The package manager reads and writes standard package-lock.json files, so switching from npm is seamless. Your existing lockfile works as-is.
# Full command reference bext install [pkg...] # Install deps (aliases: bext i, bext add) bext uninstall <pkg...> # Remove deps (alias: bext un) bext x <cmd> # Run package binary (alias: bext execute) bext link [pkg] # Link packages (alias: bext ln) bext view <pkg> # View package info bext publish # Publish to registry # All utoo PM commands via pass-through bext ut run <script> # Run package.json scripts bext ut init # Create package.json bext ut list <pkg> # List dependencies bext ut config list # Show PM configuration bext ut clean # Clean package cache
For the bundler, use bext pack. It runs Turbopack under the hood for Rust-native build performance.
# Download the bundler companion bext download ut/pack # Production build bext pack --mode build --project-path . # Dev server with watch mode bext pack --mode dev --watch true
Durable WorkflowsAdvancedBackground jobs with retries and checkpoints.30 min
Durable workflows survive process restarts. Define a workflow as an async generator — each yield is a checkpoint that bext persists. If the process crashes, the workflow resumes from the last checkpoint.
// src/workflows/process-order.ts
import { workflow } from "bext/workflows";
export const processOrder = workflow("process-order", async function* (orderId: string) {
// Step 1: Validate inventory
const items = yield* validateInventory(orderId);
// Step 2: Charge payment (idempotent)
const charge = yield* chargePayment(orderId, items.total);
if (!charge.ok) throw new Error("Payment failed");
// Step 3: Ship order
yield* createShipment(orderId, items);
// Step 4: Send confirmation email
yield* sendConfirmation(orderId, charge.receiptUrl);
return { orderId, status: "completed" };
});Trigger the workflow from any route handler. bext returns a workflow ID you can poll for status. Each step retries up to 3 times by default with exponential backoff.
// src/app/api/orders/route.ts
import { processOrder } from "@/workflows/process-order";
export async function POST(req: Request) {
const { orderId } = await req.json();
const run = await processOrder.start(orderId);
return Response.json({ workflowId: run.id, status: run.status });
}
// src/app/api/orders/[id]/status/route.ts
import { processOrder } from "@/workflows/process-order";
export async function GET(_req: Request, { params }: { params: { id: string } }) {
const run = await processOrder.get(params.id);
return Response.json({
status: run.status, // "running" | "completed" | "failed"
currentStep: run.step, // last completed checkpoint index
result: run.result, // available when completed
});
}Configure retry behavior and timeouts per workflow. Failed workflows move to a dead-letter queue you can inspect and replay.
// Override defaults per workflow
export const processOrder = workflow("process-order", handler, {
retries: 5,
backoff: "exponential",
timeout: "10m",
onFailure: "dead-letter",
});