diff --git a/.env.example b/.env.example
index 4ef221d96f4d561e3d9238847bf441eb6f2a3704..ba76ff58b1d5c2efc18274ce16393ec2077fd99d 100644
--- a/.env.example
+++ b/.env.example
@@ -1 +1,6 @@
-ALLOWED_ORIGINS=https://yourdomain.com
\ No newline at end of file
+ALLOWED_ORIGINS=https://hizla.io
+DB=db
+VERBOSE=1
+LISTEN_ADDR=127.0.0.1:3000
+HCAPTCHA_SITE_KEY=unset
+HCAPTCHA_SECRET=unset
\ No newline at end of file
diff --git a/app.go b/app.go
index df0e0bc00b02fef6b8e5ef1c2107a68b1cb728d3..2f2e8b76055aa48c8dedceccab3b1f27a846deba 100644
--- a/app.go
+++ b/app.go
@@ -6,9 +6,10 @@ import (
 	"os"
 	"time"
 
-	"github.com/gofiber/fiber/v2"
-	"github.com/gofiber/fiber/v2/middleware/cors"
-	"github.com/gofiber/fiber/v2/middleware/limiter"
+	"github.com/gofiber/contrib/hcaptcha"
+	"github.com/gofiber/fiber/v3"
+	"github.com/gofiber/fiber/v3/middleware/cors"
+	"github.com/gofiber/fiber/v3/middleware/limiter"
 	"github.com/syndtr/goleveldb/leveldb"
 )
 
