diff --git a/config/config.go b/config/config.go
index 98bafa7..940e63a 100644
--- a/config/config.go
+++ b/config/config.go
@@ -36,6 +36,7 @@ type C struct {
 		AccountSid string
 		AuthToken  string
 		SkipVerify bool
+		BaseURL    string
 	}
 	Auth struct {
 		ValidMinutes int
diff --git a/internal/auth/auth.go b/internal/auth/auth.go
index b270489..6c938c3 100644
--- a/internal/auth/auth.go
+++ b/internal/auth/auth.go
@@ -13,9 +13,22 @@ import (
 const AuthSessionName = "auth_session"
 
 type RequestAuth struct {
-	Valid  bool
-	Roles  []string
-	Expire time.Time
+	Present bool
+	Valid   bool
+	Roles   []string
+	Expire  time.Time
+}
+
+func (a RequestAuth) HasRole(role Role) bool {
+	if !a.Valid {
+		return false
+	}
+	for _, r := range a.Roles {
+		if r == string(role) {
+			return true
+		}
+	}
+	return false
 }
 
 func Middleware(store sessions.Store) echo.MiddlewareFunc {
@@ -31,6 +44,7 @@ func Middleware(store sessions.Store) echo.MiddlewareFunc {
 
 			var auth RequestAuth
 			if expireTs, ok := sess.Values["expire"].(string); ok {
+				auth.Present = true
 				if expireTime, err := time.Parse(time.RFC3339, expireTs); err != nil {
 					log.Printf("invalid expireTime: %v", expireTs)
 				} else {
@@ -116,5 +130,9 @@ func Login(c echo.Context) (err error) {
 
 }
 func GetRequestAuth(c echo.Context) RequestAuth {
-	return c.Get("auth_" + AuthSessionName).(RequestAuth)
+	if a, ok := c.Get("auth_" + AuthSessionName).(RequestAuth); ok {
+		return a
+	} else {
+		return RequestAuth{Present: false, Valid: false}
+	}
 }
diff --git a/internal/servetpl/funcmap/funcmap.go b/internal/servetpl/funcmap/funcmap.go
index 9a0c689..2a6be46 100644
--- a/internal/servetpl/funcmap/funcmap.go
+++ b/internal/servetpl/funcmap/funcmap.go
@@ -60,6 +60,9 @@ func Invoke(name string, target reflect.Value, args ...reflect.Value) (any, erro
 		}
 		target = reflect.ValueOf(t)
 	}
+	for i, arg := range args {
+		log.Printf("invoke %s arg[%d]=%v", name, i, arg)
+	}
 	ret := target.Call(args)
 	if len(ret) == 0 {
 		return nil, nil
@@ -68,6 +71,9 @@ func Invoke(name string, target reflect.Value, args ...reflect.Value) (any, erro
 	if err, ok := ret[len(ret)-1].Interface().(error); ok && err != nil {
 		return nil, err
 	}
+	for i, r := range ret {
+		log.Printf("invoke %s ret[%d]=%v", name, i, r)
+	}
 
 	switch len(ret) {
 	case 0:
diff --git a/internal/session/manager.go b/internal/session/manager.go
index dca84ce..d042d11 100644
--- a/internal/session/manager.go
+++ b/internal/session/manager.go
@@ -1,6 +1,8 @@
 package session
 
 import (
+	"log"
+
 	"github.com/gorilla/sessions"
 	"github.com/labstack/echo/v4"
 )
@@ -28,8 +30,10 @@ func ManagedSession(c echo.Context) (p Provider, close func()) {
 			checkedOutSessions[name] = s
 			return s.Values
 		}, func() {
-			for _, s := range checkedOutSessions {
-				s.Save(c.Request(), c.Response())
+			for name, s := range checkedOutSessions {
+				if err := s.Save(c.Request(), c.Response()); err != nil {
+					log.Printf("error saving session %s: %v", name, err)
+				}
 			}
 		}
 }
diff --git a/internal/twilio/twilio_test.go b/internal/twilio/twilio_test.go
deleted file mode 100644
index 73f9cc6..0000000
--- a/internal/twilio/twilio_test.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package twilio
-
-import (
-	"bytes"
-	"net/http/httptest"
-	"net/url"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-)
-
-func TestTwilioValidate(t *testing.T) {
-	params := map[string]string{
-		"CallSid": "CA1234567890ABCDE",
-		"Caller":  "+12349013030",
-		"Digits":  "1234",
-		"From":    "+12349013030",
-		"To":      "+18005551212",
-	}
-	bodyForm := make(url.Values)
-	for k, v := range params {
-		bodyForm.Set(k, v)
-	}
-
-	req := httptest.NewRequest("POST", "https://mycompany.com/myapp.php?foo=1&bar=2",
-		bytes.NewBufferString(bodyForm.Encode()))
-	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
-	require.NoError(t, req.ParseForm())
-
-	assert.ErrorContains(t, TwilioValidate(req), "no twilio signature")
-	req.Header.Set("X-Twilio-Signature", "garbage")
-	assert.ErrorContains(t, TwilioValidate(req), "twilio signature verification failed")
-	req.Header.Set("X-Twilio-Signature", "0/KCTR6DLpKmkAf8muzZqo1nDgQ=")
-	assert.NoError(t, TwilioValidate(req))
-}
diff --git a/internal/twilio/verify.go b/internal/twilio/verify.go
index 674af13..aeb22d2 100644
--- a/internal/twilio/verify.go
+++ b/internal/twilio/verify.go
@@ -1,6 +1,7 @@
 package twilio
 
 import (
+	"crypto/subtle"
 	"fmt"
 	"log"
 	"net/http"
@@ -9,8 +10,10 @@ import (
 	"strings"
 
 	"github.com/labstack/echo/v4"
+	"github.com/labstack/echo/v4/middleware"
 
 	"github.com/eternal-flame-AD/yoake/config"
+	"github.com/eternal-flame-AD/yoake/internal/auth"
 	"github.com/twilio/twilio-go/client"
 )
 
@@ -22,14 +25,33 @@ func firstUrlValues(val url.Values) map[string]string {
 	return res
 }
 
-func VerifyMiddleware(prefix string) echo.MiddlewareFunc {
+func VerifyMiddleware(prefix string, baseurlS string) echo.MiddlewareFunc {
+	baseURL, err := url.Parse(baseurlS)
+	if err != nil {
+		log.Fatalf("invalid twilio baseurl: %v", baseurlS)
+	}
+	log.Printf("twilio baseurl is %v", baseURL)
+	var basicAuth echo.MiddlewareFunc
+	if userpass := baseURL.User.String(); userpass != "" {
+		basicAuth = middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
+			ui := url.UserPassword(username, password)
+			return subtle.ConstantTimeCompare([]byte(ui.String()), []byte(userpass)) == 1, nil
+		})
+	}
 	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		verifySignature := func(c echo.Context) error {
+			if reqAuth := auth.GetRequestAuth(c); reqAuth.Valid && reqAuth.HasRole(auth.RoleAdmin) {
+				return next(c)
+			}
 
-		return func(c echo.Context) error {
 			cleanPath := path.Clean(c.Request().URL.Path)
 			//log.Printf("cleanPath: %s", cleanPath)
 			if cleanPath == prefix || strings.HasPrefix(cleanPath, prefix+"/") {
-				if err := TwilioValidate(c.Request()); err != nil {
+				fullReq := c.Request().Clone(c.Request().Context())
+				log.Printf("original request URL: %v, scheme=%s, host=%s, user=%s", c.Request().URL, c.Request().URL.Scheme, c.Request().URL.Host, c.Request().URL.User)
+				fullReq.URL = baseURL.ResolveReference(c.Request().URL)
+				fullReq.URL.User = nil
+				if err := TwilioValidate(c, fullReq); err != nil {
 					c.String(http.StatusOK, "We are sorry. Request Validation Failed. This is not your fault.")
 					log.Printf("twilio verify failed: %v", err)
 					return err
@@ -37,10 +59,14 @@ func VerifyMiddleware(prefix string) echo.MiddlewareFunc {
 			}
 			return next(c)
 		}
+		if basicAuth != nil {
+			return basicAuth(verifySignature)
+		}
+		return verifySignature
 	}
 }
 
-func TwilioValidate(req *http.Request) error {
+func TwilioValidate(c echo.Context, req *http.Request) error {
 	conf := config.Config().Twilio
 	signature := req.Header.Get("X-Twilio-Signature")
 	if signature == "" {
@@ -51,7 +77,11 @@ func TwilioValidate(req *http.Request) error {
 	}
 	requestValidator := client.NewRequestValidator(conf.AuthToken)
 	if req.Method == "POST" {
-		if !requestValidator.Validate(req.URL.String(), firstUrlValues(req.PostForm), signature) {
+		form, err := c.FormParams()
+		if err != nil {
+			return err
+		}
+		if !requestValidator.Validate(req.URL.String(), firstUrlValues(form), signature) {
 			return fmt.Errorf("twilio signature verification failed")
 		}
 	} else if req.Method == "GET" {
diff --git a/server/webroot/log.go b/server/webroot/log.go
index 801b2bf..c5bbd14 100644
--- a/server/webroot/log.go
+++ b/server/webroot/log.go
@@ -127,8 +127,8 @@ var (
 		LogError:         true,
 		LogContentLength: true,
 		LogResponseSize:  true,
-		LogHeaders:       []string{},
+		LogHeaders:       []string{"Content-Type"},
 		LogQueryParams:   []string{},
-		LogFormValues:    []string{},
+		LogFormValues:    []string{"From", "To"},
 	}
 )
diff --git a/server/webroot/server.go b/server/webroot/server.go
index cd0e56c..b3d74fb 100644
--- a/server/webroot/server.go
+++ b/server/webroot/server.go
@@ -66,8 +66,8 @@ func Init(hostname string) {
 		}
 	},
 		middleware.Gzip(),
-		logMiddleware("twilio", twilio.VerifyMiddleware("/twilio")),
 		auth.Middleware(sessionCookie),
+		logMiddleware("twilio", twilio.VerifyMiddleware("/twilio", config.Config().Twilio.BaseURL)),
 		middleware.Rewrite(map[string]string{"*/": "$1/index.html"}),
 		logMiddleware("template", servetpl.ServeTemplateDir(webroot.Root)),
 		logMiddleware("static", middleware.Static(webroot.Root)))
diff --git a/webroot/twilio/sms/entrypoint.tpl.xml b/webroot/twilio/sms/entrypoint.tpl.xml
new file mode 100644
index 0000000..72cca33
--- /dev/null
+++ b/webroot/twilio/sms/entrypoint.tpl.xml
@@ -0,0 +1,5 @@
+{{ template "/twilio/head.tpl.xml" . }}
+{{ $session := (get .Global "twilio_session") }}
+<Response>
+    <Redirect method="POST">/twilio/sms/menu.xml</Redirect>
+</Response>
\ No newline at end of file
diff --git a/webroot/twilio/sms/menu.tpl.xml b/webroot/twilio/sms/menu.tpl.xml
new file mode 100644
index 0000000..29f6fba
--- /dev/null
+++ b/webroot/twilio/sms/menu.tpl.xml
@@ -0,0 +1,6 @@
+{{ template "/twilio/head.tpl.xml" . }}
+{{ $session := (get .Global "twilio_session") }}
+
+<Response>
+<Message>Hello! This is yumechi.</Message>
+</Response>
\ No newline at end of file
diff --git a/webroot/twilio/voice/entrypoint.tpl.xml b/webroot/twilio/voice/entrypoint.tpl.xml
index 1705c15..4ca4197 100644
--- a/webroot/twilio/voice/entrypoint.tpl.xml
+++ b/webroot/twilio/voice/entrypoint.tpl.xml
@@ -2,6 +2,7 @@
 {{ $session := (get .Global "twilio_session") }}
 
 {{ $num_visited := (get $session "num_visited") }}
+{{ if not $num_visited }}{{ $num_visited = 0 }} {{ end }}
 {{ set $session "num_visited" (math "argv(1) + 1" $num_visited) }}
 <Response>
     <Say voice="alice" language="en-US">This is Anne!</Say>