Quickstart: in the browser

A database in the page. @fenecdb/web is the engine as WebAssembly and a dependency-free client, so a script opens a database, writes rows and queries them with no server. A few lines and a Vite project, and about five minutes.

You need Node and npm. Nothing here leaves your browser, and the Playground is the same engine in this tab if you would rather type than install.

  1. Make a project and install the package.

    npm create vite@latest my-app -- --template vanilla --no-immediate
    cd my-app
    npm install @fenecdb/web

    With --template and --no-immediate Vite asks nothing and starts nothing: it makes the folder and stops, and the install brings Vite along with the package.

  2. Replace src/main.js with this. It opens a database, makes a collection, writes two rows and shows what two queries return: the rows from 2020 on, newest first, and a search for a word spelled wrong.

    src/main.js
    import { Fenec } from '@fenecdb/web';
    import wasm from '@fenecdb/web/fenec.wasm?url';
    
    const db = await Fenec.open(wasm);
    
    db.run('create collection notes (title text @text, year int @sorted)');
    db.run('put notes {title: $1, year: $2}', ['Dunes', 2021]);
    db.run('put notes {title: $1, year: $2}', ['Night at the oasis', 2024]);
    
    const { rows } = db.run('get notes where year >= $1 order year desc', [2020]);
    const hits = db.rows('get notes select title match title $1 fuzzy', ['oasys']);
    
    const out = document.createElement('pre');
    out.textContent = JSON.stringify(rows) + '\n' + JSON.stringify(hits);
    document.body.append(out);
  3. Start the dev server.

    npm run dev
    prints
    > my-app@0.0.0 dev
    > vite
    
      VITE v8.3.5  ready in 1161 ms
    
      ➜  Local:   http://localhost:5173/
      ➜  Network: use --host to expose
  4. Open http://localhost:5173. The page shows both answers, as JSON.

    the page
    [{"id":2,"title":"Night at the oasis","year":2024},{"id":1,"title":"Dunes","year":2021}]
    [{"title":"Night at the oasis","_score":0.62239116}]
  • The module is a URL. Fenec.open takes the address of fenec.wasm, which Vite gives for a file imported with ?url; npm run build copies it into dist/assets and the page asks for it there. No Vite configuration is needed.
  • db.run is synchronous: it returns the rows and not a promise, since a statement is one call into the module. db.rows is run with the rows alone.
  • The query builder writes the same statements, each value a parameter: await db.from('notes').where('year', '>=', 2020).order('year', 'desc').rows() returns the first answer's rows (The query builder).

Keep it between visits

The database lives in the tab's memory: a reload starts it empty, and the script above makes the collection again. persist stores it in IndexedDB, the image once and then only the writes since, and restore reads it back. This version writes the rows on the first visit alone, and says which visit it is.

src/main.js
import { Fenec, persist, restore } from '@fenecdb/web';
import wasm from '@fenecdb/web/fenec.wasm?url';

const db = await Fenec.open(wasm);

const kept = await restore(db);   // what an earlier visit stored, if one did
if (!kept) {
  db.run('create collection notes (title text @text, year int @sorted)');
  db.run('put notes {title: $1, year: $2}', ['Dunes', 2021]);
  db.run('put notes {title: $1, year: $2}', ['Night at the oasis', 2024]);
  await persist(db);              // into IndexedDB
}

const { rows } = db.run('get notes where year >= $1 order year desc', [2020]);
const hits = db.rows('get notes select title match title $1 fuzzy', ['oasys']);

const out = document.createElement('pre');
out.textContent = (kept ? 'restored from IndexedDB' : 'first visit') + '\n'
  + JSON.stringify(rows) + '\n' + JSON.stringify(hits);
document.body.append(out);

Save it and the page reloads on its own and says first visit. Reload it once more, and the rows come from IndexedDB.

the first visit
first visit
[{"id":2,"title":"Night at the oasis","year":2024},{"id":1,"title":"Dunes","year":2021}]
[{"title":"Night at the oasis","_score":0.62239116}]
a reload
restored from IndexedDB
[{"id":2,"title":"Night at the oasis","year":2024},{"id":1,"title":"Dunes","year":2021}]
[{"title":"Night at the oasis","_score":0.62239116}]

A page that writes later calls persist(db) after the write. To start again, clear the site's data in your browser's developer tools.

No bundler, no npm, and Node

Another bundler does what Vite does: import @fenecdb/web and hand Fenec.open the module's URL, however that bundler spells a file's address. A page whose queries run on a server imports @fenecdb/web/client and loads no module at all (Which package).

With no bundler, any static file server shows the same page: serve node_modules as it is and hand Fenec.open the module's path. Save this as index.html beside node_modules, run python3 -m http.server 8000 (or npx serve) and open http://localhost:8000; it shows the first visit's two lines.

index.html
<!doctype html>
<meta charset="utf-8">
<pre id="out">loading…</pre>
<script type="module">
import { Fenec } from './node_modules/@fenecdb/web/fenec.js';

const db = await Fenec.open('./node_modules/@fenecdb/web/fenec.wasm');

db.run('create collection notes (title text @text, year int @sorted)');
db.run('put notes {title: $1, year: $2}', ['Dunes', 2021]);
db.run('put notes {title: $1, year: $2}', ['Night at the oasis', 2024]);

const { rows } = db.run('get notes where year >= $1 order year desc', [2020]);
const hits = db.rows('get notes select title match title $1 fuzzy', ['oasys']);

document.querySelector('#out').textContent =
  JSON.stringify(rows) + '\n' + JSON.stringify(hits);
</script>

It does not open over file://. A page opened from disk stays on loading…: the browser refuses a module import from a page with no HTTP origin. Any static server works: python3 -m http.server, npx serve, nginx.

No npm: the same files, the module and the collate/ data beside it, are fenec-web-v0.1.12.tar.gz on the release.

In Node, where fetch cannot resolve a relative path, hand Fenec.open the bytes:

query.mjs
import { readFile } from 'node:fs/promises';
import { Fenec } from '@fenecdb/web';

const wasm = await readFile(new URL(import.meta.resolve('@fenecdb/web/fenec.wasm')));
const db = await Fenec.open(wasm);

db.run('create collection notes (title text @text, year int @sorted)');
db.run('put notes {title: $1, year: $2}', ['Dunes', 2021]);
db.run('put notes {title: $1, year: $2}', ['Night at the oasis', 2024]);

console.log(db.run('get notes where year >= $1 order year desc', [2020]).rows);
node query.mjs prints
[
  { id: 2, title: 'Night at the oasis', year: 2024 },
  { id: 1, title: 'Dunes', year: 2021 }
]

Next

  • JavaScript and TypeScript

    The query builder, live queries as an app's state, a database kept in a file of the page, a schema declared in code, and which package a page needs.

  • Sync

    A replica in the page that reads without the network and writes on its own, with a server keeping every page's writes.

  • Quickstart: the server

    The same collection on fenec-server, reached over HTTP from any language, with a page that runs no module at all.

  • FenecQL

    The language: a tour from the first write to a lookup and a total.