Memory with a host backend
A logical memory does not require a backend by itself. Here, required_resources makes this backend mandatory when the resource document is supplied. Keep requireResourceBindings: true (CLI: --require-resource-bindings) to refuse execution before dispatch even when that document is omitted. A short secret can collide with ordinary text or object keys: use a dedicated high-entropy credential; the runtime deliberately rejects any matching substring.
Write a customer note to a real local HTTP service, then read it back. Touba governs logical memory, scopes, grants, secrets and effects; the host implements storage. This demonstration service keeps data in process memory: stopping it loses the notes. For persistence, continue with the PostgreSQL tutorial.
Prerequisites: Touba distribution, Node.js 24 and npm. Docker is only required for the second execution mode. In a macOS/Linux terminal:
mkdir -p http-memory-demo
cd http-memory-demo
mkdir -p handlers backend docker vendor
npm init -y
node --input-type=module -e 'import {execFileSync} from "node:child_process"; import {copyFileSync} from "node:fs"; const v=JSON.parse(execFileSync("touba",["version","--json"],{encoding:"utf8"})); copyFileSync(v.distribution.sdk_archive,"vendor/touba-sdk.tgz");'
npm install --offline --ignore-scripts --save-exact ./vendor/touba-sdk.tgz
1. Declare the memory workflow
module examples.memory_backend_host
memory CustomerMemory
type MemoryRequest
customer_id is Text
note is Text
type MemoryResult
customer_id is Text
stored_note is Text
backend is Text
tool WriteMemoryNote
description "Store one customer note in the authorized HTTP memory backend."
uses memory CustomerMemory write
takes request as MemoryRequest
gives MemoryResult
requires network, secret_read, memory_write
secrets MEMORY_BACKEND_TOKEN
tool ReadMemoryNote
description "Read the stored customer note from the authorized HTTP backend."
uses memory CustomerMemory read
takes request as MemoryRequest
gives MemoryResult
requires network, secret_read, memory_read
secrets MEMORY_BACKEND_TOKEN
skill StoreAndReadCustomerMemory
description "Store the requested note and verify it by reading the backend."
takes request as MemoryRequest
gives MemoryResult
uses WriteMemoryNote
uses ReadMemoryNote
memory CustomerMemory read
memory CustomerMemory write
do
WriteMemoryNote
with request as request
into stored
ReadMemoryNote
with request as request
into loaded
give loaded
strategy RunCustomerMemory
takes request as MemoryRequest
gives MemoryResult
uses StoreAndReadCustomerMemory
do
StoreAndReadCustomerMemory
with request as request
into result
show
with message as "Memory backend returned: " ++ result.stored_note
give result
2. Provide the complete HTTP backend
import { createServer } from "node:http";
import { pathToFileURL } from "node:url";
export async function startMemoryBackend({ token, host = "127.0.0.1", port = 0 }) {
if (!token) throw new Error("A backend token is required");
const notes = new Map();
const server = createServer(async (request, response) => {
const send = (status, body) => {
response.writeHead(status, { "content-type": "application/json" });
response.end(JSON.stringify(body));
};
try {
if (request.url === "/health") return send(200, { status: "ok" });
if (request.headers.authorization !== "Bearer " + token) return send(401, { error: "Unauthorized" });
const match = /^\/notes\/([^/]+)$/.exec(new URL(request.url, "http://local").pathname);
if (!match) return send(404, { error: "NotFound" });
const id = decodeURIComponent(match[1]);
if (request.method === "POST") {
let raw = "";
for await (const chunk of request) {
raw += chunk;
if (Buffer.byteLength(raw) > 8192) return send(413, { error: "PayloadTooLarge" });
}
const { note } = JSON.parse(raw);
if (typeof note !== "string" || !note.trim()) return send(400, { error: "InvalidNote" });
notes.set(id, { customer_id: id, stored_note: note, write_count: (notes.get(id)?.write_count ?? 0) + 1 });
} else if (request.method !== "GET") return send(405, { error: "MethodNotAllowed" });
return notes.has(id) ? send(200, notes.get(id)) : send(404, { error: "MissingNote" });
} catch { send(400, { error: "InvalidRequest" }); }
});
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(port, host, resolve); });
return { endpoint: "http://127.0.0.1:" + server.address().port,
close: () => new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())) };
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
await startMemoryBackend({ token: process.env.MEMORY_BACKEND_TOKEN,
host: process.env.HOST ?? "127.0.0.1", port: Number(process.env.PORT ?? 8787) });
}
This bounded loopback service is a teaching adapter, not a production database. It authenticates requests, validates note input and counts actual writes.
3. Implement the read/write handler
The validated resource binding supplies the endpoint; the secret is injected
only into the handler. A write returns an applied receipt only after the HTTP
request succeeds. retry_mode: none does not promise idempotency or rollback.
import { defineTool, createToolResult, ToubaHandlerError } from "@touba/sdk";
const access = process.argv[2];
if (!["read", "write"].includes(access)) throw new Error("Expected read or write adapter mode");
const name = access === "write" ? "WriteMemoryNote" : "ReadMemoryNote";
await defineTool({
toolRef: "tool:examples.memory_backend_host/" + name,
requiredGrants: ["network", "secret_read", "memory_" + access],
requiredSecretHandles: ["MEMORY_BACKEND_TOKEN"],
inputSignature: [{ name: "request", type_ref: "type:examples.memory_backend_host/MemoryRequest" }],
outputType: "type:examples.memory_backend_host/MemoryResult",
run: async ({ request, context }) => {
let requestStarted = false;
try {
if (access === "write" && !context.effect) throw new ToubaHandlerError("EffectContextRequired", "A write requires an effect context.");
const binding = context.resourceBindings.find(item => item.resource_ref === "memory:examples.memory_backend_host/CustomerMemory");
if (binding?.kind !== "memory_backend" || binding.provider !== "host.http.memory_backend") {
throw new ToubaHandlerError("MemoryBindingInvalid", "The expected memory backend is unavailable.");
}
const url = new URL(binding.config.endpoint_url);
if (url.protocol !== "http:" || url.hostname !== "127.0.0.1") throw new ToubaHandlerError("MemoryEndpointInvalid", "This demo requires loopback HTTP.");
url.pathname = "/notes/" + encodeURIComponent(request.customer_id);
requestStarted = true;
const response = await fetch(url, { method: access === "write" ? "POST" : "GET",
signal: AbortSignal.timeout(10000),
headers: { authorization: "Bearer " + context.env.MEMORY_BACKEND_TOKEN, "content-type": "application/json" },
...(access === "write" ? { body: JSON.stringify({ note: request.note }) } : {}) });
if (!response.ok) throw new ToubaHandlerError("MemoryHttpError", "The memory backend returned HTTP " + response.status);
const body = await response.json();
if (typeof body.customer_id !== "string" || typeof body.stored_note !== "string") {
throw new ToubaHandlerError("MemoryResultInvalid", "The backend returned an invalid record.");
}
const value = { customer_id: body.customer_id, stored_note: body.stored_note, backend: "host_http_memory" };
return access === "write" ? createToolResult(value, {
effectReceipt: context.createEffectReceipt({ outcome: "applied" }) }) : value;
} catch (error) {
if (access === "write" && context.effect) throw new ToubaHandlerError("MemoryWriteUnconfirmed",
"The write did not return a confirmed result.", { effect_receipt: context.createEffectReceipt({
outcome: requestStarted ? "unknown" : "not_applied" }) });
throw error;
}
},
});
4. Declare the host configuration
import { writeFileSync } from "node:fs";
export function configure(endpoint) {
const memory = "memory:examples.memory_backend_host/CustomerMemory";
const manifest = { manifest_version: "touba-manifest-v1", bindings: {} };
for (const [name, access] of [["WriteMemoryNote", "write"], ["ReadMemoryNote", "read"]]) {
manifest.bindings["tool:examples.memory_backend_host/" + name] = {
handler: { kind: "process", command: process.execPath, args: ["handlers/memory.mjs", access], cwd: "." },
grants: ["network", "secret_read", "memory_" + access], secrets: ["MEMORY_BACKEND_TOKEN"],
memories: [{ ref: memory, access }], timeout_ms: 15000, max_payload_bytes: 65536,
effect_contract: access === "write" ? { kind: "external_write", retry_mode: "none" } : { kind: "none" },
};
}
const resources = { resource_bindings_schema_version: "touba-resource-bindings-v1", required_resources: [memory], resources: {
[memory]: { kind: "memory_backend", provider: "host.http.memory_backend", config: { endpoint_url: endpoint },
grants: ["memory.read", "memory.write"], secrets: ["MEMORY_BACKEND_TOKEN"],
required_secrets: ["MEMORY_BACKEND_TOKEN"], limits: { max_payload_bytes: 65536 } },
} };
const save = (name, value) => writeFileSync(name, JSON.stringify(value, null, 2) + "\n");
save("touba.manifest.json", manifest);
save("touba.resources.json", resources);
save("touba.secrets.json", { secrets_version: "touba-secrets-v1", secrets: {
MEMORY_BACKEND_TOKEN: { source: "env", name: "MEMORY_BACKEND_TOKEN" } } });
resources.resources[memory].grants = ["memory.read"];
save("denied.resources.json", resources);
resources.resources[memory].config.token = "forbidden-demo-value";
save("invalid.resources.json", resources);
}
The script writes complete V1 files with the actual Node path and backend port.
The resource grants memory.read/memory.write complement the source scopes
and manifest's memories array. A token, DSN or API key never belongs in config.
5. Execute and verify
The script starts an isolated service, tests three refusals before dispatch, performs one write/read, checks the backend write count and closes its service.
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { randomUUID } from "node:crypto";
import { ToubaEngine, ToubaEngineError } from "@touba/sdk";
import { startMemoryBackend } from "./backend/server.mjs";
import { configure } from "./configure.mjs";
const run = promisify(execFile);
const token = "isolated-memory-token-" + randomUUID();
const project = "touba-memory-demo-" + randomUUID().slice(0, 8);
const docker = process.argv.includes("--docker");
const compose = (...args) => run("docker", ["compose", "-p", project, "-f", "compose.json", ...args],
{ env: { ...process.env, MEMORY_BACKEND_TOKEN: token }, maxBuffer: 4 * 1024 * 1024 });
let backend;
try {
let endpoint;
if (docker) {
await compose("up", "--build", "--detach", "--wait", "--wait-timeout", "90");
const { stdout } = await compose("port", "backend", "8787");
endpoint = "http://" + stdout.trim();
} else {
backend = await startMemoryBackend({ token });
endpoint = backend.endpoint;
}
configure(endpoint);
const engine = new ToubaEngine({ projectRoot: ".", env: { MEMORY_BACKEND_TOKEN: token } });
const options = { source: "memory.tb", entryRef: "strategy:examples.memory_backend_host/RunCustomerMemory",
manifest: "touba.manifest.json", secrets: "touba.secrets.json", resourceBindings: "touba.resources.json",
requireResourceBindings: true, effectJournal: "memory.effect-journal.sqlite",
inputs: { request: { customer_id: "ACME-001", note: "Renewal risk escalated to the account team" } } };
for (const [resourceBindings, reason] of [[undefined, "ResourceBindingMissing"],
["denied.resources.json", "ResourceBindingGrantMissing"], ["invalid.resources.json", "ResourceBindingPayloadForbidden"]]) {
await assert.rejects(engine.run({ ...options, resourceBindings }), error => {
assert.ok(error instanceof ToubaEngineError);
assert.equal(error.details.reason, reason);
assert.equal(error.details.trace.events.filter(event => event.kind === "tool_binding_authorized").length, 0);
return true;
});
}
const result = await engine.run(options);
assert.equal(result.status, "success");
assert.equal(result.result.stored_note, options.inputs.request.note);
const record = await (await fetch(endpoint + "/notes/ACME-001", { headers: { authorization: "Bearer " + token } })).json();
assert.equal(record.write_count, 1);
for (const value of [token, endpoint, options.inputs.request.note]) assert.ok(!JSON.stringify(result.trace).includes(value));
console.log(JSON.stringify(result.result));
console.log("HTTP_MEMORY_WRITE_READ_AND_REFUSALS_OK " + (docker ? "docker" : "local"));
} finally {
if (backend) await backend.close();
if (docker) await compose("down", "--volumes", "--remove-orphans");
}
node exercise.mjs
Expected: HTTP_MEMORY_WRITE_READ_AND_REFUSALS_OK local. The SDK's explicit
effectJournal option durably records writes. A refusal before writing does
not create a note. A later failure cannot undo an already acquired effect.
6. Run the same backend in Docker
Create these two files; the build context is the project root, so every copied path below exists. The host port is allocated dynamically to avoid conflicts.
FROM node:24.22.1-alpine
WORKDIR /app
COPY backend/server.mjs ./server.mjs
CMD ["node", "server.mjs"]
{
"services": {
"backend": {
"build": {
"context": ".",
"dockerfile": "docker/Dockerfile"
},
"environment": {
"MEMORY_BACKEND_TOKEN": "${MEMORY_BACKEND_TOKEN:?required}",
"HOST": "0.0.0.0",
"PORT": "8787"
},
"ports": [
"127.0.0.1::8787"
],
"healthcheck": {
"test": [
"CMD",
"node",
"-e",
"fetch('http://127.0.0.1:8787/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
],
"interval": "1s",
"timeout": "5s",
"retries": 30
}
}
}
}
node exercise.mjs --docker
Expected: HTTP_MEMORY_WRITE_READ_AND_REFUSALS_OK docker. This command creates
a uniquely named Compose project and removes only that project's containers and
volumes in finally. It does not use or stop any existing project.
The source, SDK handler, authority checks and business assertions are identical in both modes. No PostgreSQL, encryption or OS sandbox is implied by this example. Continue with PostgreSQL memory.