Place authentication middleware in the following order:
/tasks/*
)/backend/routes/webhookAuthCheck.ts
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.
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.
Test in this exact order:
POST /tasks/test Expected: 401 "Authentication required"
POST /tasks/test Headers: {"X-API-KEY": "wrong-key"} Expected: 403 "Authentication failed"
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.
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);
});
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}
Update /backend/routes/views/dashboard.tsx
:
Import the URL utility in the dashboard:
Add this import at the top of /backend/routes/views/dashboard.tsx
:
import { getAbsoluteUrl } from "../../utils/url.ts";
Add webhook configuration variables after health data:
// Check webhook configuration
const webhookSecret = Deno.env.get("NOTION_WEBHOOK_SECRET");
const webhookConfigured = !!webhookSecret;
const webhookEndpoint = getAbsoluteUrl(c, '/tasks/notion-webhook');
Note: We use getAbsoluteUrl()
here because the webhook endpoint needs to be shared with external services (Notion), unlike internal links which should remain relative.
Add CSS styles for webhook section (after existing styles):
.webhook-section {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 20px;
margin-top: 20px;
}
.webhook-section h3 {
color: #333;
margin-top: 0;
margin-bottom: 15px;
}
.webhook-url {
margin-bottom: 20px;
}
.webhook-endpoint {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 14px;
background: white;
padding: 12px;
border: 1px solid #dee2e6;
border-radius: 4px;
word-break: break-all;
user-select: all;
}
.test-section {
border-top: 1px solid #dee2e6;
padding-top: 20px;
margin-top: 20px;
}
.test-section p {
margin-bottom: 15px;
color: #495057;
}
.test-section code {
background: #f8f9fa;
padding: 2px 4px;
border-radius: 3px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
}
.test-controls {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 15px;
}
.test-btn {
background: #28a745;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.test-btn:hover {
background: #218838;
}
.test-btn:disabled {
background: #6c757d;
cursor: not-allowed;
}
.api-key-input {
padding: 6px 10px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 14px;
width: 200px;
}
.test-result {
background: white;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 15px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
min-height: 60px;
white-space: pre-wrap;
}
.result-success {
border-color: #28a745;
background: #d4edda;
}
.result-error {
border-color: #dc3545;
background: #f8d7da;
}
Add simplified webhook section HTML (after health section):
<div class="webhook-section">
<h3>Webhook Endpoint</h3>
<div class="webhook-url">
<div class="webhook-endpoint">${webhookEndpoint}</div>
</div>
<div class="test-section">
<p><strong>To test:</strong> Get the <code>NOTION_WEBHOOK_SECRET</code> value from your <code>vars.env</code> file (provided by your administrator)</p>
<form class="test-controls" onsubmit="testWebhook(event)">
<input type="text" class="api-key-input" id="customApiKey" placeholder="Enter webhook secret" required>
<button type="submit" class="test-btn">Test Webhook</button>
</form>
<div id="testResult" class="test-result">Ready to test webhook authentication...</div>
</div>
</div>
Add JavaScript for form submission (before closing body tag):
<script>
async function testWebhook(event) {
// Prevent form submission from reloading the page
if (event) {
event.preventDefault();
}
const resultDiv = document.getElementById('testResult');
const customKeyInput = document.getElementById('customApiKey');
// Clear previous result
resultDiv.className = 'test-result';
resultDiv.textContent = 'Testing...';
try {
const customKey = customKeyInput.value.trim();
if (!customKey) {
resultDiv.textContent = 'Please enter the webhook secret to test';
resultDiv.className = 'test-result result-error';
return;
}
const headers = { 'X-API-KEY': customKey };
const response = await fetch('/tasks/test', {
method: 'POST',
headers: headers
});
const responseData = await response.json();
const timestamp = new Date().toLocaleTimeString();
if (response.ok) {
resultDiv.textContent = `[${timestamp}] ✅ Webhook authentication successful!\n\nStatus: ${response.status} ${response.statusText}\nResponse: ${JSON.stringify(responseData, null, 2)}`;
resultDiv.className = 'test-result result-success';
} else {
resultDiv.textContent = `[${timestamp}] ❌ Webhook authentication failed\n\nStatus: ${response.status} ${response.statusText}\nResponse: ${JSON.stringify(responseData, null, 2)}`;
resultDiv.className = 'test-result result-error';
}
} catch (error) {
const timestamp = new Date().toLocaleTimeString();
resultDiv.textContent = `[${timestamp}] ❌ Error testing webhook: ${error.message}`;
resultDiv.className = 'test-result result-error';
}
}
</script>
/tasks/debug-webhook
(GET) - should show webhook configuration/tasks/test
(POST) without header - should return 401/tasks/test
(POST) with wrong key - should return 403/tasks/test
(POST) with correct key - should return 200Make sure these are true:
Update README.md to include webhook authentication section and environment variables.
Make sure these are true:
Townie, stop here! Before proceeding to additional steps, confirm that this step is working correctly.
If everything is working as intended: conclude this step with these messages:
NOTION_WEBHOOK_SECRET
from the
vars.env
to test the webhook endpoint successfully