• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
lightweight

lightweight

scaffold

Unlisted
Like
scaffold
Home
Code
13
.claude
1
backend
7
frontend
5
shared
3
.vtignore
AGENTS.md
CLAUDE.md
README.md
SCAFFOLD.md
deno.json
guidelines.md
H
main.http.tsx
webhook-auth-notes.md
Branches
1
Pull requests
Remixes
History
Environment variables
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
webhook-auth-notes.md
Code
/
webhook-auth-notes.md
Search
11/5/2025
webhook-auth-notes.md

⏺ 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

  1. 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)
  1. Authentication Middleware File
  • Location: backend/routes/authCheck.ts
  • Exports: webhookAuth function
  • Framework: Hono middleware pattern (Context, Next)
  1. 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
  1. Application in Main Entry Point
  • Location: main.http.tsx
  • Pattern: Array-based route protection with exceptions

Implementation approach:

  1. Define protected route prefixes in array: ['/tasks', '/api']

  2. Define public exceptions in array: ['/api/health']

  3. 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()

  1. Route Mounting Order Important: Apply authentication middleware BEFORE mounting routes
  2. Hono app initialization
  3. Error handler
  4. Static file serving (if needed)
  5. Authentication middleware (app.use)
  6. 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.

FeaturesVersion controlCode intelligenceCLIMCP
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
PricingNewsletterBlogAboutCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
© 2025 Val Town, Inc.