Docs
Docs
Guides

Go

Serve every customer domain from one Go service with net/http middleware that checks the Apexly signature and resolves the tenant.

How requests arrive

Apexly forwards each visitor's request to your origin with the original Host header (r.Host), X-Forwarded-Proto: https, and X-Apexly-Domain-Id, X-Apexly-App-Id, X-Apexly-Timestamp and X-Apexly-Signature.

Verify the signature

X-Apexly-Signature is the hex HMAC-SHA256 of <timestamp>.<host> keyed with your signing secret (whsec_…). This function is tested against the shared vectors on every build.

package apexly

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"regexp"
	"strconv"
	"strings"
	"time"
)

var port = regexp.MustCompile(`:\d+$`)

// VerifyOrigin reports whether X-Apexly-Signature is valid for host (the edge signs "<timestamp>.<host>").
func VerifyOrigin(host, timestamp, signature, secret string, now time.Time) bool {
	ts, err := strconv.ParseUint(timestamp, 10, 40)
	if err != nil || len(timestamp) > 12 {
		return false
	}
	if d := now.Unix() - int64(ts); d > 300 || d < -300 {
		return false
	}
	name := strings.ToLower(port.ReplaceAllString(host, ""))
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(timestamp + "." + name))
	got, err := hex.DecodeString(signature)
	return err == nil && hmac.Equal(got, mac.Sum(nil))
}

Allow your customers' hostnames

net/http serves any Host, so wrap your handler in a middleware that lets your own hosts through and requires a valid signature for everything else:

package main

import (
	"context"
	"net/http"
	"os"
	"time"

	"example.com/hireflow/apexly"
)

var ownHosts = map[string]bool{"app.hireflow.com": true}

type tenantKey struct{}

func withTenant(next http.Handler) http.Handler {
	secret := os.Getenv("APEXLY_SIGNING_SECRET")
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !ownHosts[r.Host] {
			ok := apexly.VerifyOrigin(r.Host, r.Header.Get("X-Apexly-Timestamp"),
				r.Header.Get("X-Apexly-Signature"), secret, time.Now())
			if !ok {
				http.Error(w, "Unknown host", http.StatusMisdirectedRequest)
				return
			}
			tenant, err := tenants.ByHost(r.Context(), r.Host)
			if err != nil {
				http.NotFound(w, r)
				return
			}
			r = r.WithContext(context.WithValue(r.Context(), tenantKey{}, tenant))
		}
		next.ServeHTTP(w, r)
	})
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("POST /webhooks/apexly", apexlyWebhook)
	mux.HandleFunc("/", home)
	http.ListenAndServe(":8080", withTenant(mux))
}

Receive webhooks

Read the body with a size limit, verify Apexly-Signature with VerifySignature from the Webhooks page, then decode.

func apexlyWebhook(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
	if err != nil || !apexly.VerifySignature(r.Header.Get("Apexly-Signature"), body, secret, time.Now()) {
		http.Error(w, "bad signature", http.StatusBadRequest)
		return
	}
	var event struct {
		Type string `json:"type"`
		Data struct {
			Domain struct {
				Metadata map[string]string `json:"metadata"`
			} `json:"domain"`
		} `json:"data"`
	}
	if err := json.Unmarshal(body, &event); err == nil && event.Type == "domain.active" {
		markLive(event.Data.Domain.Metadata["customer"])
	}
	w.WriteHeader(http.StatusNoContent)
}

Try it

Run the Quickstart with a test key, then use the sandbox origin to see the headers your service will receive.

What to read next

Was this helpful?
Edit this page