Developer integration guide
QuorumRelay is a standalone civic-data engine for your own application, service, script or agent. It does not require a Townlight installation, an application database, a model provider or a hosted QuorumRelay account. Use the Python API in process, or run the optional service for HTTP, TypeScript and MCP consumers.
This guide describes 0.1.0.dev8, schema 2.0.0. It is a development preview. Provider coverage varies by operation. Live account and vendor-write acceptance remain deferred; publication of the code and documentation is not a claim that those checks passed. Start with the user manual, architecture and capability evidence.
1. Choose an integration path
| Your application | Use | You own |
|---|---|---|
| Python application or worker | create_engine() and typed models |
Context, persistence, lifecycle and approved operations |
| Shell, scheduled task or another language | JSON/JSONL CLI | Process, input/output files and exit handling |
| Web backend, Node or another language | HTTP JSON operations and file streams | Service process, authentication and tenant policy |
| TypeScript application | QuorumRelayClient over HTTP |
An explicitly configured service URL and service token |
| Tool-using agent | MCP stdio or Streamable HTTP | Host process or authenticated service and tool policy |
| New provider or storage backend | Connector/Storage extension protocols | Verified provider mapping and storage isolation/concurrency |
The four reference applications illustrate these paths. They do not define the product boundary. A fifth application uses the same interfaces and contracts.
2. Install from public source
Python 3.11+ is required; current Windows CI uses Python 3.13. Node is needed only to build the TypeScript client or website. The client declares Node 20.3+; the repository CI uses Node 24. No package-index release is claimed.
git clone https://github.com/townlight/quorumrelay.git
cd quorumrelay
python -m venv .venv
# Activate .venv using your shell's activation command.
python -m pip install -e ".[service]"
quorumrelay providers
For an application deployment, pin a reviewed commit or explicit wheel rather
than following a moving branch. Install the core without extras for an in-process
integration; add [service] for HTTP/MCP. The optional [browser] extra and
Playwright's Chrome installation are needed only for the guarded WebLink browser
session described in its guide.
quorumrelay is the canonical import and command. civic_data_connectors and
civic-data-connectors remain compatible aliases sharing the same implementation
objects. Installing a package never starts a server or downloads a model.
3. Preserve the complete result
Every interface returns this envelope; do not reduce it to data.records:
{
"schema_version": "2.0.0",
"status": "partial",
"data": {},
"errors": [],
"coverage": {},
"operation_id": null
}
| Status | Consumer behavior |
|---|---|
completed |
The reported bounded operation completed; inspect its stated scope |
partial |
Keep usable data and disclose missing text/pages/files, limits or incomplete scope |
cancelled |
Keep completed evidence/receipts; do not assume an in-flight write was undone |
failed |
Show structured error codes and actionable details; inspect any retained state |
unsupported |
This requested implementation is unavailable; it is not proof the vendor lacks the feature |
HTTP 200 alone is not completion. errors and coverage carry provider failures,
truncation, unknown date/body matches and citation limits. A saved capture cannot
establish a complete upstream archive. An accepted asynchronous task is not a
completed mutation.
4. Python: capture, preview, save, search and export
This complete example uses an explicitly synthetic saved response and makes no provider request. Run it from an application-owned writable directory:
import json
from quorumrelay import OperationContext, Source, create_engine
from quorumrelay.storage import SQLiteStorage
raw = json.dumps([{
"EventId": 7,
"EventBodyName": "Council",
"EventDate": "2026-11-11T00:00:00",
"EventTime": "7:00 PM",
"EventComment": "Parks budget"
}]).encode()
source = Source("legistar", "https://example-city.legistar.com/Calendar.aspx",
input_format="legistar-events-json")
store = SQLiteStorage("my-app-captures.sqlite")
engine = create_engine(storage=store)
preview = engine.normalize(source, raw, input_format=source.input_format)
if preview.status not in {"completed", "partial"}:
raise RuntimeError(preview.public_dict())
namespace = preview.data["source"]["namespace"]
context = OperationContext(
principal="editor", client_id="my-application", tenant="workspace-42",
allowed_namespaces=frozenset({namespace}))
saved = engine.execute("import_bundle", {
"result": preview.public_dict(), "persist": True
}, context=context)
hits = engine.execute("search", {
"query": {"terms": ["Council"], "limit": 10}
}, context=context)
exported = engine.execute("export", {
"bundle": preview.data, "format": "zip"
}, context=context)
for name, result in (("save", saved), ("search", hits), ("export", exported)):
print(name, result.public_dict())
SQLiteStorage opens and closes its connections per operation; it has no long-lived
connection or close() method. Your custom storage may own a different lifecycle.
Normalization and preview do not create your application's canonical records.
import_bundle stores connector evidence only when explicitly asked. Your product
still decides how to review, reconcile and save canonical records, and whether to
publish anything. See the executable independent Python example.
Identity and isolation
Keep (provider, namespace, record_type, external_id) together. A provider ID
alone is not globally unique. The application tenant/profile ID is separate from
the provider's stable source namespace. Preserve identity.derivation and
identity.stability: a supplied URL or capture-position key is not an invented
native provider ID. Preserve original, raw capture hashes, source revisions,
relationships and citations alongside normalized fields.
Use the same trusted principal/client/tenant to reopen stored state. Changing
that tuple selects a different storage scope. Do not accept it from untrusted
operation JSON. Configure credentials in private OperationContext.credentials,
not source URLs. Native mappings describe legacy
identity boundaries; do not guess aliases across portal/API migrations.
Live discovery and known-record retrieval
from quorumrelay import Bounds
source = Source("legistar", "https://webapi.legistar.com/v1/olympia")
result = engine.discover(source, bounds=Bounds(
date_from="2026-11-01", date_to="2026-11-30",
max_pages=2, max_records=20, deadline_seconds=20))
detail = engine.fetch_record(source, "7011", bounds=Bounds(deadline_seconds=15))
These are real bounded public reads, not the synthetic example above. Run them only against your explicitly configured source. Provider access can change; read capabilities and the returned coverage. Date endpoints are inclusive. A supplied continuation belongs to its source/query/caller/current grants; some public-form adapters keep private process-local sessions and expire after 15 minutes. Restart or expiry can require beginning a fresh bounded operation. Never edit a token.
Fetch a linked attachment using the returned record and document index:
engine.fetch_attachment(source, record, index=0, context=context). Retain its
original capture in scoped storage, or supply the original capture bytes through
the helper's capture= argument. Arbitrary client URLs are not attachment grants.
Local search versus native search
search without native searches supplied records/bundles or the caller's stored
evidence. It does not fetch linked PDFs, run OCR or invent a transcript. Text
segments retain their field, page/timestamp, language and citation provenance.
native = engine.execute("search", {
"native": True,
"source": source.public_dict(),
"query": {"terms": ["Council"], "limit": 10},
"bounds": {"max_pages": 2, "max_records": 20}
})
Native adapters use verified provider candidate routes and apply local exact
matching over returned metadata/text. Field coverage differs by provider; this
does not turn provider results into full-archive or full-transcript search.
An unavailable native hook reports the gap rather than silently running local
search. extract_text is also available through Python's engine for supplied
supported bytes; it is not in the 16 public transport operation names.
5. CLI and JSONL
'{}' | quorumrelay providers
quorumrelay discover --request request.json --content calendar.json
quorumrelay search --jsonl --request queries.jsonl --database state.sqlite
quorumrelay export --request export.json --database state.sqlite --output evidence.zip
Each request is a JSON object. JSONL means one request/result per nonblank line,
sharing one process/engine/store. --content supplies saved input bytes;
content_base64 is the inline JSON equivalent. --file-root restricts explicit
local paths. --output creates a new export file and keeps stdout as JSON
metadata. --write-file CHANGE PART PATH and --export-file INDEX PATH stage
explicit large-file parts for matching metadata-only entries; they do not grant
write permission. Parser and transfer limits remain separate.
Exit codes are 0 completed, 2 partial, 3 cancelled, 4 failed, 5 unsupported and 64 invalid framing/configuration. Do not treat useful partial data as a silent success or discard it because the exit code is nonzero. See full flags and file contracts.
6. HTTP service and curl
For a first local read-only session:
quorumrelay serve --database app-state.sqlite
curl --fail-with-body http://127.0.0.1:8765/v1/info
curl --fail-with-body -H "Content-Type: application/json" --data '{}' \
http://127.0.0.1:8765/v1/operations/providers
In PowerShell use curl.exe to select curl explicitly. The process owner stops
the service with Ctrl+C. It is loopback-only by default. For a request file use
--data-binary @request.json; encode parser bytes as content_base64, or upload
an explicitly authorized file and pass its opaque file_handle.
| Endpoint | Meaning |
|---|---|
GET /v1/info |
Version/schema and service information |
GET /openapi.json |
Source-generated OpenAPI 3.1 contract |
POST /v1/operations/{operation} |
Execute one shared operation |
POST /v1/jobs/{operation} |
Submit bounded asynchronous work; inspect its operation ID |
POST /v1/files |
Multipart file or raw octet-stream upload |
GET /v1/files/{handle} |
Authenticated, scoped byte stream |
/mcp |
Optional official-SDK Streamable HTTP transport |
OpenAPI download, generated API reference and JSON schemas are generated from the actual implementation. They are not a running public QuorumRelay service. This website serves documentation; applications host their own API.
Configure a trusted service principal
Create a private configuration file with a locally generated high-entropy token. The following marker is deliberately not a deployable credential:
{
"service": {
"host": "127.0.0.1",
"port": 8765,
"tokens": {
"REPLACE-WITH-A-GENERATED-SERVICE-TOKEN": {
"principal": "editor",
"client_id": "my-app",
"tenant": "workspace-42",
"operations": ["providers", "capabilities", "discover", "fetch", "normalize", "search", "import_preview", "import_bundle", "export", "status", "cancel"],
"authorization": []
}
}
}
}
Start with quorumrelay serve --config private-service.json --database state.sqlite.
Send Authorization: Bearer SERVICE_TOKEN. Protect the file with OS permissions.
Provider credentials, if needed, belong inside that trusted principal's
credentials configuration; they are never public operation arguments.
Non-loopback binding requires allow_network, authenticated tokens and an
explicit allowed_hosts list. Configure exact allowed_origins, HTTPS/reverse
proxy and resource ceilings yourself; no hosted deployment or login provider is
created for you. Service tokens must never be forwarded to provider URLs.
Long jobs and files
Submission returns HTTP 202 with an operation ID and a partial/running result,
not completed work. Use status with {"operation_id":"..."} to read the final
result and cancel to request cooperative cancellation. A service restart does
not fabricate a resumed worker. Recover the existing receipt before deciding
whether to retry an uncertain write.
For large exports send "stream": true. Read data.artifact_id and fetch its
file stream; verify the advertised length/hash. Upload handles, exported
artifacts, expiry and current grants are scoped and rechecked. Defaults separate
16 MiB parser inputs from 200 MiB transfer bytes; operators may lower ceilings.
The HTTP service also caps operation JSON. File details explain
neutral ZIP/JSONL and legacy handles.
7. TypeScript / JavaScript
Build the source package, then install its tarball into your unrelated project:
cd client
npm ci
npm run build
npm pack
# In your own application:
npm install /path/to/quorumrelay-client-0.1.0-dev.8.tgz
import { QuorumRelayClient, ServiceError } from "quorumrelay-client";
const client = new QuorumRelayClient({
baseUrl: "http://127.0.0.1:8765",
token: process.env.QUORUMRELAY_SERVICE_TOKEN,
timeoutMs: 90_000,
});
const info = await client.info();
if (info.data.version !== "0.1.0.dev8") throw new Error("Service pin mismatch");
try {
const result = await client.execute("providers", {});
console.log(result.status, result.data, result.errors, result.coverage);
} catch (error) {
if (error instanceof ServiceError) console.error(error.httpStatus, error.result);
throw error;
}
execute, submit, info, upload(Blob) and download(handle) accept an
optional abort signal. download returns a streaming Response; the caller owns
consumption/closure. Timeout/network/abort errors differ from service HTTP errors
and from an engine's partial/failed envelope. The client never starts a service.
Do not embed server/provider tokens in public browser bundles. Use your backend
as the authenticated service client unless an explicit browser deployment is
configured. See client source/API and the executable
unrelated Node example.
8. MCP stdio and Streamable HTTP
The service uses the official Python MCP SDK 2.3.0. Each tool accepts
{"request": <shared engine request>} and returns the envelope in
structuredContent plus JSON text. Tools are not automatic permission grants.
For a stdio host, launch the executable with arguments such as:
{
"command": "quorumrelay",
"args": ["mcp", "--database", "/app/state.sqlite", "--config", "/app/trusted-principal.json"]
}
Adapt that command specification to your host's configuration format. Stdout is MCP only. The process owner supplies identity/grants/credentials; stdio does not issue OAuth tokens. A minimal official SDK client is:
import anyio
from mcp import Client
from mcp.client.stdio import StdioServerParameters
async def main():
params = StdioServerParameters(command="quorumrelay", args=["mcp"])
async with Client(params, read_timeout_seconds=90) as client:
response = await client.call_tool("providers", {"request": {}})
print(response.structured_content)
anyio.run(main)
For HTTP set service.mcp=true and connect to the service root plus /mcp.
Authenticated HTTP MCP additionally needs public_url and issuer_url for
protected-resource metadata. The built-in opaque-token policy is not a JWT
signature verifier or authorization server. Python hosts can supply an external
official-SDK token verifier and trusted principal resolver. Validate the audience
and keep service/OAuth identity separate from provider credentials.
Tested revisions are 2026-07-28 and 2025-11-25 on both transports. Modern requests
use per-request version/capabilities; legacy uses initialize/initialized and
SDK-managed sessions. Do not mix their framing/session rules. Cancellation is
cooperative; a completed or uncertain mutation remains in its receipt. Large MCP
uploads use upload_begin/chunk/finish/abort; exported resource links expose
bounded chunks with offsets and hashes. See the full MCP reference and
executable modern/legacy examples.
9. Exact write lifecycle
Writes are separate from ingestion. A configured native adapter does not grant the actor permission or establish live vendor acceptance. The application must supply durable storage, current operation/source policy, provider credentials, explicit writable resources and approval of the exact immutable plan.
- Call
plan_writewith the source and typed changes/files. - Display the actual planned native request, before state, changes and limits.
- Obtain product approval and retain
plan_idplusplan_hash. - Call
commit_writewith only those exact values under the same current scope. - Read
receipt; reconcile ambiguous/lost responses before retrying.
Do not pass identity, credentials or authorization in operation JSON. CLI/HTTP
principal policies require the operations plus write:plan / write:commit
grants; a hash itself is not a grant. Native provider policy additionally binds
namespaces, operations and resource/meeting IDs. Current policy revocation is
checked again at commit and readback. Create operations are not blindly replayed
after a lost response. The exact write contract documents
Legistar, Wistia, Repository and Essential mappings and unresolved live behavior.
Publication is allowed while this vendor acceptance remains deferred. Never turn a synthetic receipt into a claim that a real tenant was changed. The executable examples refuse non-mock commits; they cannot be repurposed as live acceptance without a separately configured designated test environment.
10. Schemas and connector/storage extensions
from quorumrelay.schemas import json_schema
result_schema = json_schema("result")
Schemas use JSON Schema 2020-12 and schema version 2.0.0. Result/status and core fields are versioned; raw original values and unknown additive fields are preserved separately. Generated TypeScript definitions come from the same schema resources. See all generated schema downloads.
Implement Connector from quorumrelay.extensions, run
check_connector(connector), then pass it to create_engine(connectors=(... ,)).
The conformance helper checks hook shape/capability return type without opening
provider sockets or performing writes. It does not certify a provider contract.
New connectors use the same record/result/security boundary across all transports.
An additive parser can be registered with engine.register_parser; baseline
formats cannot be replaced through this path. The runnable
example connector demonstrates registration.
A custom Storage implements atomic put/get/list, caller scope and version
comparison. Preserve create_only / expected_version behavior and immutable
captures. Optional streaming/batch hooks improve large-file behavior; consult
the storage contract. A connector's plan_write method
cannot bypass the shared WriteAdapter/plan/receipt coordinator.
11. Verify an integration without vendor writes
Use the synthetic SDK/CLI/HTTP/Node/MCP examples in examples.
Keep their mock=True checks. Verify preview versus explicit persistence,
cross-tenant denial, useful partial results, duplicate/restart behavior, portable
export hashes and exact receipts. Reuse the same engine fixture across interfaces
instead of implementing provider rules in each client. Public source text is data,
not instructions for an agent or service.
Repository checks are documented in contributing. They do not automatically prove your deployment's credentials, provider access, reverse proxy, OAuth authority, installed application or live write/readback behavior. Record those separately. See release notes for development artifacts and the user manual for operational troubleshooting.