@nickstrad/prototyper
v0.1.0
Published
SQLite-only browser prototyping with Effect, a terminal, and portable TypeScript contracts
Readme
@nickstrad/prototyper
An ESM-only toolkit for SQLite browser prototypes, with first-class TypeScript types, an Effect database service, a reusable React database editor, an application terminal, a Fetch API explorer, and native SQLite for Node and Deno. A generator creates standalone CLI/API/web/combined prototypes. The source repository includes a root workbench and four examples. Package publication is not part of this repository workflow.
Browser SQLite and editor
During your build, copy the self-hosted assets into a static public directory:
import { copySqliteAssets } from "@nickstrad/prototyper/assets";
await copySqliteAssets("public/sqlite");Create one session for your prototype, then share it with the application and editor. React 19.3+ is required when using the editor:
import { Effect } from "effect";
import { createBrowserSqliteSession } from "@nickstrad/prototyper/sqlite";
import { DatabaseEditor } from "@nickstrad/prototyper/database-editor";
import "@nickstrad/prototyper/database-editor/style.css";
const session = await createBrowserSqliteSession({
assetBaseUrl: new URL("sqlite/", document.baseURI),
persistence: "memory",
schema: "CREATE TABLE notes(id INTEGER PRIMARY KEY, title TEXT NOT NULL);",
seed: "INSERT INTO notes(title) VALUES ('First note');",
});
console.log(
await Effect.runPromise(session.database.execute("SELECT * FROM notes")),
);
// Render inside your React application:
<DatabaseEditor session={session} />;
// When the prototype closes, unmount the UI and await session.dispose().For cancellable startup, pass signal: controller.signal to
createBrowserSqliteSession. Abort it if the host closes before the promise
resolves. After successful acquisition, the signal no longer controls the
session; call session.dispose() when closing. This startup cancellation does
not enable interruption of a running SQLite query.
The editor runs the actual upstream SQLite WASM shell. .help, .tables,
.schema, .mode table, .mode csv, .mode json and .headers use SQLite's
own interpreter and output. Application queries and console commands share one
live connection, including uncommitted transaction state. .open and
.connection are blocked because they would replace that connection.
Use the correct asset base URL below a hosting subdirectory. Serve all four
runtime files together. No runtime CDN or application server is needed. The
service defaults to memory. Request opfs-sahpool with a stable storageKey to
persist across reloads without special headers. A second tab using that key or
unavailable OPFS falls back to memory with a visible reason; inspect
session.database.persistence.actual. Reset restores the initial schema/seed,
and import retains persistence. Export/import reject open transactions.
Cancellation of a running query is unavailable. The /sqlite/transfer entry
provides exportSqlite(database), importSqlite(database, bytes) and
validateSqliteImage(bytes) as typed Effects. Transfer errors distinguish
validation failures, preserved data, and unavailable recovery. The editor uses
these helpers and keeps the shared connection alive after import.
session.database exposes typed Effect operations and a change stream.
session.subscribeChanges(listener) synchronously registers an observer and
returns an unsubscribe function. Notifications include partial-write failures
and rollback invalidation; observer exceptions do not fail committed writes.
Scoped Effect acquisition and layers are exported alongside the session factory.
For direct worker access, /sqlite/worker still exports createSqliteWorker.
Node and Deno SQLite
Native code is a separate entry point and is never bundled into the browser:
import { Effect } from "effect";
import { createNativeSqliteSession } from "@nickstrad/prototyper/sqlite/native";
const session = await createNativeSqliteSession({
schema: "CREATE TABLE notes(title TEXT);",
seed: "INSERT INTO notes VALUES ('Native SQLite');",
});
try {
console.log(
await Effect.runPromise(session.database.execute("SELECT * FROM notes")),
);
} finally {
await session.dispose();
}This uses node:sqlite in Node 22.18+ or Node 24, and Deno 2.7.14. The current
factory defaults to memory; pass filename: "./prototype.sqlite" for persistent
local storage. Existing files retain their data; reset restores the original
schema/seed and persists. Both adapters enable foreign keys before seeding and
resetting. On Deno, result rows are materialized to preserve correct SQL errors
before applying maxRows; the limit bounds returned rows, not transient
allocation. /node and /deno export createNativeHost, runNativeCli, and
their respective serveNode/serveDeno HTTP listener. One host shares its
application and database across CLI and Fetch requests. Close the listener
before disposing the host. The browser editor requires a browser session with
the upstream shell.
Application terminal
import {
createDemoCommands,
createShell,
} from "@nickstrad/prototyper/terminal";
const shell = createShell(createDemoCommands());
console.log(await shell.exec('hello "Ada Lovelace" --json | jq -r .greeting'));attachShell(terminal, shell) connects an existing xterm instance; dispose the
binding when unmounting. Load @xterm/xterm/css/xterm.css in your application.
Portable contracts are exported from @nickstrad/prototyper. JavaScript entry
points have declarations and package-local source maps. There is no CommonJS
build. Styles are an explicit CSS subpath.
MIT for toolkit code. SQLite and Emscripten provenance and notices are included
in assets/sqlite/ and remain applicable to their vendored files.
Fetch API explorer
The portable /api entry exports ApiHandler and ApiRoute types; the React
explorer and its props type are exported from /api-explorer. Supply your own
Fetch handler and keep its identity stable while the prototype is open:
import type { ApiHandler, ApiRoute } from "@nickstrad/prototyper/api";
import { ApiExplorer } from "@nickstrad/prototyper/api-explorer";
import "@nickstrad/prototyper/api-explorer/style.css";
const handler: ApiHandler = async (_request) =>
new Response(JSON.stringify({ message: "Hello" }), {
headers: { "Content-Type": "application/json" },
});
const routes: readonly ApiRoute[] = [
{ method: "GET", path: "/hello", description: "Say hello" },
];
<ApiExplorer handler={handler} routes={routes} />;The explorer constructs a Request and calls the handler directly. It displays status, headers and body; it does not start a server or call fetch. It aborts obsolete requests on edit/replacement/unmount. A handler must honor its request signal if it performs cancellable work. The host retains ownership of the application runtime and database.
createDatabaseCommands(database, runtime) from /terminal registers
structured db tables, db schema [table], db query <sql> [--json],
db reset and db seed commands for the supplied service. The runner provides
runPromise (e.g. an Effect ManagedRuntime). Reset and seed both restore
original seeds. encodeCell from the package root preserves unsafe integers as
{ $type: "bigint", value: "..." } and blobs as
{ $type: "blob", base64: "..." } when encoding query results for JSON.
Generate a prototype
Once the package is available, run:
npx --package @nickstrad/[email protected] prototyper-new my-idea --type combined
cd my-idea
npm install
npm run devChoose cli, api, web, or combined; every type includes DatabaseEditor.
--directory selects a parent directory. Existing destinations are never
overwritten. The /generator entry exports generatePrototype(name, options)
and typed options. For local tarball development, pass its absolute path as
packageSpecifier to the API before installing the generated project.
Scaffolds include ESM TypeScript, npm/Deno tasks, Vite asset copying,
deterministic task data and one shared runtime.
BASE_PATH=/nested/ npm run build supports a static subdirectory. Edit
src/prototype-config.ts to request persistent browser storage; the prototype
name is its storage key.
