A federated, text-based social network with a Gemini frontend
Go
210
958 commits
updated Oct 20, 2026
tootik is a text-based social network.
tootik is federated using ActivityPub: users can join an existing instance or set up their own, then interact with users on the same instance and users of compatible servers like Mastodon, Lemmy, Sharkey, Friendica, Akkoma, GoToSocial, Mitra and PieFed.
Unlike other social networks, tootik doesn't have a browser-based interface or an app: instead, its minimalistic, text-based interface is served over Gemini:
Gemini ActivityPub (HTTPS)
→ ⇄
┏━━━━━━━━━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━┓ ┌─────────────────────────┐
┃ Bob's Gemini client ┣━━━┫ tootik instance ┠─┬─┤ Another tootik instance │
┣━━━━━━━━━━━━━━━━━━━━━━━┫ ┣━━━━━━━━━━━━━━━━━┫ │ └─────────────────────────┘
┃2024-01-01 alice ┃ ┃$ ./tootik ... ┃ │ ┌────────────────┐
┃> Hi @bob and @carol! ┃ ┗━┳━━━━━━━━━━━━━━━┛ ├─┤ Something else │
┃... ┃ ┃ │ └────────────────┘
┗━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ ┌───────────────────┐
┏━━━━━━━━━━━━━━━┻━━━━━━━┓ └─┤ Mastodon instance ├─┐
┃ Alice's Gemini client ┃ └───────────────────┘ │
┣━━━━━━━━━━━━━━━━━━━━━━━┫ ┌──────────────────┴────┐
┃2024-01-01 bob ┃ │ Carol's web browser │
┃> Hi @alice! ┃ ├───────────────────────┤
┃... ┃ │╔═╗ alice │
┗━━━━━━━━━━━━━━━━━━━━━━━┛ │╚═╝ 17h ago│
│Hi @bob and @carol! │
│ │
│ ╔═╗ bob │
│ ╚═╝ 16h ago│
│ Hi @alice! │
├───────────────────────┤
│┌────────────┐┌───────┐│
││ Hola ││Publish││
│└────────────┘└───────┘│
└───────────────────────┘
This makes tootik lightweight, private and accessible:
You can join an existing instance or set up your own.
go generate ./migrations
go run -tags fts5 ./cmd/demo
tootik supports three database/sql drivers:
github.com/mattn/go-sqlite3, enabled when CGO_ENABLED=1 or -tags sqlite_mattnmodernc.org/sqlite, enabled when CGO_ENABLED=0 or -tags sqlite_moderncgithub.com/ncruces/go-sqlite3/driver, enabled when -tags sqlite_ncrucesgo generate ./migrations
go build ./cmd/tootik -tags fts5
To build a static executable:
go generate ./migrations
CGO_ENABLED=0 go build ./cmd/tootik
To build a smaller executable without support for profiling, add the no_pprof build tag:
CGO_ENABLED=0 go build -tags no_pprof ./cmd/tootik
┏━━━━━━━┓ ┏━━━━━━━━┓ ┏━━━━━━━━━┓ ┏━━━━━━━━━┓
┃ notes ┃ ┃ shares ┃ ┃ persons ┃ ┃ follows ┃
┣━━━━━━━┫ ┣━━━━━━━━┫ ┣━━━━━━━━━┫ ┣━━━━━━━━━┫
┃object ┃ ┃note ┃ ┃actor ┃ ┃follower ┃
┃author ┃ ┃by ┃ ┃... ┃ ┃followed ┃
┃... ┃ ┃... ┃ ┃ ┃ ┃... ┃
┗━━━━━━━┛ ┗━━━━━━━━┛ ┗━━━━━━━━━┛ ┗━━━━━━━━━┛
Most user-visible data is stored in 4 tables in tootik's database:
notes, which contains Object objects that represent postsshares, which records "user A shared post B" relationshipspersons, which contains Actor objects that represent usersfollows, which records "user A follows user B" relationshipsnotes.author, shares.by, follows.follower and follows.followed point to rows in persons.
shares.note points to a row in notes.
┌───────┐ ┌────────┐ ┌─────────┐ ┌─────────┐ ┏━━━━━━━━┓ ┏━━━━━━━━┓
│ notes │ │ shares │ │ persons │ │ follows │ ┃ outbox ┃ ┃ inbox ┃
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ┣━━━━━━━━┫ ┣━━━━━━━━┫
│object │ │note │ │actor │ │follower │ ┃activity┃ ┃activity┃
│author │ │by │ │... │ │followed │ ┃sender ┃ ┃sender ┃
│... │ │... │ │ │ │... │ ┃... ┃ ┃... ┃
└───────┘ └────────┘ └─────────┘ └─────────┘ ┗━━━━━━━━┛ ┗━━━━━━━━┛
Federation happens through two tables, inbox and outbox. Both contain Activity objects that represent actions performed by the users in persons.
inbox contains activities by users on other servers, while outbox contains activities of local users.
┏━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━┓
┃ gmi.Wrap ┣━┫ gemini.Listener ┃
┗━━━━━━━━━━┛ ┗━━━━━━━━┳━━━━━━━━┛
┏━━━━━━━┻━━━━━━━━━━━┓
┃ front.Handler ┃
┗━━━━━━━━━┳━━━━━━━━━┛
┌───────┐ ┌────────┐ ┌────┸────┐ ┌─────────┐ ┌────────┐ ┌────────┐
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤
│object │ │note │ │actor │ │follower │ │activity│ │activity│
│author │ │by │ │... │ │followed │ │sender │ │sender │
│... │ │... │ │ │ │... │ │... │ │... │
└───────┘ └────────┘ └─────────┘ └─────────┘ └────────┘ └────────┘
gemini.Listener is a Gemini server that handles requests using Handler. It adds rows to persons during new user registration and changes rows when users change properties like their display name.
gemini.Listener provides Handler with a writer that builds a Gemini response and asynchronously sends it to the client in chunks, while Handler continues to handle the request and append more lines to the page.
┌──────────┐ ┌─────────────────┐
│ gmi.Wrap ├─┤ gemini.Listener │
└──────────┘ └────────┬────────┘
┌───────┴───────────┐
┏━━━━━━━━━━━┥ front.Handler │
┃ └┰────────┬───────┰─┘
┌───┸───┐ ┌──────┸─┐ ┌────┴────┐ ┌┸────────┐ ┌────────┐ ┌────────┐
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤
│object │ │note │ │actor │ │follower │ │activity│ │activity│
│author │ │by │ │... │ │followed │ │sender │ │sender │
│... │ │... │ │ │ │... │ │... │ │... │
└───────┘ └────────┘ └─────────┘ └─────────┘ └────────┘ └────────┘
In addition, Gemini requests can:
notes (new post)notes (post editing)shares (user shares a post)shares (user no longer shares a post)follows (user A followed user B)follows (user A unfollowed user B) ┌──────────┐ ┌─────────────────┐
│ gmi.Wrap ├─┤ gemini.Listener │
└──────────┘ └────────┬────────┘
┌───────┴───────────┐
┌───────────┤ front.Handler ┝━━━━━━━━━━┓
│ └┬────────┬───────┬─┘ ┃
┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴────────┐ ┌─┸──────┐ ┌────────┐
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤
│object │ │note │ │actor │ │follower │ │activity│ │activity│
│author │ │by │ │... │ │followed │ │sender │ │sender │
│... │ │... │ │ │ │... │ │... │ │... │
└───────┘ └────────┘ └─────────┘ └─────────┘ └────────┘ └────────┘
User actions like post creation or deletion are recorded as Activity objects written to outbox.
┏━━━━━━━━━━━━━━━┓
┌──────────┐ ┌─────────────────┐ ┃ outbox.Mover ┃
│ gmi.Wrap ├─┤ gemini.Listener │ ┃ outbox.Poller ┃
└──────────┘ └────────┬────────┘ ┃ fed.Syncer ┃
┌───────┴───────────┐ ┗━━━┳━━━━━┳━━━━━┛
┌───────────┤ front.Handler ├─────╂────┐┃
│ └┬────────┬───────┬─┘ ┃ │┃
┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┸┐ ┌─┴┸─────┐ ┌────────┐
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤
│object │ │note │ │actor │ │follower │ │activity│ │activity│
│author │ │by │ │... │ │followed │ │sender │ │sender │
│... │ │... │ │ │ │... │ │... │ │... │
└───────┘ └────────┘ └─────────┘ └─────────┘ └────────┘ └────────┘
tootik may perform automatic actions and push additional activities to outbox, on behalf of the user:
follows rows for this user and what other servers know ┌───────────────┐
┌──────────┐ ┌─────────────────┐ │ outbox.Mover │
│ gmi.Wrap ├─┤ gemini.Listener │ │ outbox.Poller │
└──────────┘ └────────┬────────┘ │ fed.Syncer │
┌───────┴───────────┐ └───┬─────┬─────┘
┌───────────┤ front.Handler ├─────┼────┐│
│ └┬────────┬───────┬─┘ │ ││
┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴─────┐ ┌────────┐
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤
│object │ │note │ │actor │ │follower │ │activity│ │activity│
│author │ │by │ │... │ │followed │ │sender │ │sender │
│... │ │... │ │ │ │... │ │... │ │... │
└───────┘ └────────┘ └─────────┘ └───────┰─┘ └┰───────┘ └────────┘
┏┻━━━━┻━━━━━┓
┃ fed.Queue ┃
┗━━━━━━━━━━━┛
fed.Queue polls outbox and delivers these activities to followers on other servers. It uses the deliveries table to track delivery progress and retry failed deliveries.
┌───────────────┐
┌──────────┐ ┌─────────────────┐ │ outbox.Mover │
│ gmi.Wrap ├─┤ gemini.Listener │ │ outbox.Poller │
└──────────┘ └────────┬────────┘ │ fed.Syncer │
┌───────┴───────────┐ └───┬─────┬─────┘
┌───────────┤ front.Handler ├─────┼────┐│
│ └┬────────┬───────┬─┘ │ ││
┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴─────┐ ┌────────┐
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤
│object │ │note │ │actor │ │follower │ │activity│ │activity│
│author │ │by │ │... │ │followed │ │sender │ │sender │
│... │ │... │ │ │ │... │ │... │ │... │
└───────┘ └────────┘ └────┰────┘ └───────┬─┘ └┬───────┘ └────────┘
┃ ┌┴────┴─────┐
┏━━━━━━━┻━━━━━━┓ ┏━━━━┥ fed.Queue │
┃ fed.Resolver ┣━┛ └───────────┘
┗━━━━━━━━━━━━━━┛
Resolver is responsible for fetching Actors that represent users of other servers, using an ID or a user@domain pair and WebFinger. The fetched objects are cached in persons, and contain properties like the user's inbox URL and public key.
fed.Queue uses Resolver to make a list of unique inbox URLs each activity should be delivered to. If this is a wide delivery (a public post or a post to followers) and multiple recipients reside on the same server, fed.Queue delivers the activity to this server only once.
┌───────────────┐
┌──────────┐ ┌─────────────────┐ │ outbox.Mover │
│ gmi.Wrap ├─┤ gemini.Listener │ │ outbox.Poller │
└──────────┘ └────────┬────────┘ │ fed.Syncer │
┌───────┴───────────┐ └───┬─────┬─────┘ ┏━━━━━━━━━━━━━━┓
┌───────────┤ front.Handler ├─────┼────┐│ ┏━━┫ fed.Listener ┣━━━━━━┓
│ └┬────────┬───────┬─┘ │ ││ ┃ ┗━━━━━┳━━━━━━━━┛ ┃
┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┸┐ ┌─────┸──┐ ┃
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ ┃
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ ┃
│object │ │note │ │actor │ │follower │ │activity│ │activity│ ┃
│author │ │by │ │... │ │followed │ │sender │ │sender │ ┃
│... │ │... │ │ │ │... │ │... │ │... │ ┃
└───────┘ └────────┘ └────┬────┘ └───────┬─┘ └┬───────┘ └────────┘ ┃
│ ┌┴────┴─────┐ ┃
┌───────┴──────┐ ┌────┤ fed.Queue │ ┃
│ fed.Resolver ├─┘ └───────────┘ ┃
└───────┰──────┘ ┃
┃ ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
Requests from other servers are handled by fed.Listener, a HTTP server.
It extracts the signature and key ID from a request using httpsig.Extract, uses Resolver to fetch the public key if needed, validates the request using Verify and inserts the received Activity object into inbox.
┌───────────────┐
┌──────────┐ ┌─────────────────┐ │ outbox.Mover │
│ gmi.Wrap ├─┤ gemini.Listener │ │ outbox.Poller │
└──────────┘ └────────┬────────┘ │ fed.Syncer │
┌───────┴───────────┐ └───┬─────┬─────┘ ┌──────────────┐
┌───────────┤ front.Handler ├─────┼────┐│ ┌──┤ fed.Listener ├──────┐
│ └┬────────┬───────┬─┘ │ ││ │ └─────┬────────┘ │
┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┴┐ ┌─────┴──┐ │
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ │
│object │ │note │ │actor │ │follower │ │activity│ │activity│ │
│author │ │by │ │... │ │followed │ │sender │ │sender │ │
│... │ │... │ │ │ │... │ │... │ │... │ │
└───┰───┘ └───┰────┘ └────┬────┘ └────┰──┬─┘ └┬───────┘ └──────┰─┘ │
┃ ┃ │ ┃ ┌┴────┴─────┐ ┏━━━━┻━━━━━━━━┓ │
┃ ┃ ┌───────┴──────┐ ┌──╂─┤ fed.Queue │ ┃ inbox.Queue ┃ │
┃ ┃ │ fed.Resolver ├─┘ ┃ └───────────┘ ┗━┳━┳━┳━━━━━━━┛ │
┃ ┃ └───────┬──────┘ ┗━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │
┃ ┗━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │
┗━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
└───────────────────────────────────────────────────┘
Once inserted into inbox, inbox.Queue processes the received activities:
Create activities to notesnotes according to Update activitiesAnnounce activities in sharesfollows as accepted, when the followed user sends an Accept activityfollows when a remote user sends a Follow activity to a local user ┌───────────────┐
┌──────────┐ ┌─────────────────┐ │ outbox.Mover │
│ gmi.Wrap ├─┤ gemini.Listener │ │ outbox.Poller │
└──────────┘ └────────┬────────┘ │ fed.Syncer │
┌───────┴───────────┐ └───┬─────┬─────┘ ┌──────────────┐
┌───────────┤ front.Handler ├─────┼────┐│ ┌──┤ fed.Listener ├──────┐
│ └┬────────┬───────┬─┘ │ ││ │ └─────┬────────┘ │
┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┴┐ ┌─────┴──┐ │
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ │
│object │ │note │ │actor │ │follower │ │activity│ │activity│ │
│author │ │by │ │... │ │followed │ │sender │ │sender │ │
│... │ │... │ │ │ │... │ │... │ │... │ │
└───┬───┘ └───┬────┘ └────┬────┘ └────┬──┬─┘ └┬──────┰┘ └──────┬─┘ │
│ │ │ │ ┌┴────┴─────┐┃ ┌────┴────────┐ │
│ │ ┌───────┴──────┐ ┌──┼─┤ fed.Queue │┗━━━━┥ inbox.Queue │ │
│ │ │ fed.Resolver ├─┘ │ └───────────┘ └─┬─┬─┬───────┘ │
│ │ └───────┬──────┘ └─────────────────────┘ │ │ │
│ └───────────┼───────────────────────────────────┘ │ │
└─────────────────────┼─────────────────────────────────────┘ │
└───────────────────────────────────────────────────┘
Sometimes, a received or newly created local Activity is forwarded to the followers of a local user:
outbox and forwarded to all followers of the local user.outbox and forwarded to all community members. ┌───────────────┐
┌──────────┐ ┌─────────────────┐ │ outbox.Mover │
│ gmi.Wrap ├─┤ gemini.Listener │ │ outbox.Poller │
└──────────┘ └────────┬────────┘ │ fed.Syncer │
┌───────┴───────────┐ └───┬─────┬─────┘ ┌──────────────┐
┌───────────┤ front.Handler ├─────┼────┐│ ┌──┤ fed.Listener ├──────┐
│ └┬────────┬───────┬─┘ │ ││ │ └─────┬────────┘ │
┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┴┐ ┌─────┴──┐ │
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ │
│object │ │note │ │actor │ │follower │ │activity│ │activity│ │
│author │ │by │ │... │ │followed │ │sender │ │sender │ │
│... │ │... │ │ │ │... │ │... │ │... │ │
└───┬───┘ └───┬────┘ └────┬────┘ └────┬──┬─┘ └┬──────┬┘ └──────┬─┘ │
│ │ │ │ ┌┴────┴─────┐│ ┌────┴────────┐ │
│ │ ┌───────┴──────┐ ┌──┼─┤ fed.Queue │└────┤ inbox.Queue │ │
│ │ │ fed.Resolver ├─┘ │ └───────────┘ └─┬─┬─┬─┰─────┘ │
│ │ └───────┬─┰────┘ └─────────────────────┘ │ │ ┃ │
│ └───────────┼─╂─────────────────────────────────┘ │ ┃ │
└─────────────────────┼─╂───────────────────────────────────┘ ┃ │
└─╂─────────────────────────────────────╂───────────┘
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
To display details like the user's name and speed up the verification of future incoming replies, inbox.Queue uses Resolver to fetch the Actor objects of mentioned users (if needed).
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┌───────────────┐ ┃
┌──────────┐ ┌─────────────────┐ ┃ │ outbox.Mover │ ┃
│ gmi.Wrap ├─┤ gemini.Listener │ ┃ │ outbox.Poller │ ┃
└──────────┘ └────────┬────────┘ ┃ │ fed.Syncer │ ┃
┌───────┴──────────┸┐ └───┬─────┬─────┘ ┌──────────────┐ ┃
┌───────────┤ front.Handler ├─────┼────┐│ ┌──┤ fed.Listener ├──╂───┐
│ └┬────────┬───────┬─┘ │ ││ │ └─────┬────────┘ ┃ │
┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┴┐ ┌─────┴──┐ ┏━━━━━━┻━┓ │
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ ┃ feed ┃ │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ ┣━━━━━━━━┫ │
│object │ │note │ │actor │ │follower │ │activity│ │activity│ ┃follower┃ │
│author │ │by │ │... │ │followed │ │sender │ │sender │ ┃note ┃ │
│... │ │... │ │ │ │... │ │... │ │... │ ┃... ┃ │
└─┰─┬───┘ └─┰─┬────┘ └──┰─┬────┘ └──┰─┬──┬─┘ └┬──────┬┘ └──────┬─┘ ┗━━━━━━┳━┛ │
┃ │ ┃ │ ┏━━━━━━━┛ │ ┃ │ ┌┴────┴─────┐│ ┌────┴────────┐ ┃ │
┃ │ ┃ │ ┃ ┌───────┴──────┐ ┌╂─┼─┤ fed.Queue │└────┤ inbox.Queue │ ┃ │
┃ │ ┃ │ ┃ │ fed.Resolver ├─┘┃ │ └───────────┘ └─┬─┬─┬─┬─────┘ ┃ │
┃ │ ┃ │ ┃ └───────┬─┬────┘ ┃ └─────────────────────┘ │ │ │ ┃ │
┃ │ ┃ └─╂─────────┼─┼───────╂─────────────────────────┘ │ │ ┃ │
┃ └───────╂───╂─────────┼─┼───────╂───────────────────────────┘ │ ┃ │
┃ ┃ ┃ └─┼───────╂─────────────────────────────┼───────╂───┘
┏━┻━━━━━━━━━┻━━━┻━━━┓ └───────╂─────────────────────────────┘ ┃
┃ inbox.FeedUpdater ┣━━━━━━━━━━━━━━━┛ ┃
┗━━━━━━━━━┳━━━━━━━━━┛ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
To speed up each user's feed, inbox.FeedUpdater periodically appends rows to the feed table. This table holds all information that appears in the user's feed: posts written or shared by followed users, author information and more, eliminating the need for join queries, slow filtering by post visibility, deduplication and sorting by time when a user views their feed. This table is indexed by user and time, allowing fast querying of a single feed page for a particular user.
┏━━━━━━━━━━━━━━━━┓
┏━━━━━━━━━━━━━┫ cluster.Server ┣━━━━━━┓
┃ ┗━━━━━━━━━━━━━━━━┛ ┃
┃ ┌──────────────────────────╂───────────┐
┃ │ ┌───────────────┐ ┃ │
┌──────────┐ ┌────────┸────────┐ │ │ outbox.Mover │ ┃ │
│ gmi.Wrap ├─┤ gemini.Listener │ │ │ outbox.Poller │ ┃ │
└──────────┘ └────────┬────────┘ │ │ fed.Syncer │ ┃ │
┌───────┴──────────┴┐ └───┬─────┬─────┘ ┌─────┸────────┐ │
┌───────────┤ front.Handler ├─────┼────┐│ ┌──┤ fed.Listener ├──┼───┐
│ └┬────────┬───────┬─┘ │ ││ │ └─────┬────────┘ │ │
┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┴┐ ┌─────┴──┐ ┌──────┴─┐ │
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ │ feed │ │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ ├────────┤ │
│object │ │note │ │actor │ │follower │ │activity│ │activity│ │follower│ │
│author │ │by │ │... │ │followed │ │sender │ │sender │ │note │ │
│... │ │... │ │ │ │... │ │... │ │... │ │... │ │
└─┬─┬───┘ └─┬─┬────┘ └──┬─┬────┘ └──┬─┬──┬─┘ └┬──────┬┘ └──────┬─┘ └──────┬─┘ │
│ │ │ │ ┌───────┘ │ │ │ ┌┴────┴─────┐│ ┌────┴────────┐ │ │
│ │ │ │ │ ┌───────┴──────┐ ┌┼─┼─┤ fed.Queue │└────┤ inbox.Queue │ │ │
│ │ │ │ │ │ fed.Resolver ├─┘│ │ └───────────┘ └─┬─┬─┬─┬─────┘ │ │
│ │ │ │ │ └─────┰─┬─┬────┘ │ └─────────────────────┘ │ │ │ │ │
│ │ │ └─┼───────╂─┼─┼───────┼─────────────────────────┘ │ │ │ │
│ └───────┼───┼───────╂─┼─┼───────┼───────────────────────────┘ │ │ │
│ │ │ ┃ └─┼───────┼─────────────────────────────┼───────┼───┘
┌─┴─────────┴───┴───┐ ┃ └───────┼─────────────────────────────┘ │
│ inbox.FeedUpdater ├───╂───────────┘ │
└─────────┬─────────┘ ┃ │
└─────────────╂─────────────────────────────────────────────────┘
┏━━━━━━━━┻━━━━━━━┓
┃ cluster.Client ┃
┗━━━━━━━━━━━━━━━━┛
The cluster package contains complex tests that simulate interaction between users on multiple servers. These tests are easy to write, they're fast and they run in parallel without affecting each other.
The tests use three main constructs: Client, Server and Cluster.
During tests, all http.Requests sent by tootik (like those sent by fed.Resolver) are sent through Client.
Server handles all kinds of incoming requests:
Client holds a mapping between domain names and Servers: it allows these servers to talk to each other by passing the http.Request sent by one server to the http.Handler of another.
Cluster is a high-level wrapper for easy creation of multiple Servers capable of federating with each other, given a list of domain names.
tootik is free and unencumbered software released under the terms of the Apache License Version 2.0; see LICENSE for the license text.
The ASCII art logo in front/logo.go was made using FIGlet.
1,186 followers · starred Oct 2024
160 followers · starred Apr 2026
442 followers · starred Aug 2026
82 followers · starred Oct 2024
A federated, text-based social network with a Gemini frontend
Go
210
958 commits
updated Oct 20, 2026
tootik is a text-based social network.
tootik is federated using ActivityPub: users can join an existing instance or set up their own, then interact with users on the same instance and users of compatible servers like Mastodon, Lemmy, Sharkey, Friendica, Akkoma, GoToSocial, Mitra and PieFed.
Unlike other social networks, tootik doesn't have a browser-based interface or an app: instead, its minimalistic, text-based interface is served over Gemini:
Gemini ActivityPub (HTTPS)
→ ⇄
┏━━━━━━━━━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━┓ ┌─────────────────────────┐
┃ Bob's Gemini client ┣━━━┫ tootik instance ┠─┬─┤ Another tootik instance │
┣━━━━━━━━━━━━━━━━━━━━━━━┫ ┣━━━━━━━━━━━━━━━━━┫ │ └─────────────────────────┘
┃2024-01-01 alice ┃ ┃$ ./tootik ... ┃ │ ┌────────────────┐
┃> Hi @bob and @carol! ┃ ┗━┳━━━━━━━━━━━━━━━┛ ├─┤ Something else │
┃... ┃ ┃ │ └────────────────┘
┗━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ ┌───────────────────┐
┏━━━━━━━━━━━━━━━┻━━━━━━━┓ └─┤ Mastodon instance ├─┐
┃ Alice's Gemini client ┃ └───────────────────┘ │
┣━━━━━━━━━━━━━━━━━━━━━━━┫ ┌──────────────────┴────┐
┃2024-01-01 bob ┃ │ Carol's web browser │
┃> Hi @alice! ┃ ├───────────────────────┤
┃... ┃ │╔═╗ alice │
┗━━━━━━━━━━━━━━━━━━━━━━━┛ │╚═╝ 17h ago│
│Hi @bob and @carol! │
│ │
│ ╔═╗ bob │
│ ╚═╝ 16h ago│
│ Hi @alice! │
├───────────────────────┤
│┌────────────┐┌───────┐│
││ Hola ││Publish││
│└────────────┘└───────┘│
└───────────────────────┘
This makes tootik lightweight, private and accessible:
You can join an existing instance or set up your own.
go generate ./migrations
go run -tags fts5 ./cmd/demo
tootik supports three database/sql drivers:
github.com/mattn/go-sqlite3, enabled when CGO_ENABLED=1 or -tags sqlite_mattnmodernc.org/sqlite, enabled when CGO_ENABLED=0 or -tags sqlite_moderncgithub.com/ncruces/go-sqlite3/driver, enabled when -tags sqlite_ncrucesgo generate ./migrations
go build ./cmd/tootik -tags fts5
To build a static executable:
go generate ./migrations
CGO_ENABLED=0 go build ./cmd/tootik
To build a smaller executable without support for profiling, add the no_pprof build tag:
CGO_ENABLED=0 go build -tags no_pprof ./cmd/tootik
┏━━━━━━━┓ ┏━━━━━━━━┓ ┏━━━━━━━━━┓ ┏━━━━━━━━━┓
┃ notes ┃ ┃ shares ┃ ┃ persons ┃ ┃ follows ┃
┣━━━━━━━┫ ┣━━━━━━━━┫ ┣━━━━━━━━━┫ ┣━━━━━━━━━┫
┃object ┃ ┃note ┃ ┃actor ┃ ┃follower ┃
┃author ┃ ┃by ┃ ┃... ┃ ┃followed ┃
┃... ┃ ┃... ┃ ┃ ┃ ┃... ┃
┗━━━━━━━┛ ┗━━━━━━━━┛ ┗━━━━━━━━━┛ ┗━━━━━━━━━┛
Most user-visible data is stored in 4 tables in tootik's database:
notes, which contains Object objects that represent postsshares, which records "user A shared post B" relationshipspersons, which contains Actor objects that represent usersfollows, which records "user A follows user B" relationshipsnotes.author, shares.by, follows.follower and follows.followed point to rows in persons.
shares.note points to a row in notes.
┌───────┐ ┌────────┐ ┌─────────┐ ┌─────────┐ ┏━━━━━━━━┓ ┏━━━━━━━━┓
│ notes │ │ shares │ │ persons │ │ follows │ ┃ outbox ┃ ┃ inbox ┃
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ┣━━━━━━━━┫ ┣━━━━━━━━┫
│object │ │note │ │actor │ │follower │ ┃activity┃ ┃activity┃
│author │ │by │ │... │ │followed │ ┃sender ┃ ┃sender ┃
│... │ │... │ │ │ │... │ ┃... ┃ ┃... ┃
└───────┘ └────────┘ └─────────┘ └─────────┘ ┗━━━━━━━━┛ ┗━━━━━━━━┛
Federation happens through two tables, inbox and outbox. Both contain Activity objects that represent actions performed by the users in persons.
inbox contains activities by users on other servers, while outbox contains activities of local users.
┏━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━┓
┃ gmi.Wrap ┣━┫ gemini.Listener ┃
┗━━━━━━━━━━┛ ┗━━━━━━━━┳━━━━━━━━┛
┏━━━━━━━┻━━━━━━━━━━━┓
┃ front.Handler ┃
┗━━━━━━━━━┳━━━━━━━━━┛
┌───────┐ ┌────────┐ ┌────┸────┐ ┌─────────┐ ┌────────┐ ┌────────┐
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤
│object │ │note │ │actor │ │follower │ │activity│ │activity│
│author │ │by │ │... │ │followed │ │sender │ │sender │
│... │ │... │ │ │ │... │ │... │ │... │
└───────┘ └────────┘ └─────────┘ └─────────┘ └────────┘ └────────┘
gemini.Listener is a Gemini server that handles requests using Handler. It adds rows to persons during new user registration and changes rows when users change properties like their display name.
gemini.Listener provides Handler with a writer that builds a Gemini response and asynchronously sends it to the client in chunks, while Handler continues to handle the request and append more lines to the page.
┌──────────┐ ┌─────────────────┐
│ gmi.Wrap ├─┤ gemini.Listener │
└──────────┘ └────────┬────────┘
┌───────┴───────────┐
┏━━━━━━━━━━━┥ front.Handler │
┃ └┰────────┬───────┰─┘
┌───┸───┐ ┌──────┸─┐ ┌────┴────┐ ┌┸────────┐ ┌────────┐ ┌────────┐
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤
│object │ │note │ │actor │ │follower │ │activity│ │activity│
│author │ │by │ │... │ │followed │ │sender │ │sender │
│... │ │... │ │ │ │... │ │... │ │... │
└───────┘ └────────┘ └─────────┘ └─────────┘ └────────┘ └────────┘
In addition, Gemini requests can:
notes (new post)notes (post editing)shares (user shares a post)shares (user no longer shares a post)follows (user A followed user B)follows (user A unfollowed user B) ┌──────────┐ ┌─────────────────┐
│ gmi.Wrap ├─┤ gemini.Listener │
└──────────┘ └────────┬────────┘
┌───────┴───────────┐
┌───────────┤ front.Handler ┝━━━━━━━━━━┓
│ └┬────────┬───────┬─┘ ┃
┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴────────┐ ┌─┸──────┐ ┌────────┐
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤
│object │ │note │ │actor │ │follower │ │activity│ │activity│
│author │ │by │ │... │ │followed │ │sender │ │sender │
│... │ │... │ │ │ │... │ │... │ │... │
└───────┘ └────────┘ └─────────┘ └─────────┘ └────────┘ └────────┘
User actions like post creation or deletion are recorded as Activity objects written to outbox.
┏━━━━━━━━━━━━━━━┓
┌──────────┐ ┌─────────────────┐ ┃ outbox.Mover ┃
│ gmi.Wrap ├─┤ gemini.Listener │ ┃ outbox.Poller ┃
└──────────┘ └────────┬────────┘ ┃ fed.Syncer ┃
┌───────┴───────────┐ ┗━━━┳━━━━━┳━━━━━┛
┌───────────┤ front.Handler ├─────╂────┐┃
│ └┬────────┬───────┬─┘ ┃ │┃
┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┸┐ ┌─┴┸─────┐ ┌────────┐
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤
│object │ │note │ │actor │ │follower │ │activity│ │activity│
│author │ │by │ │... │ │followed │ │sender │ │sender │
│... │ │... │ │ │ │... │ │... │ │... │
└───────┘ └────────┘ └─────────┘ └─────────┘ └────────┘ └────────┘
tootik may perform automatic actions and push additional activities to outbox, on behalf of the user:
follows rows for this user and what other servers know ┌───────────────┐
┌──────────┐ ┌─────────────────┐ │ outbox.Mover │
│ gmi.Wrap ├─┤ gemini.Listener │ │ outbox.Poller │
└──────────┘ └────────┬────────┘ │ fed.Syncer │
┌───────┴───────────┐ └───┬─────┬─────┘
┌───────────┤ front.Handler ├─────┼────┐│
│ └┬────────┬───────┬─┘ │ ││
┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴─────┐ ┌────────┐
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤
│object │ │note │ │actor │ │follower │ │activity│ │activity│
│author │ │by │ │... │ │followed │ │sender │ │sender │
│... │ │... │ │ │ │... │ │... │ │... │
└───────┘ └────────┘ └─────────┘ └───────┰─┘ └┰───────┘ └────────┘
┏┻━━━━┻━━━━━┓
┃ fed.Queue ┃
┗━━━━━━━━━━━┛
fed.Queue polls outbox and delivers these activities to followers on other servers. It uses the deliveries table to track delivery progress and retry failed deliveries.
┌───────────────┐
┌──────────┐ ┌─────────────────┐ │ outbox.Mover │
│ gmi.Wrap ├─┤ gemini.Listener │ │ outbox.Poller │
└──────────┘ └────────┬────────┘ │ fed.Syncer │
┌───────┴───────────┐ └───┬─────┬─────┘
┌───────────┤ front.Handler ├─────┼────┐│
│ └┬────────┬───────┬─┘ │ ││
┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴─────┐ ┌────────┐
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤
│object │ │note │ │actor │ │follower │ │activity│ │activity│
│author │ │by │ │... │ │followed │ │sender │ │sender │
│... │ │... │ │ │ │... │ │... │ │... │
└───────┘ └────────┘ └────┰────┘ └───────┬─┘ └┬───────┘ └────────┘
┃ ┌┴────┴─────┐
┏━━━━━━━┻━━━━━━┓ ┏━━━━┥ fed.Queue │
┃ fed.Resolver ┣━┛ └───────────┘
┗━━━━━━━━━━━━━━┛
Resolver is responsible for fetching Actors that represent users of other servers, using an ID or a user@domain pair and WebFinger. The fetched objects are cached in persons, and contain properties like the user's inbox URL and public key.
fed.Queue uses Resolver to make a list of unique inbox URLs each activity should be delivered to. If this is a wide delivery (a public post or a post to followers) and multiple recipients reside on the same server, fed.Queue delivers the activity to this server only once.
┌───────────────┐
┌──────────┐ ┌─────────────────┐ │ outbox.Mover │
│ gmi.Wrap ├─┤ gemini.Listener │ │ outbox.Poller │
└──────────┘ └────────┬────────┘ │ fed.Syncer │
┌───────┴───────────┐ └───┬─────┬─────┘ ┏━━━━━━━━━━━━━━┓
┌───────────┤ front.Handler ├─────┼────┐│ ┏━━┫ fed.Listener ┣━━━━━━┓
│ └┬────────┬───────┬─┘ │ ││ ┃ ┗━━━━━┳━━━━━━━━┛ ┃
┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┸┐ ┌─────┸──┐ ┃
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ ┃
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ ┃
│object │ │note │ │actor │ │follower │ │activity│ │activity│ ┃
│author │ │by │ │... │ │followed │ │sender │ │sender │ ┃
│... │ │... │ │ │ │... │ │... │ │... │ ┃
└───────┘ └────────┘ └────┬────┘ └───────┬─┘ └┬───────┘ └────────┘ ┃
│ ┌┴────┴─────┐ ┃
┌───────┴──────┐ ┌────┤ fed.Queue │ ┃
│ fed.Resolver ├─┘ └───────────┘ ┃
└───────┰──────┘ ┃
┃ ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
Requests from other servers are handled by fed.Listener, a HTTP server.
It extracts the signature and key ID from a request using httpsig.Extract, uses Resolver to fetch the public key if needed, validates the request using Verify and inserts the received Activity object into inbox.
┌───────────────┐
┌──────────┐ ┌─────────────────┐ │ outbox.Mover │
│ gmi.Wrap ├─┤ gemini.Listener │ │ outbox.Poller │
└──────────┘ └────────┬────────┘ │ fed.Syncer │
┌───────┴───────────┐ └───┬─────┬─────┘ ┌──────────────┐
┌───────────┤ front.Handler ├─────┼────┐│ ┌──┤ fed.Listener ├──────┐
│ └┬────────┬───────┬─┘ │ ││ │ └─────┬────────┘ │
┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┴┐ ┌─────┴──┐ │
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ │
│object │ │note │ │actor │ │follower │ │activity│ │activity│ │
│author │ │by │ │... │ │followed │ │sender │ │sender │ │
│... │ │... │ │ │ │... │ │... │ │... │ │
└───┰───┘ └───┰────┘ └────┬────┘ └────┰──┬─┘ └┬───────┘ └──────┰─┘ │
┃ ┃ │ ┃ ┌┴────┴─────┐ ┏━━━━┻━━━━━━━━┓ │
┃ ┃ ┌───────┴──────┐ ┌──╂─┤ fed.Queue │ ┃ inbox.Queue ┃ │
┃ ┃ │ fed.Resolver ├─┘ ┃ └───────────┘ ┗━┳━┳━┳━━━━━━━┛ │
┃ ┃ └───────┬──────┘ ┗━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │
┃ ┗━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │
┗━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
└───────────────────────────────────────────────────┘
Once inserted into inbox, inbox.Queue processes the received activities:
Create activities to notesnotes according to Update activitiesAnnounce activities in sharesfollows as accepted, when the followed user sends an Accept activityfollows when a remote user sends a Follow activity to a local user ┌───────────────┐
┌──────────┐ ┌─────────────────┐ │ outbox.Mover │
│ gmi.Wrap ├─┤ gemini.Listener │ │ outbox.Poller │
└──────────┘ └────────┬────────┘ │ fed.Syncer │
┌───────┴───────────┐ └───┬─────┬─────┘ ┌──────────────┐
┌───────────┤ front.Handler ├─────┼────┐│ ┌──┤ fed.Listener ├──────┐
│ └┬────────┬───────┬─┘ │ ││ │ └─────┬────────┘ │
┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┴┐ ┌─────┴──┐ │
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ │
│object │ │note │ │actor │ │follower │ │activity│ │activity│ │
│author │ │by │ │... │ │followed │ │sender │ │sender │ │
│... │ │... │ │ │ │... │ │... │ │... │ │
└───┬───┘ └───┬────┘ └────┬────┘ └────┬──┬─┘ └┬──────┰┘ └──────┬─┘ │
│ │ │ │ ┌┴────┴─────┐┃ ┌────┴────────┐ │
│ │ ┌───────┴──────┐ ┌──┼─┤ fed.Queue │┗━━━━┥ inbox.Queue │ │
│ │ │ fed.Resolver ├─┘ │ └───────────┘ └─┬─┬─┬───────┘ │
│ │ └───────┬──────┘ └─────────────────────┘ │ │ │
│ └───────────┼───────────────────────────────────┘ │ │
└─────────────────────┼─────────────────────────────────────┘ │
└───────────────────────────────────────────────────┘
Sometimes, a received or newly created local Activity is forwarded to the followers of a local user:
outbox and forwarded to all followers of the local user.outbox and forwarded to all community members. ┌───────────────┐
┌──────────┐ ┌─────────────────┐ │ outbox.Mover │
│ gmi.Wrap ├─┤ gemini.Listener │ │ outbox.Poller │
└──────────┘ └────────┬────────┘ │ fed.Syncer │
┌───────┴───────────┐ └───┬─────┬─────┘ ┌──────────────┐
┌───────────┤ front.Handler ├─────┼────┐│ ┌──┤ fed.Listener ├──────┐
│ └┬────────┬───────┬─┘ │ ││ │ └─────┬────────┘ │
┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┴┐ ┌─────┴──┐ │
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ │
│object │ │note │ │actor │ │follower │ │activity│ │activity│ │
│author │ │by │ │... │ │followed │ │sender │ │sender │ │
│... │ │... │ │ │ │... │ │... │ │... │ │
└───┬───┘ └───┬────┘ └────┬────┘ └────┬──┬─┘ └┬──────┬┘ └──────┬─┘ │
│ │ │ │ ┌┴────┴─────┐│ ┌────┴────────┐ │
│ │ ┌───────┴──────┐ ┌──┼─┤ fed.Queue │└────┤ inbox.Queue │ │
│ │ │ fed.Resolver ├─┘ │ └───────────┘ └─┬─┬─┬─┰─────┘ │
│ │ └───────┬─┰────┘ └─────────────────────┘ │ │ ┃ │
│ └───────────┼─╂─────────────────────────────────┘ │ ┃ │
└─────────────────────┼─╂───────────────────────────────────┘ ┃ │
└─╂─────────────────────────────────────╂───────────┘
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
To display details like the user's name and speed up the verification of future incoming replies, inbox.Queue uses Resolver to fetch the Actor objects of mentioned users (if needed).
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┌───────────────┐ ┃
┌──────────┐ ┌─────────────────┐ ┃ │ outbox.Mover │ ┃
│ gmi.Wrap ├─┤ gemini.Listener │ ┃ │ outbox.Poller │ ┃
└──────────┘ └────────┬────────┘ ┃ │ fed.Syncer │ ┃
┌───────┴──────────┸┐ └───┬─────┬─────┘ ┌──────────────┐ ┃
┌───────────┤ front.Handler ├─────┼────┐│ ┌──┤ fed.Listener ├──╂───┐
│ └┬────────┬───────┬─┘ │ ││ │ └─────┬────────┘ ┃ │
┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┴┐ ┌─────┴──┐ ┏━━━━━━┻━┓ │
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ ┃ feed ┃ │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ ┣━━━━━━━━┫ │
│object │ │note │ │actor │ │follower │ │activity│ │activity│ ┃follower┃ │
│author │ │by │ │... │ │followed │ │sender │ │sender │ ┃note ┃ │
│... │ │... │ │ │ │... │ │... │ │... │ ┃... ┃ │
└─┰─┬───┘ └─┰─┬────┘ └──┰─┬────┘ └──┰─┬──┬─┘ └┬──────┬┘ └──────┬─┘ ┗━━━━━━┳━┛ │
┃ │ ┃ │ ┏━━━━━━━┛ │ ┃ │ ┌┴────┴─────┐│ ┌────┴────────┐ ┃ │
┃ │ ┃ │ ┃ ┌───────┴──────┐ ┌╂─┼─┤ fed.Queue │└────┤ inbox.Queue │ ┃ │
┃ │ ┃ │ ┃ │ fed.Resolver ├─┘┃ │ └───────────┘ └─┬─┬─┬─┬─────┘ ┃ │
┃ │ ┃ │ ┃ └───────┬─┬────┘ ┃ └─────────────────────┘ │ │ │ ┃ │
┃ │ ┃ └─╂─────────┼─┼───────╂─────────────────────────┘ │ │ ┃ │
┃ └───────╂───╂─────────┼─┼───────╂───────────────────────────┘ │ ┃ │
┃ ┃ ┃ └─┼───────╂─────────────────────────────┼───────╂───┘
┏━┻━━━━━━━━━┻━━━┻━━━┓ └───────╂─────────────────────────────┘ ┃
┃ inbox.FeedUpdater ┣━━━━━━━━━━━━━━━┛ ┃
┗━━━━━━━━━┳━━━━━━━━━┛ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
To speed up each user's feed, inbox.FeedUpdater periodically appends rows to the feed table. This table holds all information that appears in the user's feed: posts written or shared by followed users, author information and more, eliminating the need for join queries, slow filtering by post visibility, deduplication and sorting by time when a user views their feed. This table is indexed by user and time, allowing fast querying of a single feed page for a particular user.
┏━━━━━━━━━━━━━━━━┓
┏━━━━━━━━━━━━━┫ cluster.Server ┣━━━━━━┓
┃ ┗━━━━━━━━━━━━━━━━┛ ┃
┃ ┌──────────────────────────╂───────────┐
┃ │ ┌───────────────┐ ┃ │
┌──────────┐ ┌────────┸────────┐ │ │ outbox.Mover │ ┃ │
│ gmi.Wrap ├─┤ gemini.Listener │ │ │ outbox.Poller │ ┃ │
└──────────┘ └────────┬────────┘ │ │ fed.Syncer │ ┃ │
┌───────┴──────────┴┐ └───┬─────┬─────┘ ┌─────┸────────┐ │
┌───────────┤ front.Handler ├─────┼────┐│ ┌──┤ fed.Listener ├──┼───┐
│ └┬────────┬───────┬─┘ │ ││ │ └─────┬────────┘ │ │
┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┴┐ ┌─────┴──┐ ┌──────┴─┐ │
│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ │ feed │ │
├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ ├────────┤ │
│object │ │note │ │actor │ │follower │ │activity│ │activity│ │follower│ │
│author │ │by │ │... │ │followed │ │sender │ │sender │ │note │ │
│... │ │... │ │ │ │... │ │... │ │... │ │... │ │
└─┬─┬───┘ └─┬─┬────┘ └──┬─┬────┘ └──┬─┬──┬─┘ └┬──────┬┘ └──────┬─┘ └──────┬─┘ │
│ │ │ │ ┌───────┘ │ │ │ ┌┴────┴─────┐│ ┌────┴────────┐ │ │
│ │ │ │ │ ┌───────┴──────┐ ┌┼─┼─┤ fed.Queue │└────┤ inbox.Queue │ │ │
│ │ │ │ │ │ fed.Resolver ├─┘│ │ └───────────┘ └─┬─┬─┬─┬─────┘ │ │
│ │ │ │ │ └─────┰─┬─┬────┘ │ └─────────────────────┘ │ │ │ │ │
│ │ │ └─┼───────╂─┼─┼───────┼─────────────────────────┘ │ │ │ │
│ └───────┼───┼───────╂─┼─┼───────┼───────────────────────────┘ │ │ │
│ │ │ ┃ └─┼───────┼─────────────────────────────┼───────┼───┘
┌─┴─────────┴───┴───┐ ┃ └───────┼─────────────────────────────┘ │
│ inbox.FeedUpdater ├───╂───────────┘ │
└─────────┬─────────┘ ┃ │
└─────────────╂─────────────────────────────────────────────────┘
┏━━━━━━━━┻━━━━━━━┓
┃ cluster.Client ┃
┗━━━━━━━━━━━━━━━━┛
The cluster package contains complex tests that simulate interaction between users on multiple servers. These tests are easy to write, they're fast and they run in parallel without affecting each other.
The tests use three main constructs: Client, Server and Cluster.
During tests, all http.Requests sent by tootik (like those sent by fed.Resolver) are sent through Client.
Server handles all kinds of incoming requests:
Client holds a mapping between domain names and Servers: it allows these servers to talk to each other by passing the http.Request sent by one server to the http.Handler of another.
Cluster is a high-level wrapper for easy creation of multiple Servers capable of federating with each other, given a list of domain names.
tootik is free and unencumbered software released under the terms of the Apache License Version 2.0; see LICENSE for the license text.
The ASCII art logo in front/logo.go was made using FIGlet.
1,186 followers · starred Oct 2024
160 followers · starred Apr 2026
442 followers · starred Aug 2026
82 followers · starred Oct 2024