11 KiB
Plan: SQLite Integration, No-Login Mode, Bootstrap Relay Fetch
Overview
Three related features that build on each other:
- SQLite — persistent local storage for Nostr events and future data
- No-Login mode — bypass Nostr login, use the browser as a normal browser
- 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()aftersettings_load()inmain(). - Call
db_close()inon_window_destroy()beforegtk_main_quit().
Phase 2: No-Login Mode
Login dialog changes (src/login_dialog.c)
Button layout (action area, left to right):
No Login— far left (NEW)Cancel— centerSign 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_clickedhandler that setsctx->done = TRUEand responds with a new response code (e.g.GTK_RESPONSE_OTHERor a custom value likeGTK_RESPONSE_APPLY).
login_result_t changes (src/login_dialog.h):
- The existing
method == KEY_STORE_METHOD_NONEwithsigner == NULLalready represents "no identity." The dialog will return success (0) withmethod = KEY_STORE_METHOD_NONEand emptypubkey_hex.
login_dialog_run changes:
- After
gtk_dialog_runreturns, check for the no-login response code. If it's the no-login response, setresult->method = KEY_STORE_METHOD_NONE, clear the result, and return 0 (success).
src/main.c changes
- In
do_login(), after a successful login withmethod == KEY_STORE_METHOD_NONEand empty pubkey, setg_state.readonly = TRUEandg_logged_in = TRUEbut don't set a signer. The browser works normally —window.nostrwon'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-loginflag that skips the login dialog entirely (setsg_logged_in = TRUEwith 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:
- Parse
bootstrap_relaysfrom settings into an array of URLs. - Build a cJSON filter:
{"authors": [pubkey], "kinds": [0, 3, 10002]}. - Call
synchronous_query_relays_with_progress(relay_urls, relay_count, filter, RELAY_QUERY_ALL_RESULTS, &result_count, 15, NULL, NULL, 0). - For each returned event, call
db_store_event(event). - Log the results:
[relay] Fetched N events from M relays. - 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
sudo apt install libsqlite3-devmake— verify clean build- Test No-Login mode:
./browser.sh start --no-login- Verify browser opens without login dialog, normal browsing works
- Verify
window.nostrreturns "No identity loaded" errors
- Test login + relay fetch:
./browser.sh start --login-method generate- Verify log shows
[relay] Fetched N events from M relays - Verify
~/.sovereign_browser/browser.dbcontains events
- Test settings page:
- Navigate to
sovereign://settings - Verify "Bootstrap Relays" section appears with the 3 default relays
- Edit relays, save, restart, verify persistence
- Navigate to
- 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 |