const db = new PGClient(); // or: MySQLClient | FlashQL | EdgeClient | etc const result = await db.query( `SELECT { id, profile: { name, email }, parent: parent_user ~> { name, email } } FROM users;`, // live mode { live: true } );
Show result: output shape + live behaviour
// Structured output (via "{ ... }"): result.rows[0].profile.name; // Foreign key traversal (via "~>"): result.rows[0].parent.name; // Live queries (via { live: true }): // result.rows updates automatically as underlying data changes // (any reactive system can observe these updates) Observer.observe(result.rows[0].parent, 'email', (c) => { console.log(c.value, c.oldValue); });
LinkedQL brings:
- live queries, streaming, subscriptions, and sync
- expressive shorthands for relationships and JSON
- automatic schema versioning and query-time version safety
Runs across:
- PostgreSQL, MySQL/MariaDB, local and in-memory storage
- server, browser, edge, and worker runtimes
- local and remote data as one relational graph
→ one application-level interface in all
→ under 80 KiB (min+zip) in all
One SQL interface for local, remote, and live data
The big picture? SQL, reimagined for modern apps ↗.
Important
LinkedQL is backed by 1,000+ tests and growing, with strong coverage across FlashQL, live queries, edge transport, federation, WAL, sync, and parser/engine behavior.
The main areas still being expanded are broader dialect parity, migrations, and deeper driver hardening across environments.
LinkedQL is distributed as an npm package. Install it with:
npm install @linked-db/linked-qlThe package provides clients for all supported SQL dialects — including FlashQL, the embeddable SQL engine for local or offline use.
Import and use the Client for your database. LinkedQL works the same across all clients.
| Dialect | Import Path | Guide |
|---|---|---|
| PostgreSQL | @linked-db/linked-ql/postgres |
PostgreSQL ↗ |
| MySQL | @linked-db/linked-ql/mysql |
MySQL ↗ |
| MariaDB | @linked-db/linked-ql/mariadb |
MariaDB ↗ |
| FlashQL (In-Memory) | @linked-db/linked-ql/flashql |
FlashQL ↗ |
| EdgeClient | @linked-db/linked-ql/edge |
Edge / Browser ↗ |
| EdgeWorker | @linked-db/linked-ql/edge-worker |
Edge Worker ↗ |
See also:
LinkedQL is designed to be used as a drop-in replacement for a traditional database client. That basically looks like this:
import { PGClient } from '@linked-db/linked-ql/postgres';
const db = new PGClient({
host: 'localhost',
user: 'postgres',
password: 'password',
database: 'myapp',
});
await db.connect();
// Standard SQL query — no new syntax required
const result = await db.query(`
SELECT id, name
FROM public.users
ORDER BY id
`);
console.log(result.rows);
await db.disconnect();PGClientimplements the LinkedQL query interface over a native PostgreSQL connection- The API is intentionally familiar and minimal
But, this is also not just a wrapper over node-postgres. It's the full LinkedQL contract with just a PostgreSQL backend.
In many environments (browser, edge, tests), a direct database connection is not available.
LinkedQL gives you FlashQL for this. FlashQL is the embeddable runtime of LinkedQL — a full SQL engine that runs inside your application. See FlashQL Overview ↗.
import { FlashQL } from '@linked-db/linked-ql/flashql';
const db = new FlashQL();
await db.connect();
// FlashQL accepts multiple statements in a single call
await db.query(`
-- Define schema locally
CREATE TABLE public.users (
id INT PRIMARY KEY,
name TEXT
);
-- Seed local data
INSERT INTO public.users (id, name)
VALUES (1, 'Ada'), (2, 'Linus');
`);
// Query behaves exactly like a remote database
const result = await db.query(`
SELECT *
FROM public.users
ORDER BY id
`);
console.log(result.rows);
await db.disconnect();FlashQLis not a mock store. It's a full relational engine, and the same LinkedQL contract- Execution is entirely local – with a configurable backend
FlashQL brings:
- a transaction-first, MVCC architecture (similar to PostgreSQL's MVCC architecture)
- WAL-backed change tracking (similar to PostgreSQL's Write-Ahead Log (WAL))
- support for views, CTEs, joins, relational queries, and more
FlashQL is best understood as:
LinkedQL, embedded.
It enables:
- local-first architectures
- offline execution
- edge-native data processing
- deterministic replay and sync
without introducing a second query language or API.
LinkedQL exposes a minimal and consistent database interface:
await db.query(sql, options);
await db.query(sql, { live: true, ...options });
await db.stream(sql, options);
await db.transaction(fn);
await db.wal.subscribe(selector, handler);
await db.sync.sync(); // (FlashQL)The same surface applies whether db is a direct PostgreSQL client, a local FlashQL engine, or an EdgeClient.
For result shapes, options, and API detail, see the Query API docs ↗.
db.query() is the base operation.
const result = await db.query(`
SELECT id, name
FROM users
ORDER BY id
`);- Executes a SQL query and returns a materialized result set
- Supports multi-statement execution
Currently available in FlashQL, PostgreSQL, Edge clients. Coming soon to MySQL/MariaDB
Live queries are queries with real-time results. They execute once, but stay in sync with the database over time.
You turn on live mode with { live: true }. You get back a live, self-updating result set.
const liveResult = await db.query(
`SELECT *
FROM users
ORDER BY id`,
{ live: true }
);- Returns a self-updating result set (
result.rows) that will grow, shrink, and mutate in-place to reflect the latest truth of the query as changes happen to the underlying tables - Not limited by query complexity – whether it's joins, filters, aggregates, ordering, or other constructs
- Fully supprted across:
- dialects: PostgreSQL, FlashQL, etc. (MySQL/MariaDB support coming soon)
- runtimes and deployment models: client / server / worker / edge
This is fully covered in Live Queries ↗ and the Realtime Engine deep dive ↗.
db.stream() returns rows lazily instead of materializing the full result up front.
const asyncIterable = await db.stream(`SELECT * FROM users`);
for await (const row of asyncIterable) {
console.log(row);
}- Returns an async iterable
- Lazily fetches rows on demand as you iterate
- The best way to avoids materializing very large result sets in memory all at once
This is covered in Streaming ↗.
db.transaction() provides an atomic execution boundary for one or more operations.
await db.transaction(async (tx) => {
await db.query(`INSERT INTO users (name) VALUES ('Ada')`, { tx });
// optional additional statements
});- Provides an atomic execution boundary
- All operations succeed or fail together
txscopes all queries within the transaction
Named after PostgreSQL's Write Ahead Log (WAL),
db.wal.subscribe() lets you subscribe to structured table-level changefeeds.
await db.wal.subscribe({ public: ['users'] }, (commit) => {
console.log(commit.entries);
});- Subscribes to structured table-level change events
- Each
commitcontains row-level mutations - Enables reactive and event-driven workflows
See Changefeeds (WAL) ↗.
db.sync.sync() is the API for sync and materialized views in FlashQL.
You begin by creating views and pointing them to local or remote tables. You call sync() to execute the contract.
await db.sync.sync();- Triggers the synchronization of declared views with origin tables
- Coordinates background data movement and consistency
- Designed for resilience and network instabilities
This is covered in FlashQL Sync ↗.
| Operation | Behavior |
|---|---|
db.query() |
Materialized result |
db.query(live: true) |
Live, self-updating result |
db.stream() |
Lazy-loading result |
db.transaction() |
Atomic execution |
db.wal.subscribe() |
Changefeed stream |
db.sync.sync() (FlashQL) |
State synchronization |
LinkedQL builds on standard SQL while extending it with constructs that better express modern application data patterns, relationships, and user intent.
LinkedQL allows you to construct structured objects directly in SQL.
SELECT {
id: u.id,
name: u.name
} AS user
FROM users u;SELECT {
id: u.id,
name: u.name,
profile: {
email: u.email,
age: u.age
}
} AS user
FROM users u;This removes:
- manual application-level mapping logic
- repetitive transformation layers
- mismatch between backend and frontend data shapes
The query itself defines the output
Relational queries are often written in terms of joins, but consumed as nested graphs.
LinkedQL introduces DeepRef operators to express that graph directly:
| Operator | Meaning |
|---|---|
~> |
forward traversal (follow a reference) |
<~ |
reverse traversal (find dependents) |
SELECT
id,
parent_user ~> email AS parent_email
FROM users;- Starts from a foreign key (
parent_user) - Follows it to the related row
- Reads
email— without explicitly writing a join
SELECT
id,
(parent_user <~ users).email AS child_email
FROM users;- Walks “backwards” through a relationship
- Resolves rows that reference the current row
SELECT
id,
{ name, email } as profile,
parent_user ~> { id, name, email } AS parent
FROM users;or:
SELECT {
id,
profile: { name, email },
parent: parent_user ~> { id, name, email }
} FROM users;DeepRefs let you express queries in terms of:
the data graph you think in — not the join mechanics SQL requires
This eliminates:
- join boilerplate
- alias bookkeeping
- cognitive overhead in complex queries
For deeper syntax and traversal patterns, see DeepRefs ↗.
The same relationship-aware model applies to writes.
INSERT INTO users
(email, parent_user ~> (id, email))
VALUES
('ada@example.com', ROW (50, 'parent@example.com'));- Inserts the main row
- Inserts or resolves the related row
- Wires the relationship automatically
UPDATE users
SET
email = 'ada.lovelace@example.com',
parent_user ~> email = 'parent+updated@example.com'
WHERE id = 1;Traditional SQL requires:
- multiple statements or CTEs
- strict ordering of operations
- manual foreign-key coordination
LinkedQL keeps the statement SQL-shaped, while relationship-aware payloads desugar into the required lower-level operations. See Structured Writes ↗.
Upsert syntax varies across databases:
- PostgreSQL →
INSERT + ON CONFLICT - MySQL →
INSERT + ON DUPLICATE KEY - MariaDB → similar but not identical
LinkedQL provides a single, predictable form across dialects:
UPSERT INTO users (id, name)
VALUES (1, 'Ada')
RETURNING id, name;- Inserts if the row does not exist
- Updates if it does
- Returns consistent results across runtimes
See UPSERT ↗.
Available only in FlashQL. Coming soon to PostgreSQL, MySQL/MariaDB
Queries can explicitly declare the schema versions they depend on.
SELECT *
FROM public.users@=3;SELECT
u.id,
u.name,
p.title
FROM public.users@=3 u
LEFT JOIN public.posts@=5 p
ON p.author_id = u.id;- Binds queries to expected schema versions
- Fails early if schema has evolved beyond expectations
- Makes schema assumptions visible, reviewable, and enforceable in the query itself
Queries are implicitly written against specific schema shapes. Those assumptions fail at runtime when those schemas evolve.
LinkedQL turns those assumptions into explicit, enforceable contracts. See Version Binding ↗.
These features are designed to be composable.
SELECT {
id: u.id,
name: u.name,
parent: u.parent_user ~> { id, email }
}
FROM public.users@=3 u;Traditional SQL answers:
“What rows do I want?”
LinkedQL additionally answers:
“What shape do I want?” “How are entities connected?” “What assumptions am I making?”
—all in the same query.
This allows SQL to operate directly on application-shaped data, not just tables.
This section is about deploying LinkedQL as a system, not just calling it as an API.
Modern apps rarely have a single execution site or a single storage location. Some queries run against PostgreSQL or MySQL on the server. Some data needs to live inside the app–the model supported by FlashQL. Some flows need edge transport. Some views should stay remote, some should be cached locally, and some should stay continuously synced.
LinkedQL is designed for these patterns.
This is where the architecture story comes in:
- where SQL executes
- where data lives
- how remote and local data meet
- how sync and reactivity fit into the same model
The scenarios below are intentionally generous. They are not just showing that something is possible; they are showing the shape of the architecture, why it is shaped that way, and what role each component plays.
In many applications, the environment issuing queries cannot directly connect to the database.
For example, a database running on the server is often inaccessible to:
- client-side applications (running in the browser)
- applications running on the edge (on edge functions)
LinkedQL introduces a transport protocol that allows db.query() and the rest of the API surface to work across these boundaries: the Edge Protocol, delivered via an EdgeClient / EdgeWorker pair.
import { EdgeClient } from '@linked-db/linked-ql/edge';
// Represents a remote LinkedQL-capable endpoint
const db = new EdgeClient({
url: '/api/db',
type: 'http',
});
const result = await db.query(`
SELECT id, name
FROM public.users
ORDER BY id
`);import { EdgeWorker } from '@linked-db/linked-ql/edge-worker';
import { PGClient } from '@linked-db/linked-ql/postgres';
// Real database connection
const upstream = new PGClient({
host: 'localhost',
user: 'postgres',
password: 'password',
database: 'myapp',
});
await upstream.connect();
// Adapter that exposes the LinkedQL protocol over HTTP
const worker = new EdgeWorker({ client: upstream });
// Now the handler (at "/api/db") that exposes the worker:
export async function POST(request) {
// EdgeClient encodes operations as (op, args)
const op = new URL(request.url).searchParams.get('op');
const args = await request.json();
// Delegate execution to the upstream client
const result = await worker.handle(op, args);
return Response.json(result ?? {});
}The above in a Webflo application would look like the following:
Show code
export async function POST(event, next) {
//await event.user.signIn();
if (next.stepname) return await next();
const op = event.url.query.op;
const args = await event.request.json();
return await worker.handle(op, args, event.client, () => {
event.waitUntil(new Promise(() => { }));
}) || {}; // Always return something to prevent being a 404
}-
EdgeClientis not just a fetch wrapper -
EdgeClientimplements the same client contract asPGClientandFlashQL. The difference is that execution happens across a transport boundary instead of locally or over a direct database connection. -
EdgeWorker:- receives protocol calls
- maps them to a real LinkedQL client (
PGClient,FlashQL, etc.) - and returns results in the same shape
- Removes the need for a separate API layer for database queries
- Preserves the
db.query()abstraction across runtime boundaries - Avoids leaking database credentials to untrusted environments
For runtime setup details, see Dialects & Clients ↗.
This is not just a browser → server abstraction.
EdgeClient can be used anywhere a transport boundary exists:
- server → server
- edge → origin
- worker → worker
- browser (main thread) → web worker (as seen in Scenario 4 below)
The same protocol used over HTTP can also run over message channels.
A typical use case is:
- An app runs on the browser's main thread
- A LinkedQL instance (backed by IndexedDB) runs in a web worker
import { EdgeClient } from '@linked-db/linked-ql/edge';
// Instead of HTTP, this connects to a worker
const db = new EdgeClient({
url: '/db-worker.js',
type: 'worker',
});
const result = await db.query(`
SELECT id, name
FROM public.users
`);import { EdgeWorker } from '@linked-db/linked-ql/edge-worker';
import { PGClient } from '@linked-db/linked-ql/postgres';
const upstream = new PGClient({ /* config */ });
await upstream.connect();
// Automatically wires message port → LinkedQL protocol → upstream client
EdgeWorker.webWorker({ client: upstream });- The same LinkedQL Edge Protocol is used
- Only the transport changes (message ports, instead of HTTP)
- The calling code (
db.query()) remains identical
For more on the runtime setup side, see Dialects & Clients ↗.
As against just sending raw SQL over HTTP, the client (EdgeClient) sends structured operations that map directly to the LinkedQL client interface (e.g. query(), stream(), subscribe(), etc.).
This preserves:
- execution semantics
- result shapes
- and advanced capabilities like live queries
In this scenario, we demonstrate a hybrid data architecture where the goal is to:
Query remote data as if it were local, while controlling what stays remote, what gets cached locally, and what stays in sync.
This is where LinkedQL moves beyond “client” and becomes a data architecture layer.
At a high level, this system works like this:
- You define where data comes from (local vs remote)
- You define how it is stored (none, cached, or realtime)
- LinkedQL ensures everything behaves like a single database
The idea starts with the local database – this time, instantiated with a hook to the remote database.
const db = new FlashQL({
// The hook to remote
async onCreateForeignClient() {
return new EdgeClient({ url: '/api/db', type: 'http' });
}
});
await db.connect();
// The queries
// which can span local and remote tables
await db.query(`
SELECT * FROM remote.users
`);This query may:
- hit a remote database,
- use a local replica,
- or combine both
But it always behaves like a single SQL query.
The system is introduced step by step below. Much of that is mere configurations.
We start with FlashQL as our local database.
Then we teach it how to reach a remote database when needed.
import { FlashQL } from '@linked-db/linked-ql/flashql';
import { EdgeClient } from '@linked-db/linked-ql/edge';
// The local database
const db = new FlashQL({
// Called whenever a query references a foreign origin and needs a client
async onCreateForeignClient(origin) {
if (origin === 'primary') {
// This client will be used to reach the remote database we've designated as 'primary'
return new EdgeClient({
url: '/api/db',
type: 'http',
});
}
throw new Error(`Unknown origin: ${origin}`);
}
});
await db.connect();- FlashQL is your local relational engine
"primary"is simply the name we want to give to a remote database. (FlashQL lets the origin details be application-defined. It can be a bare identifier as used here, or a URL, or something else.)EdgeClientis how FlashQL will talk to that remote system
At this point:
- nothing is mirrored yet
- no data is fetched
- we’ve only defined how to reach the remote from the local
But the local database by itself is ready for use as before:
// FlashQL accepts multiple statements in a single call
await db.query(`
-- Define schema locally
CREATE TABLE public.users (
id INT PRIMARY KEY,
name TEXT
);
-- Seed local data
INSERT INTO public.users (id, name)
VALUES (1, 'Ada'), (2, 'Linus');
`);Moving on to the goal of not just a local database but one that can mirror remote data sources, we'll now create the local "containers" for the remote data.
First, we create a local "namespace" – more traditionally called a "schema" – that contains the tables we'll use to mirror remote tables.
await db.query(`
CREATE SCHEMA remote
WITH (
replication_origin = 'primary',
replication_origin_type = 'edge'
)
`);remoteis a real local namespace (schema) that can have tables and views (VIEWS) just like a regular namespace- The difference is what happens when you create a view inside it: those views will automatically resolve from the remote origin instead of the local database
Think of the views + foreign origin combination as the way to mirror remote data sources.
We move on to that part now.
Now we create views that mirror foreign tables.
LinkedQL provides three mirroring modes:
| Mode | Behavior |
|---|---|
Views defined as: persistence="origin" |
These views don't copy the remote data locally; they simply act as local references to remote data – resolved at query-time. These are called "origin views" |
Views defined as: persistence="materialized" |
These views copy the remote data locally and behave as local tables from that moment on; cached data is refreshed manually. These are called "materialized views" |
Views defined as: persistence="realtime" |
These views copy the remote data locally and behave as local tables from that moment on; but most notably, local data is kept in sync with remote data. These are called "realtime views" |
In short:
origin→ data always remotematerialized→ data cached locally (with optional manual refresh)realtime→ data cached locally and continuously synced
Those three modes are the heart of the local-first story:
- use
originwhen freshness matters more than offline access - use
materializedwhen you want a local cache you can refresh deliberately - use
realtimewhen the local copy should stay warm automatically after initial sync
We demonstrate each mode below.
await db.query(`
CREATE ORIGIN VIEW remote.users AS
SELECT * FROM public.users
`);- No data is stored locally
- Every query hits the remote database
- Acts as a live window into the remote table
This is federation:
Querying external data as if it were part of your local schema
This is the lightest-weight mode. It gives you unification without local storage cost.
await db.query(`
CREATE MATERIALIZED VIEW remote.orders AS
SELECT * FROM public.orders
`);- Data is stored locally inside FlashQL
- It does not update automatically
- It must be refreshed explicitly
This is materialization:
Keeping a local snapshot of remote data for performance or offline use
This is the mode to reach for when:
- the dataset is expensive to fetch repeatedly
- it should remain queryable while offline
- and "fresh on demand" is good enough
await db.query(`
CREATE REALTIME VIEW remote.posts AS
SELECT *
FROM public.posts
WHERE post_type = 'NEWS'
`);- Data is stored locally
- Changes from the remote database are streamed in
- The local copy stays continuously updated
This is realtime mirroring:
A local table that tracks and syncs with the remote table over time
This is the richest mode:
- it starts with local state
- keeps that state queryable even when the app is temporarily disconnected
- and then catches up again when connectivity returns
On having defined the views, you activate the synchronization via:
await db.sync.sync();sync() is the coordination engine for "materialized" and "realtime" views.
It is what turns definitions (VIEWS) into state (local data + subscriptions).
It:
- fetches data for all
materializedviews - does the same for
realtimeviews and starts syncing right away – with backpressure and replay support:- performs catch-up if the app was offline
- ensures local state matches expected remote state
- Idempotent → safe to call multiple times
- Resumable → continues from last known state
- Network-aware → designed for reconnect flows
First: the initial call after defining views:
await db.sync.sync();Second: the optional app-level wiring to the network signal switch:
window.addEventListener('online', () => {
db.sync.sync(); // re-sync on reconnect
});At that point, your local database is no longer just "configured". It is now hydrated, subscribed where necessary, and ready to behave like a unified relational graph.
Note that the initial
db.sync.sync()can be automatically-handled by FlashQL. Simply passautoSync: truein constructor parameters.
At query time, LinkedQL builds a composed execution plan:
originviews are resolved remotelymaterializedandrealtimeviews are resolved locally- results are merged into a single relational execution
const result = await db.query(`
SELECT
u.id,
u.name,
o.total,
p.title
FROM remote.users u -- origin VIEW: resolved on demand from remote DB
LEFT JOIN remote.orders o -- materialized VIEW: served locally
ON o.customer_id = u.id
LEFT JOIN remote.posts p -- realtime VIEW: served locally, kept in sync
ON p.author_id = u.id
LEFT JOIN public.test t -- regular table: served locally
ON t.user_id = u.id
ORDER BY u.id
`);remote.users→ fetched on demand from the remote DBremote.orders→ served from the local cache created by materializationremote.posts→ served locally, then kept hot by realtime syncpublic.test→ an ordinary local table with no remote involvement- The planner treats all of them as one relational graph even though they come from different storage modes
This is the key architectural promise of LinkedQL:
You choose data placement and sync policy per relation, but you still query the result as one database.
For the FlashQL side of this model, see Federation & Sync ↗ and FlashQL Sync ↗.
- You always write:
db.query(SQL) - yet
dbcan be:- a real database
- a local engine
- a remote bridge
- or a composed graph of both
- Views define:
- where data is resolved (local vs remote)
- and how it is kept in sync
sync()keeps everything consistent
| Capability | Description |
|---|---|
| ⚡ Live Queries | Turn on reactivity over SQL with { live: true }, including joins, aggregates, ordering, and windowed result updates. |
| 🔗 DeepRef Operators | Traverse relationships using simple path notation (~> / <~). Insert or update nested structures using the same syntax. |
| 🧩 JSON Literals | Bring JSON-like clarity to SQL with first-class support for JSON notation and object-shaped payloads. |
| 🪄 Upserts | Use a literal UPSERT statement and familiar conflict-handling flows across supported runtimes. |
| 🧠 Version Binding | Bind queries to expected relation versions so an app can fail fast when storage shape no longer matches assumptions. |
| 💾 Edge & Offline Runtime | Run or embed SQL locally in FlashQL — in browsers, workers, or edge devices — with persistence and replay. |
| 🌐 Federation & Sync | Unify remote databases and local stores into a single relational graph with materialized and realtime synced views. |
| Feature | Description |
|---|---|
💻 Classic client.query() Interface |
Same classic client interface, with advanced capabilities for modern applications. |
| 🔗 Multi-Dialect Support | A universal parser that understands PostgreSQL, MySQL, MariaDB, and FlashQL — one client, many dialects. |
| 💡 Lightweight Footprint | A full reactive data layer in one compact library — under 80 KiB (min/zip). |
| 🎯 Automatic Schema Inference | No upfront schema work. LinkedQL auto-discovers your schema and stays schema-driven across complex tasks. |
| 🧱 Embeddable Storage Engine | FlashQL brings MVCC, WAL persistence, selectors, views, sync metadata, and replay into an embeddable engine. |
| 🛰️ Realtime + Sync Pipeline | Local materialized and realtime views can be refreshed and resumed through a single sync entry point. |
| 🪄 Diff-Based Migrations | Planned: evolve schemas declaratively through change detection instead of hand-written migration scripts. |
| Strength | Description |
|---|---|
| MVCC Storage Model (FlashQL) | FlashQL is built around transactional MVCC semantics rather than mutable in-place state. |
| Transaction-First API | Query, stream, live query, edge transport, and explicit transaction flows all align around transactional execution. |
| Views As Primitives (FlashQL) | origin, materialized, and realtime views are first-class building blocks, not bolted-on adapters. |
| Modern WAL API | client.wal.subscribe(...) gives app-facing changefeed consumption without forcing raw database replication ergonomics into the UI layer. |
| Concurrency & Replay (FlashQL) | WAL persistence, replay, catch-up, and conflict-aware storage behavior make local-first flows practical instead of purely optimistic. |
| Version Binding | Queries can assert the relation versions they were designed against and fail fast if app assumptions drift from storage. |
Visit the LinkedQL documentation site ↗
| Jump to | |
|---|---|
| Getting Started ↗ | Get started with LinkedQL in under three minutes. No database required |
| Capabilities Overview ↗ | Jump to the Capabilities section. |
| Streaming ↗ | Follow lazy result iteration and large-result usage. |
| Changefeeds (WAL) ↗ | Read about table-level commit subscriptions. |
| Version Binding ↗ | Bind queries to explicit relation versions. |
| Meet FlashQL ↗ | Meet FlashQL — LinkedQL's embeddable SQL engine. |
| FlashQL Sync ↗ | Explore the sync API and local-first orchestration path. |
| Engineering Deep Dive ↗ | Dig into LinkedQL's engineering in the engineering section. |
LinkedQL is in active development — and contributions are welcome!
Here’s how you can jump in:
- Issues → Spot a bug or have a feature idea? Open an issue.
- Pull requests → PRs are welcome for fixes, docs, or new ideas.
- Discussions → Not sure where your idea fits? Start a discussion.
⤷ clone → install → test
git clone https://github.com/linked-db/linked-ql.git
cd linked-ql
git checkout next
npm install
npm test- Development happens on the
nextbranch — be sure to switch to it as above after cloning. - Consider creating your feature branch from
nextbefore making changes (e.g.git checkout -b feature/my-idea). - Remember to
npm testbefore submitting a PR. - Check the Progress section above to see where help is most needed.
MIT — see LICENSE