Serverless & multi-tenant hosting
A common hosted pattern is one SQLite file per tenant, kept in object storage (S3, GCS, R2). Each request pulls the file, runs the engine in-process, and pushes the file back. This is how SynapseTree, the hosted Core LLM Wiki service, runs. If you’d rather not run this yourself, SynapseTree provides it as a service.
Recommended config
Section titled “Recommended config”const wiki = new WikiMemory(db, { llmProvider: tenantProvider, // tenant's own key; implement generateTextWithUsage for exact metering config: { librarian: { strategy: 'ops' }, // similarity-gated, supersedes instead of overwriting maintenance: 'deferred', // no background maintenance; setup may start a no-model removal drain reportLlmUsage: true, // emit llm_usage diagnostics }, onDiagnostic: (d) => meter.record(tenantId, d),});Request lifecycle
Section titled “Request lifecycle”- Pull the tenant’s
.sqlitefile. Use a conditional fetch (ETag) to skip unchanged files. - Open.
new WikiMemory(…)andawait wiki.setup(). This is cheap when the schema is current.setup()starts no background work by default. LeavedrainExternalDeletionsOnSetupoff on a serverless host and let the'externalDeletions'job inrunPendingMaintenancedeliver owed removals: that job honoursdeadlineMs. - Handle the request:
write(entityId, { …, occurred_at: clientTimestamp })read(entityId, query, { tokenBudget })
- Optionally run maintenance within the time you have left:
When this resolves the database has no open work, so the file is consistent to push. One exception to “nothing is running”: a call to your removal hook that hitconst r = await wiki.runPendingMaintenance({ tokenBudget: 20_000, deadlineMs: remainingMs - 1500 });
deletionHookTimeoutMsmay still be running in your hook. The engine has stopped waiting for it, and its delete stays owed and is retried on a later run. If you turned ondrainExternalDeletionsOnSetupanddeadlineMscut short the wait for that drain, callawait wiki.drain()before you push. - Push the file back. It’s consistent, because there’s no detached work and no open transaction.
runPendingMaintenance() waits for that drain, up to its deadlineMs. If you skip step 4, call await wiki.drain() before you push. Neither waits for a hook call that already timed out; its delete stays owed.
Skipping step 4 is fine. The backlog carries over to the next request or to a queue worker that
calls getPendingMaintenance().
Long migrations inside a lease
Section titled “Long migrations inside a lease”On a large store, one-time upgrade work can take minutes. If you open the store inside a time-limited writer lease, refuse that work there and run it out of band:
import { WikiMigrationRequiredError } from '@equationalapplications/core-llm-wiki';
try { await wiki.setup({ migrations: 'refuse' });} catch (err) { if (!(err instanceof WikiMigrationRequiredError)) throw err; // err.pendingWork describes it. Release the lease, run `await wiki.setup()` out of lease // (resumable), then re-acquire and open with 'refuse' again.}'auto' refuses a schema migration but allows the cheaper source_ref normalization step.
Hard rules
Section titled “Hard rules”- Single writer per file. In-process locks don’t protect you across processes, so use a lease.
- Never persist mid-maintenance in
'auto'mode. Use'deferred'(the default), or calldrain()first. - Entity IDs come from your auth layer, never from request input you haven’t checked.
The full SynapseTree write-up, covering costs, the MCP surface, and the entity_id scheme, is in
docs/synapsetree-integration.md.