Provider Scenes

A provider scene is a provider authored as data. Instead of writing a TypeScript module, you create a scene that contains a handful of typed rows — and Daslab loads it as a provider at boot, indistinguishable from a hand-coded one.

The point: providers stop being source code only we can write and start being scenes you can apply, version, fork, and (eventually) publish.

A provider is mostly data

The provider model from Providers is fine when there are ten of them. At fifty, the boilerplate adds up. At five hundred — the order of magnitude we'd land on if every user could publish their own — it stops being a code-shaped problem and starts being a content-shaped one.

A provider is mostly data: identity, asset type shapes, tool schemas, an execution strategy. The actual code part is usually one or two functions. So we carved a kernel that lets a scene express all the data parts, plus a small standardised execution path for the code parts.

Three asset types describe a provider

All three are ordinary typed rows:

  • daslab/provider_def — top-level identity. One per provider scene. Holds id, name, icon, color, logo, visibility, connection text. The presence of this asset is what makes the scene a provider.
  • daslab/asset_type_def — a typed shape the provider declares. Mark exactly one of them isAccountType: true — that's the credential-bearing account asset type (or a no-auth marker for credential-free providers).
  • daslab/tool_def — a typed callable. Has a name, description, JSON Schema for inputs, and an impl describing how to run it.

Each row's fields JSONB holds the actual config; there are no schema changes needed to add a new provider.

Tool implementation

Two impl kinds for now:

  • http_call — declarative HTTP recipe (method, url, query, headers, body, output.path). Template values pull from the tool's input and the account's credential blob. A tiny interpreter turns the recipe into a fetch() call. No code to write or run.
  • code — JavaScript source that reads from ctx.input / ctx.credential / ctx.fetch and returns a value. The source lives in a real .js file alongside the blueprint and is inlined at apply time via $file, and runs isolated with hard timeouts.

A minimal example

A scene blueprint with one provider, one account asset type, two code tools — the actual blueprint we use to ship Techmeme:

{
  "id": "scn_kernel_techmeme",
  "parentId": "scn_daslab_assets",
  "name": "Techmeme Provider",
  "icon": "newspaper",
  "tint": "#2D6A2E",
  "assets": [
    {
      "id": "provider", "type": "daslab/provider_def", "name": "Techmeme",
      "fields": {
        "providerId": "techmeme",
        "visibility": "public",
        "requiresConnection": true
      }
    },
    {
      "id": "account", "type": "daslab/asset_type_def", "name": "Techmeme Account",
      "fields": {
        "typeId": "account",
        "isAccountType": true,
        "auth": { "type": "none" },
        "cardinality": "singleton"
      }
    },
    {
      "id": "tool_feed", "type": "daslab/tool_def", "name": "techmeme_feed",
      "fields": {
        "toolName": "techmeme_feed",
        "description": "Get today's top tech news stories from Techmeme.",
        "readOnly": true,
        "inputSchema": {
          "type": "object",
          "properties": {
            "date":  { "type": "string", "description": "YYYY-MM-DD; defaults to today." },
            "limit": { "type": "number", "description": "Max stories (default 20)." }
          }
        },
        "impl": {
          "kind": "code",
          "source": { "$file": "./techmeme/feed.js" },
          "timeoutMs": 12000
        }
      }
    }
  ]
}
./techmeme/feed.js is plain JS — no escaping, no JSON quoting, linted and diffed like any other file:
const API_BASE = "https://techmemer.usemodule.com";
const date = ctx.input.date;
const limit = (ctx.input.limit && Number(ctx.input.limit)) || 20;
const url = date ? API_BASE + "/?date=" + encodeURIComponent(date) : API_BASE + "/";
const res = await ctx.fetch(url);
if (!res.ok) throw new Error("Techmeme API error: " + res.status);
const data = await res.json();
// …reshape and return as string

Apply and load

The flow:

  1. Author the blueprint + source files.
  2. Apply it: daslab blueprint apply ./scenes/techmeme.blueprint.json. The applier resolves $file refs, writes the asset rows into Postgres under your chosen parent scene.
  3. On server boot, the registry scans for scenes containing a daslab/provider_def asset and registers each as a provider.

From there on, the provider is indistinguishable from a TS-coded one: tools show up in the agent loop, the account asset appears in the iOS add-asset sheet, tool calls dispatch normally.

Scoping

A provider scene's tools become available in a consumer scene only when that consumer scene has the provider's account asset linked. This matches how all other providers in Daslab work — provider lives where you add it. Set requiresConnection: true on the provider_def to get this behaviour; keylessDefault: true (auto-inferred for auth: "none") lets the user materialize a virtual account by tapping Add in iOS.

This avoids the trap where authless providers bolt their tools onto every chat's prompt globally — bloat that hurts time-to-first-token across the workspace.

Where they live

Provider scenes can live anywhere in your scene tree. Daslab maintains a built-in world called Daslab Assets that holds the platform-published provider scenes (currently Techmeme Provider and npm Provider). Other scenes — yours, your team's, public-marketplace ones — can apply blueprints under that world or under any other parent.

The world boundary is the visibility boundary: who can see a scene, who can adopt its assets. Same rules as everywhere else in Daslab.

What's the same as a code-defined provider

  • Same registration as any other provider.
  • Same auth flows (api_key, none).
  • Same iOS UX (add asset, browse, tool calls).
  • Same MCP exposure, same OTel tracing, same metrics.

What's different

  • Source of truth is assets rows, not code.
  • Updates apply, they don't hot-reload — changes to a $file-referenced source take effect on the next apply.
  • One account asset type per scene — a single gating account type; child types via parent: aren't supported.
  • No OAuth — provider scenes carry api_key and none auth only.

Two production providers run this way

Two providers run in production today entirely from DB blueprints: Techmeme (authless, two code tools) and npm (api_key credential carrier, zero tools). Both replaced their previous hand-coded TS modules with no behavioural difference to users.

What's next

  • Providers — the code-defined model, still the majority.
  • Blueprints — scenes as templates; the same applier handles provider scenes and regular scenes.
  • Cells — the typed-row substrate everything sits on top of.

Updated 2026-05-15