FeaturesTemplatesShowcaseTownie
AI
BlogDocsPricing
Log inSign up
lightweight
lightweightglimpse2-runbook
Public
Like
glimpse2-runbook
Home
Code
2
_townie
8
main.tsx
Branches
2
Pull requests
Remixes
3
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
/
_townie
/
3-WEBHOOKS.md
Code
/
_townie
/
3-WEBHOOKS.md
Search
8/5/2025
Viewing readonly version of main branch: v10
View latest version
3-WEBHOOKS.md

Webhook Authentication Architecture Instructions

Townie instructions (Townie should follow these)

Middleware Order

Place authentication middleware in the following order:

  1. Notion webhook auth middleware (X-API-KEY for /tasks/*)
  2. Global user auth middleware (for other protected routes)
  3. Route handlers

Webhook Authentication

Scope

  • Apply only to routes under /tasks/*
  • All /tasks endpoints expect POSTs from Notion webhooks

Implementation

  • Create /backend/routes/webhookAuthCheck.ts
  • Extract X-API-KEY header from incoming requests
  • Compare against NOTION_WEBHOOK_SECRET environment variable
  • Use constant-time comparison to prevent timing attacks
  • Return 401/403 for authentication failures with generic error messages
  • Log failed authentication attempts for monitoring

Route Implementation Requirements

CRITICAL: All webhook routes MUST use POST method:

// ✅ CORRECT - Webhooks are POST requests
app.post("/webhook-endpoint", async (c) => {
  // Handle webhook
});

// ❌ WRONG - This won't work with webhooks
app.get("/webhook-endpoint", async (c) => {
  // Webhooks can't reach this
});

Why: Notion (and most webhook providers) send POST requests, never GET.

Middleware Application (EXACT CODE)

Apply middleware in this EXACT order in main.tsx:

// 1. Webhook auth - ONLY for /tasks/* routes
app.use("/tasks/*", webhookAuth);

// 2. User auth - for ALL routes EXCEPT /tasks/*
app.use("/api/*", authCheck);
app.use("/views/*", authCheck);
app.use("/", authCheck);

// Add root route - now serves the authenticated dashboard
app.get("/", dashboardRoute);

// 3. Mount routes AFTER middleware
app.route("/tasks", taskRoutes);

Important: We've changed the global authentication from app.use("*") to more specific auth route handling.

Before:

app.use("*", authCheck);  // Applied to ALL routes including /tasks/*

After:

app.use("/api/*", authCheck);   // Applied to API routes
app.use("/views/*", authCheck); // Applied to view routes
app.use("/", authCheck);        // Applied to root route

This ensures:

✅ / (root) still requires user authentication ✅ /api/* routes still require user authentication ✅ /views/* routes still require user authentication ✅ /tasks/* routes only use webhook authentication (no user auth)

Common mistake: Using app.use("*", webhookAuth) - this applies to all routes incorrectly.

Flow Logic

  • Request to /tasks/* → Check X-API-KEY → If valid, proceed to route handler (skip global auth)
  • Request to /tasks/* → Check X-API-KEY → If invalid, return 401 (never reaches global auth)
  • Request to other routes → Skip webhook auth → Go to global auth → Then route handler

Testing Webhook Authentication

Test in this exact order:

  1. Test without header (should fail):

POST /tasks/test Expected: 401 "Authentication required"

  1. Test with wrong key (should fail):

POST /tasks/test Headers: {"X-API-KEY": "wrong-key"} Expected: 403 "Authentication failed"

  1. Test with correct key (should succeed):

POST /tasks/test
Headers: {"X-API-KEY": "your-configured-secret-value"} Expected: 200 (reaches your route handler)

If step 3 returns 404: Your route is probably defined as GET instead of POST.

Debugging Webhook Auth Issues

Add this temporary debug endpoint:

app.get("/debug-webhook", (c) => {
  const secret = Deno.env.get("NOTION_WEBHOOK_SECRET");
  return c.json({
    hasSecret: !!secret,
    secretLength: secret?.length || 0,
  });
});

Expected response: {"hasSecret":true,"secretLength":>0}

Add this catch all endpoint:

// Catch-all handler for undefined webhook endpoints
// This must be the LAST route defined to catch unmatched paths
app.post("*", (c) => {
  const path = c.req.path;
  const method = c.req.method;
  const returnObj = {
    error: "Endpoint not found",
    path: path,
    method: method,
    availableEndpoints: [
      "GET /tasks/debug-webhook",
      "POST /tasks/test",
      "POST /tasks/notion-webhook",
    ],
  };
  console.log(returnObj);

  return c.json(returnObj, 404);
});

Complete Working Example

Here's a complete working webhook endpoint:

// backend/routes/tasks/_tasks.routes.ts
import { Hono } from "npm:hono@3.12.12";

const app = new Hono();

app.post("/notion-webhook", async (c) => {
  // At this point, webhook auth has already passed
  const body = await c.req.json();
  console.log("Notion webhook received:", body);

  // Process the webhook...

  return c.json({ success: true });
});

export default app;

Test with:

POST /tasks/notion-webhook Headers: {"X-API-KEY": "your-configured-secret-value"} Body: {"test": "data"} Expected: 200 {"success": true}

Security Considerations

  • Webhook middleware should return early (success or failure) without calling next()
  • Never pass webhook requests to the global auth middleware
  • Maintain complete separation between webhook auth and user auth mechanisms
  • Consider rate limiting on /tasks endpoints
  • Use generic error messages that don't reveal authentication mechanism details

Route Structure

  • /tasks/* → Webhook auth required (Notion POSTs)
  • /api/* → User auth required (if applicable)
  • /* → Public routes (static files, etc.)

Key Changes Made:

  1. Added Environment Setup section with clear instructions for setting real values (without revealing actual values)
  2. Added Route Implementation Requirements emphasizing POST method requirement
  3. Added exact middleware application code to prevent ordering mistakes
  4. Added comprehensive testing steps with expected responses (using generic placeholders)
  5. Added debugging section with tools to troubleshoot issues
  6. Added complete working example to verify implementation against
  7. Emphasized critical points with bold text and warning boxes
  8. Added common mistakes and how to avoid them

These revisions address all the issues we encountered while keeping sensitive values generic and secure.

Go to top
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Product
FeaturesCLIAI agentsCode intelligenceSlack integrationsGTMPricing
Developers
DocsStatusAPI ExamplesNPM Package Examples
Explore
ShowcaseTemplatesNewest ValsTrending ValsNewsletter
Company
AboutBlogCareersBrandhi@val.town
Terms of usePrivacy policyAbuse contact
© 2025 Val Town, Inc.