Open-source project
proofrock/ws4sqlite avatar
proofrock/ws4sqlite

ws4sqlite: putting a REST interface on top of SQLite files

Query sqlite via json+http

556 stars16 forksGoISC

At a glance

What is it?
ws4sqlite is a Go server that lets you run SQL queries and statements against one or more SQLite files over HTTP with a JSON body. It ships as a single executable, adds transactions, batching, stored statements, scheduled tasks and CORS control, and has a security section that covers read-only databases and stored statements as a way to keep SQL out of client hands.
Who is it for?
Adopt ws4sqlite if you have SQLite files that need to be reachable from somewhere other than the process that owns them, because it is a thin, single-binary HTTP layer over the real SQLite engine rather than a reimplementation of it. Do not adopt it as a replacement for a client-server database, since SQLite's concurrency model and its single-writer nature still apply underneath.
Can I use it commercially?
Yes. ISC is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 85 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The core idea: JSON over HTTP

ws4sqlite is a server application that, applied to one or more SQLite files, allows you perform SQL queries and statements on them via REST, or better, JSON over HTTP.

The phrase or better is doing some work. REST in the strict sense implies resources, URLs and verbs, and this is not that. It is an RPC shape dressed as HTTP: a POST to a path naming a database, with a JSON body describing the statements to run.

The stated use cases are the ones where remote access to a SQLite database is useful or needed, for example a data layer for a remote application, possibly serverless or even called from a web page, after security considerations of course.

That last parenthetical is a warning the author put in the middle of the use cases, and it is the right place for it. Exposing a database over HTTP to a browser is exactly the situation where the stored statements feature and the read-only mode exist.

The quick example shows the shape of it. Launch the server against a file:

bash
ws4sqlite --db mydatabase.db

Then POST to the database path on the default port:

json5
// Set Content-type: application/json
{
    "resultFormat": "map", // "map" or "list"; if omitted, "map"
    "transaction": [
        {
            "statement": "INSERT INTO TEST_TABLE (ID, VAL, VAL2) VALUES (:id, :val, :val2)",
            "values": { "id": 1, "val": "hello", "val2": null }
        },
        {
            "query": "SELECT * FROM TEST_TABLE"
        }
    ]
}

Two things stand out in that body. The resultFormat key selects between maps and lists for result rows, and it defaults to map. And the transaction key holds an array, so a single call can carry several statements, which are executed together.

The answer mirrors the request structure, one entry in the results array per statement in the transaction:

json
{
    "results": [
        {
            "success": true,
            "rowsUpdated": 1
        },
        {
            "success": true,
            "resultSet": [
                { "ID": 1, "VAL": "hello", "VAL2": null }
            ]
        }
    ]
}

So an insert reports rowsUpdated and a select reports a resultSet, and each carries its own success flag. That per-statement success flag is what makes the managed error mode work, which is the next thing worth explaining.

Transactions, error handling and batching

The feature list has three related entries and they are the parts of the design that go furthest beyond what a plain SQL client would do.

All queries of a call are executed in a transaction. So the array in the request body is not a batch of independent commands, it is one unit of work. If one statement fails and the failure is configured to roll back, the whole thing rolls back.

And for each query or statement you can specify if a failure should rollback the whole transaction, or the failure is limited to that query. That is the managed errors feature, and it is the interesting one. It gives you the choice between all-or-nothing and best-effort within a single request, which matters when a batch contains work of different importance.

The results array showing a success flag per entry is the visible consequence. In the example above, a failed statement in a best-effort transaction would report success false while the others still reported success true, and the transaction would have committed what it could.

Batching is the other feature in this group. It is batching of multiple value sets for a single statement, which is different from what most people mean by batching. Rather than running one statement with one set of parameters, you can supply many sets and have the statement run for each, which is the difference between a round trip per row and a single request for a bulk insert.

Parameters may be passed to statements positionally as lists or by name as maps. Named parameters are what the example uses, with the statement written as INSERT INTO TEST_TABLE (ID, VAL, VAL2) VALUES (:id, :val, :val2) and the values supplied as a map keyed by the same names. That is the form that scales, because it does not break when the statement changes.

