Wellbeing 2.2: Sovereign Health Standardization, NIP-44 Privacy & The Anatomy of a Nostr Bug Hunt

A deep dive into NIP-101h.9 Kind 1359 step standardization, NIP-01 event taxonomy, Dalvik bytecode disassembly for Amber NIP-44 background encryption, overlay permission excision, Golang relay normalization, and go install distribution.

Wellbeing 2.2: Sovereign Health Standardization, NIP-44 Privacy & The Anatomy of a Nostr Bug Hunt

A deep dive into NIP-101h.9 Kind 1359 step standardization, NIP-01 event taxonomy, disassembling Dalvik bytecode to solve Amber’s NIP-44 background encryption trap, Android 14+ Health Connect sandboxing, and eliminating the zombie d-tag migration loop across mobile and web.


🏃 1. NIP-101h.9 & The Kind 1359 Standard: Harmonizing Health Telemetry

When we initially architected the Wellbeing ecosystem, each application tracked daily telemetry using custom Parameterized Replaceable Events (Kinds in the 30000..39999 range). While effective for basic key-value storage, health and fitness tracking in the broader Nostr ecosystem has converged around NIP-101h.9 for step count metrics.

With Wellbeing 2.2, Holy Fit has transitioned fully to native Kind 1359 events, creating seamless interoperability with third-party Nostr clients, fitness leaderboards, and web dashboards.

Standardized Tag Architecture

Per the NIP-101h.9 specification:

  • Event Kind: 1359
  • Content: The numeric step count as a string (or NIP-44 ciphertext if encrypted).
  • Required Tags:
    • ["unit", "steps"]
    • ["t", "health"]
    • ["t", "step_count"]
    • ["period", "daily"]
  • Optional Tags:
    • ["timestamp", "YYYY-MM-DD"] - The date/period end time.
    • ["source", "Holy Fit"] - The recording client or device.

Notice something fundamental: There is NO d-tag in NIP-101h.9.

{
  "kind": 1359,
  "pubkey": "<hex_pubkey>",
  "created_at": 1788382519,
  "tags": [
    ["unit", "steps"],
    ["t", "health"],
    ["t", "step_count"],
    ["period", "daily"],
    ["timestamp", "2026-09-02"],
    ["source", "Holy Fit"]
  ],
  "content": "14720",
  "id": "<event_id>",
  "sig": "<schnorr_signature>"
}

⚡ 2. NIP-01 Taxonomy: Regular Events vs. Replaceable Events

A common architectural trap in Nostr development is confusing Regular Events with Parameterized Replaceable Events:

Event Type Kind Range (NIP-01) Identity & Replacement Rules Example
Regular Events 1000 <= kind < 10000 Append-only. Relays store every event. Never use d-tags. Kind 1359 (Steps), Kind 1 (Notes)
Replaceable Events 10000 <= kind < 20000 Relays keep only the latest event per pubkey + kind. Kind 10323 (Micro-relay lists)
Parameterized Replaceable 30000 <= kind < 40000 Relays keep only the latest event per pubkey + kind + d tag. Kind 30342 (Nunlock), Kind 30343 (SisterCharge)

Why Kind 1359 Uses timestamp, NOT d

  • Append-Only Streams: Kind 1359 is a regular event. When your phone pushes step updates throughout the day (morning walk, afternoon jog, evening total), each push is a new signed event in an immutable audit stream. Relays do not overwrite previous events because there is no replacement identifier.
  • Temporal Scoping: NIP-101h.9 identifies the day using the ["timestamp", "YYYY-MM-DD"] tag.
  • Client-Side Resolution: Wellbeing web dashboards and mobile clients group events by timestamp and pick the record with the highest created_at timestamp. This provides your latest step total with zero latency while preserving intraday history.

🔐 3. NIP-44 Privacy vs. Public Broadcast: Switching Without Breakage

Wellbeing apps give users complete sovereignty over their data: you can publish step counts, screen unlocks, and battery stats publicly to compete on leaderboards, or encrypt them with NIP-44 (Version 2) so that only your private key can decrypt your history.

       [ Device Step Sensor / Health Connect / Battery State ]
                                  │
                                  ▼
                    [ Private Mode Toggled? ]
                         ├── YES ──► Silent Amber NIP-44 Encrypt ──► Ciphertext Payload
                         └── NO  ──► Direct Metric String        ──► Plaintext Payload
                                  │
                                  ▼
                  [ Amber ContentProvider Background Sign ]
                                  │
                                  ▼
                    [ Relay Ingestion (Kind 1359 / 3034x) ]

