⏺ Authentication Strategy for Protecting API and
Webhook Endpoints
Overview
This authentication approach protects all /api/_
and /tasks/_ endpoints from unauthorized access
while keeping specific endpoints (like
/api/health) public. It uses a simple shared
secret passed via HTTP header, with graceful
degradation for development mode.
Components Required
- Environment Variable
- Name: WEBHOOK_SECRET
- Purpose: Shared secret for authenticating
webhook and API requests
- Storage: Val Town environment variables (or
your platform's env var system)
- Value: Any secure random string (32+
characters recommended)
- Behavior: If not set, authentication is
bypassed (development mode with console warning)
- Authentication Middleware File
- Location: backend/routes/authCheck.ts
- Exports: webhookAuth function
- Framework: Hono middleware pattern (Context,
Next)
- Middleware Function Logic
The webhookAuth function should:
- Accept Hono Context and Next parameters
- Extract X-API-KEY header from request
(case-insensitive: x-api-key)
- Read WEBHOOK_SECRET from environment variables
- If WEBHOOK_SECRET not configured:
- Log warning to console
- Call await next() to continue (bypass auth)
- Return
- If X-API-KEY header missing or doesn't match
secret:
- Return 401 JSON response with error message
- Do NOT call next()
- If authentication succeeds:
- Call await next() to continue to route
handler
- Application in Main Entry Point
- Location: main.http.tsx
- Pattern: Array-based route protection with
exceptions
Implementation approach:
-
Define protected route prefixes in array:
['/tasks', '/api']
-
Define public exceptions in array:
['/api/health']
-
Create single app.use('*', ...) middleware
that:
- Extracts request path from context
- Checks if path is in public exceptions
(exact match) - If public, call await next() and return - Checks if path starts with any protected
prefix - If protected, call webhookAuth(c, next)
(delegates to auth middleware) - If not protected, call await next()
- Route Mounting Order
Important: Apply authentication middleware
BEFORE mounting routes
- Hono app initialization
- Error handler
- Static file serving (if needed)
- Authentication middleware (app.use)
- Route mounting (app.route)
Usage Patterns
For Webhook Callers (Notion buttons, external
services):
- Add custom HTTP header to webhook
configuration
- Header name: X-API-KEY
- Header value: The value of WEBHOOK_SECRET env
var
- Example in Notion: Custom headers → Add →
X-API-KEY = your-secret-value
For API Requests (curl, fetch, etc.):
Include header: X-API-KEY: your-secret-value
For Internal Endpoint Calls:
- When one route needs to call another protected
route internally
- Fetch the WEBHOOK_SECRET from environment:
Deno.env.get('WEBHOOK_SECRET')
- Include in headers: { 'X-API-KEY':
webhookSecret }
- Example: Background job triggers another
endpoint
Key Design Decisions
Why array-based approach:
- Single middleware application point
- Easy to add/remove protected routes
- Easy to add/remove public exceptions
- Self-documenting (arrays clearly show what's
protected)
- No need for multiple app.use() calls
Why shared secret vs. individual keys:
- Simpler for single-user/single-team apps
- No key management complexity
- Sufficient for webhook protection use case
- Can be upgraded to JWT/OAuth later if needed
Why graceful degradation:
- Allows development without auth setup
- Console warning alerts developer
- Production should always set the secret
Why /api/health is public:
- Needed for frontend dashboard to load without
auth
- Doesn't expose sensitive write operations
- Shows configuration status (helpful for setup)
- Can be monitored externally without
credentials
Security Considerations
What this protects:
- Prevents unauthorized webhook triggers
- Prevents unauthorized API data access
- Prevents page ID enumeration attacks
- Prevents write operations from bad actors
What this doesn't protect:
- Secret itself (must be transmitted securely)
- Doesn't prevent secret leakage if exposed in
logs/errors
- Doesn't rate limit (consider adding
separately)
- Doesn't expire (consider rotating secrets
manually)
Recommendations:
- Never commit secrets to git
- Use environment variables only
- Log failed auth attempts for monitoring
Testing the Implementation
Test 1: Public endpoint works without auth
- Request /api/health with no headers
- Should return 200 with data
Test 2: Protected endpoint blocks without auth
- Request /api/pages/recent with no headers
- Should return 401 with error message
Test 3: Protected endpoint allows with valid
auth
- Request /api/pages/recent with X-API-KEY:
correct-secret
- Should return 200 with data
Test 4: Protected endpoint blocks with invalid
auth
- Request /api/pages/recent with X-API-KEY:
wrong-secret
- Should return 401 with error message
Test 5: Development mode warning
- Remove WEBHOOK_SECRET from environment
- Make any request
- Should see console warning and allow all
requests
Documentation to Include
In README/Setup docs:
- List WEBHOOK_SECRET as required environment
variable
- Explain what it protects and why it's
important
- Show example of setting it
- Show example of using it in webhook
configuration
- Explain security implications of not setting
it
In code comments:
- Document which routes are protected
- Document which routes are exceptions
- Explain why each route is protected or public
- Include example header usage in comments
Migration Notes
If adapting to another framework:
- Middleware signature will differ (Express,
Fastify, etc.)
- Header extraction method may differ
- Response format may differ
- Core logic remains the same
If adapting to different auth method:
- Keep array-based route protection pattern
- Replace shared secret check with JWT/OAuth
validation
- Keep graceful degradation for development
- Keep public exceptions pattern
This approach is simple, secure enough for
webhook protection, and easy to understand and
maintain. It's appropriate for single-user or
small-team applications where shared secrets are
acceptable.