Base
Base initialization, service registration, query fallback, hooks, and HTTP integration.
Base is the server-side runtime entry of VisibleBase.
You use it for four things:
- initialize the default database and runtime env
- register services such as
text()andstream() - describe execution with
default()andmatch() - expose the runtime through
serve(),handleRequest(), orrouter()
Minimal usable example
import { Base } from "@visiblebase/base";
const base = new Base();
base.text()
.default({
model: "gpt-5.4",
temperature: 0.2,
})
.handle(async (ctx) => {
return {
id: crypto.randomUUID(),
role: "assistant",
parts: [
{
type: "text",
text: String(ctx.query.prompt ?? ""),
state: "done",
},
],
};
});
await base.serve({
host: "127.0.0.1",
port: 3001,
});Two important semantics here:
default()fills query fallback, not “the model registry”- handlers always read the final request shape from
ctx.query
The execution flow inside Base
user_token, reads product_id, and identifies the target service first.ctx.queryBase starts from the client query, then merges the object from default() as a fallback.ctx.query.model, then walks match() handlers in order.before → handler → after, and onError when execution fails.Initialization
Automatic initialization
const base = new Base();Default behavior:
- database path:
./.base/visiblebase.sqlite - default Runtime tables:
products,models - first boot fills
VISIBLEBASE_ADMIN_SECRET_KEY - first boot fills
VISIBLEBASE_TOKEN_SIGNING_KEY - the first
serve(),handleRequest(),models(), orinvoke()call initializes Base automatically
Explicit database or .env
const base = new Base({
database_url: process.env.VISIBLEBASE_DATABASE_URL,
env_file_path: ".env.local",
});Business schema and table()
const base = new Base({
schema: {
notes,
},
});
await base.table("notes").insert({
id: "note_1",
title: "First note",
});
const notes = await base.table("notes").select();Key points:
new Base({ schema })registers your business tables, such as task, usage, order, or product data tablesbase.table(name)supportsselect(),insert(),update(), anddelete()- the first
table()operation initializes Base's default Runtime tables and your business tables automatically - only if you need to replace Base's Runtime-owned
products/modelstable objects should you pass Runtime schema to the firstinit()call
Reading models and services
base.models()
const models = await base.models();This returns the model directory currently exposed by Base.
base.services()
const services = base.services();This returns service names registered in the current process. It does not scan the database automatically.
Registering services
Built-in shortcuts
VisibleBase includes these shortcuts:
base.text()base.stream()base.image()base.video()base.tts()base.asr()
All of them are equivalent to base.service(name).
The built-in shortcuts also constrain handler return types:
base.text()returnsUIMessagebase.stream()returnsResponsebase.image()returnsUIMessagebase.video()returnsUIMessage
If you need a fully custom result shape, register a custom service with base.service(name) and call it through client.invoke<T>().
Custom service
base.service("rewrite")
.default({
model: "gpt-5.4",
tone: "formal",
})
.handle(async (ctx) => {
return {
ok: true,
query: ctx.query,
model: ctx.model.model_id,
};
});default()
default() defines query fallback:
base.text().default({
model: "gpt-5.4",
temperature: 0.2,
});It can also be a function so the final query depends on the current user or product:
base.text().default((ctx) => {
if (ctx.user.metadata?.plan === "pro") {
return {
model: "gpt-5.4",
temperature: 0.4,
};
}
return {
model: "gpt-4.1-mini",
temperature: 0.2,
};
});Merge order:
- read the client query
- merge the object returned by
default()as fallback - resolve the final model from
ctx.query.model
If ctx.query.model is still missing, Base returns 422.
match() and handle()
match()
match() chooses the real execution path:
base.text()
.default({ model: "gpt-5.4" })
.match((ctx) => ctx.model.provider === "openai", async (ctx) => {
return openai.responses.create({
model: ctx.model.upstream_model,
input: ctx.query.prompt,
});
})
.match((ctx) => ctx.model.provider === "anthropic", async (ctx) => {
return anthropic.messages.create({
model: ctx.model.upstream_model,
messages: [{ role: "user", content: String(ctx.query.prompt ?? "") }],
});
});Rules:
match()runs in registration order- the first match wins
- if nothing matches, Base returns
422
handle()
handle() is the simplest fallback form. It is equivalent to an always-matching handler:
base.text()
.default({ model: "gpt-5.4" })
.handle(async (ctx) => {
return {
id: crypto.randomUUID(),
role: "assistant",
parts: [
{
type: "text",
text: String(ctx.query.prompt ?? ""),
state: "done",
},
],
};
});Hooks
Global hooks with base.all()
base.all().before(async (ctx) => {
await quotaService.check({
product_id: ctx.product.product_id,
user_id: ctx.user.user_id,
model: ctx.model.model_id,
});
});Service-specific hooks
base.text().after(async (ctx, result) => {
await usageStore.record({
product_id: ctx.product.product_id,
user_id: ctx.user.user_id,
service: ctx.service,
model: ctx.model.model_id,
result,
});
});Keep hooks focused on quota, billing, logging, or audit work. Do not move your whole business system into Base.
How handler errors are returned
When a handler throws, Base runs service-level and global onError hooks first, then converts the error into an HTTP response. The default status is 500; if the error object has statusCode, Base returns that status.
import type { ErrorWithStatus } from "@visiblebase/base";
base.text().handle(async () => {
const error = new Error("quota exceeded") as ErrorWithStatus;
error.statusCode = 429;
throw error;
});Use onError for logging, cleanup, and monitoring. It does not swallow the original error.
HTTP integration
serve()
await base.serve({
host: "127.0.0.1",
port: 3001,
});Use this when you want the fastest standalone HTTP server.
handleRequest()
export default {
fetch(request: Request) {
return base.handleRequest(request);
},
};Use this when you already have a Fetch-style runtime.
router()
app.route("/base", base.router());Use this when you want a mountable router helper for frameworks such as Hono-style servers.
You also do not need to call init() first here.
router()itself only returns a mountable runtime object- the first request that enters that router goes through
handleRequest(), which initializes Base automatically