# Newlay — Kotlin Multiplatform Nostr Relay (NIP-29 implementáció)

**Projekt:** Newlay  
**Szerző:** opensauce (Relay Tools Code)  
**Forrás:** https://code.relay.tools/opensauce/newlay  
**Verzió:** 0.1.0-dev (M12 milestone — NIP-29 relay-based groups)  
**Létrehozva:** 2026-07-11 (első commit), utolsó aktivitás 2026-07-12  
**Licenc:** MIT  
**Nostr Compass projektlistában:** **NINCS** (808 projekt, nincs benne, várhatóan a következő heti frissítésben kerül be)

---

## 1. Newlay repo áttekintés

A Newlay egy **high-speed, high-reliability Nostr relay Kotlin Multiplatform**-ban írva (JVM Linux szerverekhez / desktopokhoz, Android telefon-relay-ként). Egyetlen code base, két futtatható cél: `app-jvm` (CLI entrypoint) és `app-android` (foreground service + widget UI).

### Támogatott NIP-ek
`1, 9, 11, 29, 40, 42, 43, 45, 50, 70, 77, 86, 65535 (LIMITS)` + Blossom BUD-01/02/04/06/12

### Core principles
- **Dynamic per-websocket rate limiting** — tiered ACL-ok, mixed-mode NIP-42 auth (auth növeli a limitet), valós idejű capability feedback a NIP-65535 (LIMITS) sockethián.
- **Egy LMDB env, egy writer, egy transaction per batch** — event-ek, minden index, search, tombstones, metadata **atomikusan** commitálódnak. Nincs index catch-up state, soha.
- **Active node** — kétirányú NIP-77 negentropy sync, outbox-model spidering (követi egy owner user followjait a megfelelő relay-eken, self-re-planning), GrapeRank Web-of-Trust LMDB-n számolva → ACL tier-ekbe + spam filteringbe táplálva.

### Modulok (14 darab)
| Modul | Tartalom |
|---|---|
| `core` | tiszta protokoll: event model, canonical JSON + id hash, filter matcher, tokenizer, negentropy codec |
| `crypto` | secp256k1-kmp Schnorr verification facade |
| `store` | LMDB env, indexek, query planner, single-writer pipeline, search index |
| `policy` | limit profile-ok, rate limiter-ek, ACL engine, **group directory**, LIMITS/NIP-11 renderer |
| `server` | Ktor CIO websocket relay, NIP-11/86, Blossom routes, **NIP-29 service** |
| `client` | outbound relay connections, sync jobs (negentropy initiator) |
| `spider` | outbox discovery, set-cover planning, live upstream streams |
| `wot` | GrapeRank edge indexek + score számítás |
| `apphost` | shared host glue (spider controller, status contributor, mirror fetcher) mindkét app-hoz |
| `app-jvm` | CLI entrypoint (config file → running relay) |
| `app-android` | telefon-relay: foreground service + minimal widget UI ugyanazon engine felett |

### Backup & restore
`newlay backup` / `newlay restore` szerveren (live backup támogatott), Backup/Restore gombok a telefonon (system file picker). Hordozható archívumok verziók és platformok között is replay-elhetők — telefon ↔ szerver mindkét irányba; raw LMDB snapshot mód a nagy same-platform relay-ekhez.

### Konfiguráció
Egyetlen TOML file (`--config <path>`, default `./newlay.toml`). Csak a `relay.url` kötelező — minden más kulcsnak van értelmes defaultja, és az ismeretlen kulcsok warning-ot (nem error-t) adnak a forward compatibility kedvéért.

Top-level szekciók (a teljesség igénye nélkül): `[relay]`, `[limits_message]`, `[cost_weights]`, `[tier.<name>]` + `assignment` + `acl rule`, `[wot]`, **`[nip43]` / `[nip29]`** (mindkettőhöz `identity_key_file` kell), `[spider]`, `[blossom]`, `[search]`, `[ip_gate]`, `[log]` / `[status]`.

