Files
Claude e4da199dc0 fix: clear every compiler and Gradle warning across all builds
Compiled every module and target from scratch (--rerun-tasks: JVM main and
test, commonMain metadata, linuxX64 native, Android KMP targets, all four
amethyst debug variants with unit and instrumented tests, nappletHost,
benchmark, baselineprofile) and fixed each warning it printed. All of them
now compile without a single warning.

Deprecated APIs
- rememberModalBottomSheetState -> a shared rememberModalSheetState helper
  in commonsUI over Material3's rememberBottomSheetState, with the same
  anchors the deprecated function used (27 call sites).
- LocalClipboardManager -> LocalClipboard with the existing suspend
  setText/getText extensions; Clipboard.nativeClipboard ->
  nativeClipboardManager.
- Skiko PlatformBackHandler: compose-ui's BackHandler -> navigationevent's
  NavigationBackHandler. The ui-backhandler dependency it needed is gone;
  navigationevent-compose already arrives as an api dep of navigation3-ui.
- LocalLifecycleOwner moved to androidx.lifecycle.compose.
- SingleSampleMediaSource (HLS side-loaded captions) -> a lazily loaded
  ProgressiveMediaSource with SubtitleExtractor, the way media3's own
  DefaultMediaSourceFactory builds them. Beyond the deprecation this fixes
  a crash: the old path handed TextRenderer raw text/vtt samples, and with
  legacy decoding off (the default) TextRenderer checkState-fails on those.
  The lazy single-track switch is package-private in media3, so a small
  bridge in androidx.media3.exoplayer.source reaches it.

Build scripts
- amethyst: read the git branch through providers.exec instead of
  ProcessBuilder (external process at configuration time, an error in
  Gradle 11).
- root: drop the deprecated spotless { predeclareDeps() }; declaring the
  spotlessPredeclare format already enables predeclaration.
- gradle.properties: kotlin.native.ignoreDisabledTargets=true, silencing
  the expected "macOS test task disabled" notice on Linux/Windows hosts.

Code
- Redundant !!, ?., elvis, casts and always-true conditions the compiler
  proved away (OkHttp 5 non-null bodies, NIP-87 non-null d-tags, smart
  casts).
- ThreadLocal.get() is nullable in Android's SDK annotations: assert at
  the cipher reads, and bound ScratchLocal to T : Any.
- UnicodeNormalizerTest's String.codePoints() helper was shadowed on JVM by
  java.lang.String.codePoints(), so failure messages printed an IntStream;
  renamed to hexCodePoints().
- EventFactoryKind38000Test used an `is` check the compiler proves always
  false (an error from language version 2.4); now assertIsNot.
- @OptIn(ExperimentalCoroutinesApi::class) on the commons tests that use
  the test dispatchers.

Not fixable from this repo: AGP 9.4.1 itself still calls the deprecated
Configuration.setVisible (shows under --warning-mode all).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SazxgvMHyaPGET4ishFuTM
2026-10-08 00:05:17 +00:00
..

geode

A standalone Nostr relay for the JVM, built on Quartz's relay-server code (Ktor CIO). It speaks the core relay protocol plus NIP-11 (info doc), NIP-42 (AUTH), NIP-45 (COUNT), NIP-50 (full-text search), NIP-77 (Negentropy sync), NIP-86 (relay management) and NIP-FE (REQ/COUNT/EVENT over plain HTTP), stores events in SQLite (or a filesystem backend), and can mirror upstream relays strfry-router style.

geode depends only on :quartz — no Android, no Compose. amy serve (the Amethyst CLI) embeds the same engine in-process.

Install

Pick the channel that matches how you deploy. For a real relay, the Docker image or the .deb/.rpm + systemd are the two production paths; Homebrew and the portable tarball are handy for local testing.

Images are published to GHCR on every release:

# Ephemeral — in-memory store, events vanish on restart:
docker run --rm -p 7447:7447 ghcr.io/vitorpamplona/geode:latest

# Persistent + configured:
docker run -d --name geode -p 7447:7447 \
  -v $PWD/geode.toml:/etc/geode/geode.toml:ro \
  -v geode-data:/var/lib/geode \
  ghcr.io/vitorpamplona/geode:latest --config /etc/geode/geode.toml

with in_memory = false and file = "/var/lib/geode/events.db" in your geode.toml. Pin a version (:1.16.0) instead of :latest for reproducible deploys. To build the image yourself, from the repo root:

docker build -f geode/Dockerfile -t geode:local .

Debian/Ubuntu (.deb) and Fedora/RHEL (.rpm)

Download geode-<version>-linux-x64.deb (or .rpm) from the release page and install it — a minimal JRE is bundled, so no system Java is required. It installs to /opt/geode/ with the launcher at /opt/geode/bin/geode.

