2026-02-16 12:13:10 -05:00
package webpush
import (
"database/sql"
2026-02-17 12:04:51 -05:00
"fmt"
2026-03-02 19:45:35 -05:00
2026-03-10 22:17:40 -04:00
ntfydb "heckel.io/ntfy/v2/db"
2026-02-16 12:13:10 -05:00
)
const (
2026-02-21 21:29:29 -05:00
postgresCreateTablesQuery = `
2026-02-16 12:13:10 -05:00
CREATE TABLE IF NOT EXISTS webpush_subscription (
id TEXT PRIMARY KEY ,
endpoint TEXT NOT NULL UNIQUE ,
key_auth TEXT NOT NULL ,
key_p256dh TEXT NOT NULL ,
user_id TEXT NOT NULL ,
subscriber_ip TEXT NOT NULL ,
updated_at BIGINT NOT NULL ,
warned_at BIGINT NOT NULL DEFAULT 0
) ;
CREATE INDEX IF NOT EXISTS idx_webpush_subscriber_ip ON webpush_subscription ( subscriber_ip ) ;
2026-02-22 16:21:27 -05:00
CREATE INDEX IF NOT EXISTS idx_webpush_updated_at ON webpush_subscription ( updated_at ) ;
CREATE INDEX IF NOT EXISTS idx_webpush_user_id ON webpush_subscription ( user_id ) ;
2026-02-16 12:13:10 -05:00
CREATE TABLE IF NOT EXISTS webpush_subscription_topic (
subscription_id TEXT NOT NULL REFERENCES webpush_subscription ( id ) ON DELETE CASCADE ,
topic TEXT NOT NULL ,
PRIMARY KEY ( subscription_id , topic )
) ;
CREATE INDEX IF NOT EXISTS idx_webpush_topic ON webpush_subscription_topic ( topic ) ;
2026-02-16 22:39:54 -05:00
CREATE TABLE IF NOT EXISTS schema_version (
store TEXT PRIMARY KEY ,
2026-02-16 12:13:10 -05:00
version INT NOT NULL
) ;
`
2026-02-21 21:29:29 -05:00
postgresSelectSubscriptionIDByEndpointQuery = ` SELECT id FROM webpush_subscription WHERE endpoint = $1 `
postgresSelectSubscriptionCountBySubscriberIPQuery = ` SELECT COUNT(*) FROM webpush_subscription WHERE subscriber_ip = $1 `
postgresSelectSubscriptionsForTopicQuery = `
2026-02-16 12:13:10 -05:00
SELECT s . id , s . endpoint , s . key_auth , s . key_p256dh , s . user_id
FROM webpush_subscription_topic st
JOIN webpush_subscription s ON s . id = st . subscription_id
WHERE st . topic = $ 1
ORDER BY s . endpoint
`
2026-02-21 21:29:29 -05:00
postgresSelectSubscriptionsExpiringSoonQuery = `
2026-02-16 12:13:10 -05:00
SELECT id , endpoint , key_auth , key_p256dh , user_id
FROM webpush_subscription
WHERE warned_at = 0 AND updated_at <= $ 1
`
2026-02-22 14:25:15 -05:00
postgresUpsertSubscriptionQuery = `
2026-02-16 12:13:10 -05:00
INSERT INTO webpush_subscription ( id , endpoint , key_auth , key_p256dh , user_id , subscriber_ip , updated_at , warned_at )
VALUES ( $ 1 , $ 2 , $ 3 , $ 4 , $ 5 , $ 6 , $ 7 , $ 8 )
ON CONFLICT ( endpoint )
2026-02-16 18:53:12 -05:00
DO UPDATE SET key_auth = excluded . key_auth , key_p256dh = excluded . key_p256dh , user_id = excluded . user_id , subscriber_ip = excluded . subscriber_ip , updated_at = excluded . updated_at , warned_at = excluded . warned_at
2026-02-16 12:13:10 -05:00
`
2026-02-21 21:29:29 -05:00
postgresUpdateSubscriptionWarningSentQuery = ` UPDATE webpush_subscription SET warned_at = $1 WHERE id = $2 `
postgresUpdateSubscriptionUpdatedAtQuery = ` UPDATE webpush_subscription SET updated_at = $1 WHERE endpoint = $2 `
postgresDeleteSubscriptionByEndpointQuery = ` DELETE FROM webpush_subscription WHERE endpoint = $1 `
postgresDeleteSubscriptionByUserIDQuery = ` DELETE FROM webpush_subscription WHERE user_id = $1 `
postgresDeleteSubscriptionByAgeQuery = ` DELETE FROM webpush_subscription WHERE updated_at <= $1 `
2026-02-16 12:13:10 -05:00
2026-02-21 21:29:29 -05:00
postgresInsertSubscriptionTopicQuery = ` INSERT INTO webpush_subscription_topic (subscription_id, topic) VALUES ($1, $2) `
postgresDeleteSubscriptionTopicAllQuery = ` DELETE FROM webpush_subscription_topic WHERE subscription_id = $1 `
postgresDeleteSubscriptionTopicWithoutSubscriptionQuery = ` DELETE FROM webpush_subscription_topic WHERE subscription_id NOT IN (SELECT id FROM webpush_subscription) `
2026-02-16 12:13:10 -05:00
)
// PostgreSQL schema management queries
const (
2026-02-21 21:29:29 -05:00
pgCurrentSchemaVersion = 1
postgresInsertSchemaVersionQuery = ` INSERT INTO schema_version (store, version) VALUES ('webpush', $1) `
postgresSelectSchemaVersionQuery = ` SELECT version FROM schema_version WHERE store = 'webpush' `
2026-02-16 12:13:10 -05:00
)
2026-02-19 22:34:53 -05:00
// NewPostgresStore creates a new PostgreSQL-backed web push store using an existing database connection pool.
2026-03-10 22:17:40 -04:00
func NewPostgresStore ( d * ntfydb . DB ) ( * Store , error ) {
if err := setupPostgres ( d . SetupPrimary ( ) ) ; err != nil {
2026-02-16 12:13:10 -05:00
return nil , err
}
2026-03-01 13:19:53 -05:00
return & Store {
2026-03-10 22:17:40 -04:00
db : d ,
2026-03-01 13:19:53 -05:00
queries : queries {
2026-02-21 21:29:29 -05:00
selectSubscriptionIDByEndpoint : postgresSelectSubscriptionIDByEndpointQuery ,
selectSubscriptionCountBySubscriberIP : postgresSelectSubscriptionCountBySubscriberIPQuery ,
selectSubscriptionsForTopic : postgresSelectSubscriptionsForTopicQuery ,
selectSubscriptionsExpiringSoon : postgresSelectSubscriptionsExpiringSoonQuery ,
2026-02-22 14:25:15 -05:00
upsertSubscription : postgresUpsertSubscriptionQuery ,
2026-02-21 21:29:29 -05:00
updateSubscriptionWarningSent : postgresUpdateSubscriptionWarningSentQuery ,
updateSubscriptionUpdatedAt : postgresUpdateSubscriptionUpdatedAtQuery ,
deleteSubscriptionByEndpoint : postgresDeleteSubscriptionByEndpointQuery ,
deleteSubscriptionByUserID : postgresDeleteSubscriptionByUserIDQuery ,
deleteSubscriptionByAge : postgresDeleteSubscriptionByAgeQuery ,
insertSubscriptionTopic : postgresInsertSubscriptionTopicQuery ,
deleteSubscriptionTopicAll : postgresDeleteSubscriptionTopicAllQuery ,
deleteSubscriptionTopicWithoutSubscription : postgresDeleteSubscriptionTopicWithoutSubscriptionQuery ,
2026-02-16 19:53:34 -05:00
} ,
2026-02-16 12:13:10 -05:00
} , nil
}
2026-03-02 12:58:01 -05:00
func setupPostgres ( db * sql . DB ) error {
2026-02-17 12:04:51 -05:00
var schemaVersion int
2026-02-21 21:29:29 -05:00
err := db . QueryRow ( postgresSelectSchemaVersionQuery ) . Scan ( & schemaVersion )
2026-02-16 12:13:10 -05:00
if err != nil {
2026-03-02 12:58:01 -05:00
return setupNewPostgres ( db )
2026-02-16 12:13:10 -05:00
}
2026-02-17 12:04:51 -05:00
if schemaVersion > pgCurrentSchemaVersion {
return fmt . Errorf ( "unexpected schema version: version %d is higher than current version %d" , schemaVersion , pgCurrentSchemaVersion )
2026-02-16 22:39:54 -05:00
}
return nil
2026-02-16 12:13:10 -05:00
}
2026-03-02 19:45:35 -05:00
func setupNewPostgres ( sqlDB * sql . DB ) error {
2026-03-10 22:17:40 -04:00
return ntfydb . ExecTx ( sqlDB , func ( tx * sql . Tx ) error {
2026-03-02 19:45:35 -05:00
if _ , err := tx . Exec ( postgresCreateTablesQuery ) ; err != nil {
return err
}
if _ , err := tx . Exec ( postgresInsertSchemaVersionQuery , pgCurrentSchemaVersion ) ; err != nil {
return err
}
return nil
} )
2026-02-16 12:13:10 -05:00
}