Switching Seamlessly Between Modes

  • When switching between Private (encrypted) and Public (plaintext) modes, your device signs and publishes new events with the chosen format.
  • Wellbeing Web Dashboards handle hybrid histories natively:
    1. Plaintext events are parsed immediately for public viewing.
    2. NIP-44 encrypted events are decrypted on-the-fly when logged in with a NIP-07 browser extension (like Amber or Alby).
    3. When viewing public profiles or running in read-only mode, the dashboard gracefully respects privacy and skips raw ciphertext without breaking graph calculations.

🛠️ 4. Behind the Scenes: The Anatomy of a Bug Hunt & Bytecode Reverse Engineering

If you’ve ever had code that worked fine for months suddenly misbehave across multiple layers, this debugging story is for you:

Bug 1: The Amber NIP-44 ContentProvider Mystery (Disassembling Dalvik Bytecode)

When NIP-44 encryption was activated, apps in the background (such as SisterCharge’s 15-minute battery sync worker) began failing silently. When instrumented with detailed logging, Amber was returning:

extractCursorString: columns=[signature,event,result], count=1
extractCursorString: rawVal='Could not decrypt the message'

Even when explicitly querying content://com.greenart7c3.nostrsigner.NIP44_ENCRYPT, Amber responded with an error claiming it could not decrypt!

To get to the ground truth, we pulled Amber’s APK directly from the connected Android test device via ADB and disassembled its Dalvik bytecode using dexdump across classes2.dex.

What the Bytecode Revealed

Inside SignerProviderQuery.query and SignerProviderQuery$query$result$1:

  1. URI Operation Dispatch: Amber strips content://com.greenart7c3.nostrsigner. from the URI. For ...NIP44_ENCRYPT, it sets SignerType.NIP44_ENCRYPT.
  2. The Projection Parameter Mapping:
    v29 = projection[0] (the data payload)
    v32 = projection[1] (parsed as the RECIPIENT pubkey!)
    v3  = projection[2] (loaded via LocalPreferences as the ACCOUNT pubkey)
    
  3. The Parameter Shift Trap:
    Our Android clients were following an old NIP-55 draft convention:
    arrayOf(plainText, "nip44_encrypt", recipientHex)
    Because Amber already knew the operation from the URI authority (NIP44_ENCRYPT), Amber read projection[1] ("nip44_encrypt") as the recipient’s public key!
  4. The Generic Error Fallback:
    Amber called:
    account.nip44Encrypt(plainText, pubkey = "nip44_encrypt")
    Naturally, secp256k1 threw an IllegalArgumentException because "nip44_encrypt" was not a valid 64-character hex pubkey. Amber’s generic coroutine catch block caught the exception and defaulted to returning:
    "Could not decrypt the message"!

The Canonical Amber NIP-55 Projection

The true, canonical projection for Amber ContentProvider encryption and decryption is clean and positional:

// Encryption
val uri = Uri.parse("content://com.greenart7c3.nostrsigner.NIP44_ENCRYPT")
val cursor = context.contentResolver.query(
    uri,
    arrayOf(plainText, recipientHex, myPubkey),
    null, null, null
)

// Decryption
val uri = Uri.parse("content://com.greenart7c3.nostrsigner.NIP44_DECRYPT")
val cursor = context.contentResolver.query(
    uri,
    arrayOf(encryptedText, senderHex, myPubkey),
    null, null, null
)

The moment we aligned our clients with this projection, Amber returned valid NIP-44 ciphertext (AsY1bXjj...) in sub-20ms with zero UI prompts.


Bug 2: The Android 14 Background Health Sandbox

Android 14 introduced strict background permission guards for Health Connect:

  • When the Holy Fit app was open on screen, step queries succeeded.
  • The moment the phone was locked or Holy Fit moved to the background, Android threw:
    SecurityException: ... does not have android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND
  • The Fix: Added the background health permission to AndroidManifest.xml and handled graceful foreground/background fallback polling.

Bug 3: The “Zombie d-Tag” Loop: The Root Conceptual Mistake

