NOTION_API_KEY - Your Notion integration API keyNOTION_WEBHOOK_SECRET - Matches the X-API-KEY custom header coming from Notion webhooks to /tasks/* endpoint(s)GLANCE_DEMOS_DB_ID - Your Notion database ID for demos, which are personalized to prospects and have unique URLsGLANCE_CONTENT_DB_ID - Your Notion database ID for demo page content, which are compiled into demosGLANCE_INTERACTIONS_DB_ID - Your Notion database ID for pageview metrics and the likeRead through all of these instructions in order to understand the architecture of this Val, and implementation details important to this Val, and then once you have it all in your memory you should get to work.
If anything is unclear, you should stop and ask me for clarification before proceeding. Do not write code if you are unsure of what is expected.
Important: there are checkpoints in these instructions where you are directed to stop and ask permission before proceeding. Follow those instructions and do not proceed without permission.
This Val follows a strict layered architecture. Understanding these layers is critical before writing any code:
Services (/backend/services/):
Controllers (/backend/controllers/):
Routes (/backend/routes/):
We use a consistent two-layer response handling strategy:
Layer 1: HTTP Transport Layer (response.ok)
Layer 2: Service Business Logic (result.success)
Create this directory structure with README.md files:
Each route subdirectory should have a _[directory_name].routes.ts file that mounts routes for that section.
Townie, stop here! Ask for permission to proceed.
main.tsx Set up a Hono app like this:
import { Hono } from "npm:hono@3.12.12";
// Import route modules
import apiRoutes from "./backend/routes/api/\_api.routes.ts";
import taskRoutes from "./backend/routes/tasks/\_tasks.routes.ts";
import viewRoutes from "./backend/routes/views/\_views.routes.ts";
const app = new Hono();
// Unwrap Hono errors to see original error details
app.onError((err, c) => {
throw err;
});
// Mount route modules
app.route("/api", apiRoutes);
app.route("/tasks", taskRoutes);
app.route("/views", viewRoutes);
export default app.fetch;
Route Module Template Each _[directory].routes.ts file should follow this pattern:
import { Hono } from "npm:hono@3.12.12";
const app = new Hono();
// TODO: Add routes here
export default app;
Townie, stop here! Ask for permission to proceed.
Create /backend/services/notion.service.ts with ONLY direct Notion API calls:
Notion Service Requirements:
import { Client } from "npm:@notionhq/client";const notion = new Client({ auth: Deno.env.get("NOTION_API_KEY") });getDatabases() - search for all databasesgetDatabaseById(databaseId: string) - retrieve specific databaseService Function Pattern:
export async function getDatabaseById(databaseId: string) {
try {
const response = await notion.databases.retrieve({
database_id: databaseId,
});
return {
success: true,
data: response,
timestamp: new Date().toISOString(),
};
} catch (error) {
return {
success: false,
error: error.message,
timestamp: new Date().toISOString(),
};
}
}
Important: Services should NOT handle environment variables for database IDs or orchestrate multiple calls. That's controller responsibility.
Townie, stop here! Ask for permission to proceed.
Create /backend/controllers/health.controller.ts that orchestrates services:
Controller Requirements:
import { getDatabaseById } from "../services/notion.service.ts";(GLANCE_DEMOS_DB_ID, GLANCE_CONTENT_DB_ID, GLANCE_INTERACTIONS_DB_ID)Required Controller Functions:
getConfiguredDatabases() - Check all configured databases using environment variablesgetHealthStatus() - Generate formatted health response for APIController Function Pattern:
export async function getConfiguredDatabases() {
const demosDbId = Deno.env.get("GLANCE_DEMOS_DB_ID");
// ... get other DB IDs
const databases = { demos: null, content: null, interactions: null };
const errors: string[] = [];
// For each configured database:
if (demosDbId) {
const result = await getDatabaseById(demosDbId); // Call service
if (result.success) {
databases.demos = result.data;
} else {
errors.push(`Demos DB: ${result.error}`);
}
} else {
errors.push("GLANCE_DEMOS_DB_ID not configured");
}
// ... repeat for other databases
return {
success: errors.length === 0,
data: databases,
errors: errors.length > 0 ? errors : undefined,
timestamp: new Date().toISOString(),
};
}
Townie, stop here! Ask for permission to proceed.
Add Health Endpoint
In /backend/routes/api/_api.routes.ts:
import { Hono } from "npm:hono@3.12.12";
import { getHealthStatus } from "../../controllers/health.controller.ts";
const app = new Hono();
app.get("/health", async (c) => {
const healthStatus = await getHealthStatus();
return c.json(healthStatus);
});
export default app;
Add Root Route
In main.tsx, add a root route:
// Add this before mounting route modules
app.get("/", async (c) => {
return c.html(`<h1>Welcome</h1>`);
});
Townie, stop here! Ask for permission to proceed.
Before proceeding, verify your implementation follows the architecture:
The health endpoint should return:
{
"status": "ok",
"timestamp": "2025-07-14T18:26:28.270Z",
"service": "glance-demos",
"databases": {
"configured": true,
"demos": "connected",
"content": "connected",
"interactions": "connected"
}
}
This architecture is intended to create clear separation of concerns and should make the codebase maintainable and testable.
Townie, stop here! Before proceeding to additional steps, confirm that this step is working correctly.
Important: If there was an error regarding environment variables, remind the user to upload vars.env to the Environment Variables section of this Val.
Once everything is working as intended, always conclude this step with these messages:
/_townie/02-auth.md.Don't write any code.