Add an organization to your application
Your application serves many organizations. LBB has no customer object. The organization is your word for a namespace. The unit of work you give it, such as a project, a product line or an engagement, is a graph. See how to model your app for the three levels.
Adding an organization is a list of calls. You run them from your backend when the organization signs up. No call changes the server. Every step except step 2 is safe to send again.
| Step | Call | What it makes |
|---|---|---|
| 1 | catalog.createNamespace |
The namespace of the organization. |
| 2 | createGraph |
One graph, with an empty ontology. |
| 3 | catalog.adoptGraph |
The graph becomes a member of the namespace. |
| 4 | ontology.starters.apply |
A base ontology on the graph. |
| 5 | files.upload or integrations.create |
The first data. |
| 6 | workflows.starters.use |
One workflow for each starter workflow you run. |
| 7 | access.createReadToken |
A short-lived token for the organization’s users. |
Before you start
Section titled “Before you start”- Use an environment API key. Every call below needs it. Keep it on your server. It reads and writes every graph of the environment.
- Check the catalog. Steps 1 and 3 need the application catalog.
GET /versionlistsapplication_catalog_v1when the server supports it. The catalog must also be enabled on the deployment. - Check read tokens. Step 7 needs
scoped_graph_reads_v1and the access feature, which is a separate gate. Skip step 7 until your operator enables it. - Check workflows. Step 6 needs the workflow engine. On a local server, run it
with
--workflows.
Graph ids have 1 to 32 lowercase letters, digits, - or _. Derive one from your
own ids, so that names and addresses stay out of storage keys and logs.
import { createHash } from "node:crypto";import { readFile } from "node:fs/promises";import { LbbClient } from "@littlebigbrain/client";
const admin = new LbbClient({ baseUrl: process.env.LBB_URL!, apiKey: process.env.LBB_API_KEY!,});
const organization = { id: "42", name: "acme" };const unit = { id: "7" };
const graphId = `u-${createHash("sha256") .update(`${organization.id}/${unit.id}`) .digest("hex") .slice(0, 24)}`;import hashlibimport os
import httpxfrom lbb import LbbClient
url = os.environ["LBB_URL"]api_key = os.environ["LBB_API_KEY"]admin = LbbClient(url, api_key=api_key)
organization = {"id": "42", "name": "acme"}unit = {"id": "7"}
graph_id = "u-" + hashlib.sha256( f"{organization['id']}/{unit['id']}".encode()).hexdigest()[:24]1. Create the namespace
Section titled “1. Create the namespace”A namespace name has 1 to 64 lowercase letters, digits, - or _. external_key
is your own id for the organization. It is unique in the environment, so you can
find the namespace by it later. The idempotency key makes a retry safe after a lost
reply. Send the same key with the same request.
const namespace = await admin.catalog.createNamespace( { name: organization.name, external_key: `organization:${organization.id}` }, { idempotencyKey: `namespace-${organization.id}` },);const namespaceId = namespace.identity.id;namespace = admin.catalog.create_namespace( {"name": organization["name"], "external_key": f"organization:{organization['id']}"}, idempotency_key=f"namespace-{organization['id']}",)namespace_id = namespace["identity"]["id"]A namespace can hold other namespaces, up to 16 levels with the root. Pass
parent_id to create a level below one, for example projects below acme.
catalog.resolve("acme/projects") finds a namespace by its path.
2. Create the graph
Section titled “2. Create the graph”const project = admin.withScope({ graph: graphId });await project.createGraph();project = LbbClient(url, api_key=api_key, graph=graph_id)project.create_graph()The graph starts with an empty ontology. Run this step once for each unit of work.
A unit that starts from an earlier one can use POST /v1/graph/fork instead, which
copies a graph in the same environment.
3. Adopt the graph
Section titled “3. Adopt the graph”Adoption names the namespace as the owner of the graph. It needs the exact epoch of the graph, as a decimal string. A stale epoch fails, so a recreated graph is never adopted by mistake. The graph id and its history do not change.
const scope = await project.request<{ epoch: string }>("GET", "/v1/workflows/scope");await project.catalog.adoptGraph( { owner_id: namespaceId, expected_graph_epoch: scope.epoch, external_key: `unit:${unit.id}`, }, { idempotencyKey: `adopt-${graphId}` },);scope = httpx.get( f"{url}/v1/workflows/scope", params={"graph": graph_id}, headers={"Authorization": f"Bearer {api_key}"},).json()project.catalog.adopt_graph( { "owner_id": namespace_id, "expected_graph_epoch": scope["epoch"], "external_key": f"unit:{unit['id']}", }, idempotency_key=f"adopt-{graph_id}",)Use a client without the client-wide expectedGraphEpoch option for the catalog
calls. Adoption is permanent in this release. A namespace and a binding cannot be
moved or removed.
4. Apply a base ontology
Section titled “4. Apply a base ontology”An ontology starter is a versioned base for CRM, documents or work management. Preview first. The preview writes nothing.
const starters = admin.graph(graphId).ontology.starters;const preview = await starters.apply("documents", { dryRun: true });console.log(preview.applied_ops.length, preview.ontology_version);const applied = await starters.apply("documents");console.log(applied.status.state); // "applied"starters = admin.graph(graph_id).ontology.starterspreview = starters.apply("documents", dry_run=True)applied = starters.apply("documents")print(applied.status.state.value) # "applied"Applying again changes nothing. A term the graph holds differently makes the call
refuse with 409 starter_conflict, and nothing is written. The organization’s own
classes and fields land as extensions beside the base.
5. Add the first data
Section titled “5. Add the first data”Choose the one that fits the unit of work.
Upload a file. The server stores the bytes once under their SHA-256 and adds a
Document record. The same bytes under the same name change nothing.
const bytes = await readFile("manual.pdf");const { file } = await project.files.upload(bytes, { name: "manual.pdf", contentType: "application/pdf",});console.log(file.sha256);answer = project.files.upload("manual.pdf")print(answer["file"]["sha256"])Connect a source. integrations.create seals the credential, creates the graph
when it does not exist, applies the connector’s starter, checks the fit and starts
the first sync. For a source, send it in place of steps 2 and 4, then adopt the graph
as in step 3. It needs developer access on the environment. See
integrations for your customers.
await admin.integrations.create({ graph: graphId, id: "hubspot", kind: "hubspot", credentials: { HUBSPOT_TOKEN: process.env.HUBSPOT_TOKEN! },});admin.integrations.create( graph=graph_id, id="hubspot", kind="hubspot", credentials={"HUBSPOT_TOKEN": os.environ["HUBSPOT_TOKEN"]},)6. Create the workflows
Section titled “6. Create the workflows”A workflow belongs to one graph. Create each one from its starter workflow. Keep the list in your code, and run the same list for every new graph.
const workflows = admin.graph(graphId).workflows;await workflows.starters.use({ starter: "documents.parse" });await workflows.starters.use({ starter: "documents.link", gate: { below: 0.8 } });project.use_workflow_starter("documents.parse")project.use_workflow_starter("documents.link", gate=0.8)The same request again changes nothing, so a retry is safe. A workflow reads every existing instance first, then follows each published commit. See from documents to a graph for the whole chain and workflows for workflows that run in your worker.
7. Issue a read token
Section titled “7. Issue a read token”Your backend issues the token after it has checked the signed-in user. The token grants SPARQL reads on the namespace and on every graph adopted below it later. It lives for 1 to 900 seconds. It cannot write, read files or start workflows.
const credential = await admin.access.createReadToken({ grants: [{ kind: "namespace", namespace_id: namespaceId }], ttl_seconds: 300,});console.log(credential.expires_at_ms);credential = admin.access.create_read_token( { "grants": [{"kind": "namespace", "namespace_id": namespace_id}], "ttl_seconds": 300, })Give credential.token to the user’s session. It is a bearer credential on the
standard SPARQL route:
curl -X POST "$LBB_URL/sparql?graph=$GRAPH_ID" \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/sparql-query' \ -H 'Accept: application/sparql-results+json' \ --data 'SELECT ?name WHERE { ?d <https://littlebigbrain.com/p/name> ?name } LIMIT 10'Revoke a token early with access.revokeToken(credential.credential_id). See
application read tokens
for the routes a token can use.
Check the result
Section titled “Check the result”curl "$LBB_URL/v1/catalog/graphs?owner_id=$NAMESPACE_ID" \ -H "Authorization: Bearer $LBB_API_KEY"curl "$LBB_URL/v1/ontology/starters?graph=$GRAPH_ID" \ -H "Authorization: Bearer $LBB_API_KEY"curl "$LBB_URL/v1/files/counts?graph=$GRAPH_ID" \ -H "Authorization: Bearer $LBB_API_KEY"curl "$LBB_URL/v1/workflows/triggered?graph=$GRAPH_ID" \ -H "Authorization: Bearer $LBB_API_KEY"The first call lists the graphs of the namespace. The others show the starter status, the files by parse status, and the workflows with their state.
An interrupted adoption finishes when you send the same idempotency key with the
same request again. catalog.pendingOperations() lists the ones that are still open.
What this recipe does not cover
Section titled “What this recipe does not cover”- Removing an organization. A namespace cannot be deleted, renamed or moved in
this release. Deleting a graph (
POST /v1/graph/delete?graph=<id>&confirm=<id>) stops reads through its binding. The namespace stays. - A closed graph. No call closes a graph for writes while it stays readable.
- One call for all steps. Each step above is its own call.
Read the graphs of the organization together with a namespace query.