Publishers

Publishers describe a service that receives and processes events generated server-side by various Hiro systems, such as achievements, economy, teams, and more. They act as listeners that respond to events, enabling you to implement workflows like custom analytics tracking, custom rewards, or integrating with external services.

Use cases #

Publishers can be used to extend the capabilities of existing Hiro systems with custom workflows that go beyond the standard mechanisms.

Extending existing Hiro systems #

The Rewards system grants players currencies, energies, and items. Publishers handle scenarios it doesn’t cover, like updating player stats when events occur.

This Publisher grants experience points when a player claims an achievement:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// ExpRewardPublisher grants experience points when achievements are claimed
type ExpRewardPublisher struct {
    systems hiro.Hiro
}

// Ensure ExpRewardPublisher implements the Publisher interface
var _ hiro.Publisher = (*ExpRewardPublisher)(nil)

// Send processes events and grants exp for achievement claims
func (p *ExpRewardPublisher) Send(ctx context.Context, logger runtime.Logger,
nk runtime.NakamaModule, userID string, events []*hiro.PublisherEvent) {
    for _, event := range events {
        // Only process achievement claim events
        if event.Name != "achievementClaimed" {
            continue
        }

        // Look up the achievement configuration
        achievements, _, _ := p.systems.GetAchievementsSystem().GetAchievements(ctx, logger, nk, userID)

        // Find the achievement that was claimed
        for _, achievement := range achievements {
            if achievement.Id != event.SourceId {
                continue
            }

            // Check if this achievement rewards exp
            if expStr, ok := achievement.AdditionalProperties["reward_exp"]; ok {
                exp, _ := strconv.ParseInt(expStr, 10, 64)

                // Update the player's exp stat
                p.systems.GetStatsSystem().Update(ctx, logger, nk, userID, nil, []*hiro.StatUpdate{{
                    Name:     "exp",
                    Value:    exp,
                    Operator: hiro.StatUpdateOperator_STAT_UPDATE_OPERATOR_DELTA,
                }})
            }
            break
        }
    }
}


// This publisher doesn't need to act on authentication, so it's a no-op.
func (p *ExpRewardPublisher) Authenticate(ctx context.Context, logger runtime.Logger,
    nk runtime.NakamaModule, userID string, created bool) {}

Register the publisher after initializing your Hiro systems:

1
systems.AddPublisher(&ExpRewardPublisher{systems: systems})

Analytics and LiveOps #

Publishers can be used for sending gameplay events to analytics platforms. Hiro’s built-in SatoriPersonalizer handles this for Satori, publishing events like achievement claims, purchases, and energy usage that Satori uses for player segmentation and targeting. You can register additional Publishers to send events to your own analytics backend or third-party services alongside SatoriPersonalizer.

Built-in publishers #

SatoriPersonalizer #

SatoriPersonalizer is Hiro’s built-in Publisher implementation. It implements both the Publisher and Personalizer interfaces, creating a bidirectional adapter between Hiro and Satori:

RoleDirectionPurpose
PublisherWrites to SatoriSends analytics events
PersonalizerReads from SatoriFetches feature flags and live events to customize gameplay configs

This two-way flow is key to Satori’s LiveOps capabilities. As a Publisher, it sends player events (achievements claimed, purchases made, etc.) to Satori. Satori can then use this behavioral data to segment players and deliver targeted configuration back through feature flags, which SatoriPersonalizer reads as a Personalizer.

When you initialize SatoriPersonalizer, you must specify which events you want to publish using functional options. For example, to publish authenticate, achievements, economy, and energy events:

1
2
3
4
5
6
systems.AddPersonalizer(hiro.NewSatoriPersonalizer(ctx,
    hiro.SatoriPersonalizerPublishAuthenticateEvents(),
    hiro.SatoriPersonalizerPublishAchievementsEvents(),
    hiro.SatoriPersonalizerPublishEconomyEvents(),
    hiro.SatoriPersonalizerPublishEnergyEvents(),
))

To publish all events, pass in hiro.SatoriPersonalizerPublishAllEvents(). If you do not want to publish any events (that is, to use SatoriPersonalizer solely for its Personalizer functionality), pass in ctx alone.

Note: Use AddPersonalizer() when registering SatoriPersonalizer as Hiro will automatically register it as a Publisher as well. Use AddPublisher() for custom implementations like the examples above. You can register multiple publishers; events are sent to all of them.

For more details on SatoriPersonalizer, see Personalizers.

Authentication options #

There are two options for publishing authentication events to Satori:

OptionBehavior
SatoriPersonalizerPublishAuthenticateEvents()Authenticates without creating a server-side session (recommended)
SatoriPersonalizerPublishAuthenticateEventsWithSession()Creates a new random session ID for each authentication

These options authenticate the player to Satori immediately after Nakama authentication, creating them in Satori if they don’t already exist. This is essential if you want to fetch Feature Flags for new players—without one of these options, SatoriPersonalizer cannot retrieve flags for players who haven’t been created in Satori yet.

Use the no-session option (SatoriPersonalizerPublishAuthenticateEvents) unless you have a specific use case requiring server-controlled sessions. Satori automatically associates events with the most recent client session, so manually creating server-side sessions is unnecessary and can skew session-related metrics (duration, events per session, and so on). Read more about Sessions.

Publisher interface #

The Publisher interface defines two methods that must be implemented:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type Publisher interface {
    // Authenticate is called every time a user authenticates with Hiro.
    // The 'created' flag is true if this is a newly created user account.
    Authenticate(ctx context.Context, logger runtime.Logger,
        nk runtime.NakamaModule, userID string, created bool)

    // Send is called when there are one or more events generated.
    Send(ctx context.Context, logger runtime.Logger,
        nk runtime.NakamaModule, userID string, events []*PublisherEvent)
}

PublisherEvent structure #

The PublisherEvent structure contains information about an event:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type PublisherEvent struct {
    Name      string            // Event name
    Id        string            // Unique event identifier
    Timestamp int64             // Unix timestamp
    Metadata  map[string]string // Additional event data
    Value     string            // Event value
    System    System            // The Hiro system that generated this event
    SourceId  string            // Identifier of the event source
    Source    any               // Configuration of the event source
}