Vault

PostgreSQL Store

Production-ready PostgreSQL backend using Grove ORM with pgdriver.

The PostgreSQL store (herald/store/postgres) provides a production-ready backend for Herald using Grove ORM with pgdriver. It stores all Herald entities in 7 tables with JSONB columns for maps and arrays, indexed for efficient querying, and supports upsert with ON CONFLICT.

Installation

go get github.com/xraph/herald
go get github.com/xraph/grove
go get github.com/xraph/grove/drivers/pgdriver

Creating a Store

import (
    "github.com/xraph/grove"
    "github.com/xraph/grove/drivers/pgdriver"
    "github.com/xraph/herald/store/postgres"
)

// Open a PostgreSQL connection via Grove.
db := grove.Open(pgdriver.New(
    pgdriver.WithDSN("postgres://user:pass@localhost:5432/herald?sslmode=disable"),
))

// Create the Herald store.
store := postgres.New(db)

// Run migrations (idempotent -- safe on every startup).
if err := store.Migrate(ctx); err != nil {
    log.Fatal("migration failed:", err)
}

Constructor

func New(db *grove.DB) *Store

New accepts a *grove.DB backed by pgdriver and returns a *Store that satisfies the store.Store interface. Internally it calls pgdriver.Unwrap(db) to access the PostgreSQL-specific query builder.

Tables

The store creates 7 tables. All use TEXT primary keys (TypeIDs), TIMESTAMPTZ timestamps defaulting to NOW(), and JSONB for structured data.

herald_providers

Stores notification provider configurations (email, SMS, push drivers).

ColumnTypeNotes
idTEXT PRIMARY KEYTypeID (hpvd_...)
app_idTEXT NOT NULLApplication scope
nameTEXT NOT NULLHuman-readable name
channelTEXT NOT NULLemail, sms, push, inapp
driverTEXT NOT NULLDriver name (smtp, resend, twilio, fcm)
credentialsJSONB NOT NULL DEFAULT '{}'Driver credentials
settingsJSONB NOT NULL DEFAULT '{}'Driver settings
priorityINTEGER NOT NULL DEFAULT 0Lower = higher priority
enabledBOOLEAN NOT NULL DEFAULT TRUEActive flag
created_atTIMESTAMPTZ NOT NULL DEFAULT NOW()
updated_atTIMESTAMPTZ NOT NULL DEFAULT NOW()

Indexes: (app_id, channel), (app_id)

herald_templates

Stores notification template definitions.

ColumnTypeNotes
idTEXT PRIMARY KEYTypeID (htpl_...)
app_idTEXT NOT NULLApplication scope
slugTEXT NOT NULLURL-safe identifier
nameTEXT NOT NULLDisplay name
channelTEXT NOT NULLChannel type
categoryTEXT NOT NULL DEFAULT 'transactional'auth, transactional, marketing, system
variablesJSONBExpected template variables
is_systemBOOLEAN NOT NULL DEFAULT FALSESystem template flag
enabledBOOLEAN NOT NULL DEFAULT TRUEActive flag
created_atTIMESTAMPTZ NOT NULL DEFAULT NOW()
updated_atTIMESTAMPTZ NOT NULL DEFAULT NOW()

Unique constraint: (app_id, slug, channel) Indexes: (app_id), (app_id, channel)

herald_template_versions

Stores locale-specific content for templates.

ColumnTypeNotes
idTEXT PRIMARY KEYTypeID (htpv_...)
template_idTEXT NOT NULL REFERENCES herald_templates(id) ON DELETE CASCADEParent template
localeTEXT NOT NULL DEFAULT ''Locale code (e.g., en, fr)
subjectTEXTEmail subject line
htmlTEXTHTML body
textTEXTPlain text body
titleTEXTPush/in-app title
activeBOOLEAN NOT NULL DEFAULT TRUEActive flag
created_atTIMESTAMPTZ NOT NULL DEFAULT NOW()
updated_atTIMESTAMPTZ NOT NULL DEFAULT NOW()

Unique constraint: (template_id, locale) Indexes: (template_id)

herald_messages

Stores the delivery log of sent or queued notifications.

ColumnTypeNotes
idTEXT PRIMARY KEYTypeID (hmsg_...)
app_idTEXT NOT NULLApplication scope
env_idTEXT NOT NULL DEFAULT ''Environment scope
template_idTEXT NOT NULL DEFAULT ''Template used
provider_idTEXT NOT NULL DEFAULT ''Provider used
channelTEXT NOT NULLChannel type
recipientTEXT NOT NULLRecipient address
subjectTEXT NOT NULL DEFAULT ''Message subject
bodyTEXT NOT NULL DEFAULT ''Message body (truncated)
statusTEXT NOT NULL DEFAULT 'queued'queued, sending, sent, failed, bounced, delivered
errorTEXT NOT NULL DEFAULT ''Error message on failure
metadataJSONB NOT NULL DEFAULT '{}'User-defined metadata
asyncBOOLEAN NOT NULL DEFAULT FALSEAsync delivery flag
attemptsINTEGER NOT NULL DEFAULT 0Delivery attempts
sent_atTIMESTAMPTZWhen sent
delivered_atTIMESTAMPTZWhen delivered
created_atTIMESTAMPTZ NOT NULL DEFAULT NOW()

