Files
sovereign_browser/plans/sqlite-relay-login.md
T

11 KiB

Plan: SQLite Integration, No-Login Mode, Bootstrap Relay Fetch

Overview

Three related features that build on each other:

  1. SQLite — persistent local storage for Nostr events and future data
  2. No-Login mode — bypass Nostr login, use the browser as a normal browser
  3. Bootstrap relay fetch — after login, fetch the user's kind 0/3/10002 events from configured relays and cache them in SQLite
flowchart TD
    A[Browser startup] --> B{Login dialog}
    B -->|Sign In| C[Login with Nostr key]
    B -->|No Login| D[Skip Nostr — normal browser mode]
    B -->|Cancel| E[Exit]
    
    C --> F[Fetch kind 0, 3, 10002 from bootstrap relays]
    F --> G[Store events in SQLite]
    G --> H[Browser ready with identity + cached events]
    
    D --> I[Browser ready — no identity, no relay fetch]
    
    subgraph "SQLite database"
        J[events table]
        K[event_tags table]
        L[relays table]
        M[key_value table]
    end
    
    G --> J
    G --> K
    H --> M

Phase 1: SQLite Integration

Install dependency

sudo apt install libsqlite3-dev

This provides sqlite3.h, libsqlite3.so, and sqlite3.pc (pkg-config).

New files: src/db.h / src/db.c

A thin wrapper around SQLite3 that provides:

/* db.h */
#pragma once
#include <glib.h>
#include "../nostr_core_lib/cjson/cJSON.h"

/* Initialize the database at ~/.sovereign_browser/browser.db.
 * Creates tables if they don't exist. Call once at startup. */
int  db_init(void);

/* Close the database. Call at shutdown. */
void db_close(void);

/* ── Events ─────────────────────────────────────────────── */

/* Store a Nostr event (upsert — replaces if same event_id exists).
 * Parses the cJSON event and stores it in the events table + tags table. */
int  db_store_event(const cJSON *event);

/* Fetch the most recent event of a given kind for a pubkey.
 * Returns a newly allocated cJSON event, or NULL if not found.
 * Caller must cJSON_Delete() the result. */
cJSON *db_get_latest_event(const char *pubkey_hex, int kind);

/* Fetch all events of a given kind for a pubkey (newest first).
 * Returns a cJSON array. Caller must cJSON_Delete() the result. */
cJSON *db_get_events(const char *pubkey_hex, int kind, int limit);

/* ── Key-Value store (for misc settings/cache) ──────────── */

int  db_kv_set(const char *key, const char *value);
const char *db_kv_get(const char *key);  /* returns pointer, valid until next db_kv_get call */

Database schema

-- Nostr events (kind 0 = profile, 3 = contacts, 10002 = relay list, etc.)
CREATE TABLE IF NOT EXISTS events (
    id          TEXT PRIMARY KEY,   -- event ID (hex, 64 chars)
    pubkey      TEXT NOT NULL,      -- author pubkey (hex)
    kind        INTEGER NOT NULL,
    created_at  INTEGER NOT NULL,   -- unix timestamp
    content     TEXT,               -- event content
    sig         TEXT,               -- signature
    raw_json    TEXT NOT NULL,      -- full event JSON for round-tripping
    fetched_at  INTEGER NOT NULL    -- when we cached it
);

-- Event tags (for querying by tag values, e.g. relay URLs in kind 10002)
CREATE TABLE IF NOT EXISTS event_tags (
    event_id  TEXT NOT NULL,
    tag_name  TEXT NOT NULL,        -- first element of tag array
    tag_value TEXT,                 -- second element (if any)
    position  INTEGER NOT NULL,     -- tag index in the event
    FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
);

