Realm event in. Browser notification out.

pingpong · TurnPlayed
TurnPlayed game=0000042 next=g1k7…x2q turn=7
notification · the player's browser
Your turnGame 0000042, turn 7pingpong · just now
gnochat · Mentioned
Mentioned channel=general by=g1zm…9ra who=g1k7…x2q
notification · the member's browser
g1zm…9ra mentioned youin #generalgnochat · just now
govdao · ProposalClosed
ProposalClosed id=0000012 result=passed
notification · every voter's browser
Proposal 12 passedTreasury budget, Q4govdao · just now
auctions · Outbid
Outbid lot=0000309 by=g1p0…7cd was=g1k7…x2q
notification · the bidder's browser
You were outbidLot 309, now 1 250 GNOTauctions · just now
vesting · UnlockReached
UnlockReached height=1300000 account=g1k7…x2q
notification · the holder's browser
Block 1 300 000 reachedYour tokens are unlockedvesting · just now

How it works

gnotif sends browser notifications when your realm emits an event, even when the page that asked for them is closed. A trigger in the gnotif registry says who an event notifies. The public server at https://gnotif.xyz watches the testnet and sends the push, so there is no notification server to run.

  1. dapp realmemits events, declares its trigger
  2. gnotif registryholds the triggers, on chain
  3. tx-indexerreads the chain, serves GraphQL
  4. gnotifdmatches events, sends the pushes
  5. push serviceWeb Push, run by the browser's vendor
  6. sw.jsshows the notification, on the dapp's origin

The page calls gnotifd only to subscribe and to set its opt-ins. The notification travels through the browser's push service and is shown by the service worker the dapp serves from its own origin.

Add notifications to a dapp

Emit

Emit an event from your realm

pingpong, the demo game, names the next player on every turn.

Getting started →
chain.Emit("TurnPlayed", "game", g.ID, "next", g.Next.String(), "turn", strconv.Itoa(g.Turn))

Declare

Declare a trigger in the registry

The realm declares the trigger itself, so the registry marks it verified. Call DeclareTriggers once after the deploy.

Getting started →
import "gno.land/r/<namespace>/gnotif/v0"

var declared bool

func DeclareTriggers(cur realm) {
	if declared {
		panic("triggers already declared")
	}
	declared = true
	gnotif.Declare(cross(cur), cur.PkgPath(), "TurnPlayed", "", "next",
		"Your turn", "Game {game}, turn {turn}", "/?game={game}")
}

Subscribe

Subscribe the browser from your page

Copy node_modules/gnotif/src/sw.js to the folder your site serves at its root. From a click, call enable(), then opt the browser in with the player's address.

Getting started →
import { Gnotif } from "gnotif";

const gnotif = new Gnotif({ server: "https://gnotif.xyz" });
const yourTurn = (await gnotif.triggers()).find(
  (t) =>
    t.target === "gno.land/r/<namespace>/pingpong/v0" &&
    t.event === "TurnPlayed" &&
    t.verified,
);
if (yourTurn === undefined) {
  throw new Error("This gnotif server does not offer pingpong's trigger.");
}

button.addEventListener("click", async () => {
  await gnotif.enable();
  await gnotif.setOptins([{ trigger: yourTurn.id, value: playerAddress }]);
});

Operate

Run your own gnotifd

gnotifd is one binary with one SQLite file. It reads a tx-indexer and signs its pushes with a VAPID key pair. A container image is published for every release.

Running gnotifd →