This is a playground for SPARQL queries, running fully in the browser. The editor is based on Monaco.
It ships with a small demo dataset served by Oxigraph compiled to WebAssembly, and it can also query external SPARQL endpoints.
The left panel holds the list of connections:
- TBBT (Oxigraph in browser) — the built-in store. It needs no network and cannot be deleted.
- Any SPARQL endpoint you add yourself. For each one you can pick how the
query is sent (form-encoded
POST,POSTwith anapplication/sparql-querybody, orGET), add custom headers, and set basic-auth credentials. Try connection probes the endpoint withASK {}, the cheapest query that still proves it speaks SPARQL.
Connections and the per-connection query history are kept in the browser's local storage, and Clear all stored data wipes them. Nothing is ever sent anywhere except to the endpoint you are querying.
Warning
Basic-auth credentials are stored in local storage in plain text and sent on every request to that endpoint. Do not reuse a password you care about.
Because queries are issued straight from the browser, an external endpoint has
to send CORS headers (Access-Control-Allow-Origin) for this site, and a plain
http:// endpoint cannot be reached from the https:// deployment. The app
says so explicitly when a request fails that way.
Paste an IRI, press Dereference, and get a page for it: its label and types, every statement where it is the subject grouped by predicate, and — under Referenced by — every statement where it is the object. Values that are IRIs are links, so you can walk the graph one resource at a time.
You do not have to type IRIs by hand. Any IRI in a query result is clickable, and the Explore panel and canvas nodes both have a button to open the resource they refer to. From the resource page you can go the other way: open the query behind it, drop it onto the Explore canvas, or share it.
Blank nodes are expanded in place, two levels deep, so an address or a nested
structure reads as itself rather than as _:b0. They have no identity outside
the query that found them, so this has to happen in the same query.
Everything is one query — the same one open as a query hands to the editor, so what you see is exactly what was asked. It is bounded at 500 statements, with outgoing statements ranked first so that a resource with a long tail of references keeps its own description; the page says so when it is truncated.
Looked-up resources are kept in this browser, per connection, and listed in the sidebar.
The Explore tab swaps the editor for a browser and a canvas, against the same connection.
On the left, every class in the dataset with its instance count; open one to page through its instances. Both lists load more as you scroll. Each row has a button to open the matching query back in Query mode.
Nodes are named with the endpoint's own label, looked up in this order:
rdfs:labelschema:namefoaf:namedcterms:title- the last segment of the IRI, when none of the above exists
Where several are present the first one listed wins, preferring your own
language and then a label with no language tag. The list lives in
LABEL_PREDICATES in lib/explore.ts.
Drag a class or an instance onto the canvas — or use its +. However a node arrives, including through another node's predicate list, it is checked against everything already on the canvas in both directions, so a node that connects back to something you added earlier shows that edge too. Between classes it also looks for schema-level links: a predicate whose subjects are instances of one class and whose objects are instances of another.
Every predicate joining two nodes gets its own arrow. Parallel edges fan apart and their labels slide to different points along their curves, and a resource pointing at itself is drawn as a loop.
Hovering a node reveals a button to open it in Resource mode directly — useful for a leaf that has nothing leading out of it and so no reason to open the predicate list. Values are leaves by nature: they carry no IRI, so selecting one shows what it is instead of offering a page it cannot have.
Click a node to list its predicates, then a predicate to list its values. Tick any of them — or Select all — to drop them onto the canvas, already wired to the node they came from. Literals land as nodes too.
Drag nodes to arrange them, drag the background to pan, scroll to zoom, and use the percentage in the header to reset the view. The tidy button runs a force-directed layout and fits the result to the window; it starts from where the nodes already are, so running it twice settles rather than reshuffles.
Each connection has its own set of canvases, listed as tabs above the graph. Add one with +, click the active tab to rename it, and use its × to delete it. Everything — node positions, zoom, which tab was open — is saved in this browser per connection, so it is all still there after a reload.
Emptying a canvas drops its stored entry, deleting a connection takes its canvases with it, and Clear all stored data removes them all.
The share button on a canvas produces a link that carries the whole graph, its layout and its name, alongside the endpoint — the same fragment-based link described below. Opening it adds the canvas as a new tab rather than replacing anything. Large canvases make long links, and the dialog says so when one gets long enough that chat clients are likely to cut it.
Every explore query is bounded with LIMIT, and the canvas asks nothing of the
endpoint until you interact with it. Two caveats on large public endpoints: the
class list is a GROUP BY over every typed subject, and the schema-level link
query is the most expensive thing here — both can be slow, and a failure leaves
the canvas without those edges rather than breaking it.
The editor is backed by Qlue-ls, a SPARQL language server compiled to WebAssembly, so all of this runs in the browser with no language server to host:
- Completion that follows the grammar — query forms at the start,
DISTINCTafterSELECT,FILTER/OPTIONAL/BINDinside aWHERE, solution modifiers after it. Press Ctrl+Space to invoke it. - Prefixes declare themselves. Write
foaf:nameand the matchingPREFIX foaf: <http://xmlns.com/foaf/0.1/>is inserted for you. A prefix the playground does not know is left alone and flagged instead of guessed at. - Diagnostics for syntax errors, undeclared prefixes, unused declarations and IRIs that could be shortened.
- Quick fixes on those diagnostics (Ctrl+.), such as replacing a full IRI with its prefixed form.
- Formatting, via the button in the query panel or Shift+Alt+F.
The known prefixes are listed in COMMON_PREFIXES in
lib/languageServer.ts — add your own vocabularies
there.
The language server loads lazily and in the background: the editor is usable immediately and gains these features a moment later. Qlue-ls can also draw completions from the endpoint itself (real subjects and predicates), but that needs per-engine completion queries and would fire SPARQL at the endpoint as you type, so it is deliberately left off.
The Share button in the query panel builds a link that carries the query and
the connection it runs against. Everything is packed into the URL fragment
(after the #), which browsers never send to a server — so the link works on a
static host and its contents stay between you and whoever you send it to.
Opening a shared link:
- if the endpoint is already in the recipient's list, their own connection is used, credentials and all;
- otherwise the connection is added to their list and selected.
When the connection has custom headers or basic auth, sharing asks whether to include them. It does not by default: the link then carries only the endpoint, and the recipient has to fill in the credentials themselves before the query will run. The app tells them so when they open it.
Warning
Choosing to include credentials puts them in the link in plain sight (base64 is encoding, not encryption). Anyone with the link can query that endpoint as you, and it will persist in chat logs, mail and browser history.
The address bar tracks the mode you are in and, in Resource mode, the IRI — so a refresh (or a bookmark) puts you back in the same place. Everything else that makes up "where you were" is already kept in this browser: the active connection, its canvases, and the query in the editor.
Navigation uses the query string (?mode=explore); share links use the
fragment (#s=…), so the two never collide.
Tooltips and confirmations are the app's own, not the browser's. Any element
carrying data-tooltip gets one from a single delegated layer
(components/Tooltips.tsx); it appears after a short
hover, immediately on keyboard focus, never on touch, and — being a popover — it
renders above modal dialogs. Destructive actions ask through
components/ConfirmProvider.tsx, a
promise-shaped stand-in for window.confirm.
npm run test:unit # node --test, no browser
npm run test:e2e # playwright, against the built static export
npm test # bothUnit tests live in test/ and use Node's own test runner. Node runs
the TypeScript directly — there is no build step and no test framework — with
one caveat: type stripping needs every type import marked type, which
verbatimModuleSyntax enforces, and a ten-line resolver hook
(test/hooks.mjs) to map extensionless imports onto .ts.
They cover the pure logic: IRI escaping and query building, the result parsers,
graph and layout maths, share-link encoding, and everything that reads or
writes browser storage.
End-to-end tests live in e2e/ and drive the real static export —
the artefact that actually ships — plus a mock SPARQL endpoint
(e2e/fixtures/mock-endpoint.mjs) that covers
CORS preflight, every request method, basic auth, error statuses and a
paginating dataset. Playwright starts both servers itself, so npm run test:e2e
needs nothing running beforehand — but it does need a build:
npm run build && npm run test:e2eThe mock's request log is shared by every worker, so specs match on their own endpoint path rather than assuming the last entry is theirs.
CI runs lint, typecheck and unit tests in one job and the end-to-end suite in
another; the Pages deploy waits on both and only runs on main.
# Install required dependencies
npm install
# Start the development server
npm run devAnd then open your browser at http://localhost:3000/
To generate the static content, run the following command:
npm run buildAnd it would be available in the out directory.
Licensed under the Apache License, Version 2.0.
Made with ♥ by Ludovic Muller.