every common web language, two directions

PHP, Python, Ruby, C#, Java,
Node, Rust — one file

Embed the engine in your process, or point any MySQL driver at the wire-compatible server. The language you already have is enough.

direction 1 · embed

The database is a library

The engine compiles into your binary or runtime — or into a C shared library any FFI language loads. No server, no socket: the file is yours, reads and writes happen in-process.

PHP · Python · Ruby · C# · Java (C ABI) · Rust · JavaScript (WASM)

$handle = $ffi->inlaysql_open('app.inlay');
direction 2 · mysql wire

The database is a server

inlaysql serve --mysql opens the file and speaks MySQL's protocol. Every driver that already knows MySQL connects — no new client library, no new configuration surface.

PHP · Python · Ruby · C#/.NET · Java · Node.js · Go

inlaysql serve --mysql app.inlay \ --tls-cert … --tls-required
Use it like SQLite: download the library from the releases page, copy the ~40-line loader for your language below, open the file, run SQL. One correction first: a SQLite driver cannot open the file — the dialect is SQLite's, the bytes are InlaySQL's own (that is what makes the vector and BM25 indexes possible), so the loader below is your driver.

Direction 1 — embed it, like SQLite

Download the prebuilt library from the release (macOS Apple silicon and Linux x86_64 today — the file layer is Unix-only), copy the loader for your language, and run plain SQL with bound parameters. Every loader below is complete — that really is the whole integration.

