• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
lightweight

lightweight

scaffold

Unlisted
Like
scaffold
Home
Code
12
.claude
1
backend
7
frontend
5
shared
3
.vtignore
AGENTS.md
CLAUDE.md
README.md
deno.json
guidelines.md
H
main.http.tsx
webhook-auth-notes.md
Branches
1
Pull requests
Remixes
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
/
backend
/
README.md
Code
/
backend
/
README.md
Search
11/5/2025
Viewing readonly version of main branch: v74
View latest version
README.md

Backend

This directory contains the backend logic for the Notion webhook integration application.

Directory Structure

backend/
├── controllers/        # Business logic and orchestration
│   ├── healthController.ts
│   ├── pageController.ts
│   └── webhookController.ts (example)
├── routes/            # HTTP request/response handling
│   ├── api/          # RESTful API endpoints
│   │   ├── index.ts
│   │   └── pages.ts
│   ├── tasks/        # Webhook handlers
│   ├── views/        # User-facing pages
│   └── authCheck.ts  # Authentication middleware
├── crons/             # Val Town cron trigger handlers
│   └── (scheduled tasks)
├── email/             # Val Town email handlers & utilities
│   └── (email triggers & sending)
├── services/          # External API integrations
│   └── notion/
│       ├── index.ts
│       ├── pages.ts
│       └── databases.ts
└── utils/             # Backend-only utility functions
    └── (pure functions, no external deps)

Layer 1A: Routes (routes/) - HTTP Triggers

Responsibility: HTTP ONLY - Extract parameters, apply middleware, call controllers, format HTTP responses