Results of queries may be returned as key-value maps or as values lists, and the list format is what you want when you are piping results into another system that expects positional data rather than column names.

The underlying choice here is worth noting. ws4sqlite does not wrap everything in one generic request object. There are separate keys for a statement and a query, because a statement takes parameters and a query does not, and a server that models that distinction keeps the two paths from becoming ambiguous.

The features that make it usable as a service

The rest of the feature list is the accumulation of things that turn a proof of concept into something you would leave running.

Stored statements are the headline. Define SQL in the server, and call it from the client. That is the feature that solves the SQL injection problem structurally rather than by filtering, because the client never supplies SQL at all. It is also what makes the security guarantee in the security section possible, since the enforcement mode is that only stored statements may be used.

Serving of multiple databases in the same server instance means one process can front several files, which is the difference between running one container per database and running one. In-memory databases are supported too, which is what you want for a cache-like workload or for tests.

WAL mode is enabled by default, and it can be disabled. WAL is the write-ahead logging mode that lets readers and a writer coexist, and for a server serving concurrent requests it is usually the right default, so having it on by default and off as an option is a sensible arrangement.

Scheduled tasks are cron-like and can also run at startup, and they are configurable per database. A scheduled task can be a backup with rotation, a vacuum, or a set of SQL statements. So the same binary can be both the query endpoint and the maintenance job, and the maintenance is scheduled per database rather than globally.

Initialization statements can be provided to execute when a database is created, which is how you get schema and seed data in place without a separate migration step.

CORS mode is configurable per database. That is the mechanism for letting a browser call the server directly, and making it per-database means you can expose one read-only dataset to a front end while keeping another closed.

There is also an embedded web server to directly serve web pages that can access ws4sqlite without CORS. That solves a specific problem: instead of allowing cross-origin requests from a page, you serve the page from the same origin, which removes the CORS requirement entirely. The readme calls the performance quite fast, with a link to a features page, and the list also mentions a compact codebase, a comprehensive test suite run with make test, and support for 11 operating system and architecture combinations directly, with Docker images for amd64, arm and arm64.

Client libraries are available for Java and JVM and for Go, and the readme links both, saying they abstract the raw JSON-based communication.

The security section, and what is deliberately absent

The security features are grouped separately from the rest, and they are worth reading closely because they describe a tool that is aware of what it is.

Authentication can be configured on the client, either using HTTP Basic Authentication or specifying the credentials in the request, and on the server, either by specifying credentials, also with hashed passwords, or by providing a query to look them up in the database itself. The last option is the interesting one, because it means the credential store is a table in the same SQLite file, so there is no separate user database to keep in step.

The Not Authorized error code is customizable, if 401 is not optimal for whatever is in front of the server.

A database can be opened in read-only mode, in which case only queries are allowed. That is the single most effective control for a server you are exposing, and it is a first-class configuration rather than something you enforce by writing careful SQL.

It is possible to enforce using only stored statements, to avoid some forms of SQL injection and receiving SQL from the client altogether. The phrase some forms of is honest. Stored statements remove the injection vector in the strict sense, since the client cannot send SQL, but a stored statement with a concatenated parameter is still injectable by its author.

CORS Allowed Origin can be configured and enforced, and it is possible to bind to a network interface to limit access. The second of those is the underused one. A server that listens on all interfaces by default is exposed to whatever network it sits on, and binding to a specific interface is a one-line change that removes that exposure.

Then there are the design choices, which are the clearest statements of what the tool will not do. It is a very thin layer over SQLite, so errors and type translation are those provided by the SQLite driver. It doesn't include HTTPS, as this can be done easily and much more securely with a reverse proxy. And it doesn't support SQLite extensions, to improve portability.

That last one is a real cost and a real gain. Extensions are how you get features beyond core SQLite, and refusing them means the binary works identically everywhere SQLite itself does. The gain is portability, and the price is that a workload needing, say, a spatial extension or a custom function cannot use it.

Building and packaging it

The build is a Makefile and a Dockerfile, and both are simple enough to read in full.

The build target cleans up, makes the bin directory, and then compiles with CGO disabled using the trimpath flag before moving the binary into bin. CGO being off is what makes the result a static binary, and trimpath strips the local filesystem paths out of the executable, which is both reproducible and tidy.