sudo dpkg -i geode-1.16.0-linux-x64.deb    # or: sudo rpm -i geode-1.16.0-linux-x64.rpm

To run it as a managed service, wire up the shipped systemd unit (/opt/geode/share/geode/geode.service) — the unit file's header comment has the copy-paste steps (create the geode user, drop your config in /etc/geode/geode.toml, systemctl enable --now geode).

Homebrew

brew install vitorpamplona/tap/geode-relay    # from the tap, once published

The formula depends on openjdk and installs the no-JRE jar bundle. Reference formula: packaging/homebrew/geode-relay.rb.

Portable tarball (any Linux/macOS, no system Java)

Download geode-<version>-<os>-<arch>.tar.gz, unpack, and run:

tar xzf geode-1.16.0-linux-x64.tar.gz
./geode/bin/geode --config geode/share/geode/config.example.toml

The tarball bundles a jlink'd JRE plus share/geode/config.example.toml and share/geode/geode.service.

From source

./gradlew :geode:run --args="--config /etc/geode.toml"

Configure

geode runs with zero config (binds 0.0.0.0:7447, in-memory SQLite). For anything real, copy config.example.toml and edit it — the sections mirror nostr-rs-relay's config.toml so existing configs port almost verbatim. Precedence is CLI flags > TOML > built-in defaults.

geode --config /etc/geode/geode.toml
geode --help          # full flag list
geode --version

Key sections: [info] (NIP-11 doc), [network] (bind + thread pools), [database] (SQLite path/tuning), [options] (AUTH / verify / search), [authorization] (allow/deny lists), [[mirror]] (upstream mirroring), [http] (NIP-FE limits), [admin] (NIP-86 management), [identity] (the relay's own key, NIP-11 self) and [membership] (NIP-43). See the example file for every knob.

Commands over HTTP (NIP-FE)

Besides the websocket, geode answers one command per HTTP POST to the relay URL: the body is the frame you would send on the socket, and the answer is the socket's frames as NDJSON, streamed — no socket, no subscription left open:

curl -N --compressed -d '["REQ","q",{"kinds":[1],"limit":2}]' http://localhost:7447/
# ["EVENT","q",{"id":"…","kind":1,…}]
# ["EVENT","q",{"id":"…","kind":1,…}]
# ["EOSE","q"]
curl -d '["COUNT","c",{"kinds":[1]}]' http://localhost:7447/   # ["COUNT","c",{"count":2}]
curl -d "[\"EVENT\",$(cat signed-event.json)]" http://localhost:7447/   # ["OK","<id>",true,""]

A body that does not end on EOSE/CLOSED (REQ), COUNT/CLOSED (COUNT) or OK (EVENT) was cut off. On an AUTH-gated relay the answer is 401 until the request carries a NIP-98 Authorization: Nostr … header whose u is the relay's http URL and whose payload is the body's sha256. NIP-86 admin calls share the URL, told apart by Content-Type: application/nostr+json+rpc. Streamed answers are gzipped for clients that send Accept-Encoding: gzip (curl --compressed), sync-flushed so events still arrive as they are found; [http].gzip = false turns it off. Quartz's HttpRelayClient does all of this for clients, over OkHttp via OkHttpRelayTransport or any HttpRelayTransport.

Membership (NIP-43)

With [membership] enabled = true geode is a members-only relay: the NIP-86 pubkey allow list is the member list and gates writes. Admins mint invite codes with createclaim (e.g. amy admin RELAY create-claim CODE); a user joins by sending a kind 28934 request carrying one (codes stay valid until deleteclaim) and leaves with a kind 28936. The relay signs — with its [identity] key, advertised as NIP-11 self — and serves kind 13534 (members and their role ids), 33534 (roles from createrole / editrole; deleterole publishes a NIP-09 deletion) and 8000 / 8001 (member added / removed), keeping them in step with every join, leave and admin change. Off by default, and while off the role / claim RPCs aren't offered. Design notes: plans/2026-09-27-nip43-membership.md.

Verbs

geode [relay] [flags]        serve the relay (default)
geode import [flags] [FILE…] bulk-load NDJSON events into the store
geode export [flags]         dump the store as NDJSON to stdout

import/export are geode's strfry import / strfry export — one JSON event per line, for seeding, migrating, or backing up a relay. Both stream, so a multi-million-event corpus round-trips in roughly constant memory.

Release process

geode ships on the same tag-driven pipeline as the rest of Amethyst: pushing a v* tag runs .github/workflows/create-release.yml, whose build-geode job produces the tarball + .deb/.rpm + Homebrew jvm bundle, docker-geode builds and pushes the GHCR image, and bump-homebrew-geode-formula.yml syncs the reference formula. Design notes: plans/2026-07-24-geode-release.md.