Structure

  • routes/api/ - RESTful API endpoints for data operations

    • Example: /api/pages/:id, /api/databases/:id
    • Returns JSON responses
  • routes/tasks/ - Webhook handlers from external services

    • Example: /tasks/notion-webhook
    • Handle incoming webhook payloads
  • routes/views/ - User-facing HTML/React views

    • Example: /views/dashboard
    • Serve HTML pages
  • routes/authCheck.ts - Authentication middleware

    • webhookAuth(c, next) - Shared secret authentication using X-API-KEY header
    • Protects /api/* and /tasks/* routes
    • Applied globally in main.http.tsx

Rules

  • ❌ NO business logic in routes
  • ❌ NEVER call services directly from routes
  • ✅ Extract request parameters (query, body, headers)
  • ✅ Apply authentication middleware
  • ✅ Call controller functions
  • ✅ Map controller responses to HTTP status codes:
    • 200 - Success
    • 400 - Validation errors, bad request
    • 401 - Authentication failures
    • 500 - Server/external service errors

Example Route Pattern

import { Hono } from "npm:hono@4"; import * as pageController from '../../controllers/pageController.ts'; const pages = new Hono(); pages.get('/:id', async (c) => { const pageId = c.req.param('id'); const result = await pageController.getPage(pageId); if (!result.success) { return c.json({ error: result.error, details: result.details }, 400); } return c.json(result.data); }); export default pages;

Layer 1B: Crons (crons/) - Scheduled Triggers

Responsibility: Execute scheduled tasks on intervals using Val Town's cron feature

Val Town Cron Documentation: https://docs.val.town/vals/cron/

What Are Crons?

Crons are Val Town's scheduled task feature. They run on a specified interval (e.g., every hour, daily, weekly) and execute code automatically.

Key Characteristics:

  • Use .cron.tsx file extension
  • Run on Val Town's scheduler (not HTTP requests)
  • Execute and complete (no return value needed)
  • Follow same architecture: Cron handler → Controller → Service

When to Use Crons

Use crons for:

  • ✅ Periodic data synchronization (sync Notion data hourly)
  • ✅ Scheduled cleanup tasks (delete old records daily)
  • ✅ Regular status checks (check API health every 5 minutes)
  • ✅ Batch processing (process queued items every 10 minutes)
  • ✅ Scheduled reports (send daily summary emails)
  • ✅ Cache warming (refresh cached data periodically)

Do NOT use for:

  • ❌ Real-time processing (use routes/webhooks instead)
  • ❌ User-initiated actions (use API routes instead)
  • ❌ Immediate responses (use HTTP endpoints instead)

File Organization

Place cron handlers in backend/crons/:

backend/crons/
├── syncNotionData.cron.tsx      # Sync Notion data every hour
├── cleanupOldRecords.cron.tsx   # Delete old data daily
└── sendDailyReport.cron.tsx     # Email daily summaries

Cron Handler Pattern

File: backend/crons/syncNotionData.cron.tsx

import * as pageController from '../controllers/pageController.ts'; /** * Sync Notion data every hour * This cron runs automatically on Val Town's scheduler */ export default async function syncNotionData(interval: Interval) { console.log(`[Cron] Starting Notion sync at ${new Date().toISOString()}`); try { // Call controller (same as routes do) const result = await pageController.syncAllPages(); if (!result.success) { console.error(`[Cron] Sync failed: ${result.error}`); return; } console.log(`[Cron] Sync completed: ${result.data.synced} pages updated`); } catch (err) { console.error(`[Cron] Unexpected error:`, err); } }

Cron Configuration

Crons are configured in Val Town's UI with:

  • Schedule: Every hour, daily, weekly, custom cron expression
  • Example: 0 * * * * (every hour at minute 0)

Rules

  • ✅ Call controllers (same as routes)
  • ✅ Use try/catch for error handling
  • ✅ Log start and completion
  • ✅ Keep handlers simple (logic in controllers)
  • ❌ NO return value required (execute and complete)
  • ❌ NO HTTP response formatting
  • ❌ NO authentication needed (runs as scheduled)

Architecture Flow

Scheduler → Cron Handler → Controller → Service → External API

Example:

Every Hour → syncNotionData.cron.tsx → pageController.syncAllPages() → notionService.queryDatabase() → Notion API

Layer 1C: Email (email/) - Email Triggers & Utilities

Responsibility: Handle incoming emails and send outgoing emails using Val Town's email system

Val Town Email Documentation:

  • Email triggers: https://docs.val.town/vals/email/
  • Email utilities: https://docs.val.town/reference/std/email/

What Is Email?

Val Town provides two email capabilities:

  1. Email triggers - Receive emails at a Val Town email address
  2. Email utilities - Send emails from your vals

File Organization

backend/email/
├── inbound.email.tsx           # Handle incoming emails (trigger)
├── processTicket.email.tsx     # Process support emails (trigger)
└── notifications.ts            # Send email utilities (called by controllers)

Use Case 1: Email Trigger Handlers

When to Use:

  • ✅ Process incoming support emails
  • ✅ Create Notion pages from email
  • ✅ Auto-reply to specific email patterns
  • ✅ Parse and route inbound messages

File Extension: .email.tsx

Handler Pattern:

import { email } from "https://esm.town/v/std/email"; import * as ticketController from '../controllers/ticketController.ts'; /** * Handle incoming support emails * Triggered when email is received at your-val@valtown.email */ export default async function processTicket(email: Email) { console.log(`[Email] Received from: ${email.from}`); console.log(`[Email] Subject: ${email.subject}`); try { // Call controller to process email const result = await ticketController.createTicketFromEmail({ from: email.from, subject: email.subject, body: email.text, }); if (!result.success) { console.error(`[Email] Failed to create ticket: ${result.error}`); // Send error response return await email.send({ to: email.from, subject: "Re: " + email.subject, text: "Sorry, we couldn't process your email. Please try again.", }); } // Send success response return await email.send({ to: email.from, subject: "Re: " + email.subject, text: `Ticket created: ${result.data.ticketId}`, }); } catch (err) { console.error(`[Email] Unexpected error:`, err); } }

Email Trigger Rules:

  • ✅ Use .email.tsx extension
  • ✅ Call controllers for business logic
  • ✅ Can send reply emails
  • ✅ Return email response (unlike crons)
  • ✅ Log incoming email details
  • ❌ NO HTTP responses (use email.send instead)

Use Case 2: Email Sending Utilities

When to Use:

  • ✅ Send notifications from controllers
  • ✅ Email reports generated by crons
  • ✅ Send alerts from services
  • ✅ Confirmation emails from API routes

File Extension: .ts (regular TypeScript)

Utility Pattern:

// backend/email/notifications.ts import { email } from "https://esm.town/v/std/email"; /** * Send a notification email about a Notion page update * Called by controllers/services */ export async function sendPageUpdateNotification( to: string, pageTitle: string, changes: string[] ) { try { await email.send({ to: to, subject: `Notion Page Updated: ${pageTitle}`, text: ` The following changes were made: ${changes.map(c => `- ${c}`).join('\n')} View page: [link] `.trim(), }); return { success: true, data: null, error: null }; } catch (err: any) { return { success: false, data: null, error: err.message || 'Failed to send email' }; } } /** * Send a daily summary report * Called by cron handlers */ export async function sendDailySummary( to: string, summary: { pages: number; updates: number } ) { try { await email.send({ to: to, subject: "Daily Notion Summary", text: ` Daily Summary Report ==================== Pages synced: ${summary.pages} Updates made: ${summary.updates} Generated at: ${new Date().toISOString()} `.trim(), }); return { success: true, data: null, error: null }; } catch (err: any) { return { success: false, data: null, error: err.message || 'Failed to send summary' }; } }

Email Utility Rules:

  • ✅ Export functions that send emails
  • ✅ Return {success, data, error} format
  • ✅ Can be called by controllers, services, or crons
  • ✅ Handle errors gracefully
  • ✅ Use Val Town's std/email library
  • ❌ NO business logic (that's in controllers)

Calling Email Utilities

From Controllers:

// backend/controllers/pageController.ts import * as emailNotifications from '../email/notifications.ts'; export async function updatePageAndNotify(pageId: string, changes: any) { // Update page via service const result = await notionService.updateNotionPage(pageId, changes); if (!result.success) { return { success: false, data: null, error: result.error }; } // Send notification await emailNotifications.sendPageUpdateNotification( 'user@example.com', 'My Page', ['Title changed', 'Status updated'] ); return { success: true, data: result.data, error: null }; }

From Crons:

// backend/crons/sendDailyReport.cron.tsx import * as emailNotifications from '../email/notifications.ts'; export default async function sendDailyReport(interval: Interval) { const summary = { pages: 42, updates: 15 }; await emailNotifications.sendDailySummary( 'admin@example.com', summary ); }

Architecture Flows

Email Trigger Flow:

Incoming Email → Email Handler → Controller → Service → External API
                      ↓
                 email.send() (reply)

Email Utility Flow:

Route/Cron → Controller → Email Utility → std/email.send()

Layer 2: Controllers (controllers/)

Responsibility: Business logic, validation, orchestration, data transformation

Controller Organization

IMPORTANT: Organize controllers by purpose or context. Each controller handles a specific domain or concern.

Examples:

  • healthController.ts - System health checks (no services, just env checks)
  • pageController.ts - Notion page operations (calls Notion service)
  • webhookController.ts - Webhook processing logic

Pattern: Group related operations in one controller. Controllers can grow as features are added.

What Controllers Do

  • Validate request data
  • Orchestrate multiple service calls (or just check system state)
  • Transform and filter data
  • Remove sensitive information
  • Add business logic context to errors

Response Format (REQUIRED)

All controller functions MUST return this exact structure:

{ success: boolean, data: any | null, error: string | null, details?: string // Additional error context }

Rules

  • ❌ NEVER return HTTP responses (controllers are HTTP-agnostic)
  • ❌ NEVER call external APIs directly
  • ✅ Call service layer functions for external API operations
  • ✅ May check system state without services (e.g., health checks)
  • ✅ Return standardized response structure
  • ✅ Validate all input data
  • ✅ Filter sensitive data (e.g., button properties)
  • ✅ Work with generic Notion pages (use pageId, not task/project-specific)

Example Controller Patterns

Pattern 1: Controller with Service Calls (pageController.ts)

import * as notionService from '../services/notion/index.ts'; export async function updatePage(pageId: string, properties: any) { // Validate input if (!pageId || typeof pageId !== 'string') { return { success: false, data: null, error: "Invalid pageId", details: "pageId must be a non-empty string" }; } // Call service const result = await notionService.updateNotionPage(pageId, properties); if (!result.success) { return { success: false, data: null, error: "Failed to update page", details: result.error }; } // Filter sensitive data const filteredData = filterSensitiveProperties(result.data); return { success: true, data: filteredData, error: null }; }

Pattern 2: Controller without Service Calls (healthController.ts)

import { getProjectInfo } from '../utils/valtown.ts'; /** * Get system health status * PUBLIC endpoint - no authentication required */ export function getHealth() { // Check system state (no service calls needed) // Can use utils for server-side information return { success: true, data: { status: 'ok', timestamp: new Date().toISOString(), project: getProjectInfo(), // Val Town project info configuration: { NOTION_API_KEY: !!Deno.env.get('NOTION_API_KEY'), WEBHOOK_SECRET: !!Deno.env.get('WEBHOOK_SECRET') } }, error: null }; }

Layer 3: Services (services/)

Responsibility: External API calls ONLY - No business logic

Organization

For complex services (multiple concerns): Use directory structure

services/notion/
├── index.ts         # Initialize client, re-export all
├── pages.ts         # Page operations
└── databases.ts     # Database operations

For simple services (single concern): Use single file

services/
├── firecrawlerService.ts
└── blobService.ts

Service Index Pattern

// services/notion/index.ts import { Client } from "npm:@notionhq/client@2"; export const notion = new Client({ auth: Deno.env.get("NOTION_API_KEY") }); export * from "./pages.ts"; export * from "./databases.ts";

Service Function Pattern

// services/notion/pages.ts import { notion } from "./index.ts"; export async function getNotionPage(pageId: string) { try { const page = await notion.pages.retrieve({ page_id: pageId }); return { success: true, data: page, error: null }; } catch (err: any) { return { success: false, data: null, error: err.message || 'Failed to fetch Notion page' }; } }

Rules

  • ✅ Return structured results with {success, data, error}
  • ✅ Handle all API errors gracefully (don't throw)
  • ✅ Use environment variables for API keys
  • ✅ Export typed functions
  • ❌ NO business logic
  • ❌ NO data validation (that's controller's job)

Layer 4: Utils (utils/)

Responsibility: Pure utility functions with NO external dependencies and NO business logic

What Goes in Utils

✅ Backend-only utilities that can use Deno APIs:

  • cryptoUtils.ts - Hashing, encryption, JWT signing (Web Crypto API)
  • dateUtils.ts - Date formatting, timezone conversions
  • validationUtils.ts - Input validation helpers (using Deno features)
  • logUtils.ts - Logging formatters
  • envUtils.ts - Environment variable parsers

What Does NOT Go in Utils

❌ External API calls → use services/ ❌ Business logic → use controllers/ ❌ Browser-compatible code → use shared/utils.ts ❌ Database operations → use services/

Example Utility

// utils/cryptoUtils.ts /** * Generate a SHA-256 hash of a string */ export async function sha256(message: string): Promise<string> { const encoder = new TextEncoder(); const data = encoder.encode(message); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); }

Rules

  • ✅ Pure functions with predictable outputs
  • ✅ No side effects (no global state)
  • ✅ Single responsibility
  • ✅ Can use Deno APIs (unlike shared/utils.ts)
  • ✅ Type safety with TypeScript

Architecture Flow

Complete Flow - All Trigger Types

┌─────────────────────────────────────────────────────────────────┐
│                    TRIGGERS (Layer 1)                           │
│  ┌────────────┐    ┌───────────┐    ┌─────────────┐            │
│  │ HTTP Route │    │   Cron    │    │ Email Trigger│            │
│  │ (routes/)  │    │ (crons/)  │    │  (email/)   │            │
│  └──────┬─────┘    └─────┬─────┘    └──────┬──────┘            │
└─────────┼────────────────┼──────────────────┼──────────────────┘
          │                │                  │
          └────────────────┴──────────────────┘
                           │
                           v
                  ┌────────────────┐
                  │  Controller    │  Validate, orchestrate, transform
                  │  (Business)    │  Can use utils/ and email/
                  └────────┬───────┘
                           │
                           v
                  ┌────────────────┐
                  │  Service       │  External API calls only
                  │  (External)    │
                  └────────┬───────┘
                           │
                           v
                  ┌────────────────┐
                  │  External API  │  Notion, Firecrawler, etc.
                  └────────────────┘

Trigger-Specific Flows

HTTP Route Flow:

HTTP Request → Route (routes/) → Controller → Service → External API
                 ↓
            HTTP Response

Cron Flow:

Scheduler → Cron Handler (crons/) → Controller → Service → External API
              ↓
         Execute & Complete

Email Trigger Flow:

Incoming Email → Email Handler (email/) → Controller → Service → External API
                       ↓
                  email.send() (reply)

Email Utility Flow (called by controllers):

Controller → Email Utility (email/) → std/email.send()

NEVER skip layers!

  • HTTP routes, crons, and email triggers all call controllers
  • Controllers call services (not external APIs directly)
  • Services handle external API calls
  • Utils are called by controllers/services for helper functions
  • Email utilities can be called by controllers, services, or crons

Import Patterns

All backend code uses npm: imports for packages:

import { Hono } from "npm:hono@4"; import { Client } from "npm:@notionhq/client@2";

For Val Town utilities, use URL imports:

import { readFile, serveFile } from "https://esm.town/v/std/utils@85-main/index.ts";

Adding New Features

Choose Your Trigger Type

First, determine which trigger type fits your use case:

Trigger TypeWhen to UseFile ExtensionExample
HTTP RouteUser requests, webhooks, API endpoints.ts in routes//api/pages/:id
CronScheduled tasks, periodic sync.cron.tsx in crons/Sync data every hour
Email TriggerIncoming email handling.email.tsx in email/Process support emails
Email UtilitySend emails from code.ts in email/Send notifications

Adding HTTP Routes

  1. ✅ Create route file in appropriate directory:

    • Webhook handlers → routes/tasks/
    • API endpoints → routes/api/
    • Views → routes/views/
  2. ✅ Extract request data in route (params, query, body)

  3. ✅ Create controller function in controllers/[feature].ts

    • Implement validation
    • Implement business logic
    • Orchestrate service calls
  4. ✅ Create/update service functions in services/[service].ts

    • Make external API calls
    • Return {success, data, error} structure
  5. ✅ Return standardized response from controller

  6. ✅ Format HTTP response in route based on controller result

  7. ✅ Add TypeScript types to shared/types.ts if data is shared across layers

  8. ✅ Import route into main.http.tsx and mount on Hono app

  9. ✅ Test the endpoint

Note: Authentication is applied globally in main.http.tsx for all /api/* and /tasks/* routes (except public endpoints like /api/health). No per-route authentication setup needed.

Adding Cron Jobs

  1. ✅ Create cron file in crons/ with .cron.tsx extension

    • Example: crons/syncNotionData.cron.tsx
  2. ✅ Export default async function that accepts interval: Interval

  3. ✅ Add logging for start, completion, and errors

  4. ✅ Call controller functions (same as routes do)

    • Validate controller response
    • Log results
  5. ✅ Create/update controller function if needed

  6. ✅ Create/update service functions if needed

  7. ✅ Configure schedule in Val Town UI

    • Set cron expression (e.g., 0 * * * * for hourly)
  8. ✅ Test by running manually in Val Town

  9. ✅ Monitor logs to ensure it runs on schedule

Adding Email Triggers

  1. ✅ Create email trigger file in email/ with .email.tsx extension

    • Example: email/processTicket.email.tsx
  2. ✅ Export default async function that accepts email: Email

  3. ✅ Add logging for incoming emails

  4. ✅ Call controller functions to process email content

  5. ✅ Send response using email.send() if needed

  6. ✅ Configure email address in Val Town UI

    • Set up your-val@valtown.email
  7. ✅ Test by sending email to your Val Town address

Adding Email Utilities

  1. ✅ Create utility file in email/ with .ts extension

    • Example: email/notifications.ts
  2. ✅ Export functions that send emails

  3. ✅ Return standardized response {success, data, error}

  4. ✅ Import and call from controllers, services, or crons

  5. ✅ Test by calling from your code

Error Handling Strategy

Services: Return errors, don't throw

return { success: false, data: null, error: "API call failed" };

Controllers: Add context to service errors

if (!result.success) { return { success: false, data: null, error: "Failed to update page", details: result.error }; }

Routes: Map to HTTP status codes

if (!result.success) { return c.json({ error: result.error, details: result.details }, 400); }

Authentication

Authentication is applied globally in main.http.tsx using array-based route protection.

Implementation

Middleware: routes/authCheck.ts exports webhookAuth(c, next)

  • Uses shared secret approach with X-API-KEY header
  • Validates against WEBHOOK_SECRET environment variable
  • Gracefully degrades in development mode (logs warning if not configured)

Global Protection in main.http.tsx:

import { webhookAuth } from './backend/routes/authCheck.ts'; // Define protected route prefixes and public exceptions const protectedPrefixes = ['/api', '/tasks']; const publicExceptions = ['/api/health']; // Apply middleware before mounting routes app.use('*', async (c, next) => { const path = c.req.path; // Check if path is a public exception (exact match) if (publicExceptions.includes(path)) { await next(); return; } // Check if path starts with any protected prefix const isProtected = protectedPrefixes.some(prefix => path.startsWith(prefix)); if (isProtected) { await webhookAuth(c, next); } else { await next(); } });

Usage

In webhook configuration (Notion buttons, external services):

  • Header name: X-API-KEY
  • Header value: Value of WEBHOOK_SECRET environment variable

From API clients (curl, fetch):

curl -H "X-API-KEY: your-secret" https://your-val.town/api/pages/recent

Protected routes:

  • All /api/* routes (except /api/health)
  • All /tasks/* routes

Public routes:

  • /api/health - No authentication required
  • / - Frontend HTML
  • /frontend/* - Static files
  • /shared/* - Static files

Environment Variables

Access environment variables using:

const apiKey = Deno.env.get('NOTION_API_KEY');

NEVER hardcode secrets - always use environment variables.

Best Practices

  1. Layer Separation: Never skip layers in the architecture
  2. Generic Controllers: Work with pageId generically, not task/project-specific
  3. Error Context: Add helpful error messages with details field
  4. Type Safety: Use TypeScript types from shared/types.ts
  5. No Throwing: Services return error objects, don't throw
  6. Filter Sensitive Data: Remove button properties and other sensitive data in controllers
  7. Pure Utils: Keep utility functions pure with no side effects
  8. Single Responsibility: Each function should do one thing well

See Also

  • CLAUDE.md - Comprehensive project architecture guidelines
  • shared/types.ts - TypeScript interfaces
  • shared/utils.ts - Browser-compatible utility functions
FeaturesVersion controlCode intelligenceCLIMCP
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
PricingNewsletterBlogAboutCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
© 2025 Val Town, Inc.