Skip to main content

Install and use a package

The easiest way to add capabilities to ScramDB is to install a published package and call its functions. You do not write any code.

Install from the registry​

Install a package by name. ScramDB fetches it from the package registry, verifies it, and catalogs it, all in one statement:

SELECT scram.install('scramdb/embeddings_cosine_similarity');

Pin a specific version with @:

SELECT scram.install('scramdb/embeddings_cosine_similarity@0.1.0');

The registry is zero-config (the default is the public afterburner registry). ScramDB fetches the package, recomputes its content digest, and verifies it against the registry manifest before installing. A mismatch fails loudly and installs nothing. scram.install upserts: installing the same content again is a no-op, never an "already exists" error. On a cluster, the verified package replicates to every node.

scram.install returns the package's hex content digest. You can also upload raw bytes directly with an explicit name using scram.register(name, bytes), which skips manifest parsing entirely; see Install a local or private package below.

Installing a package catalogs it, it does not make it callable yet. A package only becomes a SQL function once you bind it with CREATE FUNCTION, the next step.

Bind and call the function​

Register the installed package as a SQL function by naming it. There is no code to write, the package supplies the body:

CREATE FUNCTION embeddings_cosine_similarity(a text, b text)
RETURNS double precision
LANGUAGE js
AS 'scramdb/embeddings_cosine_similarity';

LANGUAGE js is the keyword to use here regardless of the package's own implementation language: it names the package-reference binding form, which works for JavaScript, TypeScript, Rust, Go, C, and C++ packages alike. It does not work for LANGUAGE python or LANGUAGE ruby: those two only accept inline source (AS $$...$$), never a package reference, so there is no such thing as a Python or Ruby package to install and bind this way. You also cannot bind just one named export out of a package; a package's compiled module is one whole callable unit.

Then call it like any built-in function:

SELECT embeddings_cosine_similarity('[1, 0, 0]', '[1, 0, 0]'); -- 1.0

Or across a table, ranking the closest matches:

SELECT id, embeddings_cosine_similarity(query_vec, doc_vec) AS score
FROM documents
ORDER BY score DESC
LIMIT 10;

Because ScramDB is UTAP, this runs on your live data, including rows a transaction committed a moment ago.

CREATE OR REPLACE FUNCTION works too, but only for the function's own owner or a superuser; anyone else's replace attempt is refused, naming the ownership reason.

Call without registering a function​

For a one-off call, or from an agent or script that wants to invoke a UDF without composing a full SELECT fn(...) query, scram.invoke and its batch and columnar variants call an already-registered function directly:

-- one row at a time; STRICT functions short-circuit a NULL argument without running
SELECT scram.invoke('embeddings_cosine_similarity', '[1,0,0]', '[1,0,0]');

-- many rows in one call: rows_json is a JSON array of argument arrays, positional
SELECT scram.invoke_batch(
'embeddings_cosine_similarity',
'[["[1,0,0]","[1,0,0]"], ["[1,0,0]","[0,1,0]"]]'
);

-- run the function over every row a query produces, one column of results back
SELECT scram.invoke_columnar(
'embeddings_cosine_similarity',
'SELECT query_vec, doc_vec FROM documents'
);

All three return the result under a single column literally named result; the function you call must itself return exactly one output column, or the call is rejected.

VerbCallNotes
scram.invokeinvoke(function_name, arg1, arg2, ...)One row in, one row out.
scram.invoke_batchinvoke_batch(function_name, rows_json)rows_json is a JSON array of positional argument arrays; the whole batch runs as one call into the function. Capped at 100,000 rows and 16 MiB of JSON text per call.
scram.invoke_columnarinvoke_columnar(function_name, source_sql)source_sql is a single SELECT statement (no semicolons, no multiple statements); results are paginated internally. Capped at 10,000,000 total rows per call.

scram.invoke_batch's JSON arguments support the same types as CREATE FUNCTION (see Supported types below), except decimal/numeric, which has no JSON wire form yet and is rejected with its own message. A bytea argument in the JSON is a hex string, with or without a leading \x.

See what is installed​

scram_modules is a live catalog view of every installed module:

SELECT * FROM scram_modules;
ColumnMeaning
nameThe installed module's qualified name
versionThe registry version, NULL for a module installed from raw bytes with no registry provenance
digestContent digest
size_bytesArtifact size
installed_byThe installing role
installed_atInstall timestamp
pinThe active version pin, NULL if unpinned
invocationsCalls made through scram.invoke / invoke_batch / invoke_columnar since install

invocations only counts calls through the scram.invoke* verbs above; ordinary SELECT fn(...) calls through a CREATE FUNCTION-registered name are not counted.

Daemon packages​

Most packages are functions you call. Some are servers that need to be running, and those declare it in their own manifest:

[metadata.daemon]
autostart = true

A package carrying that declaration and granting listen in its manifold is started in-process after the preinstall sweep, on the engine's own reactor rather than a second runtime, and supervised from then on. Nothing to launch, no separate process to babysit, no init script. The shipped example is scramdb/semantics, the Semantic AI MCP server on port 9191.

scram_daemons is the live view, computed from the in-memory registry on every scan rather than read from a stored snapshot:

SELECT package, state, ports, shards_alive, restarts, last_error, started_at, digest
FROM scram_daemons;
ColumnMeaning
packageThe installed package running as a daemon
statestarting, running, failed, or disabled
portsComma-joined granted listen ports; an empty string means the manifold granted Any
shards_aliveLive shards right now. Only HTTP daemons expand past one
restartsSupervised restarts since boot
last_errorThe reason it last failed, NULL while healthy
started_atFirst successful start, NULL until there has been one
digestContent digest of the package actually running

