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
:
Add webhook configuration variables after health data:
// Check webhook configuration
const webhookSecret = Deno.env.get("NOTION_WEBHOOK_SECRET");
const webhookConfigured = !!webhookSecret;
const webhookEndpoint = `${c.req.url.split('/')[0]}//${c.req.url.split('/')[2]}/tasks/notion-webhook`;
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 successfullyTell the user that the next step is to add an endpoint to /routes that returns the data from the Glimpse Demos database in Notion.
Lastly, tell the user to copy this line and paste it into Townie:
Add an endpoint at
/views/glimpse/:id
by following the instructions in/_townie/04-view.md