> A NIP-11 `name`/`description`/`icon` config értékek **seed-ek**: a NIP-86 `changerelayname/description/icon` RPC LMDB `relay_meta` row-okba perzisztálja az override-okat, amelyek rendereléskor előnyt élveznek (config = fallback alattuk).

---

## 2. NIP-29 spec implementáció (M12 milestone)

A Newlay NIP-29 implementációja a `server` és `store` modulokban található, és a **canonical moderation event log = authority**, a 39000–39003 state event-ek és a `GroupDirectory` cache **derived**-ek.

### 2.1 Kind-ok (object Nip29Kinds, a GroupMaintainer.kt-ban)

| Kind | Irány | Szerep |
|---|---|---|
| 9000 | user | PUT_USER (admin/moderator hozzáad egy tagot) |
| 9001 | user | REMOVE_USER |
| 9002 | user | EDIT_METADATA |
| 9005 | user | DELETE_EVENT |
| 9007 | user | CREATE_GROUP |
| 9008 | user | DELETE_GROUP |
| 9009 | user | CREATE_INVITE |
| 9021 | user | JOIN_REQUEST |
| 9022 | user | LEAVE_REQUEST |
| 39000 | **relay** | METADATA (addressable) |
| 39001 | **relay** | ADMINS (addressable) |
| 39002 | **relay** | MEMBERS (addressable) |
| 39003 | **relay** | ROLES (addressable) |

> `RELAY_METADATA: Set<Int> = setOf(METADATA, ADMINS, MEMBERS, ROLES)` — ezek kizárólag a relay identity key által írhatók alá; nem-identity signer → `restricted:` reject.

### 2.2 GroupFlags (bitmaszk)

```kotlin
const val PRIVATE: Int = 1 shl 0   // 1 — csak tagok olvashatják az event-eket
const val CLOSED: Int = 1 shl 1    // 2 — új tag invite code-dal vagy admin approval
const val RESTRICTED: Int = 1 shl 2 // 4 — csak tagok írhatnak (más olvashat)
const val HIDDEN: Int = 1 shl 3    // 8 — 39xxx metadata sem látszik nem-tagoknak
```

### 2.3 Nip29Settings (config-ból, a [nip29] szekció)

```kotlin
data class Nip29Settings(
    val enabled: Boolean = false,                // master switch (no identity → off)
    val allowUnmanaged: Boolean = true,          // events may go to unknown group_id
    val requirePrevious: Boolean = false,        // enforce timeline previous refs
    val minPreviousRefs: Int = 3,                // min refs when requirePrevious on
    val acceptForks: Boolean = false,            // tolerate unseen previous refs
    val latePublicationWindowSecs: Long = 3600,  // 0 = disable
    val defaultPrivate: Boolean = false,
    val defaultClosed: Boolean = false,
    val defaultRestricted: Boolean = false,
    val inviteMaxUses: Int = 100,                // 9009 default uses-limited
    val inviteExpiryHours: Int = 48,             // 9009 default expiry, 0 = never
)
```

> A NIP-29 **kikapcsol**, ha nincs `identity_key_file` konfigurálva, függetlenül az `enabled` flagtől.

### 2.4 Nip29Service fő folyamata (handle())

```kotlin
suspend fun handle(event: Event): Nip29Endpoints.Outcome {
    // 1. Relay-authored 39xxx: only identity may publish; else reject("restricted", ...)
    if (Nip29Kinds.isRelayMetadata(event.kind)) {
        return if (event.pubkey == identityPubkey) { storeRaw(event); Ok("") }
               else reject("restricted", "kind X may only be published by relay identity")
    }

    val groupId = event.firstTagValue("h") ?: return reject("invalid", "must carry \"h\" tag")
    val state = directory.group(groupId)

    // 2. Unmanaged + allowUnmanaged off → reject (kivéve CREATE/JOIN bootstrap)
    if (!state.managed && !settings.allowUnmanaged && kind !in bootstrapKinds)
        return reject("invalid", "group X does not exist on this relay")

    // 3. Late publication + previous refs guard (kivéve 39xxx)
    latePublication(event)?.let { return it }
    previousRefs(groupId, event)?.let { return it }

    // 4. Kind dispatch
    return when (event.kind) {
        JOIN_REQUEST   -> handleJoin(groupId, state, event)
        LEAVE_REQUEST  -> handleLeave(groupId, event)
        CREATE_GROUP   -> handleModeration(groupId, event, create = true)
        DELETE_GROUP, PUT_USER, REMOVE_USER, EDIT_METADATA, DELETE_EVENT, CREATE_INVITE
                       -> handleModeration(groupId, event, create = false)
        else           -> handleUserEvent(groupId, state, event)
    }
}
```