@@ -17,15 +18,15 @@ func serve(sig chan os.Signal, db *leveldb.DB) error {
 
 	// cors
 	app.Use(cors.New(cors.Config{
-		AllowOrigins: conf[allowedOrigins],
-		AllowHeaders: "Origin, Content-Type, Accept",
+		AllowOrigins: []string{conf[allowedURL]},
+		AllowHeaders: []string{"Origin", "Content-Type", "Accept"},
 	}))
 
 	// rate limiting
 	app.Use(limiter.New(limiter.Config{
 		Max:        5,
 		Expiration: 7 * 24 * time.Hour, // 1 week expiration
-		LimitReached: func(c *fiber.Ctx) error {
+		LimitReached: func(c fiber.Ctx) error {
 			log.Printf("Rate limit exceeded for IP: %s", c.IP())
 			return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
 				"message": "Rate limit exceeded. Max 5 registrations per week.",
@@ -33,13 +34,31 @@ func serve(sig chan os.Signal, db *leveldb.DB) error {
 		},
 	}))
 
+	var captcha fiber.Handler
+	hCaptchaEnable := conf[hCaptchaSiteKey] != "unset" && conf[hCaptchaSecretKey] != "unset"
+
+	if hCaptchaEnable {
+		// create hCaptcha middleware if enabled
+		captcha = hcaptcha.New(hcaptcha.Config{
+			SecretKey: conf[hCaptchaSecretKey],
+		})
+
+		log.Printf("hCaptcha enabled with site key %q", conf[hCaptchaSiteKey])
+	} else {
+		// empty middleware if disabled
+		captcha = func(c fiber.Ctx) error {
+			return c.Next()
+		}
+
+		log.Printf("hCaptcha disabled because one or both of %q and %q are unset",
+			confEnv[hCaptchaSiteKey][0], confEnv[hCaptchaSecretKey][0])
+	}
+
 	// /register
-	routeRegister(app, db)
+	routeRegister(app, db, captcha)
 
-	// Graceful shutdown
-	app.Use(func(c *fiber.Ctx) error {
-		return c.Next()
-	})
+	// /hcaptcha-site-key
+	routeHCaptchaSiteKey(app, !hCaptchaEnable, conf[hCaptchaSiteKey])
 
 	// graceful shutdown
 	go func() {
diff --git a/captcha.go b/captcha.go
new file mode 100644
index 0000000000000000000000000000000000000000..f2d517d514d49c5f41f1f7d69333fe2023b58f15
--- /dev/null
+++ b/captcha.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+	"github.com/gofiber/fiber/v3"
+)
+
+type respHSiteKey struct {
+	Success bool   `json:"success"`
+	SiteKey string `json:"hcaptcha_site_key"`
+}
+
+// Route to expose hCaptcha site key.
+// Returns a constant pre-generated response
+// to avoid unnecessary allocations or serialisations
+func routeHCaptchaSiteKey(app *fiber.App, stub bool, siteKey string) {
+	var resp string
+	if stub {
+		resp = mustConstResp(newMessage(false, "hCaptcha is not enabled on this instance."))
+	} else {
+		resp = mustConstResp(respHSiteKey{true, siteKey})
+	}
+
+	app.Get("/captcha", func(c fiber.Ctx) error {
+		c.Set("Content-Type", "application/json; charset=utf-8")
+		return c.SendString(resp)
+	})
+}
diff --git a/conf.go b/conf.go
index cb9cafb6c9df22993b66ac9dfe2e536b8e8346cd..a95b71fafe42fc5bae60d13befa533a67b89674b 100644
--- a/conf.go
+++ b/conf.go
@@ -8,27 +8,29 @@ import (
 const (
 	dbPath uint8 = iota
 	listenAddr
-	allowedOrigins
+	allowedURL
+	hCaptchaSiteKey
+	hCaptchaSecretKey
 	verboseLogging
-
-	confLen
 )
 
 // env variable, default pairing
-var confEnv = [confLen][2]string{
-	{"DB", "db"},
-	{"LISTEN_ADDR", "127.0.0.1:3000"},
-	{"ALLOWED_ORIGINS", "https://hizla.io"},
-	{"VERBOSE", "1"},
+var confEnv = [...][2]string{
+	dbPath:            {"DB", "db"},
+	listenAddr:        {"LISTEN_ADDR", "127.0.0.1:3000"},
+	allowedURL:        {"ALLOWED_URL", "https://hizla.io"},
+	hCaptchaSiteKey:   {"HCAPTCHA_SITE_KEY", "unset"},
+	hCaptchaSecretKey: {"HCAPTCHA_SECRET_KEY", "unset"},
+	verboseLogging:    {"VERBOSE", "1"},
 }
 
 // resolved config values
-var conf [confLen]string
+var conf [len(confEnv)]string
 
 var verbose bool
 
 func init() {
-	for i := 0; i < int(confLen); i++ {
+	for i := 0; i < len(confEnv); i++ {
 		if v, ok := os.LookupEnv(confEnv[i][0]); !ok {
 			conf[i] = confEnv[i][1]
 		} else {
diff --git a/go.mod b/go.mod
index 9236dc386fbdb1360fdb2bdd28b0ef6fcbc7c196..ad3be326fc2c784e15c8e9314ae14bf981a33a0e 100644
--- a/go.mod
+++ b/go.mod
@@ -3,24 +3,20 @@ module git.randomchars.net/hizla/waitlist/backend
 go 1.22.3
 
 require (
-	github.com/andybalholm/brotli v1.0.5 // indirect
-	github.com/cespare/xxhash/v2 v2.3.0 // indirect
-	github.com/dgraph-io/ristretto v1.0.0 // indirect
-	github.com/dustin/go-humanize v1.0.1 // indirect
-	github.com/gofiber/fiber/v2 v2.52.5 // indirect
+	github.com/andybalholm/brotli v1.1.1 // indirect
+	github.com/gofiber/contrib/hcaptcha v0.1.1 // indirect
+	github.com/gofiber/fiber/v3 v3.0.0-beta.3 // indirect
+	github.com/gofiber/utils/v2 v2.0.0-beta.6 // indirect
 	github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
-	github.com/google/uuid v1.5.0 // indirect
-	github.com/klauspost/compress v1.17.0 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/klauspost/compress v1.17.11 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
-	github.com/mattn/go-runewidth v0.0.15 // indirect
 	github.com/philhofer/fwd v1.1.2 // indirect
-	github.com/pkg/errors v0.9.1 // indirect
-	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/syndtr/goleveldb v1.0.0 // indirect
 	github.com/tinylib/msgp v1.1.8 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
-	github.com/valyala/fasthttp v1.51.0 // indirect
+	github.com/valyala/fasthttp v1.56.0 // indirect
 	github.com/valyala/tcplisten v1.0.0 // indirect
-	golang.org/x/sys v0.25.0 // indirect
+	golang.org/x/sys v0.26.0 // indirect
 )
diff --git a/go.sum b/go.sum
index b39830b4228c522354b3e26b3db4b708ab30c7f9..20edfa0723acce32d88dff9267ccffa04dcbc1de 100644
--- a/go.sum
+++ b/go.sum
@@ -1,48 +1,49 @@
-github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
-github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
-github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
-github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84=
-github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc=
-github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
-github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
+github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
+github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
+github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
-github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
+github.com/gofiber/contrib/hcaptcha v0.1.1 h1:zc81+nY83BIieIzg9tr8mlFNGw2NpPjTyDoPsdkG7bQ=
+github.com/gofiber/contrib/hcaptcha v0.1.1/go.mod h1:6/2mucDLwGEmwahbmRO07l+4zxjqFBhq7dVQ35C8bBY=
+github.com/gofiber/fiber/v3 v3.0.0-beta.3 h1:7Q2I+HsIqnIEEDB+9oe7Gadpakh6ZLhXpTYz/L20vrg=
+github.com/gofiber/fiber/v3 v3.0.0-beta.3/go.mod h1:kcMur0Dxqk91R7p4vxEpJfDWZ9u5IfvrtQc8Bvv/JmY=
+github.com/gofiber/utils/v2 v2.0.0-beta.4 h1:1gjbVFFwVwUb9arPcqiB6iEjHBwo7cHsyS41NeIW3co=
+github.com/gofiber/utils/v2 v2.0.0-beta.4/go.mod h1:sdRsPU1FXX6YiDGGxd+q2aPJRMzpsxdzCXo9dz+xtOY=
+github.com/gofiber/utils/v2 v2.0.0-beta.6 h1:ED62bOmpRXdgviPlfTmf0Q+AXzhaTUAFtdWjgx+XkYI=
+github.com/gofiber/utils/v2 v2.0.0-beta.6/go.mod h1:3Kz8Px3jInKFvqxDzDeoSygwEOO+3uyubTmUa6PqY+0=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
-github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
-github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
+github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
-github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
 github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
-github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
 github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
 github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
 github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
-github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
+github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
+github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
+github.com/valyala/fasthttp v1.56.0 h1:bEZdJev/6LCBlpdORfrLu/WOZXXxvrUQSiyniuaoW8U=
+github.com/valyala/fasthttp v1.56.0/go.mod h1:sReBt3XZVnudxuLOx4J/fMrJVorWRiWY2koQKgABiVI=
 github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
 github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
+github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -66,10 +67,10 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
-golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
-golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
+golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
diff --git a/register.go b/register.go
index 38f846b80933dfd4718dca090482aa3b821d24c0..02b4f60031e328308469e0fa080950fd8192bb3f 100644
--- a/register.go
+++ b/register.go
@@ -4,7 +4,7 @@ import (
 	"log"
 	"regexp"
 
-	"github.com/gofiber/fiber/v2"
+	"github.com/gofiber/fiber/v3"
 	"github.com/syndtr/goleveldb/leveldb"
 )
 
@@ -15,11 +15,12 @@ type registration struct {
 }
 
 // Waitlist registration route
-func routeRegister(app *fiber.App, db *leveldb.DB) {
-	app.Post("/register", func(c *fiber.Ctx) error {
+func routeRegister(app *fiber.App, db *leveldb.DB, captcha fiber.Handler) {
+	app.Post("/register", func(c fiber.Ctx) error {
 		req := new(registration)
 
-		if err := c.BodyParser(req); err != nil {
+		// Parse and validate the request
+		if err := c.Bind().Body(req); err != nil {
 			if verbose {
 				log.Printf("invalid request from %q: %v", c.IP(), err)
 			}
@@ -65,5 +66,5 @@ func routeRegister(app *fiber.App, db *leveldb.DB) {
 		return c.JSON(fiber.Map{
 			"message": "Email registered successfully",
 		})
-	})
+	}, captcha)
 }
diff --git a/resp.go b/resp.go
new file mode 100644
index 0000000000000000000000000000000000000000..6f763994f5759e0c3144312848cd57ead4ace751
--- /dev/null
+++ b/resp.go
@@ -0,0 +1,21 @@
+package main
+
+import "encoding/json"
+
+// pre-generate constant responses
+func mustConstResp(a any) string {
+	if p, err := json.Marshal(a); err != nil {
+		panic(err.Error())
+	} else {
+		return string(p)
+	}
+}
+
+type respM struct {
+	Success bool   `json:"success"`
+	Message string `json:"message"`
+}
+
+func newMessage(success bool, message string) *respM {
+	return &respM{success, message}
+}