It's kinda like a forum.
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 be null
. Root posts have no parents.idUser
is the id
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. Use id
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 whose idParent
is equal to this post's id
. This is not necessarily an exhaustive list. Comparing the number of elements in this field to the count
field can tell you if there are more children to load.
children
will always be sorted by id
, with newer posts (larger id
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": "☃" }
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:
POST /codes
)POST /tokens
)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.
POST /posts/:idParent
token
idParent
idParent
is optional. If omitted, this will create a post with idParent
set to null
.x-www-form-urlencoded
body is expected with
content
(any string)
idUser
will be resolvedcount
other than 0
, that's a bugchildren
$ 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
GET /posts/:id
depth
: how deeply to recursively load children
1
0
, the response will not include children
at all0
and 1
right nowafter
: the id
of a post
depth
is 0
children
list (recursively, if multiple depths are ever supported)limit
: the maximum number of children
to load
50
depth
is 0
1
to 500
depth
is greater than 0
, it will include children
idUser
will be resolved for the root post and all children, recursivelycount
is always the total number of children, regardless of the limit
GET /posts
after
)after
: the id
of a post
before
: the id
of a post
limit
: the maximum number of posts to return
200
1
to 500
after
is specified, and there were more than limit
posts to return, this returns... some error code. I'm not sure what though. 410
, maybe?
children
fields, sorted by id
from newest to oldestidUser
will be resolvedPOST /users
x-www-form-urlencoded
email
: the email address for the user.name
: the username. Must contain only alphanumeric characters.200
with the newly created user
400
if the username contains non-alphanumeric characters409
if an account already exists with the specified username or email address, with no response bodyPOST /codes
with the given email addressPOST /codes
x-www-form-urlencoded
email
: the email address of the user for which you would like to create a code200
status code, regardless of whether email
corresponds to a valid email address
DELETE /codes/:code
POST /tokens
x-www-form-urlencoded
code
: a code obtained from a call to POST /codes
idUser
resolved into user
401
GET /tokens
id
specified
DELETE /tokens/:id
id
: the id
of the token to revoke
200
, 404
, or 401
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.
count
value for its parent. It's important that this value stays up-to-date for accurate paging.GET /posts?after=id
, where id
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.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 the Access-Control-Allow-Origin
header and respond to OPTIONS
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.