34 const binary = new Uint8Array(await blobResp.arrayBuffer());
35 const SQL = await initSqlJs();
36 const db = new SQL.Database(binary);
37
38 // Helper: Validate SQL query returns a result
9import favicon from "./frontend/favicon.http.tsx";
10import loginRoute from "./frontend/routes/login.tsx";
11import adminDashboard from "./backend/database/usage-dashboard/index.ts"
12
13const MODULE_URL = import.meta.resolve("./frontend/index.tsx");
126GlobalRateLimitedChatOpenAI(model: string, requestsPerSecond: number): Decorator for ChatOpenAI that enforces a global rate limit (requests per second) using a persistent SQLite table.
127GlobalRateLimiter: Class that implements the rate limiting logic. It checks the number of requests in the current time window and throws an error if the limit is exceeded. It uses a table (global_rate_limit_1) in Val Town's SQLite.
128ensureGlobalRateLimitTableExists: Ensures the rate limit tracking table exists in the database at startup.
129Usage
130Use ChatOpenAI(model) for direct, unlimited access to OpenAI chat completions.
225```
226โโโ backend/
227โ โโโ database/
228โ โ โโโ migrations.ts # Schema definitions
229โ โ โโโ queries.ts # DB query functions
284 ```
285
286### Database Patterns
287- Run migrations on startup or comment out for performance
288- Change table names when modifying schemas rather than altering
4
5export const turso = createClient({
6 url: process.env.TURSO_DATABASE_URL,
7 authToken: process.env.TURSO_AUTH_TOKEN,
8});
179```
180โโโ backend/
181โ โโโ database/
182โ โ โโโ migrations.ts # Schema definitions
183โ โ โโโ queries.ts # DB query functions
239 ```
240
241### Database Patterns
242- Run migrations on startup or comment out for performance
243- Change table names when modifying schemas rather than altering
221```typescript
222export default async function (interval: Interval) {
223 const pages = await notion.databases.query({
224 database_id: Deno.env.get("GLANCE_DEMOS_DB_ID"),
225 });
226
244});
245
246const databaseId = Deno.env.get("GLANCE_DEMOS_DB_ID");
247```
248
250
251- `NOTION_API_KEY`: Notion integration token
252- `GLANCE_DEMOS_DB_ID`: Notion database ID for demo pages
253- `GLANCE_INTERACTIONS_DB_ID`: Notion database ID for interaction tracking
254
255## Project Conventions
291- `/api/*`: JSON data endpoints for frontend consumption
292- `/demo/*`: Personalized demo page serving with data injection
293- `/tasks/*`: Notion webhook processing and database updates
294
295## External Dependencies
300
301- **Email and Alerting**: Slack notifications and email alerts are configured within Notion
302- **Database Management**: Three core Notion databases power the application:
303 - **Glancer Demos Database**: Stores demo configurations, visitor information, and personalization data
304 - **Glancer Interactions Database**: Warehouses demo events, clicks, and user behavior analytics
305 - **Glancer Agents Database**: Manages agent assignments, availability, and contact information
306
307The Val Town application serves as an automation and presentation layer that extends Notion's native capabilities, providing real-time cobrowsing experiences while maintaining all data persistence and workflow management within the Notion ecosystem.
12 status: "connected",
13 message: "Successfully connected to Notion API",
14 databases: response.results.map((db) => ({
15 title: db.title?.[0]?.plain_text || "Untitled",
16 id: db.id,
20
21// update the cache
22// this cron updates a blob that stores the JSON that shows the databases that val.town is connected to
23// we present that at root and embed that into Notion
24// to show everyone that the connection is healthy btw Notion and val.town
29 filter: {
30 property: "object",
31 value: "database",
32 },
33 });
2import { Client } from "npm:@notionhq/client";
3import { blobKeyForDemoCache } from "../../shared/utils/blobKeyForDemoCache.ts";
4import { getRelatedPagesFromDatabase } from "../controllers/relatedPages.controller.ts";
5
6// Initialize Notion client
11export default async function (interval: Interval) {
12 // this cron runs every minute
13 // it saves a blob for every page in the "Glancer demos" database
14 // that holds the Notion data and markup needed for every demo
15 // this way, to build a demo in the browser (ie., when /demo/:id is called),
18 const twoDaysAgo = new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString();
19 try {
20 const pages = await notion.databases.query({
21 database_id: Deno.env.get("GLANCE_DEMOS_DB_ID"),
22 filter: {
23 // only get recently edited demos; we don't need to keep stale demos in the cache
29 });
30 console.log("pages to cache: ", pages.results.length);
31 // for each page in the demo database, save a blob
32 for (const page of pages.results) {
33 // remove properties that are not used in the demo and don't need to be saved to the blob
48 page.properties = scrubbed;
49
50 // get "Glancer content" database pages added to the demo page
51 const relatedPages = await getRelatedPagesFromDatabase(page.id);
52
53 // use the same blob key that /views/demo/demo.ts calls to build the page
7});
8
9export async function getRelatedPagesFromDatabase(pageId: string) {
10 try {
11 const response = await notion.databases.query({
12 database_id: Deno.env.get("GLANCE_CONTENT_DB_ID"),
13 filter: {
14 property: "Glancer demos", // the property in the Glancer content database that connects to the /demo
15 relation: {
16 contains: pageId, // the Glancer demos property holds the /demo page id to which it is related
25 });
26 // loop through the response and attach a "page_contents" object to each page
27 // page_content is the markdown in Notion in each "Glancer content" database page
28 for (const page of response.results) {
29 page.page_content = await getPageContents(page.id);
15# healthCheck.ts
16
17This cron runs through all of the databases listed in the Glancer Val's environment variables, queries them, and stores the results in blob storage. The output is JSON rendered as HTML, in an HTTP Val.
18
19We embed that HTTP Val into the Glancer app in Notion. This way, we can see the health of the integration between Notion and val.town from Notion, and it loads instantly, without having to query Notion.