// PHP 7.4+: FFI is built in. The whole binding — copy into your project.
$db = new InlaySQL('app.inlay');
$db->run('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
$db->run('INSERT INTO users (name) VALUES (?)', ['Ada']);
$rows = $db->run('SELECT id, name FROM users');

// The ~40-line class this uses:
final class InlaySQL {
    private \FFI $ffi;
    private $handle;

    public function __construct(string $path) {
        $this->ffi = \FFI::cdef('
            typedef struct InlaysqlHandle InlaysqlHandle;
            InlaysqlHandle *inlaysql_open(const char *path);
            void inlaysql_close(InlaysqlHandle *handle);
            int inlaysql_exec(InlaysqlHandle *, const char *sql,
                              const char *params, char **out_json);
            const char *inlaysql_last_error(void);
            void inlaysql_free_string(char *s);
        ', 'libinlaysql_ffi.dylib');   // .so on Linux

        $this->handle = $this->ffi->inlaysql_open($path);
        if (\FFI::isNull($this->handle))
            throw new RuntimeException($this->ffi->inlaysql_last_error());
    }
    public function __destruct() { $this->ffi->inlaysql_close($this->handle); }

    public function run(string $sql, array $params = []): array {
        $out = $this->ffi->new('char *');
        $code = $this->ffi->inlaysql_exec($this->handle, $sql,
            $params === [] ? null : json_encode($params), \FFI::addr($out));
        if ($code !== 0)
            throw new RuntimeException($this->ffi->inlaysql_last_error() . " — $sql");
        $result = json_decode(\FFI::string($out), true);
        $this->ffi->inlaysql_free_string($out);
        return $result;   // {"columns":[…],"rows":[[…]]}
    }
}

From the release archive, poc.php is a runnable version with the error path shown. Laravel/Eloquent over this same file: the MySQL wire.

# Python: standard library only (ctypes). The whole binding.
db = InlaySQL('./libinlaysql_ffi.so', 'app.inlay')   # .dylib on macOS
db.run('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)')
db.run('INSERT INTO users (name) VALUES (?)', ['Ada'])
print(db.run('SELECT id, name FROM users')['rows'])

import ctypes, json

class InlaySQL:
    def __init__(self, lib_path, db_path):
        self.lib = ctypes.CDLL(lib_path)
        lib = self.lib
        lib.inlaysql_open.argtypes = [ctypes.c_char_p];  lib.inlaysql_open.restype = ctypes.c_void_p
        lib.inlaysql_exec.argtypes = [ctypes.c_void_p, ctypes.c_char_p,
                                      ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p)]
        lib.inlaysql_exec.restype = ctypes.c_int
        lib.inlaysql_last_error.restype = ctypes.c_char_p
        lib.inlaysql_free_string.argtypes = [ctypes.c_char_p]
        lib.inlaysql_close.argtypes = [ctypes.c_void_p]
        self.handle = lib.inlaysql_open(db_path.encode())
        if not self.handle:
            raise RuntimeError(lib.inlaysql_last_error().decode())

    def run(self, sql, params=None):
        out = ctypes.c_char_p()
        code = self.lib.inlaysql_exec(self.handle, sql.encode(),
            json.dumps(params).encode() if params is not None else None,
            ctypes.byref(out))
        if code != 0:
            raise RuntimeError(self.lib.inlaysql_last_error().decode())
        result = json.loads(out.value)
        self.lib.inlaysql_free_string(out)
        return result

Runnable: poc.py in the release archive.

# Ruby: one gem (gem install ffi). The whole binding.
db = InlaySQL.new('app.inlay')
db.run('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)')
db.run('INSERT INTO users (name) VALUES (?)', ['Ada'])
p db.run('SELECT id, name FROM users')['rows']

require 'ffi'; require 'json'

class InlaySQL
  module Native
    extend FFI::Library
    ffi_lib File.expand_path('./libinlaysql_ffi.dylib', __dir__)
    attach_function :inlaysql_open, [:string], :pointer
    attach_function :inlaysql_close, [:pointer], :void
    attach_function :inlaysql_exec, [:pointer, :string, :string, :pointer], :int
    attach_function :inlaysql_last_error, [], :string
    attach_function :inlaysql_free_string, [:pointer], :void
  end

  def initialize(db_path)
    @handle = Native.inlaysql_open(db_path)
    raise "open failed: #{Native.inlaysql_last_error}" if @handle.null?
  end

  def run(sql, params = nil)
    out = FFI::MemoryPointer.new(:pointer)
    code = Native.inlaysql_exec(@handle, sql, params && JSON.generate(params), out)
    raise Native.inlaysql_last_error unless code.zero?
    result = JSON.parse(out.read_pointer.read_string)
    Native.inlaysql_free_string(out.read_pointer)
    result
  end

  def close = Native.inlaysql_close(@handle)
end

Runnable: poc.rb in the release archive.

use inlaysql::{Database, Value};

let mut db = Database::open("app.inlay")?;

db.execute("CREATE TABLE IF NOT EXISTS docs (
    id INTEGER PRIMARY KEY, title TEXT, body TEXT)", &[])?;

db.execute("INSERT INTO docs (title, body) VALUES (?, ?)",
    &[Value::Text(title.into()), Value::Text(body.into())])?;

let rows = db.query("SELECT id, title FROM docs WHERE id = ?",
    &[Value::Integer(id)])?;

Crate: inlaySQL/inlaysql#![forbid(unsafe_code)], thread-per-handle MVCC.

import { openDatabase } from "@inlaysql/core";
import { opfs } from "@inlaysql/storage";
import { defineModel, field, install, repo } from "@inlaysql/orm";

const Page = defineModel("pages", {
  id: field.integer().primaryKey(),
  body: field.text().index("bm25"),
  embedding: field.vector(384).index("hnsw").embedFrom("body"),
});

const db = await openDatabase({ source: opfs("app.inlay"), create: true });
await install(db, Page);
await repo(db, Page).insert({ body: "full text" });  // embedding computed

const hits = await repo(db, Page).search("full text", { mode: "hybrid" });

Runs in Node, Deno, Bun, the browser and edge runtimes. Live: the SDK demo. Repo: inlaySQL/inlaysql-js.

Direction 2 — the MySQL wire, for your ORM

Start the server once; every sample below assumes it is running. The connection is plaintext until you hand it a certificate — and beyond localhost that is not a suggestion: a --bind that reaches another machine is refused unless the database has accounts of its own, the bootstrap password is not empty, and --tls-cert plus --tls-required are given.

inlaysql serve --mysql app.inlay --password-env INLAYSQL_PASSWORD

# Beyond localhost — the server refuses to start without all of this:
inlaysql user add app.inlay --user app --password-env INLAYSQL_PASSWORD --superuser
inlaysql serve --mysql app.inlay --bind 10.0.1.14 \
  --tls-cert server.pem --tls-key key.pem --tls-required
// PDO — the same shape Laravel/Eloquent runs on. A stock Laravel 11
// skeleton migrates and serves against this server today.
$pdo = new PDO(
    'mysql:host=127.0.0.1;port=3306;dbname=app;charset=utf8mb4',
    'inlaysql', getenv('INLAYSQL_PASSWORD'),
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION],
);

$stmt = $pdo->prepare('INSERT INTO docs (title, body) VALUES (?, ?)');
$stmt->execute([$title, $body]);

foreach ($pdo->query('SELECT id, title FROM docs ORDER BY id LIMIT 20') as $row) {
    echo $row['title'], PHP_EOL;
}