### 2.5 Closed-group held-join minta (NIP-29 design choice)

A `handleJoin` különösen szép:

- Ha a user **már tag** → `Duplicate("duplicate: you are already a member of this group")`
- Ha a group **closed**:
  - Ha van érvényes invite code → auto-admit (relay-signed 9000 put-user szintetizálódik)
  - Ha **NINCS** invite code → a 9021 event **tárolódik** (indexelve `#h` szerint), és **held** státuszba kerül, amíg egy admin 9000-zal approve-olja vagy 9001-gyel denyolja. Admins listázzák: `{"kinds":[9021],"#h":["<group>"]}`
- Ha a group **restricted** (és user nem tag) → `restricted: only members may write to group X`

> A `handleJoin` NEM reject-eli a closed group join-t invite kód nélkül — ezt a NIP-29 spec is így írja elő, és a Newlay hűen követi.

### 2.6 GroupAction (authorizációs ellenőrzés)

```kotlin
enum class GroupAction { MODERATE, WRITE }

// GroupDirectory.can() — az identity kulcs MINDIG átmegy (master key)
suspend fun can(groupId: String, pubkey: PubKey, action: GroupAction): Boolean {
    if (pubkey == identityPubkey) return true
    val g = group(groupId)
    return when (action) {
        GroupAction.MODERATE -> g.roles(pubkey).any(GroupState::isPrivilegedRole)
        GroupAction.WRITE    -> g.isMember(pubkey)
    }
}
```

### 2.7 REQ visibility gate (gateReq)

Kétfázisú:

1. **Filter-szintű check** — ha a filter explicit `#h` tag-et tartalmaz egy `private` groupra, és az authed pubkey-ek közt nincs tag → `Closed("auth-required: …")` vagy `Closed("restricted: …")` (a wire message a NIP-01 prefixet viseli).
2. **Per-event predicate** (lock-free) — `EventVisibility { event -> directory.visibleTo(groupId, event.kind, authed) }`. A `restrictedIndex` Map-et a REQ time-ban rögzíti az authed setre, a fan-out során alkalmazza.

> A `hidden` group 39xxx metadata sem látszik nem-tagoknak (a `gateReq` a relay-authored kind-ekre is alkalmazza a `dTag`-ot groupId-ként).

### 2.8 Late publication guard

```kotlin
private fun latePublication(event: Event): Outcome? {
    if (settings.latePublicationWindowSecs <= 0) return null
    val nowSecs = nowMillis() / 1000
    if (event.createdAt < nowSecs - settings.latePublicationWindowSecs)
        return reject("invalid", "late publication: event is older than the group's window")
    return null
}
```

### 2.9 Previous refs (timeline enforcement)

```kotlin
private suspend fun previousRefs(groupId: String, event: Event): Outcome? {
    val refs = event.tags.firstOrNull { it.firstOrNull() == "previous" }?.drop(1) ?: emptyList()
    val isBootstrap = event.kind in setOf(CREATE_GROUP, JOIN_REQUEST, LEAVE_REQUEST)
    if (settings.requirePrevious && !isBootstrap && refs.size < settings.minPreviousRefs)
        return reject("invalid", "at least ${settings.minPreviousRefs} previous refs are required")
    if (settings.acceptForks) return null
    for (ref in refs) {
        if (!directory.previousSeen(groupId, ref))
            return reject("invalid", "previous ref $ref not found in this relay's timeline")
    }
    return null
}
```

> A `previous` tag-ek a NIP-29 spec szerint az adott group utolsó N event id-jének first-8-hex prefix-ei. A `requirePrevious` opcionális (default off), a `acceptForks` opcionálisan engedi a nem-látott ref-eket (group átköltözés esetén).

