From Helper Script to Delivery Boundary
Sending one message through one provider is easy. The system problem begins when every product learns its own slightly different way to reach Telegram, Discord, Slack, or email. Provider credentials spread. Templates diverge. Rate limits become whatever the last caller remembered to implement.
Apollo turns that into a boundary. Callers hit provider-shaped routes. The service owns request validation, sender identity, templates, authentication, rate limits, and transport-specific delivery. The abstraction is not a fake universal message object. The abstraction is a consistent accountable entrypoint over distinct providers.
The commit history matters because it shows the shape being discovered: Vercel Go examples, Telegram delivery, provider package splits, mail, rate limits, Auth0, a temporary monorepo detour, a return to single repo, Slack, and Discord multi-webhook fanout.
Provider Differences Stay Visible
Provider logic stays visible because the differences are real. Discord uses embeds and can fan out to multiple webhooks. Slack uses a compact block payload. Telegram has bot semantics and chat addresses. Email is rendering plus delivery.
That is a better service boundary than pretending every provider accepts the same payload. Product code gets a stable route. The delivery layer keeps enough provider shape to debug failures locally.
webhooks := strings.Split(environment.Settings.Discord.Webhook, ",")
results := make([]result, len(webhooks))
var wg sync.WaitGroup
wg.Add(len(webhooks))
for i, wh := range webhooks {
go func(i int, wh string) {
defer wg.Done()
body := strings.NewReader(string(jsonPayload))
resp, err := http.Post(strings.TrimSpace(wh), "application/json", body)
if err != nil {
results[i].err = fmt.Errorf("request to webhook [%s] failed: %w", wh, err)
return
}
defer resp.Body.Close()
results[i].resp = resp
}(i, wh)
}
wg.Wait()Auth and Limits Happen Before Delivery
The difference between a notification helper and a notification service is everything around the send call. Apollo validates JWTs before provider handlers run and checks that the caller has the notification write scope. Provider credentials are not the first line of defense.
Rate limits happen at the edge before the Go handlers do provider work. Anonymous and authenticated callers receive different sliding-window budgets, and the identifier includes request path plus network or auth-derived context. The provider adapter sends the message; the service boundary decides whether the send should happen at all.
func WithAuthentication(handler http.HandlerFunc) http.HandlerFunc {
middleware := createJWTMiddleware()
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Context().Value(jwtmiddleware.ContextKey{}).(*validator.ValidatedClaims)
if token == nil {
RespondWithStatusAndMessage(w, http.StatusUnauthorized, "no token found")
return
}
claims := token.CustomClaims.(*CustomClaims)
if !claims.HasScope("write:apollo-notifications") {
RespondWithStatusAndMessage(w, http.StatusForbidden, "insufficient scope")
return
}
handler.ServeHTTP(w, r)
})
return http.HandlerFunc(middleware.CheckJWT(next).ServeHTTP)
}Email Is Rendering Plus Delivery
Mail is the clearest example of provider-specific behavior. A mail send is not just an HTTP request to SendGrid. It needs markdown conversion, preview text, sender branding, HTML templating, and a delivery status check.
The template is embedded into the Go binary, so the serverless function carries the rendering surface with it. That keeps callers from constructing raw branded email HTML in product code.
func RenderStandardTemplate(from string, preview string, message string) (string, error) {
md := []byte(message)
html := mdToHTML(md)
return useTemplate(preview, from, string(html))
}
func useTemplate(preview string, from string, message string) (string, error) {
data := struct {
From string
Preview string
Message template.HTML
}{
Preview: preview,
From: from,
Message: template.HTML(message),
}
t, err := template.ParseFS(files, "templates/standard.html")
if err != nil {
return "", err
}
writer := new(bytes.Buffer)
err = t.Execute(writer, data)
return writer.String(), err
}Serverless Constraints Shape the Code
Telegram exposes the serverless compromise directly. The implementation initializes a bot for sending and stops update polling because the function is not a long-running bot process. The docs called that out early: sending works, listening and reacting are a different lifecycle.
That is the recurring lesson. Serverless notification delivery can centralize policy and adapters, but it is not a durable messaging system. The repository does not show queues, retries, delivery receipts, audit logs, provider backoff, or dead-letter handling. Apollo is a boundary experiment, not a complete notification platform.
func initializeBot() *tgbotapi.BotAPI {
if bot != nil {
return bot
}
bot, err := tgbotapi.NewBotAPI(environment.Settings.Telegram.BotToken)
sys.FailOnError(err, "failed to create telegram connection")
bot.StopReceivingUpdates()
return bot
}