-- Indexes for common queries
CREATE INDEX IF NOT EXISTS idx_events_pubkey_kind ON events(pubkey, kind, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_event_tags_name_value ON event_tags(tag_name, tag_value);

-- Simple key-value store for misc data
CREATE TABLE IF NOT EXISTS key_value (
    key   TEXT PRIMARY KEY,
    value TEXT
);

Makefile changes

CFLAGS  += $(shell pkg-config --cflags sqlite3)
LDLIBS  += $(shell pkg-config --libs   sqlite3)
SRC += src/db.c

Integration in src/main.c

  • Call db_init() after settings_load() in main().
  • Call db_close() in on_window_destroy() before gtk_main_quit().

Phase 2: No-Login Mode

Login dialog changes (src/login_dialog.c)

Button layout (action area, left to right):

  1. No Login — far left (NEW)
  2. Cancel — center
  3. Sign In — right

Changes:

  • Remove underscores from button labels: "_Cancel""Cancel", "_Sign In""Sign In".
  • Add a third button: GtkWidget *no_login_btn = gtk_button_new_with_label("No Login");
  • Pack it first (far left): gtk_box_pack_start(GTK_BOX(action_area), no_login_btn, FALSE, FALSE, 0); before cancel and login buttons.
  • Wire a new on_no_login_clicked handler that sets ctx->done = TRUE and responds with a new response code (e.g. GTK_RESPONSE_OTHER or a custom value like GTK_RESPONSE_APPLY).

login_result_t changes (src/login_dialog.h):

  • The existing method == KEY_STORE_METHOD_NONE with signer == NULL already represents "no identity." The dialog will return success (0) with method = KEY_STORE_METHOD_NONE and empty pubkey_hex.

login_dialog_run changes:

  • After gtk_dialog_run returns, check for the no-login response code. If it's the no-login response, set result->method = KEY_STORE_METHOD_NONE, clear the result, and return 0 (success).

src/main.c changes

  • In do_login(), after a successful login with method == KEY_STORE_METHOD_NONE and empty pubkey, set g_state.readonly = TRUE and g_logged_in = TRUE but don't set a signer. The browser works normally — window.nostr won't be available (the bridge will return "No identity loaded" for sign requests), but all normal browsing works.
  • Skip the relay fetch (Phase 4) when no identity is loaded.

CLI support (src/cli.c / src/cli.h)

  • Add --no-login flag that skips the login dialog entirely (sets g_logged_in = TRUE with no signer). This is equivalent to clicking "No Login" but from the command line.

Phase 3: Bootstrap Relays in Settings

Settings struct (src/settings.h / src/settings.c)

Add a new field to browser_settings_t:

#define SETTINGS_BOOTSTRAP_RELAYS_MAX  2048
// ...
char bootstrap_relays[SETTINGS_BOOTSTRAP_RELAYS_MAX];  /* newline-separated relay URLs */

Default value in settings_set_defaults():

wss://laantungir.net/relay\nwss://relay.primal.net\nwss://relay.damus.io

Parse/save in settings_load() / settings_save() as bootstrap_relays=... (URL-encoded or newline-separated, stored as a single key=value line).

Settings page (src/nostr_bridge.c)

Add a new section "Bootstrap Relays" to the sovereign://settings page (between Agent Server and Security):

<h2>Bootstrap Relays</h2>
<p class='note'>Relays queried after login to fetch your profile (kind 0),
contacts (kind 3), and relay list (kind 10002). One URL per line.</p>
<div class='field'>
  <div><div class='setting-name'>Relay URLs</div>
    <div class='setting-desc'>One wss:// URL per line</div></div>
  <div><textarea id='bootstrap_relays' rows='4' cols='40'>...</textarea>
    <button class='save-btn' onclick="save('bootstrap_relays')">Save</button></div>
</div>

The handle_settings_set key/value path needs a new case for bootstrap_relays that stores the newline-separated value into bs->bootstrap_relays and calls settings_save().


Phase 4: Post-Login Relay Fetch

New file: src/relay_fetch.h / src/relay_fetch.c

/* relay_fetch.h */
#pragma once
#include "../nostr_core_lib/cjson/cJSON.h"

/* Fetch the user's kind 0, 3, and 10002 events from the bootstrap relays.
 * Stores results in the SQLite database via db_store_event().
 * Runs synchronously (called from a background thread or with a timeout).
 * 
 *   pubkey_hex    — user's hex pubkey
 *   relay_urls    — NULL-terminated array of relay URLs
 *   relay_count   — number of relays
 * 
 * Returns the number of events fetched and stored, or -1 on error. */
int relay_fetch_bootstrap(const char *pubkey_hex,
                          const char **relay_urls,
                          int relay_count);

Implementation (src/relay_fetch.c)

Uses synchronous_query_relays_with_progress() from nostr_core_lib:

  1. Parse bootstrap_relays from settings into an array of URLs.
  2. Build a cJSON filter: {"authors": [pubkey], "kinds": [0, 3, 10002]}.
  3. Call synchronous_query_relays_with_progress(relay_urls, relay_count, filter, RELAY_QUERY_ALL_RESULTS, &result_count, 15, NULL, NULL, 0).
  4. For each returned event, call db_store_event(event).
  5. Log the results: [relay] Fetched N events from M relays.
  6. Free the results array and filter.

Integration in src/main.c

After successful login (in do_login() or right after it returns), if a signer/pubkey is available:

if (g_state.pubkey_hex[0] != '\0') {
    /* Parse bootstrap relays from settings. */
    /* Call relay_fetch_bootstrap() in a background thread to avoid
     * blocking the UI. Use g_thread_new() or a GTask. */
    g_thread_new("relay-fetch", relay_fetch_thread, NULL);
}

The fetch runs in a background thread so the browser is usable immediately. Events are stored in SQLite as they arrive. A future enhancement could notify the UI when the fetch completes.

Threading consideration

nostr_core_lib's synchronous_query_relays_with_progress is a blocking call that uses its own WebSocket event loop. It should be safe to call from a background thread as long as we don't touch GTK widgets from that thread. The db_store_event() calls only touch SQLite (which is thread-safe with proper locking — we'll use sqlite3_mutex or open the DB with SQLITE_OPEN_FULLMUTEX).


Phase 5: Build, Verify, Test

  1. sudo apt install libsqlite3-dev
  2. make — verify clean build
  3. Test No-Login mode:
    • ./browser.sh start --no-login
    • Verify browser opens without login dialog, normal browsing works
    • Verify window.nostr returns "No identity loaded" errors
  4. Test login + relay fetch:
    • ./browser.sh start --login-method generate
    • Verify log shows [relay] Fetched N events from M relays
    • Verify ~/.sovereign_browser/browser.db contains events
  5. Test settings page:
    • Navigate to sovereign://settings
    • Verify "Bootstrap Relays" section appears with the 3 default relays
    • Edit relays, save, restart, verify persistence
  6. Test login dialog buttons:
    • Verify "No Login" button is far left
    • Verify "Cancel" and "Sign In" labels have no underscores

File change summary

File Change
src/db.h NEW — SQLite wrapper API
src/db.c NEW — SQLite implementation (events, tags, key-value)
src/relay_fetch.h NEW — relay fetch API
src/relay_fetch.c NEW — fetch kind 0/3/10002 from bootstrap relays
src/settings.h Add bootstrap_relays field + max constant
src/settings.c Parse/save bootstrap_relays with defaults
src/nostr_bridge.c Add "Bootstrap Relays" section to settings page + set handler
src/login_dialog.c Add "No Login" button, remove underscores, new handler
src/login_dialog.h Document no-login result (method=NONE, empty pubkey)
src/main.c Call db_init()/db_close(), handle no-login result, trigger relay fetch
src/cli.h Add --no-login flag
src/cli.c Parse --no-login, skip login dialog
Makefile Add sqlite3 to pkg-config, add src/db.c + src/relay_fetch.c to SRC