---

## 3. NIP-29 storage (LMDB)

### 3.1 GroupDatabases (3 LMDB DB ugyanabban az env-ben)

| DB | Flags | Key | Value |
|---|---|---|---|
| `group_meta` | — | groupId (UTF-8) | `GroupMetaCodec` packed record |
| `group_member` | — | `u16(idLen) ‖ groupId ‖ pubkey(32)` | roleBits u32 ‖ since u64 |
| `group_invite` | — | `groupId ‖ 0x00 ‖ code(UTF-8)` | `expiresAt u64 BE ‖ usesRemaining u32 BE` |

> **A `group_member` key length-prefixed**, hogy egy group id ne lehessen byte-prefixe egy másiknak. A `group_invite` kulcsában a NUL (0x00) szeparátor biztosítja, hogy `(groupId, code)` párok ne legyenek egymás prefixei (UTF-8-ben a NUL sosem fordul elő).

### 3.2 GroupMaintainer (writer-batch integráció)

A `GroupMaintainer` a `store.execute { txn -> ... }` single-writer batch-en belül dolgozik:

- A moderation event (9000-9009) **ugyanabban a writer batch-ben** alkalmazza a `group_meta` / `group_member` row változásokat, mint az event store-olása — **atomi commit**, vagy minden commitálódik, vagy semmi.
- 39xxx state event-ek **nem itt** jönnek létre, hanem a `Nip29Service.regenerateGroupState()` hívja meg őket, ami a cache invalidation után a `directory.group(groupId)` által visszaadott `GroupState`-ből szintetizálja a 39000-39003-at, aláírja a relay identity kulccsal, és store-olja normál ingest-en (addressable → supersede-eli az előzőt).
- A `previous-ring` listener (`GroupRingListener`) a `NotificationBus`-on figyel, és minden committed `#h`-tagged event id-jét bejegyzi a `GroupState.previous` ringbe (first-8-hex prefix formában).

### 3.3 NIP-43 invite rendszer (GroupInviteStore)

A 9009 create-invite → 9021 redeem flow:

- A `9009` event per-code `["uses","N"]` és NIP-40 `["expiration","<ts>"]` tag-ekkel override-olhatja a relay-szintű default-okat.
- A `redeem()` a writer batch-en belül fut → az utolsó use decrement race-free (nincs double-spend).
- `absent` / `expired` / `exhausted` → **mind `false` visszatérés** (szándékosan összemosva: a NIP-43 szerinti `restricted:` válasz nem szivárogtatja, hogy melyik eset volt).

---

## 4. NIP-29 szálkezelés, reconnect, reconciliation

### 4.1 GroupRingListener (NotificationBus subscriber)

A `GroupRingListener` a `store.NotificationBus`-ra iratkozik fel, és minden committed event-nél, ha van `h` tag, bejegyzi az event id first-8-hex prefixét a `GroupState.previous` ringbe. A subscription **drop-oldest buffer**-t használ (`capacity = 1024`), így soha nem állítja meg a writer-t; ha egy batch drop-olódik, a directory **boot range-scan**-je a következő aktivációkor pótolja.

### 4.2 Boot reconciliation

```kotlin
/** Boot reconciliation: regenerate 39xxx for every group that has `group_meta`. */
suspend fun reconcile() {
    val groupIds = store.read { txn ->
        val ids = ArrayList<String>()
        txn.forEachFrom(groupDbs.groupMeta) { key, _ -> ids.add(key.decodeToString()); true }
        ids
    }
    for (id in groupIds) {
        directory.invalidate(id)
        regenerateGroupState(id)
    }
}
```

> A canonical moderation event log + a `group_*` row-ok a single source of truth. A 39xxx event-ek és a `GroupDirectory` cache derived. Boot során a `group_meta` tábla scan-ből minden ismert group 39xxx-e újraszintetizálódik és aláíródik.

### 4.3 Group purging (delete-group privacy)

