Concepts
The graph
Everything in Anahita is a node, and every relationship between two things is an edge. A person, a group, a post, a hashtag, a location and a feed are nodes; following, liking, tagging, citing and saving are edges. This page is about what the two are, how their types are named, and every type there is.
Why one graph
A school's community holds people, groups, events, notes, articles, photos
and videos, and they are all connected: who follows what, who is in which
photo, which article cites which note, which posts are about #seeds. Kept
as one graph, each of these is read, permitted, searched and suggested in
the same way:
- One rule for who may see what, applied in one place, to every kind of node (Who may see a node).
- One shape for search and suggestions to walk: "posts liked by people you follow" and "people cited by people you follow" are the same walk over different edges (How things are found).
- Something new needs no new tables. A custom feed is a node, and keeping it on the home page is an edge (A feed is a node). A post from Mastodon is a node too, so following it, liking it and blocking its author need nothing new.
The graph is owned by one service, graph-grpc-service. Every other service
reads and writes it through that one, over gRPC
(Architecture).
Nodes
A node is a row of the nodes table. Every kind shares it; what a column
means can depend on the kind, and a column a kind has no use for is left
empty.
| Column | What |
|---|---|
id |
Its number on this installation |
uuid |
What another server is told it is called: random, and never changed |
type |
What kind of node it is (How a type is named) |
name, alias, body |
What it is called, its handle or address, and what it says |
owner_id |
The profile it is on: the person or group a post was posted to. On a profile, empty |
created_by |
Who made it, who need not be its owner: a person's post in a group is owned by the group |
parent_id, root_id |
On a reply, what it answers and the post at the top of its thread. On a repost, the original |
quote_id |
On a note that quotes a post, that post |
access |
Who may see it (Who may see a node) |
meta |
What one kind keeps that the others do not, as JSON: a feed's definition, for one |
uri, origin_domain |
What it is called on the server it came from, when it came from another one |
follower_count, vote_up_count, comment_count, citation_count, … |
Counts the graph keeps in step with the edges, so that a list does not count each time it is read |
How a type is named
A type is a name from a fixed grammar, read left to right from the most general to the most particular:
node.<family>.<service>.<entity>.v1 node.actor.person-service.person.v1
edge.<family>.<name>.v1 edge.actor.follow.v1
- The family is what everything of that kind has in common. Every
node.actor.can be followed, blocked and posted to; everynode.medium.is a post, with an owner, an author and an audience. Code that asks "is this a profile?" asks whether the type begins withnode.actor.. - The service is the one that answers for that kind of node. A group's
is still
group-service, an older name, because a type is never renamed. - The version is there because a type is stored for good: in every row,
in every edge's
node_a_typeandnode_b_type, and in what other services keep. A change to what a type means would be a new version, read beside the old, never an edit of the old.
Every kind of node
| Family | Type | What |
|---|---|---|
| Actors | node.actor.person-service.person.v1 |
A person with an account here |
node.actor.group-service.group.v1 |
A group, which people follow and post in | |
node.actor.actor-service.event.v1 |
An event, which people say they are going to; often hosted by a group | |
node.actor.actor-service.website.v1 |
A website's feed, verified, as a profile to follow (Websites) | |
node.actor.activitypub-service.person.v1 |
Somebody on a server of the fediverse | |
node.actor.atproto-service.person.v1 |
Somebody on Bluesky | |
| Media | node.medium.text-service.note.v1 |
A note: a short post, and the kind that arrives from another server |
node.medium.text-service.article.v1 |
An article: long, with a title; what a website posts for each item of its feed | |
node.medium.text-service.topic.v1 |
A topic: a conversation with a title, started on a profile, with replies under it | |
node.medium.photo-service.photo.v1 |
A photo post, of one or more photos (Photo posts) | |
node.medium.video-service.video.v1 |
A video post (Videos) | |
| Tags | node.tag.hashtag-service.hashtag.v1 |
A hashtag (Hashtags and locations) |
node.tag.location-service.location.v1 |
A location posts are tagged with | |
| Feeds | node.feed.feed-service.generator.v1 |
A custom feed: what it shows, kept in its meta (Custom feeds) |
| Reposts | node.base.repost-service.repost.v1 |
A repost: its parent_id is the original, and its owner the profile that reposted it |
A reply is a node of the same type as a note, with a parent_id. People
elsewhere have types of their own, not the person type with a flag:
whatever lists the people of this installation by type then leaves them out
without having to be told.
Edges
An edge is a row of the edges table: a type, and two nodes, node_a and
node_b, with each one's type beside its id. It points from a to b, and
reads as a sentence: Ana follows the library.
| Column | What |
|---|---|
type |
What the relationship is |
node_a_id, node_a_type |
Where it is from |
node_b_id, node_b_type |
Where it is to |
meta |
What it says beyond that, as JSON: whether a tag of a person has been approved, for one |
ordering |
Its place among its siblings, for the ones kept in an order somebody chose: feeds on the home page |
created_by, created_at |
Who made it, and when; what fades with time is counted from this |
There is one edge of a type between the same two nodes, never two
(uc_edge): liking a post twice is liking it.
Every kind of edge
| Type | From | To | Means |
|---|---|---|---|
edge.actor.follow.v1 |
A profile | A profile | Follows it |
edge.actor.follow-request.v1 |
A profile | A profile | Has asked to follow it, and is waiting for an answer |
edge.actor.block.v1 |
A profile | A profile | Blocks it: neither sees the other |
edge.actor.admin.v1 |
A person | A group or an event | Is one of its administrators |
edge.actor.host.v1 |
A group | An event | Hosts it; what puts it on the group's list of events |
edge.actor.invite.v1 |
A person | A profile | Was invited to follow it, by created_by |
edge.base.voteup.v1 |
A person | A post | Likes it |
edge.base.bookmark.v1 |
A person | A post | Saved it. Private: nobody else is told, the post's author included, and nothing is counted from it |
edge.base.subscription.v1 |
A person | A post or a profile | Is notified of what happens to it (Notifications) |
edge.tag.hashtag.v1 |
A hashtag | A post | The post carries the hashtag |
edge.tag.location.v1 |
A location | A post | The post is tagged with the location |
edge.tag.mention.v1 |
A profile | A post | The post names the profile: "this is for you" |
edge.tag.person.v1 |
A person | A photo or video post | The person is in it, tagged by its author; shown once they approve |
edge.medium.citation.v1 |
A post | A post | The first shows the second within its text, as an article or a topic does an address on a line of its own |
edge.medium.hidden-reply.v1 |
A post | A reply | The reply is hidden from the post's thread; the reply stays its author's |
edge.feed.pin.v1 |
A person | A custom feed | Keeps it on their home page, in the place its ordering says |
Who may see a node
Every node has an audience, its access: public, registered,
followers, leaders, mutuals, admins or myself, each measured
against the profile the node is on, not whoever wrote it
(The audiences). A block, either way, hides
everything.
The graph applies these to every read, whatever asked: a list, a search, a suggestion, a feed. What a service is sent is already what the viewer may see, so no service has a rule of its own to get wrong (Permissions).
Nodes from other servers
A person on Mastodon or Bluesky, and what they post, are nodes here once
somebody here has met them: looked them up, followed them, or been replied
to. Each has a uri, what it is called where it lives, and an
origin_domain; a node without a uri was made here. See
What a node is called elsewhere
and Federation.
Events are not types
What happens to the graph is told to the services that care over the bus,
RabbitMQ, by events with names in the same grammar and an action at the end:
node.medium.text-service.note.added.v1, edge.follow.socialgraph.added.v1.
These are the names of things that happened, not of nodes or edges, and are
never stored as a type. They are listed in anahita-libs/event
(Architecture).
Where the code is
| What | Where |
|---|---|
| The two tables | migration-job/schema/init.sql |
| The type names | anahita-libs/constants/entity_types.go, and beside the code of the kind (anahita-libs/feeds, anahita-libs/videos, text-service/constants) |
| The audiences | anahita-libs/constants/access.go |
| Every read and write | graph-grpc-service/data/repositories |
| How other services call it | anahita-libs/proto/graphpb, over gRPC (Services over gRPC) |
From anahitasocial/anahita-services, 2026-10-27.
Written by Claude, reviewed by Rastin Mehr · About this