Two of those columns are deliberately honest about what they do not know. last_error is NULL while healthy rather than an empty string dressed up as an answer, and started_at stays NULL until a start has actually succeeded instead of reporting the epoch.

A package that fails to start is a loud ERROR and a failed row. Boot is never wedged by it, and a daemon that died is never silently absent from the view. A pool whose shards all die is restarted with bounded exponential backoff, base 2 seconds and doubling, up to 5 restarts; past that it stays failed with the reason attached, so a crash loop terminates in something you can query rather than churning forever.

Tuning and the master switch live in [udf.daemon].

Manage versions​

List what a registry offers, move to a newer version, or pin to an exact one:

SELECT scram.versions('scramdb/embeddings_cosine_similarity'); -- available versions
SELECT scram.upgrade('scramdb/embeddings_cosine_similarity'); -- move to the newest allowed version
SELECT scram.pin('scramdb/embeddings_cosine_similarity', '@0.1.0'); -- hold at a version

scram.versions returns one row per version the registry advertises: version, digest, yanked, size_bytes, published_at (nullable), and installed (whether that exact version is currently installed locally). scram.pin accepts either '@<version>' or 'sha256:<digest>' and makes no network call, it just re-affirms the module's current bytes under that pin. scram.upgrade resolves the latest version allowed by any pin (or by an optional range argument, scram.upgrade(name, '^1')) and is a no-op if you are already there.

Install a local or private package​

For a package that is not in a registry, your own build, an air-gapped deployment, or an agent uploading bytes over the wire, hand the .afb bytes to scram.install directly. The package is self-describing, so ScramDB derives its name and digest from the bytes:

SELECT scram.install(X'<the .afb file bytes as hex>'); -- or '\x...'::bytea

To install under a name you choose yourself, skipping manifest parsing entirely, use scram.register:

SELECT scram.register('my-namespace/my-package', X'<the .afb file bytes as hex>');

In a real application you send the bytes through your client's parameter binding rather than typing them out. In offline mode (air-gapped deployments), the registry name form fails by design and this direct upload is the intended route, since it never dials out.

To remove an installed package, scram.uninstall('namespace/pkg') drops it; uninstalling a name that is not installed is an error, not a silent no-op.

Configure the registry​

Registry behavior lives under [udf.registry] in the server configuration:

KeyDefaultMeaning
urlhttps://registry.afterburner.shBase registry URL for the name-form verbs.
token_fileunset (anonymous)Path to a file holding a bearer token, read fresh on every registry call. Must be readable only by the server's own user (mode 0600 or tighter); a group- or world-readable token file is refused at config load.
allow["*"]Glob allow-list over "namespace/name", checked before any network call.
deny[]Glob deny-list, checked before allow. A match is refused loudly, naming the pattern, even if allow would have permitted it.
offlinefalsetrue refuses every name-form verb (scram.install('ns/pkg'), scram.upgrade, scram.versions) instead of dialing out. The bytes-upload form (scram.install(bytea) / scram.register) still works, since it never makes a network call; it is the supported path for air-gapped deployments.
auto_update"off""off": no background activity. "check": a background task resolves available upgrades and logs them, never installs. "apply": the same background task installs upgrades on its own cadence, honoring any pin.
update_interval"24h"Cadence for the auto_update background task; inert while auto_update is "off".

The parent [udf] table also carries the JavaScript execution limits mentioned earlier: fuel_per_batch, memory_bytes, timeout_ms, and output_bytes bound a single invocation, and enabled (default true) turns the whole afterburner UDF substrate off if set to false. preinstall_dir (default /opt/scramdb/dist) is where the server looks for .afb files to install automatically on startup; see Preinstalled packages below. Full field-by-field defaults live in Configuration.

Preinstalled packages​

ScramDB's starter packages ship as .afb files that the server installs automatically at startup, before it accepts connections, from [udf].preinstall_dir. This is exactly the same scram.install path you would run by hand, so it is idempotent: restarting an already-provisioned server re-installs nothing new.

Preinstalling only catalogs the packages, the same way a manual scram.install does. None of the starter functions are callable yet after preinstall alone; you still bind each one you want with its own CREATE FUNCTION ... AS 'namespace/pkg' statement, exactly as shown above. Check what is already installed with SELECT * FROM scram_modules.

Supported types​

CREATE FUNCTION arguments and return types, and scram.invoke* arguments, share the same type list:

boolean, smallint/int/bigint (and their unsigned variants), real, double precision, text, date, timestamp, bytea, jsonb.

Not supported, for any afterburner language, as either an argument or a return type: decimal/numeric, uuid, interval, and any array type. There is no native vector type either; every package that works with vectors stores one as a JSON array inside a text column, as embeddings_cosine_similarity does above.

A UDF is scalar-only: RETURNS SETOF ... is rejected. OUT/INOUT argument modes, argument DEFAULT expressions, SECURITY DEFINER, TEMPORARY functions, IF NOT EXISTS, and any schema other than public are all rejected too. A UDF always runs as the calling role.

Durability​

The udf-function catalog (what CREATE FUNCTION registered) and the installed-module catalog (what scram.install registered) are both in-memory only in this build; neither survives a restart. [udf].preinstall_dir re-installs the starter packages automatically on every restart, but any CREATE FUNCTION bindings you created, whether for a starter package or your own, do not come back on their own. Script your CREATE FUNCTION statements (and any custom scram.install calls) so they can be replayed after a restart.

Write and publish your own package​

Packages are polyglot. You can author one in JavaScript, TypeScript, Rust, Go, C, or C++, and afterburner compiles it to a single sealed .afb. Build it with the burn toolchain and publish it so others can install it by name:

burn package # build the content-addressed .afb
burn publish # publish it to a registry

Once published, anyone installs it with scram.install('your-namespace/your-package'), exactly as above. To write the function body itself, see Writing a package.