A `9008 delete-group` handler `purgeGroupFootprint(groupId)` hívással törli:
- a relay-authored 39000-39003-at (különben kliensek újra felfedeznék a groupot)
- **minden** `#h`-tagged event-et (content, join/leave request, moderation log)

> A comment külön kiemeli: *"the latter also closes a privacy hole, since a formerly `private` group's content would become world-readable once the group leaves the restricted-visibility index."*

---

## 5. Kompatibilitás a Concord spec-emmel (NIP-29 alternate design)

A Concord spec (cloud fodder, 2026-07-10) egy **alapvetően más paradigmát** képvisel. Az alábbi táblázat a két rendszer összehasonlítása:

| Szempont | **Newlay (NIP-29 stock)** | **Concord (terv)** |
|---|---|---|
| Trust anchor | **Relay identity key** (39xxx relay-authored) | **Key-holders** (community secret-ből derivált stream author pubkey) |
| Group identity | `group_id` random string, **relay-scoped** | `community_id` = hash preimage, **self-certifying, serverless** |
| Identity / access split | Egységes `group_member` row (pubkey → roles) | **Identity key** (örök, owner founder-hez kötve) + **access key** (eltávolítható, identitás marad) |
| Encryption | **Plaintext event-ek**, csak access control (private flag-gel) | **Minden üzenet layer-encrypted** (kind 1059 private stream, valódi npub author aláírással) |
| Forward secrecy | Nincs | **Nincs epoch-on belül** (szándékosan MLS ellen trade-elve) |
| Address rotáció | Nincs (fix group id) | **Epoch bump-ok** — removed tag rotálja a stream address-t, rotation unlinkability |
| Membership record | `group_member` LMDB row + moderation event log | **Nincs admission event**; possession of the keys = membership (CORD-02 §7) |
| Invite rendszer | **Egyféle**: kind 9009 invite code, uses-limited, expiring | **Három típus**: Public link (CORD-05 §1-2, nincs redemption event) / Direct Invite (§6, Registry-ben nem jelenik meg) / Whisper (out-of-band key átadás, "ungateable act") |
| Tagok száma | Tag-lista a 39002 MEMBERS event-ben (replaceable) | **Nincs explicit tag-lista**; a key-holders száma = 0 vagy több, de a relay nem tudja |
| Admin műveletek | 9000-9009 standard NIP-29, "three removals" nincs | **"Three removals" decomposition** (CORD-04): identity+access+role rotációk composition |
| Relay szerepe | Szerver = moderátor, jogalkalmazó, identity signer | **Szerver = postafiók** (tárol + indexel), nem lát tartalmat, nem moderál |
| Tamper resistance | A relay kitilthat (put-user/remove-user), de van dispute | A relay **nem tud kitiltani** (only display filter a kliensen), nincs dispute |
| Push notifications | Nincs implementálva (relay nem subscriber push) | **Voice broker / SFU** (CORD-07) |
| Kliens oldali komplexitás | **Alacsony** — normál NIP-29 kliens, pl. 0xChat, Amethyst group | **Magas** — deriváció, decryption, edition chain kezelés, frozen label registry |
| Boot komplexitás | `reconcile()` 39xxx regen (cache + store) | Cold-start kliens oldalon (community secret → community_id → regisztrációs bundle) |

### Konklúzió a kompatibilitásról

A Newlay NIP-29 implementáció **NEM kompatibilis a Concord tervvel**, mert más paradigmára épül:

1. **Különböző trust anchor** — a Newlay a relay identity kulcsot használja a 39000-39003 aláírására; a Concord-ban nincs ilyen központi kulcs, a community secret-ből deriválódik minden.
2. **Különböző encryption modell** — a Newlay plaintext event-eket tárol (a private flag csak az access controlt szabályozza); a Concord minden üzenetet layer-encrypted formában tárol, a relay nem látja a tartalmat.
3. **Különböző rotáció** — a Newlay-ben a group id stabil (a delete-group kivételével); a Concord-ban az epoch bump rotálja a stream address-t, és a régi address-ek "forever floodable" kockázatot jelentenek.
4. **Különböző invite rendszer** — a Newlay egységes 9009 invite code-ot használ; a Concord három típust különböztet meg (public link, direct invite, whisper), és egyikben sincs redemption event.
5. **Különböző admin modell** — a Newlay a 9000-9009 standard moderation event-eket használja; a Concord-ban a "three removals" decomposition az identity+access+role szétválasztására épül.

