Add prefix to event store

This commit is contained in:
Jon Staab
2025-09-24 16:52:02 -07:00
parent 307dcda4a7
commit 3c3eefc378
9 changed files with 67 additions and 47 deletions
+7 -7
View File
@@ -6,6 +6,13 @@ This is a multi-tenant relay based on [Khatru](https://gitworkshop.dev/fiatjaf.c
A single zooid instance can run any number of "virtual" relays. The `config` directory can contain any number of configuration files, each of which represents a single virtual relay.
## Environment
Zooid supports a few environment variables, which configure shared resources, like the web server or sqlite database file.
- `PORT` - the port the server will listen on for all requests. Defaults to `3334`.
- `DATABASE_PATH` - the location of the database path. Defaults to `./data.db`
## Configuration
Configuration files are written using [toml](https://toml.io). The name of the configuration file should be the hostname the relay serves, for example `relay.example.com`. Config files contain the following sections:
@@ -56,13 +63,6 @@ Defines roles that can be assigned to different users and attendant privileges.
A special `[roles.member]` heading may be used to configure policies for all relay users (that is, pubkeys assigned to other roles, or who have redeemed an invite code).
### `[data]`
Contains information related to data persistence.
- `events` - the location of the sqlite database file used to store events. Defaults to `./data/{my-relay}/events`.
- `media` - the location of the sqlite database file used to store file metadata. Defaults to `./data/{my-relay}/media`.
### Example
The below config file might be saved as `./config/my-relay.example.com` in order to route requests from `wss://my-relay.example.com` to this virtual relay.
+3 -2
View File
@@ -5,6 +5,8 @@ go 1.24.1
require (
fiatjaf.com/nostr v0.0.0-20250924142401-59bd3c29fffd
github.com/BurntSushi/toml v1.5.0
github.com/Masterminds/squirrel v1.5.4
github.com/gosimple/slug v1.15.0
github.com/mattn/go-sqlite3 v1.14.32
github.com/spf13/afero v1.15.0
github.com/stretchr/testify v1.10.0
@@ -12,8 +14,6 @@ require (
require (
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/PowerDNS/lmdb-go v1.9.3 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
@@ -23,6 +23,7 @@ require (
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/fasthttp/websocket v1.5.12 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
+4 -6
View File
@@ -31,16 +31,16 @@ github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZ
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo=
github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
@@ -62,8 +62,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
+1 -1
View File
@@ -6,7 +6,7 @@ import (
)
func (s *SqliteBackend) DeleteEvent(id nostr.ID) error {
_, err := squirrel.Delete("events").Where(squirrel.Eq{"id": id.Hex()}).RunWith(s.db).Exec()
_, err := squirrel.Delete(s.tmpl("{{.Prefix}}events")).Where(squirrel.Eq{"id": id.Hex()}).RunWith(s.db).Exec()
return err
}
+38 -22
View File
@@ -1,8 +1,10 @@
package sqlite
import (
"bytes"
"database/sql"
"fmt"
"text/template"
"fiatjaf.com/nostr/eventstore"
_ "github.com/mattn/go-sqlite3"
@@ -13,6 +15,7 @@ var _ eventstore.Store = (*SqliteBackend)(nil)
type SqliteBackend struct {
db *sql.DB
Path string
Prefix string
FTSAvailable bool
}
@@ -41,10 +44,20 @@ func (s *SqliteBackend) Init() error {
return nil
}
func (s *SqliteBackend) tmpl(t string) string {
var buf bytes.Buffer
err := template.Must(template.New("schema").Parse(t)).Execute(&buf, s)
if err != nil {
panic(err)
}
return buf.String()
}
func (s *SqliteBackend) createSchema() error {
// Create basic schema first
basicSchema := `
CREATE TABLE IF NOT EXISTS events (
basicSchema := s.tmpl(`
CREATE TABLE IF NOT EXISTS {{.Prefix}}events (
id TEXT PRIMARY KEY,
created_at INTEGER NOT NULL,
kind INTEGER NOT NULL,
@@ -54,23 +67,23 @@ func (s *SqliteBackend) createSchema() error {
sig TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at);
CREATE INDEX IF NOT EXISTS idx_events_kind ON events(kind);
CREATE INDEX IF NOT EXISTS idx_events_pubkey ON events(pubkey);
CREATE INDEX IF NOT EXISTS idx_events_kind_pubkey ON events(kind, pubkey);
CREATE INDEX IF NOT EXISTS idx_events_kind_pubkey_created_at ON events(kind, pubkey, created_at DESC);
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_events_created_at ON {{.Prefix}}events(created_at);
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_events_kind ON {{.Prefix}}events(kind);
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_events_pubkey ON {{.Prefix}}events(pubkey);
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_events_kind_pubkey ON {{.Prefix}}events(kind, pubkey);
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_events_kind_pubkey_created_at ON {{.Prefix}}events(kind, pubkey, created_at DESC);
CREATE TABLE IF NOT EXISTS event_tags (
CREATE TABLE IF NOT EXISTS {{.Prefix}}event_tags (
event_id TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
FOREIGN KEY (event_id) REFERENCES {{.Prefix}}events(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_event_tags_event_id ON event_tags(event_id);
CREATE INDEX IF NOT EXISTS idx_event_tags_key ON event_tags(key);
CREATE INDEX IF NOT EXISTS idx_event_tags_key_value ON event_tags(key, value);
`
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_event_tags_event_id ON {{.Prefix}}event_tags(event_id);
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_event_tags_key ON {{.Prefix}}event_tags(key);
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_event_tags_key_value ON {{.Prefix}}event_tags(key, value);
`)
if _, err := s.db.Exec(basicSchema); err != nil {
return fmt.Errorf("failed to create schema: %w", err)
@@ -78,23 +91,26 @@ func (s *SqliteBackend) createSchema() error {
// Try to create FTS5 schema - if it fails, continue without it
ftsSchema := `
CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
CREATE VIRTUAL TABLE IF NOT EXISTS {{.Prefix}}events_fts USING fts5(
content,
content='events',
content='{{.Prefix}}events',
content_rowid='rowid'
);
CREATE TRIGGER IF NOT EXISTS events_ai AFTER INSERT ON events BEGIN
INSERT INTO events_fts(rowid, content) VALUES (new.rowid, new.content);
CREATE TRIGGER IF NOT EXISTS {{.Prefix}}events_ai AFTER INSERT ON {{.Prefix}}events BEGIN
INSERT INTO {{.Prefix}}events_fts(rowid, content) VALUES (new.rowid, new.content);
END;
CREATE TRIGGER IF NOT EXISTS events_ad AFTER DELETE ON events BEGIN
INSERT INTO events_fts(events_fts, rowid, content) VALUES('delete', old.rowid, old.content);
CREATE TRIGGER IF NOT EXISTS {{.Prefix}}events_ad AFTER DELETE ON {{.Prefix}}events BEGIN
INSERT INTO {{.Prefix}}events_fts({{.Prefix}}events_fts, rowid, content)
VALUES('delete', old.rowid, old.content);
END;
CREATE TRIGGER IF NOT EXISTS events_au AFTER UPDATE ON events BEGIN
INSERT INTO events_fts(events_fts, rowid, content) VALUES('delete', old.rowid, old.content);
INSERT INTO events_fts(rowid, content) VALUES (new.rowid, new.content);
CREATE TRIGGER IF NOT EXISTS {{.Prefix}}events_au AFTER UPDATE ON {{.Prefix}}events BEGIN
INSERT INTO {{.Prefix}}events_fts({{.Prefix}}events_fts, rowid, content)
VALUES('delete', old.rowid, old.content);
INSERT INTO {{.Prefix}}events_fts(rowid, content)
VALUES (new.rowid, new.content);
END;
`
+3 -3
View File
@@ -76,12 +76,12 @@ func (s *SqliteBackend) QueryEvents(filter nostr.Filter, maxLimit int) iter.Seq[
func (s *SqliteBackend) buildSelectQuery(filter nostr.Filter, limit int) squirrel.SelectBuilder {
qb := squirrel.Select("id", "created_at", "kind", "pubkey", "content", "tags", "sig").
From("events").
From(s.tmpl("{{.Prefix}}events")).
OrderBy("created_at DESC")
// Handle search with FTS (if available)
if filter.Search != "" && s.FTSAvailable {
qb = qb.Join("events_fts ON events.rowid = events_fts.rowid").
qb = qb.Join(s.tmpl("{{.Prefix}}events_fts ON {{.Prefix}}events.rowid = {{.Prefix}}events_fts.rowid")).
Where(squirrel.Eq{"events_fts": filter.Search})
} else if filter.Search != "" {
// Fallback to LIKE search if FTS not available
@@ -128,7 +128,7 @@ func (s *SqliteBackend) buildSelectQuery(filter nostr.Filter, limit int) squirre
}
subQuery := squirrel.Select("event_id").
From("event_tags").
From(s.tmpl("{{.Prefix}}event_tags")).
Where(squirrel.Eq{"key": tagKey}).
Where(squirrel.Eq{"value": tagValueInterfaces})
+3 -3
View File
@@ -13,7 +13,7 @@ import (
func (s *SqliteBackend) SaveEvent(evt nostr.Event) error {
// Check if event already exists
var existingID string
qb := squirrel.Select("id").From("events").Where(squirrel.Eq{"id": evt.ID.Hex()})
qb := squirrel.Select("id").From(s.tmpl("{{.Prefix}}events")).Where(squirrel.Eq{"id": evt.ID.Hex()})
err := qb.RunWith(s.db).QueryRow().Scan(&existingID)
if err == nil {
// Event already exists
@@ -27,7 +27,7 @@ func (s *SqliteBackend) SaveEvent(evt nostr.Event) error {
}
// Insert the event
insertQb := squirrel.Insert("events").
insertQb := squirrel.Insert(s.tmpl("{{.Prefix}}events")).
Columns("id", "created_at", "kind", "pubkey", "content", "tags", "sig").
Values(
evt.ID.Hex(),
@@ -48,7 +48,7 @@ func (s *SqliteBackend) SaveEvent(evt nostr.Event) error {
// Insert single-letter tags into event_tags table
for _, tag := range evt.Tags {
if len(tag) >= 2 && len(tag[0]) == 1 {
tagQb := squirrel.Insert("event_tags").
tagQb := squirrel.Insert(s.tmpl("{{.Prefix}}event_tags")).
Columns("event_id", "key", "value").
Values(evt.ID.Hex(), tag[0], tag[1])
+2 -1
View File
@@ -12,7 +12,8 @@ func TestSqliteFlow(t *testing.T) {
os.RemoveAll("/tmp/sqlitetest.db")
sb := &SqliteBackend{
Path: "/tmp/sqlitetest.db",
Path: "/tmp/sqlitetest.db",
Prefix: "prefix",
}
err := sb.Init()
assert.NoError(t, err)
+6 -2
View File
@@ -11,6 +11,7 @@ import (
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore"
"fiatjaf.com/nostr/khatru"
"github.com/gosimple/slug"
"zooid/sqlite"
)
@@ -40,8 +41,11 @@ func MakeInstance(hostname string) (*Instance, error) {
instance := &Instance{
Host: hostname,
Config: config,
Events: &sqlite.SqliteBackend{Path: config.Data.Events},
Relay: khatru.NewRelay(),
Events: &sqlite.SqliteBackend{
Path: Env("DATABASE_PATH", "./data.db"),
Prefix: slug.Make(hostname) + "__",
},
Relay: khatru.NewRelay(),
}
instance.Relay.Info.Name = config.Self.Name