Indexes: (app_id, status), (created_at DESC)

herald_inbox

Stores in-app notifications for user inboxes.

ColumnTypeNotes
idTEXT PRIMARY KEYTypeID (hinb_...)
app_idTEXT NOT NULLApplication scope
env_idTEXT NOT NULL DEFAULT ''Environment scope
user_idTEXT NOT NULLTarget user
typeTEXT NOT NULLNotification type slug
titleTEXT NOT NULLDisplay title
bodyTEXT NOT NULL DEFAULT ''Body text
action_urlTEXT NOT NULL DEFAULT ''CTA link
image_urlTEXT NOT NULL DEFAULT ''Optional image
readBOOLEAN NOT NULL DEFAULT FALSERead state
read_atTIMESTAMPTZWhen marked read
metadataJSONB NOT NULL DEFAULT '{}'User-defined metadata
expires_atTIMESTAMPTZOptional expiration
created_atTIMESTAMPTZ NOT NULL DEFAULT NOW()

Indexes: (user_id, read, created_at DESC), (app_id, user_id)

herald_preferences

Stores per-user notification opt-in/opt-out preferences.

ColumnTypeNotes
idTEXT PRIMARY KEYTypeID (hprf_...)
app_idTEXT NOT NULLApplication scope
user_idTEXT NOT NULLUser ID
overridesJSONB NOT NULL DEFAULT '{}'Per-template channel preferences
created_atTIMESTAMPTZ NOT NULL DEFAULT NOW()
updated_atTIMESTAMPTZ NOT NULL DEFAULT NOW()

Unique constraint: (app_id, user_id)

herald_scoped_configs

Stores scoped provider configuration overrides (app/org/user level).

ColumnTypeNotes
idTEXT PRIMARY KEYTypeID (hscf_...)
app_idTEXT NOT NULLApplication scope
scopeTEXT NOT NULLapp, org, or user
scope_idTEXT NOT NULLScope entity ID
email_provider_idTEXT NOT NULL DEFAULT ''Email provider override
sms_provider_idTEXT NOT NULL DEFAULT ''SMS provider override
push_provider_idTEXT NOT NULL DEFAULT ''Push provider override
from_emailTEXT NOT NULL DEFAULT ''Sender email override
from_nameTEXT NOT NULL DEFAULT ''Sender name override
from_phoneTEXT NOT NULL DEFAULT ''Sender phone override
default_localeTEXT NOT NULL DEFAULT ''Default locale override
created_atTIMESTAMPTZ NOT NULL DEFAULT NOW()
updated_atTIMESTAMPTZ NOT NULL DEFAULT NOW()

Unique constraint: (app_id, scope, scope_id)

UPSERT Patterns

The PostgreSQL store uses INSERT ... ON CONFLICT DO UPDATE for preferences and scoped configs, ensuring idempotent writes:

// Preference upsert
_, err := s.pg.NewInsert(m).
    OnConflict("(app_id, user_id) DO UPDATE").
    Set("overrides = EXCLUDED.overrides").
    Set("updated_at = EXCLUDED.updated_at").
    Exec(ctx)

// Scoped config upsert
_, err := s.pg.NewInsert(m).
    OnConflict("(app_id, scope, scope_id) DO UPDATE").
    Set("email_provider_id = EXCLUDED.email_provider_id").
    // ... other fields
    Exec(ctx)

Lifecycle Methods

MethodBehaviour
Migrate(ctx)Creates all 7 tables and indexes via Grove's migration orchestrator
Ping(ctx)Calls db.Ping(ctx) to verify database connectivity
Close()Calls db.Close() to release the database connection

Connection Setup Example

package main

import (
    "context"
    "log"
    "os"

    "github.com/xraph/grove"
    "github.com/xraph/grove/drivers/pgdriver"
    "github.com/xraph/herald"
    "github.com/xraph/herald/store/postgres"
)

func main() {
    ctx := context.Background()

    dsn := os.Getenv("DATABASE_URL")
    if dsn == "" {
        dsn = "postgres://localhost:5432/herald?sslmode=disable"
    }

    db := grove.Open(pgdriver.New(
        pgdriver.WithDSN(dsn),
    ))

    store := postgres.New(db)
    if err := store.Migrate(ctx); err != nil {
        log.Fatal("migrate:", err)
    }
    if err := store.Ping(ctx); err != nil {
        log.Fatal("ping:", err)
    }

    h, err := herald.New(
        herald.WithStore(store),
    )
    if err != nil {
        log.Fatal("herald:", err)
    }
    defer store.Close()

    _ = h // use the Herald instance
}

When to Use

  • Production deployments -- durable, ACID-compliant storage with connection pooling.
  • Multi-instance deployments -- all application instances share the same database.
  • Teams already using PostgreSQL -- leverage existing infrastructure and expertise.
  • Full feature support -- JSONB indexing, foreign key cascades, and upsert semantics.

On this page