**A két rendszer közötti híd gyakorlatilag lehetetlen** a jelenlegi formában — a Concord-ban nincs NIP-29 moderation event (nincs 9000-9009, nincs 39000-39003). Aki Concord-ot használ, az **nem NIP-29** csoportos chat-et használ, hanem egy másik, kulcs-alapú alternatívát.

---

## 6. Következtetések a Haven fork és a NIP-29 csoportos chat döntéshez

### 6.1 Newlay vs. Haven — NIP-29 szempontból

A Haven relay (barrydeen/haven, khatru-alapú) **NEM támogatja a NIP-29-et**: bár a `allowedChatKinds` policy elfogadja a NIP-29 event kind-okat, a group state, invite code validáció, moderation action feldolgozás **nem implementált** (lásd a memory: "Haven relay NIP-29 nem implementálva").

A Newlay ezzel szemben **teljes NIP-29 implementációt** nyújt:
- 14 modul, ebből 5 dedikáltan NIP-29-hez (Nip29Service, GroupDirectory, GroupRingListener, GroupDatabases, GroupMaintainer)
- Integrált LMDB single-writer atomicity (a moderation event + a group row-ok együtt commitálódnak)
- Boot reconciliation
- Held-join minta (admin approval closed groupokban)
- Per-WS REQ visibility gate (auth-required / restricted)
- 9009 invite system, writer-batch race-free redemption
- Config-driven flags + late publication + previous refs

**Ha a cél NIP-29 csoportos chat (klasszikus, relay-moderált), a Newlay NAGYON erős alap.** A Haven fork ehhez képest minimális — a Haven NIP-29 supportjához újra kellene írni a Haven chat relay felét.

### 6.2 Ha a cél Concord-style (key-holder, MLS-ready) csoportos chat

Ebben az esetben a Newlay **nem segít**: a NIP-29 relay-side logika irreleváns. A Concord spec-et önállóan kellene implementálni (akár új relayként, akár a Newlay `policy` modulját megkerülve, a `server.Nip29Wiring` helyett egy `ConcordService` pluginnal).

### 6.3 Pragmatikus döntési fa

| Használati eset | Javasolt alap |
|---|---|
| Standard NIP-29 csoportos chat (relay moderál, plaintext, központi kulcs) | **Newlay** (NIP-29 teljes implementáció, M12 milestone kész) |
| Key-holder, "no relay trust" csoportos chat (Concord-style) | **Saját implementáció** (Newlay-ből kiindulva, de Nip29Service lecserélése ConcordService-re) |
| A meglévő Haven relay NIP-29 bővítése | **Nem javasolt** — a Haven chat relay más architektúrára épül (khatru, nem LMDB single-writer), a NIP-29 logika jobban illeszkedik a Newlay-be |
| Multi-platform (Android + Linux) relay gyors deployment | **Newlay** (Kotlin Multiplatform natív támogatás) |

---

## 7. Hivatkozások

- **Newlay repo:** https://code.relay.tools/opensauce/newlay
- **Newlay GitLab API:** https://code.relay.tools/api/v4/projects/opensauce%2Fnewlay
- **NIP-29 spec (Relay-based groups):** kind 9000-9009 moderation, 9021 join, 9022 leave, 39000-39003 relay-authored metadata
- **NIP-43 spec (Relay authentication / invite codes):** kind 28935 invite request, 28934 join
- **Concord spec (cloud fodder):** CORD-01 (private stream) / CORD-02 (membership) / CORD-03 (history) / CORD-04 (edition chains) / CORD-05 (invite types) / CORD-06 (rekey + refounding) / CORD-07 (voice broker)
- **Haven relay (alternatíva, NIP-29 nélkül):** barrydeen/haven, khatru-alapú
- **Nostr Compass projektlista:** https://nostrcompass.org/en/projects/ (808 projekt, Newlay nem szerepel)
