00Get Started
There are two ways to Bassline:
1. Use our desktop application, Homebass. (We recommend doing this!)
2. Use Bassline as a library.
If you are trying to use Bassline you should probably be doing so through HomeBass. If you have an existing application you want to speak "Bassline", use it as a library.
HomeBass
Our intuitive visual environment for learning, creating, and building with Bassline. It allows programmers to use a canvas to interact with, develop, and debug running Basslines. The Bassline team uses HomeBass for all of our internal development!
Production app coming soon!
Library
The core JS package is a minimal implementation of Bassline:
pnpm install @bassline/coreThe core is intentionally focused and small, so if it's missing something, check out the surrounding packages with ready-to-use resource implementations.
Hello World
First, ensure you have the required packages:
pnpm install @bassline/core @bassline/node @bassline/remoteThen simply define some routes to expose and connect to peers.
1import { createCells, routes } from '@bassline/core'
2import { createWsServer } from '@bassline/node'
3import { createRemote } from '@bassline/remote'
4
5// Server: expose a cell over WebSocket
6const cells = createCells()
7await cells.put({ path: '/score' }, { lattice: 'maxNumber' })
8
9const server = createWsServer()
10await server.put({ path: '/9111' }, { kit: routes({ cells }) })
11
12// Client: connect and sync
13const remote = createRemote()
14await remote.put({ path: '/server' }, { url: 'ws://localhost:9111' })
15
16// Write from client; lattice merges automatically
17await remote.put({ path: '/server/proxy/cells/score/value' }, 50)
18await remote.put({ path: '/server/proxy/cells/score/value' }, 30) // still 50, max wins
19
20// Read synced value
21const score = await remote.get({ path: '/server/proxy/cells/score/value' })
22console.log(score.body) // 5001Core Concepts
Semantic Addressing
1{ path: '/store/users/alice' } // Stored data
2{ path: '/cells/counter' } // Reactive values
3{ path: '/types/user' } // Type definitions
4{ path: '/propagators/sum' } // Computations
5{ path: '/links/to/types/user' } // Query: what references user?Everything in bassline is semantically addressed. This means that resources don't have a single concrete identity, instead they exist relative to other resources.
Routers are resources that can delegate interactions to by dispatching based on information in the headers. Paths are just a common example of semantic routing information. This lets us talk about resources as though we were interacting with a file-system.
You are free to choose whatever dispatch logic makes sense in your domain.
▶Dispatch and router source
1// Logic for splitting & delegating path info
2const splitPath = path => {
3 const [segment, ...rest] = (path ?? '/').split('/').filter(Boolean)
4 return [segment, rest.length ? '/' + rest.join('/') : '/']
5}
6const pathRoot = headers => {
7 const [segment, remaining] = splitPath(headers.path)
8 return [segment, { ...headers, path: remaining }]
9}
10// Generic resource router, pathRoot being the default dispatch fn
11function routes(map, dispatchFn = pathRoot) {
12 const dispatch = disp(map, dispatchFn)
13 return resource({
14 get: h => dispatch('get', h),
15 put: (h, b) => dispatch('put', h, b),
16 })
17}Late-binding With Kit
1import { resource, routes } from '@bassline/core'
2
3// A resource that uses its kit to access other resources
4const worker = resource({
5 put: async (h, task) => {
6 // Access other resources through h.kit
7 const config = await h.kit.get({ path: '/store/config' })
8 const result = processTask(task, config.body)
9 await h.kit.put({ path: '/cells/results' }, result)
10 return { headers: {}, body: { done: true } }
11 }
12})
13
14// Compose resources: the worker can now access store and cells
15const app = routes({ worker, store, cells })A kit is simply a resource passed via headers.
When a resource needs flexible or dynamic access to other resources that it doesn't know about ahead of time, it can do so with a kit. This makes kits a form of dependency injection for resources.
So when a resource delegates to another resource, it can provide an alternative kit to change the effective "world" a resource is interacting in.
▶Note about kits over the wire
Because resources don't have concrete identities, kits can work over network boundaries. When interacting with a kit that is located on an external machine, we can simply proxy interactions through another local resource we create.
Operations
1// Get a resource
2const response = await kit.get({ path: '/cells/counter' })
3// → { headers: {}, body: { value: 42 } }
4
5// Put a resource
6await kit.put({ path: '/cells/counter' }, { value: 43 })
7
8// Every response has the same shape: { headers, body }
9// headers: metadata (type, etag, links)
10// body: the actual dataYou interact with all resources using either get or put.
Because resources have a uniform interface, we write highly generic and flexible code for all resource implementations.
02Structures
Resource structures describe behavioral properties of a resource.
In other words, it's the semantics of a resource.
Building in bassline is finding the right structure that fits the shape of your problem.
Doing so with style is finding the right structure for your problem by rearranging existing structures.
This section covers a number of useful structures we have built for you.
Routers
1import { resource, routes } from '@bassline/core'
2
3const worker = resource({...});
4const store = resource({...});
5const cells = resource({...});
6
7// Composes and routes resources
8const app = routes({ worker, store, cells })
9
10// This one describes itself, so it's a bassline
11const basslineApp = routes({
12 '': resource({
13 get: selfDescriptionLogic,
14 }),
15 worker,
16 store,
17 cells
18})A resource that can delegate to other resources, based on information in the header of interactions.
If a router can describe itself and other resources, we call it a Bassline. Since it establishes a foundation that others can interact with.
Check out the docs here for more details.
Cells
1// Create a cell with a lattice type
2await kit.put({ path: '/cells/score' }, { lattice: 'maxNumber' })
3
4// Write values
5await kit.put({ path: '/cells/score/value' }, 50) // score is 50
6await kit.put({ path: '/cells/score/value' }, 30) // still 50 (max wins)
7await kit.put({ path: '/cells/score/value' }, 75) // now 75
8
9// Concurrent writes from different nodes finish with the same result.A resource with lattice semantics. They behave like CRDTs, meaning concurrent writes from different sources are handled gracefully.
Example Lattices
maxNumberMonotonically increasing (scores, counts, versions)
minNumberMonotonically decreasing (countdowns, deadlines)
setUnionAccumulating unique items (tags, seen IDs)
▶ Beyond Distributed State
Cells are not just useful for distributed state, because another way to view a cells purpose is maintaining a constraint on the system. The merge function allows a cell to gradually learn and enforce constraints on the possible states it can exist in.
When those constraints are not met, the cell is in a state of contradiction. Meaning that two sources of information cannot coexist, which itself is more information than we started with, meaning that we can spread this information to other resources, to attempt to resolve the contradiction, using some heuristic.
This is useful in a distributed adversarial environment. This feature can help maintain a consistent view of a network, because we can explicitly discover when a peer is providing us bad information.
We could then share this contradiction with peers, as the two contradictory claims are all we need to describe the issue to another peer, without replaying all history or interactions.
Propagators
1// Create cells for temperature conversion
2await kit.put({ path: '/cells/celsius' }, { lattice: 'lww' })
3await kit.put({ path: '/cells/fahrenheit' }, { lattice: 'lww' })
4
5// Wire them together with a propagator
6await kit.put({ path: '/propagators/c-to-f' }, {
7 inputs: ['/cells/celsius'],
8 output: '/cells/fahrenheit',
9 fn: '/fn/c-to-f'
10})
11
12// Set celsius, fahrenheit updates automatically
13await kit.put({ path: '/cells/celsius/value' }, 100)
14const f = await kit.get({ path: '/cells/fahrenheit/value' })
15// f.body === 212Propagators connect to cells and automatically write outputs when inputs change. They use named functions called handlers to transform data.
Example Handlers
sum, product, min, maxReducers for numeric values
map, filter, pickTransform and filter data
pipe, fork, convergeCompose handlers together
when, ifElse, condConditional logic
03Node
Node.js resources for HTTP, WebSocket, and file system access.
HTTP
1import { createHttpServer } from '@bassline/node'
2
3const http = createHttpServer()
4
5// Start server on port 9111 with your kit
6await http.put({ path: '/9111' }, { kit })
7
8// Server proxies GET/PUT requests to your kit
9// GET http://localhost:9111/cells/counter
10// → kit.get({ path: '/cells/counter' })
11
12// Check server status
13const status = await http.get({ path: '/9111' })
14// → { port: 9111, connections: 3, uptime: 12345 }
15
16// Stop the server
17await http.put({ path: '/9111/stop' }, {})Creates HTTP servers that proxy requests via a kit.
Bassline http servers work the same as any other with the added benefits of being a resource.
External systems can interact with Bassline resources without requiring an implementation.
WebSocket
1import { createWsServer } from '@bassline/node'
2
3const ws = createWsServer()
4
5// Start WebSocket server
6await ws.put({ path: '/9111' }, { kit })
7
8// Clients send JSON messages
9// { "path": "/cells/counter" }
10// { "path": "/cells/counter/value", "body": 42 }
11
12// Broadcast to all connected clients
13await ws.put({ path: '/9111/broadcast' }, {
14 type: 'update',
15 data: { counter: 100 }
16})WebSocket servers for real-time bidirectional streams.
These are useful in p2p scenarios and again, are just resources, so the other end doesn't need to be running Bassline.
File Store
1import { createFileStore } from '@bassline/node'
2
3const store = createFileStore('./data')
4
5// Read files
6const config = await store.get({ path: '/config.json' })
7// → { headers: {}, body: { setting: 'value' } }
8
9// Write files
10await store.put({ path: '/users/alice.json' }, {
11 name: 'Alice',
12 role: 'admin'
13})
14
15// List directory contents
16const files = await store.get({ path: '/users' })
17// → { body: ['alice.json', 'bob.json'] }A kind of store that internally uses a file system.
Extremely useful for exposing files and their data as resources.
Remote
1import { createRemote } from '@bassline/remote'
2
3const remote = createRemote()
4
5// Connect to a remote Bassline server
6await remote.put({ path: '/server1' }, {
7 url: 'ws://localhost:9111'
8})
9
10// Check connection status
11const status = await remote.get({ path: '/server1' })
12// → { status: 'connected', url: 'ws://localhost:9111' }
13
14// Proxy requests through the connection
15const data = await remote.get({ path: '/server1/proxy/cells/counter' })
16await remote.put({ path: '/server1/proxy/cells/counter/value' }, 42)
17
18// Close connection when done
19await remote.put({ path: '/server1/close' }, {})Exposes a websocket client for Bassline systems, allowing connections to remote peers via resource interactions.
Manage multiple connections and proxy requests through them to access remote resources.
04TCL
We have a TCL-ish language. We say ish because there are some slight differences, but it's mainly the same.
▶Why TCL?
TCL makes programs easily serializable because everything is a string, and also fits nicely into our extreme semantic flexibility, since all values are given meaning by the command running.
We built this for scripting Bassline. It supports variables, namespaces, procedures, and state serialization.
1import { createRuntime } from '@bassline/tcl'
2
3const rt = createRuntime()
4
5// Execute TCL scripts
6rt.run('set x 10')
7rt.run('set y [expr {$x * 2}]')
8rt.run('puts "Result: $y"')
9
10// Register custom commands
11rt.register('greet', (rt, args) => {
12 return `Hello, ${args[0]}!`
13})
14rt.run('greet World') // → "Hello, World!"
15
16// Variables and namespaces
17rt.run('namespace eval myapp { set counter 0 }')
18rt.run('incr myapp::counter')
19
20// Save and restore state
21const state = rt.save()
22// → JSON with variables, namespaces, proceduresTCL Features
run(script)Execute TCL code
register(name, fn)Add custom commands
save() / restore()Serialize runtime state
namespace evalHierarchical scoping
05Blits
1import { createBlits } from '@bassline/blit'
2
3const blits = createBlits()
4
5// Load a blit from a SQLite file
6await blits.put({ path: '/myapp' }, {
7 path: './myapp.blit'
8})
9
10// Blits persist state in SQLite with WAL mode
11// They include a TCL runtime for scripting
12
13// Execute TCL scripts
14await blits.put({ path: '/myapp/tcl/eval' }, {
15 script: 'set x [expr 1 + 2]'
16})
17
18// Access cells, store, and functions
19await blits.get({ path: '/myapp/cells/counter' })
20await blits.put({ path: '/myapp/store/config' }, { key: 'value' })
21
22// Checkpoint saves all state to SQLite
23await blits.put({ path: '/myapp/checkpoint' }, {})
24
25// Close when done
26await blits.put({ path: '/myapp/close' }, {})Blits are self-contained computation environments persisted via SQlite.
They act similar to Smalltalk images, as they include all relevant procedures, boot scripts, etc.
Blit Resources
/cells/*Lattice-based state with SQLite backing
/store/*Key-value persistence
/fn/*User-defined TCL procedures
/tcl/evalExecute TCL scripts
06AI Integration
Expose AI models as resources in Bassline.
MCP Tools
Bassline exposes tools for AI agents via MCP (Model Context Protocol). Agents can explore resources, follow links, and make changes using the same URI-based patterns as everything else.
1// Claude can interact with Bassline through MCP tools
2 {
3 name: 'bl',
4 description: `Interact with Bassline resources using the native protocol.
5
6Use GET {path:"/"} to explore available resources.
7Use GET {path:"/guide"} to learn how the system works.
8
9Key paths:
10- /cells/* - Lattice-based state (monotonic merge)
11- /store/* - Key/value storage
12- /fn/* - Stored functions
13- /tcl/eval - Evaluate TCL scripts (persistent state)
14- /guide - System documentation (readable and writable)
15
16Headers control routing and behavior. Common headers:
17- path: Resource path (required)
18- type: Content type hint (e.g., "tcl/dict", "js/num")`,
19 input_schema: {
20 type: 'object',
21 properties: {
22 method: {
23 type: 'string',
24 enum: ['get', 'put'],
25 description: 'HTTP-like method: get to read, put to write',
26 },
27 headers: {
28 type: 'object',
29 description: 'Request headers including path',
30 properties: {
31 path: { type: 'string', description: 'Resource path (e.g., /cells/counter)' },
32 type: { type: 'string', description: 'Content type for body (e.g., tcl/dict, js/num)' },
33 },
34 required: ['path'],
35 },
36 body: {
37 description: 'Body for PUT requests (string, object, or array)',
38 },
39 },
40 required: ['method', 'headers'],
41 }The MCP integration builds on top of blits, meaning the LLM has access to a full computational sandbox to work inside via a single tool.
Because the system is built for discovery, it means context fills slower, as it learns in parts.
If you want to expose Bassline packages to an agent, you can find our agent skills folder in the GitHub.
Claude
1import { createClaude } from '@bassline/services'
2
3const claude = createClaude({
4 model: 'claude-sonnet-4-20250514'
5})
6
7// Simple text completion
8const response = await claude.put({ path: '/complete' }, {
9 prompt: 'Explain CRDTs in one sentence'
10})
11
12// Full messages API with tools
13const chat = await claude.put({ path: '/messages' }, {
14 system: 'You are a helpful assistant',
15 messages: [{ role: 'user', content: 'Hello!' }],
16 tools: myTools,
17 temperature: 0.7
18})
19
20// Agentic workflow with Bassline tools
21await claude.put({ path: '/agent' }, {
22 prompt: 'Analyze the codebase and create a summary',
23 kit,
24 maxTurns: 10
25})Integrate Claude via the Anthropic API.
Ollama
1import { createOllama } from '@bassline/services'
2
3const ollama = createOllama({
4 baseUrl: 'http://localhost:11434',
5 model: 'mistral'
6})
7
8// List available models
9const models = await ollama.get({ path: '/models' })
10
11// Simple prompt-response
12const answer = await ollama.put({ path: '/ask' }, {
13 prompt: 'What is a propagator network?'
14})
15
16// Chat with message history
17const chat = await ollama.put({ path: '/chat' }, {
18 messages: [
19 { role: 'user', content: 'Hello!' },
20 { role: 'assistant', content: 'Hi there!' },
21 { role: 'user', content: 'Explain cells' }
22 ]
23})
24
25// Generate with custom options
26await ollama.put({ path: '/generate' }, {
27 prompt: 'Write a haiku about distributed systems',
28 options: { temperature: 0.9 }
29})Connect to local LLMs through Ollama. List models, generate text, and run chat conversations with tool support.
07Database Resources
Connect your existing database to Bassline, exposing tables and queries as resources.
1// Connect Bassline to your database
2import { createDbAdapter } from '@bassline/db'
3
4const db = createDbAdapter({
5 type: 'postgres',
6 connection: process.env.DATABASE_URL
7})
8
9// Mount database tables as resources
10const app = routes({ db: db.routes })
11
12// Query via Bassline paths
13const users = await app.get({ path: '/db/users?limit=10' })
14
15// Write through Bassline
16await app.put({ path: '/db/users/123' }, {
17 name: 'Alice',
18 email: 'alice@example.com'
19})
20
21// Changes can propagate to cells
22await app.put({ path: '/db/sync' }, {
23 table: 'users',
24 cell: '/cells/user-count',
25 query: 'SELECT COUNT(*) FROM users'
26})