// Upsert and RETURNING work over the wire too:
$pdo->prepare('INSERT INTO docs (id, title) VALUES (?, ?)
               ON CONFLICT (id) DO UPDATE SET title = excluded.title')
    ->execute([$id, $title]);

PHP binds integers as strings; the engine applies SQLite's comparison affinity, so WHERE id = ? with '1' finds the integer row.

import pymysql  # or mysqlclient; SQLAlchemy: mysql+pymysql://…

conn = pymysql.connect(
    host="127.0.0.1", port=3306,
    user="inlaysql", password=os.environ["INLAYSQL_PASSWORD"],
)

with conn.cursor() as cur:
    cur.execute("INSERT INTO docs (title, body) VALUES (%s, %s)", (title, body))
    cur.execute("SELECT id, title FROM docs ORDER BY id LIMIT 20")
    for row in cur.fetchall():
        print(row)

Django/SQLAlchemy point their mysql backend at the server; the SQL surface caveats are documented.

require "mysql2"

client = Mysql2::Client.new(
  host: "127.0.0.1", port: 3306,
  username: "inlaysql", password: ENV["INLAYSQL_PASSWORD"],
)

statement = client.prepare("INSERT INTO docs (title, body) VALUES (?, ?)")
statement.execute(title, body)

client.query("SELECT id, title FROM docs ORDER BY id LIMIT 20")
      .each { |row| puts row["title"] }

Rails: point database.yml at the server with the mysql2 adapter.

using MySqlConnector;

await using var conn = new MySqlConnection(
    "Server=127.0.0.1;Port=3306;User ID=inlaysql;Password=…");
await conn.OpenAsync();

await using var cmd = new MySqlCommand(
    "INSERT INTO docs (title, body) VALUES (@t, @b)", conn);
cmd.Parameters.AddWithValue("@t", title);
cmd.Parameters.AddWithValue("@b", body);
await cmd.ExecuteNonQueryAsync();

Entity Framework: the Pomelo.EntityFrameworkCore.MySql provider rides the same wire. MySqlConnector speaks the binary protocol, which is how BLOB values are written.

try (Connection c = DriverManager.getConnection(
        "jdbc:mysql://127.0.0.1:3306/app", "inlaysql", password);
     PreparedStatement insert = c.prepareStatement(
        "INSERT INTO docs (title, body) VALUES (?, ?)")) {

    insert.setString(1, title);
    insert.setString(2, body);
    insert.executeUpdate();
}

Hibernate, jOOQ, MyBatis ride the same wire. JDBC uses the binary protocol — the one path that accepts BLOB writes today.

import mysql from "mysql2/promise";

// The other Node direction is embedding — see the tabs above, or the
// live SDK demo. Over the wire:
const conn = await mysql.createConnection({
  host: "127.0.0.1", port: 3306, user: "inlaysql",
  password: process.env.INLAYSQL_PASSWORD,
});

await conn.execute("INSERT INTO docs (title, body) VALUES (?, ?)", [title, body]);
const [rows] = await conn.query("SELECT id, title FROM docs ORDER BY id LIMIT 20");

Node has both directions: the WASM SDK in-process, or mysql2 when one server process should own the file.

The fine print, stated

What crosses the C-ABI boundary: every statement returns JSON in one of three shapes ({"kind":"ddl"}, {"kind":"written","rows":n,"last_insert_id":k}, {"columns":…,"rows":…}) — the same shapes on every surface (WASM, wire, FFI). Params are a JSON array; a nested array of numbers is a vector. Errors return non-zero and inlaysql_last_error() carries the engine's message — there are no numeric codes to learn. Over the wire, the SQL surface is the limit, and the rule of this project is that a statement it cannot honour is refused, never accepted and ignored.

Works todayPlan around
Joins, subqueries, CTEs (WITH RECURSIVE), set operationsForeign keys are recorded but not enforced — SQLite's own default
Upserts, INSERT … SELECT, RETURNING on INSERT/UPDATE/DELETEADD CONSTRAINT … FOREIGN KEY after creation can't be recorded — declare keys inside CREATE TABLE
Full constraint DDL in CREATE TABLE; standalone ADD {INDEX|UNIQUE|CONSTRAINT}, TRUNCATE, RENAMEOne handle, one thread (C ABI); BLOB writes need a binary-protocol driver (Java, .NET)
Any declared type name — TIMESTAMP, JSON, LONGTEXT resolve under SQLite's affinitiesNo Windows library yet; the WASM module runs anywhere a browser or Node does

Full detail: docs/server.md and docs/clients.md.