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.