VisibleBase
Reference

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:

  1. initialize the default database and runtime env
  2. register services such as text() and stream()
  3. describe execution with default() and match()
  4. expose the runtime through serve(), handleRequest(), or router()

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

Request enters BaseIn HTTP mode Base verifies user_token, reads product_id, and identifies the target service first.
Build the final ctx.queryBase starts from the client query, then merges the object from default() as a fallback.
Resolve model and routeBase resolves the model from ctx.query.model, then walks match() handlers in order.
Run hooks and handlerbefore → 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(), or invoke() 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 tables
  • base.table(name) supports select(), insert(), update(), and delete()
  • 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/models table objects should you pass Runtime schema to the first init() 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() returns UIMessage
  • base.stream() returns Response
  • base.image() returns UIMessage
  • base.video() returns UIMessage

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:

  1. read the client query
  2. merge the object returned by default() as fallback
  3. 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

On this page