You can see a live demo running at https://basilica.horse, which currently hosts both the API and the official Om client.
A basilica is like a forum, but for a few ill-defined differences. For more detail please consult the table below, adapted from a crude sketch I made while drunk.
Forum | Basilica |
---|---|
PHP | Haskell |
90s | 2010s |
trolls | friends |
"rich formatting" | markdown |
paging | lazy tree |
threads ↑ comments ↓ | uniform hierarchy |
<form> |
HTTP API |
inline CSS | bots, webhooks, extensions |
F5 | websockets |
Basilica is usable. It is not a comprehensive, beautiful piece of software, but it works and the canonical instance of it has been live and running since 2014.
Further development is not very likely, since it works well enough for my purposes.
Basilica defines a few resources, which are always communicated in JSON.
Sometimes the API will send resolved data, which means that it will turn:
"idResource": 123
Into:
"resource": { "id": 123, ... }
When it does so will be documented in the route response.
Unless otherwise specified, no value will be null
.
{ "id": 49
, "idParent": 14
, "idUser": 43
, "at": "2014-08-17T01:19:15.139Z"
, "count": 0
, "content": "any string"
, "children": []
}
id
is a monotonically increasing identifier, and it is the only field that should be used for sorting posts.idParent
can benull
. Root posts have no parents.idUser
is theid
of the user who created the post.at
is a string representing the date that the post was created, in ISO 8601 format. This field exists to be displayed to the user; it should not be used for sorting or paging. Useid
for that.count
is the total number of children that this post has, regardless of the number of children returned in any response.children
is a list of posts whoseidParent
is equal to this post'sid
. This is not necessarily an exhaustive list. Comparing the number of elements in this field to thecount
field can tell you if there are more children to load.children
will always be sorted byid
, with newer posts (largerid
s) in the front of the list
{ "id": 32
, "email": "[email protected]"
, "name": "ian"
, "face": {}
}
email
will be omitted unless otherwise specified in the route documentationface
is an object that indicates how to render a thumbnail of the user. Currently the only valid options are:{ "gravatar": "a130ced3f36ffd4604f4dae04b2b3bcd" }
{ "string": "☃" }
- not implemented
Codes are never communicated via JSON, so it doesn't make sense to show their format. Publicly, they can be considered strings. They happen to currently be hexadecimal strings, but that's an implementation detail that may change.
{ "id": 91
, "token": "a long string"
, "idUser": 32
}
There's a goofy hand-rolled auth scheme.
There are no passwords. Authentication is done purely through email. The process looks this:
- request a code (see
POST /codes
) - Basilica emails it to you
- you trade in the code for a token (see
POST /tokens
) - you use that token to authenticate all future requests (by setting the
X-Token
header)
I'm gonna repeat that last thing because it's important: you need to set an X-Token
header to make an authenticated request. No cookies, query parameters, nothing like that. That header is the only thing that counts.
This is similar to the "forgot my password" flow found in most apps, except that you don't have to pretend to remember anything.
- requires a valid
token
- for: creating a new post as a child of the specified
idParent
idParent
is optional. If omitted, this will create a post withidParent
set tonull
.- arguments: an
x-www-form-urlencoded
body is expected withcontent
(any string)- required
- must not be the empty string
- response: the newly created post, JSON-encoded
idUser
will be resolved- if the post has a
count
other than0
, that's a bug - the post will not have
children
$ curl -i # show response headers (otherwise a 401 is very confusing)
-X POST # set the HTTP verb
--data "content=hello%20world" # escape your string!
-H "X-Token: asdf" # requires authentication
"http://localhost:3000/posts" # the actual route
- for: loading posts and post children
- arguments: query parameters
depth
: how deeply to recursively loadchildren
- not implemented
- default:
1
- if
0
, the response will not includechildren
at all - valid values: just
0
and1
right now
after
: theid
of a post- not implemented
- optional
- ignored if
depth
is0
- the response will not include any posts created before this in the
children
list (recursively, if multiple depths are ever supported)
limit
: the maximum number ofchildren
to load- not implemented
- default:
50
- ignored if
depth
is0
- valid values:
1
to500
- applies recursively, if multiple depths are ever supported
- response: a JSON-encoded post
- if
depth
is greater than0
, it will includechildren
idUser
will be resolved for the root post and all children, recursively- remember that
count
is always the total number of children, regardless of thelimit
- if
- for: loading every single post in the entire database, catching up after a disconnect (with
after
) - arguments: query parameters
after
: theid
of a post- optional
- the response will only contain posts created after the specified post
before
: theid
of a post- optional
- the response will only contain posts created before the specified post
limit
: the maximum number of posts to return- default:
200
- valid values:
1
to500
- default:
- response:
- if
after
is specified, and there were more thanlimit
posts to return, this returns... some error code. I'm not sure what though.410
, maybe?- not implemented
- otherwise, a JSON array of posts with no
children
fields, sorted byid
from newest to oldest idUser
will be resolved
- if
- for: signing up for a new account
- arguments:
x-www-form-urlencoded
email
: the email address for the user.name
: the username. Must contain only alphanumeric characters.
- response:
200
with the newly createduser
400
if the username contains non-alphanumeric characters409
if an account already exists with the specified username or email address, with no response body
- side effect: automatically invokes
POST /codes
with the given email address
- for: creating a new code, which can be used to obtain a token
- arguments:
x-www-form-urlencoded
email
: the email address of the user for which you would like to create a code
- response: this route will always return an empty response body with a
200
status code, regardless of whetheremail
corresponds to a valid email address- a timing attack can absolutely be used to determine if the email corresponds to a valid account or not; knock yourself out
- side effect: if the email address specified matches a user account, Basilica will send an email containing the newly created code.
- for: revoking a code, in case it was sent in error
- not implemented
- or documented
- for: creating a new token
- arguments:
x-www-form-urlencoded
code
: a code obtained from a call toPOST /codes
- required
- note: auth tokens don't do anything yet
- response:
- if the code is valid, a JSON-encoded token with
idUser
resolved intouser
- otherwise,
401
- if the code is valid, a JSON-encoded token with
- side effect: invalidates the code specified
- for: listing tokens
- response: an array of JSON-encoded token objects with only
id
specified- probably other stuff later
- not implemented
- for: revoking a token ("logging out")
- arguments:
id
: theid
of the token to revoke- required
- response:
200
,404
, or401
- not implemented
There is currently one websocket route, a single firehose stream of all new posts created, in JSON, with idUser
resolved. The route is just /
, with the ws
or wss
protocol.
When connected, Basilica will periodically send ping frames. If the client doesn't respond in a timely manner, that client will be closed with either a friendly or slightly hostile message.
Currently this is set to ping every 20 seconds and to disconnect clients if more than 40 seconds passes without receiving a pong. Don't rely on those values, though. Just pong the pings as quickly as you can. All websocket libraries should do this for you automatically.
- When a new post is created, clients should update their cached
count
value for its parent. It's important that this value stays up-to-date for accurate paging. - When a disconnect occurs, and it will, reconnect the socket and then call
GET /posts?after=id
, whereid
is the latest post that you knew about. It's important that you reconnect the socket before filling the gap, otherwise any post created in the brief moment after the response and before the socket comes back will be lost.
- The official browser client, with some implemented features.
Basilica uses SQLite. You need to create the database.
$ sqlite3 basilica.db ".read schema.sql"
Basilica is developed using stack
:
$ stack build
After that you can modify the conf
file. Here's a list of all keys and their meanings:
port = 3000
dbpath = "basilica.db"
client-origin = "http://localhost:3333"
client-url = "http://localhost:3333/client/"
mailgun-key = "asdf"
port
is the port that the HTTP and WS server will run on.dbpath
is the path to the SQLite file that you've initialized with the schema.client-origin
is optional. When specified, it will set theAccess-Control-Allow-Origin
header and respond toOPTIONS
requests appropriately. This is especially useful for development when you might be serving the client from a different local port.client-url
is used in emails to generate one-click login links.mailgun-key
is the API key for the Mailgun account you want to use to send emails. If omitted, codes will be written to stdout.
Then you can run it.
$ stack exec basilica
Now you're ready to basilicate.