Use server mode when you want the database to run in a separate process but keep the same query style as local ShelfDB.
This is useful when several trusted local processes should talk to one ShelfDB server, or when you want a transport boundary between your application and the database.
Embedded mode is still simpler when you can use it.
Start the server
Run with defaults:
shelfdb
That starts a TCP server on 127.0.0.1:17000 using the db directory.
Choose an explicit transport:
shelfdb --url tcp://127.0.0.1:17000
shelfdb --url unix:///tmp/shelfdb.sock
Choose the database path:
shelfdb --db ./data/app-db
Control logging:
shelfdb --log-level info
shelfdb --log-level debug
Valid URLs
ShelfDB accepts these URL forms:
tcp://host:portunix:///path/to/socket.sock
Unix Socket transport
Use a Unix socket when clients live on the same machine as the server.
shelfdb --url unix:///tmp/shelfdb.sock
This is still server mode. It is local-only and cannot be reached from another machine.
Connect from Python
Use sync code when your program is synchronous:
import shelfdb
client = shelfdb.connect("tcp://127.0.0.1:17000")
Use async code when your program is already async:
import asyncio
import shelfdb
async def main():
client = await shelfdb.connect_async("tcp://127.0.0.1:17000")
note = await client.shelf("note").key("note-1").first().run()
print(note)
asyncio.run(main())
After connecting, the query API is the same in both modes. The only async difference is that you
await connect_async(...), query.run(), and tx.commit().
Use the same query API
Sync example:
client.shelf("note").put("note-1", {"title": "remote"}).run()
note = client.shelf("note").key("note-1").first().run()
count = client.shelf("note").count().run()
client.shelf("note").put_many(
[
("note-2", {"title": "batch"}),
("note-3", {"title": "batch"}),
]
).run()
notes = client.shelf("note").keys_in(["note-3", "note-2"]).run()
Async uses the same query chain with await on execution:
await client.shelf("note").put("note-1", {"title": "remote"}).run()
note = await client.shelf("note").key("note-1").first().run()
count = await client.shelf("note").count().run()
await client.shelf("note").put_many(
[
("note-2", {"title": "batch"}),
("note-3", {"title": "batch"}),
]
).run()
notes = await client.shelf("note").keys_in(["note-3", "note-2"]).run()
put_many() returns None. keys_in() keeps the key order you requested.
Batch inputs are materialized on the client before send, so the API still accepts iterables but the RPC payload is explicit.
Result shape
Remote results are normalized into plain Python values before they are returned.
For example, a remote item looks like this:
["note-1", {"title": "remote"}]
Embedded mode uses the same item shape, but multi-item embedded run() calls return one-shot iterators
instead of materialized lists.
Transactions
Use client.transaction(write=True) to group multiple remote writes together.
Remote transaction queries queue their steps when you call .run(), unlike embedded transactions
where .run() executes immediately inside the with block.
Information:
Call tx.commit() to send the batch, or use with client.transaction(...) as tx: / async with
to auto-commit and store the commit result on tx.result.
with client.transaction(write=True) as tx:
tx.shelf("note").put("note-1", {"title": "ShelfDB"}).run()
tx.shelf("user").put("user-1", {"name": "alice"}).run()
print(tx.result)
tx.commit() returns the last queued query result, or None for an empty transaction, and stores
that value on tx.result.
For async code, use async with or await tx.commit() instead.
Transaction queries must belong to the transaction they are queued into.
Client-side logging
If you want client debug logs, configure logging before connecting:
import shelfdb.log
shelfdb.log.configure_logging("debug")
This is useful when diagnosing connection setup or RPC behavior.
Security
ShelfDB server mode is for trusted local clients, not untrusted networks.
Read Security before exposing the server beyond a simple local setup.