Why did the web dashboard and mobile app keep nagging users to migrate legacy d-tags after every push?

  1. The Core Conceptual Flaw: Kind 1359 is a regular event. It is defined by NIP-101h.9 and uses timestamp. It does not have—and should not have—a d tag.
  2. The Flawed Web Filter: In holyfit-web, the migration code was checking:
    // ❌ Old check: treated missing d-tag on Kind 1359 as legacy!
    return (dTag && dTag[1].startsWith('holyfit:')) || (e.raw.kind === 1359 && (!dTag || dTag[1] !== e.date));
    
    Because standard Kind 1359 events were published without a d tag, !dTag evaluated to true. The Web UI falsely flagged the canonical Kind 1359 event itself as a legacy event!
  3. The Relay Rejection Cycle: When the user clicked “Migrate” in the browser, the web UI tried to publish a replacement with ["d", date] while omitting ["timestamp", date]. The Go relay strictly enforces NIP-101h.9 and rejected it with missing timestamp tag.
  4. The event on the relay never changed, and on every page reload, the web UI saw the event without a d tag and restarted the loop.
  • The Resolution:
    • Stripped the extraneous d-tag requirement from Kind 1359 across Android and Web.
    • Web migration UI now strictly targets legacy Kind 30078 events.
    • Kind 1359 is recognized as the modern canonical standard, using timestamp as specified in NIP-101h.9.

🛡️ 4.4 The Big Win: Eliminating the “Display Over Other Apps” Permission

Historically, Android developers integrating background signers ran headfirst into Android 10+ Background Activity Launch (BAL) restrictions:

  • Android strictly blocks background services, broadcast receivers, and WorkManager jobs from calling context.startActivity() to open another application.
  • Because background ContentProvider encryption was previously failing due to parameter shifts, our apps had to fall back to launching SignActivity (interactive UI signing via Intents).
  • To prevent Android from terminating that background UI launch, users had to manually navigate into system settings and grant “Display over other apps” (SYSTEM_ALERT_WINDOW) during onboarding.

With the canonical NIP-55 projection in place, this overlay permission is completely obsolete:

  • ContentResolver.query() is a pure IPC Binder transaction. It never creates a window, never touches the window manager, and never attempts a background activity launch.
  • Amber processes the request entirely inside its background provider process, returning the ciphertext or Schnorr signature via the cursor.
  • In live testing with SYSTEM_ALERT_WINDOW forcibly revoked via ADB (appops set <pkg> SYSTEM_ALERT_WINDOW ignore), SisterCharge and Nunlock continue to encrypt, sign, and broadcast periodic events in under 300ms while remaining completely silent and invisible.

The Full Excision Across the Wellbeing Mobile Suite:
Every app has now been updated:

  1. Removed from Manifests: android.permission.SYSTEM_ALERT_WINDOW has been purged from AndroidManifest.xml across all apps.
  2. Simplified Onboarding: The “Display Over Other Apps” / “Appear on Top” step has been completely removed from the onboarding flows in SisterCharge, Holy Fit, Nunlock, and The Habit. Users no longer need to navigate to Android settings to grant special overlay access to get sovereign sync working.
  3. Settings Prompts Deleted: Removed checkOverlayPermission() prompts in the settings activities.

🐹 5. Backend Standardization: Golang Relays & go install Distribution

While sovereign clients and dashboards handle the user interface, the backbone of Wellbeing is our suite of six micro-relays written in Go using khatru and eventstore.

To ensure the backend evolved in lockstep with our client improvements, we executed a complete sweep across all six relay repositories, tagging and publishing v1.7.1:

1. Database Path Normalization & Clean Environment Config

Previously, different relays had varying database naming conventions and .gitignore exceptions (e.g. holyfit_data.db vs holyfit.db).

  • Normalized Defaults: Every relay’s main.go now defaults cleanly to ./<app>.db (e.g. ./cellibacy.db, ./holyfit.db, ./nunlock.db, ./sistercharge.db, ./thehabit.db, ./saintstream.db).
  • Production Templates: Each relay now includes a detailed .env.example documenting PORT, DB_PATH, ALLOWED_KINDS, and NIP-11 relay info.
  • Unified Git Ignore: Normalized .gitignore across all repos to reliably ignore *.db, .env, and compiled binaries.