The zbuild-all target does the same for a long list of platforms, building for linux on amd64, arm, arm64, riscv64 and s390x, then for darwin on amd64 and arm64, then for windows on amd64 and arm64, with the windows targets producing ws4sqlite.exe. Each build is archived into a tarball or a zip in the bin directory with the platform in the filename.

The Dockerfile has the same two-stage shape. The build stage is golang:latest, copies the source in and runs make build. The runtime stage is gcr.io/distroless/static-debian11, which is a base image with essentially nothing in it, and it copies in only the binary. The image exposes port 12321, declares a volume at /data, and sets the entrypoint to /ws4sqlite.

Distroless is the right choice for this kind of service and it is worth understanding why. A static Go binary on a minimal base means the running container contains the application and essentially nothing else, no shell, no package manager, no libc. That shrinks the image and removes the classes of attack that come with a shell in a production container.

The Dockerfile.binaries file is a separate variant, presumably for the pre-built binaries rather than building from source, and there is a BUILDING.md that the Dockerfile comment points to.

The repository layout shows the usual discipline. There is src/ for the Go code, test/ for the test suite, a profiler/ directory, a CHANGELOG.md, a ROADMAP.md, a CONTRIBUTING.md and a CODE_OF_CONDUCT.md. The licence is ISC, the homepage is a GitBook documentation site, and the release history shows v0.16.6 from 2025-10-31 alongside a v0.17dev7 development tag.

There is also a status note at the top of the readme about a fork named ws4sql that integrates DuckDB alongside SQLite, which is back on track and hopefully to be released as soon as the documentation is in good shape. That is the clearest statement of where the project is going, and it is a bigger idea than the current tool: the same HTTP interface over more than one embedded database engine.

The credits list the dependencies, which shows what the project is built on: two cron libraries, gofiber for the HTTP layer, klauspost compress for compression, mitchellh go-homedir, modernc.org sqlite as the pure Go SQLite driver, and wI2L jettison for JSON handling.

Editorial conclusion

Adopt ws4sqlite if you have SQLite files that need to be reachable from somewhere other than the process that owns them, because it is a thin, single-binary HTTP layer over the real SQLite engine rather than a reimplementation of it. Do not adopt it as a replacement for a client-server database, since SQLite's concurrency model and its single-writer nature still apply underneath. Before you commit, read the security documentation and decide how the server will be exposed, because the project deliberately does not include HTTPS and expects a reverse proxy if it is going on the internet.

Frequently asked questions

What does ws4sqlite do?

It is a server application that, applied to one or more SQLite files, allows you to perform SQL queries and statements on them over HTTP using JSON. The readme describes it as REST, or better, JSON over HTTP, and the stated use cases are a data layer for a remote application, possibly serverless or called from a web page after security considerations.

How does ws4sqlite handle transactions and errors?

All queries of a call are executed in a transaction, and for each statement you can specify whether a failure should roll back the whole transaction or be limited to that query, which the documentation calls managed errors. The response carries a results array with one entry per statement, each with its own success flag, so a best-effort call reports which statements succeeded.

What are stored statements in ws4sqlite?

They are SQL defined in the server and called by name from the client. The security documentation notes that it is possible to enforce using only stored statements, to avoid some forms of SQL injection and to stop the client receiving SQL altogether, which is the strongest control the tool offers for a server exposed to untrusted callers.

Does ws4sqlite support HTTPS and SQLite extensions?

Neither, by design. The design choices section says it doesn't include HTTPS, because this can be done easily and much more securely with a reverse proxy, and that it doesn't support SQLite extensions, to improve portability. Security is handled through authentication, read-only mode, CORS origin configuration and binding to a network interface.

What can scheduled tasks do in ws4sqlite?

They are cron-like, configurable per database, and can also run at startup. A scheduled task can be a backup with rotation, a vacuum, or a set of SQL statements. The server can also serve multiple databases in one instance, support in-memory databases, and enable WAL mode by default.

Official sources

  1. License: ISC
  2. Project website
  3. proofrock/ws4sqlite on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/proofrock-ws4sqlite.svg)](https://hysenlabs.com/projects/proofrock-ws4sqlite)

Was this useful?