A multi-tenant MySQL-compatible SQLite service. Provision databases over HTTP, then connect with any MySQL client using generated credentials. Each database is an isolated SQLite file under a single storage volume.
Go binaries using SQLite require CGO because the mattn/go-sqlite3 driver is a CGO wrapper around the C SQLite library. This proxy moves the CGO dependency into a separate process, allowing your main binary to remain pure Go while still using SQLite through a standard MySQL client.
┌─────────────────┐ ┌──────────────────────────┐ ┌─────────────┐
│ Go Binary │ TCP │ sqlite-wire-proxy │ CGO │ SQLite │
│ (no CGO) │────────▶│ (MySQL protocol :3306) │────────▶│ .db file │
│ go-sql-driver │ │ + SQL translator │ │ │
│ /mysql │ │ + Auth / Sessions │ │ │
└─────────────────┘ └──────────────────────────┘ └─────────────┘
- Multi-database storage — one process exposes many tenant SQLite files
- MySQL wire protocol — connect with
mysql, Godatabase/sql, ORMs, etc. - HTTP management API — create, list, and inspect databases
- Per-database credentials — generated username/password permanently binds a connection to one DB
- Concurrent tenants — independent connection pools per database
- WAL mode — better concurrent read performance within a tenant
- ATTACH denied — SQLite authorizer blocks filesystem escape via
ATTACH/DETACH
go build -o bin/sqlite-proxy ./cmd/sqlite-proxy
go build -o bin/sqlite-client ./cmd/sqlite-clientsqlite-proxy requires CGO (mattn/go-sqlite3). sqlite-client is a pure-Go interactive MySQL client for the proxy.
./bin/sqlite-proxy -storage ./data -port 3306 -management-port 8080Or with a config file:
./bin/sqlite-proxy -config config.example.jsoncurl -sX POST http://127.0.0.1:8080/v1/databases \
-H 'Content-Type: application/json' \
-d '{"label":"orders"}'Example response:
{
"name": "db_01jabcdefghijklmnopqrs",
"label": "orders",
"username": "u_01jabcdefghijklmnopqrs",
"password": "generated-secret",
"status": "ready",
"created_at": "2026-07-21T08:30:00Z"
}With the included interactive client:
./bin/sqlite-client \
-h 127.0.0.1 -P 3306 \
-u u_01jabcdefghijklmnopqrs \
-p generated-secret \
db_01jabcdefghijklmnopqrsOr with a standard MySQL client:
mysql -h 127.0.0.1 -P 3306 \
-u u_01jabcdefghijklmnopqrs \
-p'generated-secret' \
db_01jabcdefghijklmnopqrsGo DSN (always use interpolateParams=true):
u_01j...:generated-secret@tcp(127.0.0.1:3306)/db_01j...?interpolateParams=true
sqlite-client is a lightweight REPL that speaks the MySQL wire protocol to the proxy. It supports line editing, tab completion, multi-line input (\ at end of line), and history in ~/.sqlite_history.
./bin/sqlite-client [options] [database]| Option | Description |
|---|---|
-h, --host |
Server host (default 127.0.0.1) |
-P, --port |
Server port (default 3306) |
-u, --user |
Username from POST /v1/databases |
-p, --password |
Password from create/inspect |
--debug |
Print query execution time |
-V, --version |
Show version |
--help |
Show help |
Inside the shell:
| Command | Description |
|---|---|
help / \h |
Show commands |
quit / exit / \q |
Exit |
status / \s |
Connection status |
version / \v |
Client version |
SHOW TABLES / \t |
List tables |
| SQL | Any supported MySQL/SQLite statement |
Example session:
Connected to 127.0.0.1:3306 as u_01jabcdefghijklmnopqrs
Type 'help' for commands, 'quit' to exit.
sqlite> CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
Query OK, 0 rows affected
sqlite> INSERT INTO users (name) VALUES ('Ada');
Query OK, 1 row affected
sqlite> SELECT * FROM users;
+----+------+
| id | name |
+----+------+
| 1 | Ada |
+----+------+
1 row in set
sqlite> quit
├── cmd/
│ ├── sqlite-proxy/ # CLI binary entrypoint
│ │ └── main.go
│ └── sqlite-client/ # Interactive MySQL client
│ └── main.go
├── server/ # MySQL wire protocol handling
│ ├── handler.go # Query execution & result building
│ ├── auth.go # Authentication provider
│ ├── session.go # Connection session management
│ └── errors.go # Error code mapping
├── sqlite/ # SQLite database layer
│ └── pool.go # WAL-mode connection pool
├── storage/ # Multi-tenant storage
│ ├── store.go # Management DB catalog
│ └── database.go # Per-tenant database lifecycle
├── management/ # HTTP management API
│ └── handler.go # CRUD for tenant databases
├── translator/ # SQL dialect translation
│ └── translator.go # MySQL → SQLite SQL rewriter
├── config.go # Configuration structs
└── proxy.go # Main orchestrator
| Component | Responsibility |
|---|---|
| proxy.go | Wires everything together: listener, handler, pool |
| server/handler.go | Implements go-mysql-org/go-mysql/server.Handler. Receives MySQL commands, translates SQL, executes on SQLite, returns MySQL-formatted results |
| server/auth.go | Validates username/password credentials using MySQL's mysql_native_password authentication |
| server/session.go | Tracks active connections, session variables, and handles cleanup |
| storage/store.go | Manages the management.db catalog — authoritative source of database names, labels, and credentials |
| storage/database.go | Per-tenant database lifecycle: creates SQLite files, manages sql.DB pools, enforces isolation |
| sqlite/pool.go | Manages SQLite connections with WAL journal mode for concurrent reads |
| management/handler.go | HTTP API for POST / GET /v1/databases |
| translator/translator.go | Rewrites MySQL SQL syntax to SQLite-compatible SQL |
The translator converts MySQL-specific SQL to SQLite equivalents:
| MySQL | SQLite |
|---|---|
INT AUTO_INCREMENT |
INTEGER (implicit ROWID) |
VARCHAR(n) |
TEXT |
DATETIME |
TEXT |
BOOLEAN |
INTEGER |
ENGINE=InnoDB |
Removed |
DEFAULT CHARSET=utf8mb4 |
Removed |
| MySQL | SQLite |
|---|---|
LIMIT offset, count |
LIMIT count OFFSET offset |
NOW() |
datetime('now') |
CURDATE() |
date('now') |
UNIX_TIMESTAMP() |
strftime('%s','now') |
TRUE / FALSE |
1 / 0 |
Backticks ` |
Double quotes " |
| MySQL | SQLite |
|---|---|
SHOW TABLES |
SELECT name FROM sqlite_master WHERE type='table' |
SHOW DATABASES |
SELECT 'main' AS Database |
DESCRIBE table |
PRAGMA table_info(table) |
SET NAMES utf8mb4 |
SELECT 1 (no-op) |
USE database |
SELECT 1 (no-op) |
The proxy enables WAL (Write-Ahead Logging) mode by default, which allows:
- Multiple concurrent readers
- One writer (non-blocking reads during writes)
- Better performance for concurrent workloads
Configurable connection pool manages concurrent access:
max_connections: Maximum open connections (default 10)- Connections are reused automatically
- Busy timeout prevents lock contention errors
The proxy translates MySQL transaction commands to SQLite:
| MySQL | SQLite |
|---|---|
START TRANSACTION |
BEGIN TRANSACTION |
BEGIN |
BEGIN TRANSACTION |
COMMIT |
COMMIT |
ROLLBACK |
ROLLBACK |
SAVEPOINT name |
SAVEPOINT name (native) |
RELEASE SAVEPOINT name |
RELEASE SAVEPOINT name (native) |
ROLLBACK TO SAVEPOINT name |
ROLLBACK TO SAVEPOINT name (native) |
Example:
START TRANSACTION;
INSERT INTO users (name) VALUES ('Alice');
INSERT INTO users (name) VALUES ('Bob');
COMMIT;
-- Or with savepoints
START TRANSACTION;
INSERT INTO users (name) VALUES ('Charlie');
SAVEPOINT sp1;
INSERT INTO users (name) VALUES ('David');
ROLLBACK TO SAVEPOINT sp1; -- David is undone, Charlie remains
COMMIT;Each client connection maintains its own session state:
- Session variables are tracked per connection
- Transaction state is maintained per session
- Database context (
USE database) is tracked per session - Session cleanup happens automatically on disconnect
./data/
├── management.db # authoritative catalog + credentials
├── db_<ulid>.sqlite # tenant database
├── db_<ulid>.sqlite-wal
└── db_<ulid>.sqlite-shm
management.db is the sole source of truth. There are no per-database JSON files. Mount ./data as a single Docker volume.
Default listen address is 127.0.0.1:8080 (loopback). There is no authentication on the management API in this MVP. Only expose it on a private network if you bind to 0.0.0.0.
| Method | Path | Description |
|---|---|---|
POST |
/v1/databases |
Create a database ({"label":"..."} optional) |
GET |
/v1/databases |
List databases (no passwords) |
GET |
/v1/databases/{name} |
Inspect one database (includes password) |
Create and inspect responses set Cache-Control: no-store.
Anyone who can call the management API or read the storage volume can obtain MySQL credentials.
- Generated MySQL username uniquely maps to one database
- Handshake database name must match (or may be omitted)
- After connect,
USE other_db/COM_INIT_DBcannot switch tenants SHOW DATABASESreturns only the bound database- Each tenant has its own
sql.DBpool (-max-connsis per database) - Tenant connections deny
ATTACH/DETACHand most unsafe PRAGMAs
This is process-local logical isolation, not OS sandboxing.
| Flag | Default | Description |
|---|---|---|
-storage |
./data |
Storage root |
-address |
0.0.0.0 |
MySQL listen address |
-port |
3306 |
MySQL listen port |
-management-address |
127.0.0.1 |
Management HTTP address |
-management-port |
8080 |
Management HTTP port |
-wal |
true |
WAL mode for tenant DBs |
-max-conns |
10 |
Max connections per tenant DB |
-config |
JSON config file | |
-debug |
false |
Debug logging |
See config.example.json.
Typical Docker usage:
- Run one
sqlite-proxycontainer with a persistent volume on/storage - On tenant provision,
POST /v1/databasesand store the returned credentials - App containers connect to the proxy with those MySQL credentials — no per-tenant volume mounts
Constraints:
- Single writer process per storage volume (do not scale MySQL listeners across replicas sharing the same files)
- Back up
management.dband alldb_*.sqlite(+ WAL/SHM) together - Keep the management API private
The proxy has limited support for MySQL prepared statements. Use interpolateParams=true in your Go DSN to use the text protocol instead:
db, _ := sql.Open("mysql", "user:pass@tcp(host:3306)/db?interpolateParams=true")Not all MySQL features are supported. The translator handles common CRUD operations and GORM-generated queries. Unsupported features:
- Stored procedures
- Triggers (MySQL syntax)
ON DUPLICATE KEY UPDATE(removed, not translated)- Full-text search
- Some JSON functions
- CGO-free Go binaries: Access SQLite without CGO in your main binary
- Development: Use familiar MySQL tools with SQLite databases
- Testing: Lightweight SQL database for test environments
- Migration: Gradually move from MySQL to SQLite or vice versa
github.com/go-mysql-org/go-mysql— MySQL wire protocol servergithub.com/mattn/go-sqlite3— SQLite driver (CGO, only in proxy)github.com/go-sql-driver/mysql— MySQL client driver (for Go consumers)
MIT