The HTTP server that makes you go wtf!
This is obviously not production ready, nor is it complete, though the features described here do work.
wtfhttpd is a cgi-esque web server written in Go that asks: what if your SQL script was your backend?
It uses file-based routing to map HTTP requests directly to .sql files, executes them against a SQLite database, and renders the results as either JSON or HTML via templates.
In other words, it treats HTTP communication as a database transaction: you read the state of the world from a set of input tables and write your intended changes to an output table.
Routes are determined by the filesystem layout within the webroot directory.
webroot/index.sql->ANY /webroot/users.sql->ANY /userswebroot/users.get.sql->GET /userswebroot/users.post.sql->POST /users
Supported methods are .get, .post, .put, .patch, .delete, .options. Files without a method extension respond to any HTTP method.
wtfhttpd supports path parameters for dynamic routes. If a request is made to /users/123, it will be handled by webroot/users/{id}.get.sql and the parameter will be available in the path_params table.
For each request, wtfhttpd creates a set of temporary tables that you can query.
path_params: Contains URL path parameters.SELECT value FROM path_params WHERE name = 'id'
query_params: Contains URL query string parameters.SELECT value FROM query_params WHERE name = 'search'
request_headers: Contains all HTTP request headers.request_form: Contains all request form data fields (uploads aren't supported yet!)request_meta: Contains metadata likemethod,path, andremote_addr.env_vars: Contains environment variables from the server process that match theenv_prefixfrom config.request_cookies: Contains all cookies sent in the request headers.request_json: Contains the flattened key-value representation of a JSON request body. This table is only populated for requests with aContent-Type: application/jsonheader.
The request_json table has the following schema:
| Column | Type | Description |
|---|---|---|
path |
TEXT | A JSONPath-like string representing the unique key for a value. e.g., $.user.name or $.tags[0]. |
value |
ANY | The primitive value of the key. For objects and arrays, this will be a string like [object] or [array]. |
type |
TEXT | The original JSON type: object, array, string, number, boolean, or null. |
json |
TEXT | If the value is an object or array, this column contains the raw JSON string of that sub-tree, allowing you to use SQLite's built-in json_extract functions on nested data. For other types, this is NULL. |
wtfhttpd automatically parses JSON request bodies for requests that have a Content-Type header of application/json.
The parsed JSON is flattened and inserted into a temporary table called request_json.
For example, given this incoming JSON body:
{
"name": "Alice",
"email": "alice@example.com",
"tags": ["user", "beta"],
"settings": {
"theme": "dark"
}
}You could extract the user's name with the following query:
SELECT value FROM request_json WHERE path = '$.name';
-- Result: 'Alice'Path parameters, form data fields, and query parameters are also provided as named parameters to the SQL files.
For example, a query param called name can be referred to as @name in the SQL file, instead of having to write SELECT value FROM query_params WHERE name = 'name'
There is special handling for form data and query params that end with [], which are intended for array-like use cases (example: checkboxes).
wtfhttpd will strip the [] and encode a json array, which can be accessed in SQL as json_each(@id), assuming the param was called id[]
The order of precedence is Path > Form > Query.
Routes can terminate early by calling the special function wtf_abort.
An optional http code and message can be passed to it.
By default, the result of your final query is returned as application/json.
To control the response, you can INSERT into the response_meta table:
- Set HTTP Status:
INSERT INTO response_meta VALUES ('status', '404'); - Set a Header:
INSERT INTO response_meta VALUES ('Content-Type', 'text/plain'); - Render a Template:
INSERT INTO response_meta VALUES ('wtf-tpl', 'path/to/template.html');
For every request, wtfhttpd parses the Cookie header and populates a temporary, read-only table called request_cookies.
| Column | Type | Description |
|---|---|---|
name |
TEXT | The name of the cookie. |
value |
TEXT | The delicious, crumbly value. |
So, to get a session token, you can simply:
SELECT value FROM request_cookies WHERE name = 'session_token';To set cookies, you can INSERT into the response_cookies table. wtfhttpd will intercept these inserts and transform them into proper Set-Cookie headers.
The response_cookies Schema:
| Column | Type | Description | Default |
|---|---|---|---|
name |
TEXT | The cookie's name. (Required) | |
value |
TEXT | The cookie's value. (Required) | |
max_age |
INTEGER | Lifetime in seconds. A negative value deletes the cookie. | NULL |
expires |
TIMESTAMP | Absolute expiration time. | NULL |
path |
TEXT | The scope of the cookie. | '/' |
domain |
TEXT | The domain scope. | NULL |
secure |
INTEGER | 1 for HTTPS only. Default is 0 for localhost dev. |
0 |
http_only |
INTEGER | 1 makes it inaccessible to JavaScript. A good idea. |
1 |
same_site |
TEXT | CSRF protection. 'Strict', 'Lax', or 'None'. |
'Lax' |
Here's an example of setting cookies:
-- Set the cookie for the browser.
INSERT INTO response_cookies (name, value, max_age, secure, http_only, same_site)
VALUES ('session_token', @session_token, 86400, 1, 1, 'Strict'); -- 24 hours, HTTPS only, extra secure
Directives apply at a query level, and are parsed from SQL comments.
The available directives are:
@wtf-validate <param_name> <validation_rule>: Used for performing input validation.param_namecorresponds to the name of a bound variable,validation_ruleis a string that specifies one or more rules that are comma separated.- Example:
-- @wtf-validate name required,min=5will validate that- A named parameter
@nameis present - It has a minimum length of 5
- In case the validation fails, a HTTP 400 (Bad Request) will be returned.
- Validation rules follow this syntax
- A named parameter
- Example:
@wtf-store <variable_name>: Puts the results of that query into the variable name requested, instead of intoctx. This is useful for binding multiple queries to separate things that can be referred to in the templates or JSON responses.@wtf-capture <variable name> [single]: Puts the result of the query into a named parameter with the variable name requested. This is useful for referring to the value in later queries. If "single" is provided as the second argument, the result named parameter is bound as a scalar. If the second argument is not single or not provided, the named parameter is bound as a json encoded string of the query results.
Templates use jinja2 syntax (via Gonja), and can be anywhere in the webroot, but must have a ".tpl" somewhere in the filename.
A default variable called ctx is present in the template's context, which will contain the results of the last query.
Any stored variables created from @wtf-store are also available.
The following extra functions are available inside the sql environment:
slugify(path)- Returns a slug version of the given pathbcrypt_hash(password, [cost])- Creates a hash for secretsbcrypt_verify(password, hash)- Verifies bcrypt hashed secretschecksum_md5(content)- Creates a md5 checksum (DO NOT USE FOR PASSWORDS)checksum_sha1(content)- Creates a sha1 checksum (DO NOT USE FOR PASSWORDS)cache_get(key)- Fetch a value from the in-memory cachecache_set(key, value)- Store a value in the in-memory cachecache_delete(key)- Delete a key from the in-memory cachesecure_hex(len)- Creates a cryptographically secure hex string of the specified lengthbuild_query(json_object)- Converts a JSON object to a URL query stringparse_query(query_string)- Converts a URL query string to a JSON objecthttp_get(url, [headers_json])- Makes a GET request to the specified URLhttp_post(url, [headers_json], [body])- Makes a POST request to the specified URLhttp_put(url, [headers_json], [body])- Makes a PUT request to the specified URLhttp_patch(url, [headers_json], [body])- Makes a PATCH request to the specified URLhttp_delete(url, [headers_json])- Makes a DELETE request to the specified URLtime_now([format])- Returns the current time in the specified format (default: "2006-01-02 15:04:05")time_format(time_str, target_format, [source_format])- Formats a time string from one format to another (default source format: "2006-01-02 15:04:05")time_add(time_str, duration_str, [format])- Adds a duration to a time string (e.g., "1h30m", "-24h")time_diff(time_str1, time_str2, [format])- Returns the difference between two time strings as a durationtime_relative(time_str, [format])- Returns a human-readable relative time string (e.g., "5 minutes ago", "in 2 days")
The following functions are available for use in SQL:
http_get(url, [headers_json])http_post(url, [headers_json], [body])http_put(url, [headers_json], [body])http_patch(url, [headers_json], [body])http_delete(url, [headers_json])
Function Arguments:
url (TEXT): The full URL to request.headers_json (TEXT, Optional): A JSON string representing the headers to send. Example:'{"Authorization": "Bearer ...", "Accept": "application/json"}'.body (TEXT, Optional): The request body for POST, PUT, and PATCH.
All these functions return a single TEXT value: a JSON string containing the entire response. You can parse the details you need using SQLite's built-in json_extract function.
The returned JSON has the following schema:
{
"status_code": 200,
"status": "200 OK",
"headers": {
"Content-Type": ["application/json; charset=utf-8"],
"Content-Length": ["1234"],
"X-RateLimit-Remaining": ["4999"]
},
"body": "{\"login\":\"octocat\",\"id\":1,\"name\":\"The Octocat\"...}",
"error": null
}If a network error occurs (e.g., DNS failure, timeout), the body would be null and the error field would contain the error message.
wtfhttpd takes security seriously. SQL injection is impossible by design, as everything is either table values or named parameters, and it is impossible for a developer to perform any sort of string concatenation to create sql queries. Values passed to templates are also escaped by default, and there is no way to turn this off.
All registered routes are available in the wtf_routes table. This is used in the admin interface, but is also available for sql scripts to query.
An admin interface protected by HTTP Basic auth is available at /_wtf.
The admin interface shows some basic statistics like uptime and hits, registered routes, and has a database schema viewer, data viewer, and SQL query console.
wtfhttpd may be configured using a wtf.toml config file. The default values are shown below
host = "127.0.0.1"
port = 8080
db = "wtf.db"
web_root = "webroot"
live_reload = true
enable_admin = true
admin_username = "wtfhttpd"
admin_password = "wtfhttpd"
load_dotenv = true
env_prefix = "WTF_"- Every request runs in it's own transaction, and since sqlite doesn't support nested transactions, you may not use transactions in your sql queries.
- A
/is always added to the end of every path. This is may be fixed later.
MIT
Copyright 2025 Ishan Das Sharma
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.