-
v3.0.0
Stablereleased this
2026-08-22 03:05:23 +00:00 | 13 commits to main since this releaseNear feature parity with the official SpacetimeDB 2.x clients, typed query builder
included, and a schema generator a consumer project can actually reach. The major is
for one break - row callbacks take a context first - and everything else here is added
surface, built from thespacetimedbnpm package 2.8.2's own source so that every name
below mirrors something real.Breaking changes
Row callbacks take a context first
db.tables.message:on_insert(function(row) end) -- 2.0.0 db.tables.message:on_insert(function(ctx, row) end) -- 3.0.0 db.tables.message:on_update(function(old, new) end) -- 2.0.0 db.tables.message:on_update(function(ctx, old, new) end) -- 3.0.0The fix is mechanical and the same everywhere: add a leading
ctxto every
on_insert,on_updateandon_deletehandler. Clean cutover - no shim, no arity
sniffing - and the conformance harness, every test, the README, the wiki and the new
example are migrated in the same branch.ctxis one object per connection carrying
db,reducers,procedures,event,is_active(),disconnect()and
subscription_builder(), withctx.eventset to the tagged cause of the batch
immediately before the callbacks that batch fires.Nothing else in the 2.0.0 surface changed shape.
on_event,
Subscription:on_applied/on_error,Call:on_ok/on_error/on_out_of_energyand
everydb:method keep their signatures; the other ctx-first callbacks named below
belong to API that is new in this release, so there is nothing to migrate there.Features
Typed query builder
stdb/query.lua.tables.message:where(function(r) return r.sender:eq("yuris") end)
compiles toSELECT * FROM "message" WHERE "message"."sender" = 'yuris'. Operators
eq ne lt lte gt gte, combinatorsand_ or_ not_, andleft_semijoin/right_semijoin
under the official SDKs' own names, so a TypeScript callsite ports across.not_works
despite the grammar having noNOT, by inverting the comparisons and applying De Morgan.This is a string templater with the schema checking it, not a query planner, because the
official builder is one too: it compiles to SQL in the client (lib/query.ts'stoSql,
consumed bysubscription_builder_impl.ts:122-126), so a query always leaves the client
as text. What the schema buys is callsite errors - an unknown column, a wrong value type,
a u64 above 2^53 passed as a number, a non-hex value for anIdentitycolumn, an
unindexed join column each raise naming what was wrong, instead of the server rejecting
a string.The boundary is the subscription grammar itself:
SELECT * FROM t [JOIN t2 ON col = col] [WHERE pred], ops= != < <= > >=, no projection, no ordering, no limit, at most two
tables, both join columns indexed. Six generated queries were POSTed to a live server and
accepted, including a join andsender = ''' OR 1=1 --', where the doubled quote returns
200with no rows rather than executing.Reachable schema generation
Since 2.0.0 made the v2 BSATN wire the default, a table with no generated
typescannot
be decoded at all - it is skipped and reported asundescribed. The generator the docs
told a consumer to run wasdev/tools/gen-schema.sh: bash,curlandjq. But
.gitattributesexport-ignores/dev, so the library archive a consumer actually gets
holds 13 entries and none of them is that script. The one artefact the default wire
cannot work without was the one artefact that was never delivered.stdb/typetags.luais the jq walker ofgen-schema.sh:171-459ported to Lua, and two
paths sit on top of it.stdb.schema.fetch(opts, cb)reads
<base>/v1/database/<db>/schema?version=9through thehttp_seamand calls back a ready
gentable - the development path, no external tool at all.stdb.schema.to_lua(gen)
serialises the same table to a loadable module carrying the same-- GENERATEDheader
the shell script writes - the release path, a file to commit and pin. Both go through
typetags.build, so the fetched schema and the pinned one cannot describe a table
differently. The seam grewget(url, cb)beside its existingpost, same
(status, body)callback and the samestatus == 0meaning nothing answered.stdb/spacetimedb.editor_scriptadds Project → SpacetimeDB: Regenerate schema. It
lives understdb/becauseinclude_dirs = stdbis the only directory Defold exposes to
a consumer project, reads its defaults fromspacetimedb.base_urland
spacetimedb.databasein the consumer'sgame.project, and writes/my_schema.lua. It
carries its own ~60-line JSON decoder because the editor's Lua 5.2 runtime has no JSON
module, and requiringjqin the editor would reinstate the dependency this work exists
to remove; that decoder is editor-only and never reaches a game bundle. Shipping the
shell script and shelling out to it is not possible - a dependency's files are zip
entries, and onlyext.manifestandplugins/bin/${platform}are ever extracted.
gen-schema.shkeeps its existing job as the contributor drift check at
conformance.sh:61-76.Contexts, connection builder and accessors
stdb.builder():with_url(...):with_database(...):with_schema(gen):build()is the
official connection-builder chain over the sameconnectit has always been sugar for,
withwith_token/with_protocolandon_connect/on_connect_error/on_disconnect
takingctxfirst.db.reducers.<name>(args, cb)anddb.procedures.<name>(args, cb)
are generated from the schema and raise on a typo with the real names listed, rather than
reaching the server as an unknown reducer.db:subscription_builder()chains
on_applied/on_errorintosubscribe(queries)orsubscribe_to_all_tables()and
returns a real handle:is_active,is_ended,unsubscribe,unsubscribe_then.
db.dbis the official name for the table map, and
remove_on_insert/remove_on_update/remove_on_deletealiasoff_*- one detach under
either spelling, so neither name breaks.Connection state and latency
db:state()returnsconnecting/connected/reconnecting/closed, assigned only at
the five points the SDK already learns of a transition, so it is a plain accessor rather
than a probe.db:latency()is an exponential moving average at weight 1/8 - TCP's own
SRTT smoothing - over correlated request/response pairs the SDK already exchanges: no new
traffic, no heartbeat.db:ping(cb)forces one fresh measurement with a one-off query
againstst_tablefiltered to match nothing. Enough to build a "reconnecting..." banner,
which nothing before this could.Coroutine mode
stdb.await(fn, ...)andstdb.await_event(db, type)instdb/await.luaput a
coroutine mode beside the callbacks, for sequential flows that should read as sequential.
Opt-in through an explicit wrapper on thenakama-defoldprecedent rather than
auto-detecting a coroutine, a truthyerrraises inside the coroutine instead of being
returned for the caller to forget, and astateguard is what stops aresumeon a
coroutine that never yielded.An openable example
dev/example-chat/is a real Defold project -game.project, a GUI, an input binding -
rather than the conformance harness, plus a headless entry point that proves the wiring:
it sends a message and printsEXAMPLE: PASSonly once that row comes back through
on_insert. The v2 leg of the conformance workflow builds it headless and greps for that
line, so the first code a consumer copies is code something runs..script_api: answered, not builtThe Defold manual scopes
.script_apito native extensions, and separately says the
editor completes Lua modules a scriptrequires. A pure-Lua library needs neither, so
there is nothing to generate. Recorded as a decision on the wiki roadmap instead of
staying an open item.Bug fixes
- The README quickstart could not be followed on the default wire - it omitted
reducers/procedures, so the reducer call it told you to make failed withno schema entry for reducer send_message. Both quickstarts then called that reducer immediately
afterconnect, where there is no socket yet andstdb.connect's pre-socket seam
raisessend before the socket was created. Found by building the example against the
documented path; the sends now sit in the connected path, where the conformance harness
always had them. - A second connection could re-point the first one's JSON codec -
stdb.protocolis
pure and holds its codec in a module local, soprotocol.initre-pointed every
connection already running, not just the one being opened. It surfaced as a wrong
number rather than an error: the two-socket conformance phase handed its observer an
uninstrumented codec and the measurement on the other socket reported10000 row(s) cost 0 json.decode call(s).connection.newnow asserts that a second connection
passes the samejsonimplementation.protocol.initstays permissive - wrapping or
instrumenting the codec on purpose is a documented act, andtest_protocol.luadoes
exactly that; two connections disagreeing by accident is the bug.
What's Changed
- test: prove the light-mode claim on two sockets, and stop a codec swap in silence in #104
- ci: run the example, and stop commenting on a closed issue in #103
- feat: near parity with the official SDKs, typed query builder included in #102
- test: pin what a mutation audit could break in silence on the default wire in #101
- docs: title the README "SpacetimeDB Defold SDK" in #100
- docs: rewrite the README as an SDK introduction in #99
- fix: build conformance offline when the deps are already cached in #98
- feat: reachable schema generation, and a README that describes the SDK in #97
- docs: say how to decode a one-off row, and why we do not do it for you in #96
- docs: make the facade's own example run on the default wire in #95
Full Changelog: v2.0.0...v3.0.0
Writing rewritten proseUse as a Defold library
Add to
game.projectdependencies:https://git.nokopia.com/yuris/spacetimedb-defold/archive/v3.0.0.zip https://github.com/defold/extension-websocket/archive/refs/tags/4.2.3.zipextension-websocket is required: Defold does not resolve dependencies
transitively, so without itconnectfails at runtime.Targeting HTML5? 4.2.3 drops the subprotocol there, so it needs the
patch inpatches/AND the remote dependency above removed - keeping
both puts twowebsocketextensions in one build and the link fails.
The README has the four commands.Private repo? Append
?token=<access-token>to the URL, the same
shape Defold documents for GitLab. Basic auth in the URL
(https://user:token@...) is not a substitute: the archive endpoint
ignores URL credentials - a real account with a wrong password still
gets200on a public repo - while?token=is validated and a bad
one gets a plain401.Downloads
-
Source code (ZIP)
4 downloads
-
Source code (TAR.GZ)
1 download
- The README quickstart could not be followed on the default wire - it omitted