Basilica
It's kinda like a forum.
Install / Use
/learn @ianthehenry/BasilicaREADME
Basilica
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
Status
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.
API
Resources
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.
Post
{ "id": 49
, "idParent": 14
, "idUser": 43
, "at": "2014-08-17T01:19:15.139Z"
, "count": 0
, "content": "any string"
, "children": []
}
idis a monotonically increasing identifier, and it is the only field that should be used for sorting posts.idParentcan benull. Root posts have no parents.idUseris theidof the user who created the post.atis 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. Useidfor that.countis the total number of children that this post has, regardless of the number of children returned in any response.childrenis a list of posts whoseidParentis equal to this post'sid. This is not necessarily an exhaustive list. Comparing the number of elements in this field to thecountfield can tell you if there are more children to load.childrenwill always be sorted byid, with newer posts (largerids) in the front of the list
User
{ "id": 32
, "email": "name@example.com"
, "name": "ian"
, "face": {}
}
emailwill be omitted unless otherwise specified in the route documentationfaceis an object that indicates how to render a thumbnail of the user. Currently the only valid options are:{ "gravatar": "a130ced3f36ffd4604f4dae04b2b3bcd" }{ "string": "☃" }- not implemented
Code
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.
Token
{ "id": 91
, "token": "a long string"
, "idUser": 32
}
Authentication
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-Tokenheader)
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.
Routes
Postal Routes
POST /posts/:idParent
- requires a valid
token - for: creating a new post as a child of the specified
idParent idParentis optional. If omitted, this will create a post withidParentset tonull.- arguments: an
x-www-form-urlencodedbody is expected withcontent(any string)- required
- must not be the empty string
- response: the newly created post, JSON-encoded
idUserwill be resolved- if the post has a
countother 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
GET /posts/:id
- 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 includechildrenat all - valid values: just
0and1right now
after: theidof a post- not implemented
- optional
- ignored if
depthis0 - the response will not include any posts created before this in the
childrenlist (recursively, if multiple depths are ever supported)
limit: the maximum number ofchildrento load- not implemented
- default:
50 - ignored if
depthis0 - valid values:
1to500 - applies recursively, if multiple depths are ever supported
- response: a JSON-encoded post
- if
depthis greater than0, it will includechildren idUserwill be resolved for the root post and all children, recursively- remember that
countis always the total number of children, regardless of thelimit
- if
GET /posts
- for: loading every single post in the entire database, catching up after a disconnect (with
after) - arguments: query parameters
after: theidof a post- optional
- the response will only contain posts created after the specified post
before: theidof 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:
1to500
- default:
- response:
- if
afteris specified, and there were more thanlimitposts 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
childrenfields, sorted byidfrom newest to oldest idUserwill be resolved
- if
User Routes
POST /users
- for: signing up for a new account
- arguments:
x-www-form-urlencodedemail: the email address for the user.name: the username. Must contain only alphanumeric characters.
- response:
200with the newly createduser400if the username contains non-alphanumeric characters409if an account already exists with the specified username or email address, with no response body
- side effect: automatically invokes
POST /codeswith the given email address
Auth Routes
POST /codes
- for: creating a new code, which can be used to obtain a token
- arguments:
x-www-form-urlencodedemail: 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
200status code, regardless of whetheremailcorresponds 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.
DELETE /codes/:code
- for: revoking a code, in case it was sent in error
- not implemented
- or documented
POST /tokens
- for: creating a new token
- arguments:
x-www-form-urlencodedcode: 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
idUserresolved intouser - otherwise,
401
- if the code is valid, a JSON-encoded token with
- side effect: invalidates the code specified
GET /tokens
- for: listing tokens
- response: an array of JSON-encoded token objects with only
idspecified- probably other stuff later
- not implemented
DELETE /tokens/:id
- for: revoking a token ("logging out")
- arguments:
id: theidof the token to revoke- required
- response:
200,404, or401 - not implemented
Websockets
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.
Client Implementation Notes
- When a new post is created, clients should update their cached
countvalue 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, whereidis 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.
Basiliclients
- The official browser client, with some i
Related Skills
node-connect
344.4kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
99.2kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
344.4kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
344.4kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
