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 like
Read 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/):
- Single-purpose functions that make direct API calls to external systems
- Pure functions that handle HTTP transport layer (response.ok)
- Example: getDatabaseById(id), searchNotionDatabases()
- Should NOT handle environment variables, business logic, or orchestrate multiple calls
- Return consistent { success: boolean, data?: any, error?: string } format
Controllers (/backend/controllers/):
- Business logic and orchestration between services
- Handle environment variables and configuration
- Coordinate multiple service calls
- Implement business rules and validation
- Handle service business logic layer (result.success)
- Example: getHealthStatus(), validateConfiguredDatabases()
Routes (/backend/routes/):
- HTTP request/response handling only
- Call controllers, never services directly
- Minimal logic - just HTTP concerns (parsing, validation, response formatting)
- Example: parsing request params, calling controller, returning JSON
We use a consistent two-layer response handling strategy:
Layer 1: HTTP Transport Layer (response.ok)
- Used when checking responses from external APIs (Notion, etc.)
- Validates HTTP request succeeded (status 200-299)
- Handles network failures, auth issues, server errors
Layer 2: Service Business Logic (result.success)
- Used when checking our ServiceResponse objects
- Validates business operation succeeded
- Handles application-level failures, data validation, business rule violations
Create this directory structure with README.md files:
- main.tsx - entrypoint for this Val, where the Hono app is initialized
- /backend - directory for backend code
- /routes - HTTP route handlers organized by functionality
- /api - JSON API endpoints for frontend/backend communication
- /tasks - Webhook handlers for Notion integrations
- /views - Server-side rendered views for frontend
- /services - External API integrations (pure API calls only)
- /controllers - Business logic coordination between routes and services
- /crons - Scheduled task handlers
- /types - Shared TypeScript type definitions
- /utils - Common utility functions
- /frontend - Client-side files that render in the browser
- /shared - Files used by both backend and frontend
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 Notion:
import { Client } from "npm:@notionhq/client";
- Initialize:
const notion = new Client({ auth: Deno.env.get("NOTION_API_KEY") });
- Create these functions (API calls only, no business logic):
getDatabases()
- search for all databasesgetDatabaseById(databaseId: string)
- retrieve specific database
Service 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 service functions:
import { getDatabaseById } from "../services/notion.service.ts";
- Handle environment variables
(GLANCE_DEMOS_DB_ID, GLANCE_CONTENT_DB_ID, GLANCE_INTERACTIONS_DB_ID)
- Orchestrate multiple service calls
- Implement business logic for health checking
Required Controller Functions:
getConfiguredDatabases()
- Check all configured databases using environment variablesgetHealthStatus()
- Generate formatted health response for API
Controller 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:
- Services only make direct API calls (no environment variables, no orchestration)
- Controllers handle business logic and coordinate services
- Routes only handle HTTP concerns and call controllers
- No service calls directly from routes
- Environment variables handled in controllers, not services
- Consistent response patterns across all layers
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.
If there was an error regarding environment variables, remind the user to upload vars.env
to the Environment Variables section of this Val.
If everything is working as intended, do these things:
- Briefly summarize the scaffolding that was just completed and why it's important
- Provide the URL to the health endpoint so the user can see that the endpoint is live. (Always conclude this step with a link to the health endpoint, if you can provide one.)
- Tell the user that they can proceed to the next step:
02-auth.md
.
Don't write any code.