firestore

package
v0.17.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 3, 2026 License: Unlicense Imports: 12 Imported by: 0

README

firestore

An eventstore.Store backend backed by Google Cloud Firestore (Native mode). It also implements eventstore.Counter.

Usage

store := &firestore.FirestoreBackend{
	ProjectID:  "my-gcp-project",
	Collection: "events", // optional, defaults to "events"
}
if err := store.Init(); err != nil {
	log.Fatal(err)
}
defer store.Close()

Credentials come from Application Default Credentials. Set FIRESTORE_EMULATOR_HOST to run against the local emulator.

Composite indexes (required)

Firestore rejects most non-trivial queries unless the matching composite index exists. Deploy the indexes in firestore.indexes.json before using the backend:

gcloud firestore indexes create --index-file=firestore.indexes.json
# or, with the Firebase CLI:
firebase deploy --only firestore:indexes

The fieldOverrides also disable single-field indexing on content, sig and the raw tags field — they are never queried directly, and exempting them cuts index storage (and therefore cost) substantially.

How queries map to Firestore

Firestore allows only one disjunctive (in / array-contains-any) clause and one range field per query, and cannot OR across different fields. A nostr.Filter is richer than that, so the backend:

  1. Pushes down the created_at range plus the single most selective dimension, chosen in the order ids → tag → authors → kinds.
  2. Streams the resulting documents (paging with cursors up to the query limit) and enforces every remaining constraint client-side with filter.Matches.

The push-down is purely a cost optimization — correctness always comes from filter.Matches, so results are never wrong, only potentially more expensive when a filter can't be fully expressed as a single Firestore query.

Tags are indexed only for single-letter tag names (per NIP-01), stored in a tagvalues array as "<letter>:<value>" entries.

CountEvents

When a filter fits entirely in the pushed-down query (at most one disjunctive dimension, no search term), CountEvents uses Firestore's native aggregation, which is billed per index scan rather than per document. Otherwise it falls back to reading and matching candidates, which is more expensive.

Cost note

Firestore bills primarily per document read, not per stored gigabyte. A filter that can't be pushed down (e.g. several tag constraints at once) reads every candidate document and filters in memory, so it is best suited to archival or low-query-volume relays. High-traffic relays should measure read volume before committing.

Tests

The unit tests cover the pure filter-translation logic and need no Firestore connection:

go test ./firestore/

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type FirestoreBackend

type FirestoreBackend struct {
	sync.Mutex
	*firestore.Client

	// ProjectID is the Google Cloud project that owns the Firestore database.
	// Required. Credentials come from Application Default Credentials, or from
	// the FIRESTORE_EMULATOR_HOST environment variable when set.
	ProjectID string
	// DatabaseID selects a named Firestore database within the project.
	// Defaults to "(default)".
	DatabaseID string
	// Collection is the Firestore collection events are stored in. Defaults to
	// "events".
	Collection string
	// QueryLimit caps how many events a single QueryEvents call may return.
	QueryLimit int
	// PageSize is how many documents are fetched per Firestore round-trip while
	// paging towards QueryLimit. Larger values mean fewer round-trips but more
	// documents read when client-side filtering discards many of them.
	PageSize int
}

FirestoreBackend is an eventstore.Store backed by Google Cloud Firestore (Native mode).

Firestore's query language is much more restrictive than SQL: a single query may only carry one disjunctive ("in" / "array-contains-any") clause and one range field, and every composite of equality/array + range + order-by needs a pre-declared composite index (see firestore.indexes.json). Because of that this backend pushes down only the created_at range plus the single most selective dimension of a nostr.Filter and enforces the remaining constraints client-side with filter.Matches, so results are always correct while the push-down keeps the number of billed document reads down.

func (*FirestoreBackend) Close

func (b *FirestoreBackend) Close()

func (*FirestoreBackend) CountEvents

func (b *FirestoreBackend) CountEvents(ctx context.Context, filter nostr.Filter) (int64, error)

func (*FirestoreBackend) DeleteEvent

func (b *FirestoreBackend) DeleteEvent(ctx context.Context, event *nostr.Event) error

func (*FirestoreBackend) Init

func (b *FirestoreBackend) Init() error

func (*FirestoreBackend) QueryEvents

func (b *FirestoreBackend) QueryEvents(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error)

func (*FirestoreBackend) ReplaceEvent

func (b *FirestoreBackend) ReplaceEvent(ctx context.Context, evt *nostr.Event) error

func (*FirestoreBackend) SaveEvent

func (b *FirestoreBackend) SaveEvent(ctx context.Context, event *nostr.Event) error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL