Skip to content

About

Primal Server includes membership, discovery and media caching services for Nostr

Topics

Resources

Stars

63 stars

Watchers

3 watching

Forks

Latest commit

 

History

821 Commits

Folders and files

Repository files navigation


Logo

Overview

Primal Server includes caching, membership, discovery and media caching services for Nostr. It connects to the specified set of relays, collects all events in real time, stores them locally, and makes them available to nostr clients through a web socket-based API.

It is written in OCaml 5 (Eio). The earlier Julia and Rust implementation was retired in October 2026; its code is in the tag julia-final, which the "Julia ..." provenance comments in the OCaml sources refer to.

Layout

One dune workspace, one dune project per daemon:

Directory What
common/ primal_common, the shared library: Postgres ([%pgsql]), Nostr, HTTP/WebSocket clients and server kernel
api/ primal-api: the WebSocket cache API, the internal HTTP services, admin endpoint; primalctl
importer/ primal-importer: imports events from the fetcher's firehose into the cache DB
fetcher/ primal-fetcher: relay connections, the firehose, the relay lists (relays/)
jobs/ primal-jobs: DAG runner, TrustRank, analytics, DVM checker, rebroadcasting; the media role (processing graph, uploads, Blossom)
nix/ the dev shell for the whole workspace (flake.nix) and patches to third-party packages
sql/ the schema migrations (migrations/, applied by migrate.sh)
schema/ baseline schemas of the cache, membership and media DBs
selfhost/ primal.sh, PostgreSQL (postgres.nix), config templates, content files and relay list for a private server (below)

Each directory has a README.md with the details.

Self-hosting a private Primal server

selfhost/primal.sh runs a complete private server on one machine. It needs only nix (with flakes) and keeps all its state in var/, which git ignores:

selfhost/primal.sh init     # PostgreSQL 16 in var/pg, the cache and membership DBs, configs in var/config
selfhost/primal.sh build    # builds primal-fetcher, -importer, -api and -jobs into var/bin
selfhost/primal.sh start    # starts PostgreSQL and the four daemons in the background

Then open a Primal app and, under network settings, set the caching service to ws://127.0.0.1:8801. Other commands: status, stop, restart, logs [fetcher|importer|api|jobs], psql, bootstrap. init is safe to re-run: it applies new migrations, adds new seeds and rewrites the configs.

The first start also bootstraps the follow graph, which takes about two minutes. It fetches the seed accounts' profiles and follow lists from the relays, then the profiles of everyone they follow, then the follow lists of the 1000 most followed (selfhost/bootstrap.py, through the importer's import listener). TrustRank then ranks them, and the importer restarts once to pick up the ranks.

What runs:

Process Does Port
PostgreSQL 16 (selfhost/postgres.nix) the cache DB primal1 and the membership DB, created from schema/ 127.0.0.1:54317
primal-fetcher subscribes to the relays in selfhost/relays.txt, adds the ones it learns from relay lists, and serves the new events as a firehose 8820 (admin 8821)
primal-importer imports the firehose into the cache DB: events, profiles, follows, reactions, zaps, notifications, counters 8830 (import listener)
primal-api the WebSocket API the apps use, plus internal HTTP (link previews, nostr.json) 8801 (HTTP 8802, 8803, admin 8811)
primal-jobs TrustRank (user scores, from the seeds in selfhost/trust-seeds.txt), trending and explore feeds, and the DAG's derived tables (search, mentions, articles, zaps) admin 8840

Settings, as environment variables:

  • PRIMAL_LISTEN_HOST=0.0.0.0 serves the API to other machines. Put TLS in front of it with a reverse proxy.
  • PRIMAL_API_PORT and PRIMAL_PG_PORT move the API and the database.
  • PRIMAL_RELAYS_FILE takes another relay list; set it empty for the full lists in fetcher/relays/.
  • PRIMAL_VAR moves the state directory.

The configs come from the templates in selfhost/config/ and are written to var/config/, where every key can be changed. Each daemon's README lists the keys. The setup leaves out what needs more infrastructure:

  • the media pipeline and S3;
  • DVM feeds;
  • proxies;
  • push notifications;
  • metrics.

TrustRank spreads trust from the seed accounts over the follow graph; it runs when primal-jobs starts and then hourly. An event's score counts the reactions, replies, reposts and zaps of accounts TrustRank ranks above the humaness threshold, so scores and the trending feeds fill in within minutes of the first start. The explore and trending feeds are recomputed every 10 minutes.

The first build downloads the OCaml toolchain and takes a few minutes. Events start arriving within seconds of start.

Build

nix develop path:./nix -c dune build @all
nix develop path:./nix -c dune test

The [%pgsql] statements are type-checked at compile time against a database made from the baseline schemas (schema/README.md), for instance the one selfhost/primal.sh init creates. The dev shell sets PG* itself, so override them inside it: nix develop path:./nix -c env PGPORT=54317 PGUSER=$USER dune build @all.

Run

Every daemon reads one JSON config, given as its argument; <exe> --check-config <file> checks it without starting anything. sql/migrate.sh applies the schema migrations.

API requests

The funcalls are listed in api/lib/funcall.ml.

Examples:

["REQ", "amelx49c18", {"cache": ["net_stats"]}]
["CLOSE", "amelx49c18"]

["REQ", "p0xren2axa", {"cache": ["feed", {"pubkey": "64-hex digits of pubkey id"}]}]

["REQ", "vqvv4vc6us", {"cache": ["thread_view", {"event_id": "64-hex digits of event id"}]}]

["REQ", "ay4if6pykg", {"cache": ["user_infos", {"pubkeys": ["64-hex digits of pubkey id"]}]}]

["REQ", "2t6z17orjp", {"cache": ["events", {"event_ids": ["64-hex digits of event id"]}]}]

["REQ", "1uddc0a2fv", {"cache": ["user_profile", {"pubkey": "64-hex digits of pubkey id"}]}]

About

Primal Server includes membership, discovery and media caching services for Nostr

Topics

Resources

Stars

63 stars

Watchers

3 watching

Forks

Used by

Contributors

Languages