2. Sovereign Go Module Distribution

Because our self-hosted Gitea instance serves canonical <meta name="go-import"> metadata, any node operator or user can compile and install any Wellbeing relay directly using Go tooling:

# Install any exclusive relay directly via Go
GOPRIVATE=git.vanderwarker.family/* go install git.vanderwarker.family/wellbeing/sistercharge-relay@latest

# Or pin to the v1.7.1 release:
GOPRIVATE=git.vanderwarker.family/* go install git.vanderwarker.family/wellbeing/holyfit-relay@v1.7.1

# Run the relay binary:
~/go/bin/sistercharge-relay

3. Pre-Compiled Standalone Binaries

For users running headless VPS servers without Go toolchains installed, every v1.7.1 Gitea release now includes stripped, standalone Linux amd64 binary assets ready for instant deployment.

4. Ecosystem-Wide CHANGELOG.md Deployment

Every single repository in the Wellbeing suite—6 mobile apps, 6 web dashboards, and 6 Go relays (18 repositories in total)—now maintains an active, standardized CHANGELOG.md adhering to Keep a Changelog and Semantic Versioning.


🌐 6. Wellbeing Ecosystem Overview

All Wellbeing clients, web dashboards, and relays are now fully synchronized and standardized:

📱 Android Mobile Clients

App Version Target Kind NIP Spec Protocol Format Background Signing
🏃 Holy Fit v2.0.6 kind:1359 NIP-101h.9 Regular Stream (timestamp) Amber NIP-55 (SIGN_EVENT)
🔒 Nunlock v2.0.6 kind:30342 NIP-01 / NIP-78 Parameterized Replaceable (d) Amber NIP-55 (NIP44_ENCRYPT)
🔋 Sister Charge v2.0.7 kind:30343 NIP-01 / NIP-78 Parameterized Replaceable (d) WorkManager + Amber NIP-55
📶 Cellibacy v2.0.7 kind:34557 / kind:30345 NIP-01 / NIP-78 Parameterized Replaceable (d) Amber NIP-55 (NIP44_ENCRYPT)
🧘 The Habit v2.0.1 kind:30345 NIP-01 / NIP-78 Parameterized Replaceable (d) Amber NIP-55 (NIP44_ENCRYPT)
🎵 Saint Stream v2.0.8 kind:36787 / kind:30344 NIP-01 / NIP-78 Heartbeat & Archive Amber NIP-55 (SIGN_EVENT)

🐹 Exclusive Golang Relays

Relay Version Target Kinds Backend go install Command Release Assets
📶 Cellibacy Relay v1.7.1 34557, 5, 10323 SQLite3 go install git.vanderwarker.family/wellbeing/cellibacy-relay@v1.7.1 Linux x86_64 Binary
🏃 Holy Fit Relay v1.7.1 1359, 30078, 5, 10323 SQLite3 + Ratchet go install git.vanderwarker.family/wellbeing/holyfit-relay@v1.7.1 Linux x86_64 Binary
🔒 Nunlock Relay v1.7.1 30342, 30078, 5, 10323 SQLite3 + Ratchet go install git.vanderwarker.family/wellbeing/nunlock-relay@v1.7.1 Linux x86_64 Binary
🔋 Sister Charge Relay v1.7.1 30343, 30078, 5, 10323 SQLite3 go install git.vanderwarker.family/wellbeing/sistercharge-relay@v1.7.1 Linux x86_64 Binary
🧘 The Habit Relay v1.7.1 30345, 5, 10323 SQLite3 go install git.vanderwarker.family/wellbeing/thehabit-relay@v1.7.1 Linux x86_64 Binary
🎵 Saint Stream Relay v1.7.1 36787, 30344, 30078, 5, 10323 SQLite3 + Jukebox go install git.vanderwarker.family/wellbeing/saintstream-relay@v1.7.1 Linux x86_64 Binary

🔮 What’s Next

With standardized d-tags on replaceable apps, pure NIP-101h.9 compliance on Holy Fit, bulletproof NIP-44 background encryption, overlay-free Android permissions, and a synchronized fleet of Go micro-relays distributed via go install, the Wellbeing ecosystem is faster, quieter, and more robust than ever. Next up: cross-metric correlation engines and multi-timeframe analytics across the web dashboards!


Write a comment