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

nbbaier

counterTown

Public
Like
counterTown
Home
Code
12
.git
10
backend
4
frontend
5
shared
2
.vtignore
AGENTS.md
ARCHITECTURE.md
CLAUDE.md
README.md
REFACTORING_PLAN.md
biome.json
deno.json
Environment variables
Branches
1
Pull requests
Remixes
1
History
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
/
REFACTORING_PLAN.md
Code
/
REFACTORING_PLAN.md
Search
1/26/2026
Viewing readonly version of main branch: v129
View latest version
REFACTORING_PLAN.md

Counter Town Refactoring Plan

Modernize the project structure to align with Val Town best practices.

Target Structure

backend/
├── index.http.ts              # Hono app entry, exports app.fetch
├── database/
│   ├── migrations.ts          # CREATE TABLE IF NOT EXISTS statements
│   └── queries.ts             # trackPageView, getAnalytics, cleanupOldData
├── routes/
│   ├── tracking.ts            # /trackPageView, /pixel.gif, /trackingScript
│   ├── stream.ts              # SSE /stream endpoint
│   └── static.ts              # serveFile for frontend assets
└── lib/
    └── CounterTown.ts         # CounterTown class + config

frontend/
├── index.html                 # Extracted from counterTown.tsx
├── index.tsx                  # App entry (from counterTownClient.tsx)
├── deps.ts                    # Centralized React 18.2.0 version pins
├── components/
│   ├── Dashboard.tsx
│   ├── Navigation.tsx
│   ├── ConnectionStatus.tsx
│   ├── charts/
│   │   ├── ChartCard.tsx
│   │   └── ChartsSection.tsx
│   └── sections/
│       ├── OverallStatsSection.tsx
│       ├── SitesSection.tsx
│       ├── StatCard.tsx
│       ├── TopPathsSection.tsx
│       ├── TopReferrersSection.tsx
│       ├── VisitorGlobe.tsx
│       └── VisitorOriginsSection.tsx
└── contexts/
    └── AnalyticsContext.tsx

shared/
├── types.ts                   # PageView, SiteStats, AnalyticsData interfaces
└── utils.ts                   # formatSiteName (browser-safe, no Deno APIs)

README.md
AGENTS.md
ARCHITECTURE.md

Critical Changes

1. React 18.2.0 Upgrade (HIGH PRIORITY)

Current code uses React 17.0.2. Must upgrade and pin consistently.

frontend/deps.ts:

Create val
export { default as React } from "https://esm.sh/react@18.2.0"; export { createRoot } from "https://esm.sh/react-dom@18.2.0/client"; export { BrowserRouter, Routes, Route, useParams } from "https://esm.sh/react-router-dom@6.22.0?deps=react@18.2.0,react-dom@18.2.0";

All frontend .tsx files must start with:

Create val
/** @jsxImportSource https://esm.sh/react@18.2.0 */

2. Local Imports (HIGH PRIORITY)

Replace external esm.town imports with relative paths:

- import { initAnalytics } from 'https://esm.town/v/iamseeley/counterTownDb'; + import { initAnalytics } from './database/queries.ts';

3. Hono Backend Setup

backend/index.http.ts:

Create val
import { Hono } from "npm:hono@4"; import { serveFile, readFile } from "https://esm.town/v/std/utils@85-main/index.ts"; import tracking from "./routes/tracking.ts"; import stream from "./routes/stream.ts"; const app = new Hono(); // Re-throw errors for full stack traces app.onError((err, c) => { throw err; }); // API routes first app.route("/", tracking); app.route("/", stream); // Static assets app.get("/frontend/*", (c) => serveFile(c.req.path, import.meta.url)); app.get("/shared/*", (c) => serveFile(c.req.path, import.meta.url)); // SPA fallback (must be last) app.get("*", async (c) => { let html = await readFile("/frontend/index.html", import.meta.url); // Optional: inject initial data safely return c.html(html); }); export default app.fetch;

4. SSE Hardening

backend/routes/stream.ts improvements:

  • Add heartbeat every 15-30s: :\n\n or event: ping
  • Add header: X-Accel-Buffering: no
  • Increase poll interval from 5s to 10-30s
  • Keep abort handler for cleanup
Create val
const stream = new ReadableStream({ async start(controller) { const encoder = new TextEncoder(); // Send initial data const data = await getAnalytics(); controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`)); // Heartbeat every 20s const heartbeat = setInterval(() => { controller.enqueue(encoder.encode(`: heartbeat\n\n`)); }, 20000); // Data refresh every 15s (reduced from 5s) const refresh = setInterval(async () => { const data = await getAnalytics(); controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`)); }, 15000); req.signal.addEventListener('abort', () => { clearInterval(heartbeat); clearInterval(refresh); controller.close(); }); }, }); return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', 'Access-Control-Allow-Origin': '*', }, });

5. XSS-Safe Initial Data Injection

Use JSON script tag instead of inline JS:

<!-- In frontend/index.html --> <script type="application/json" id="initial-data"></script>
Create val
// In backend/index.http.ts const initialData = await getInitialData(); html = html.replace( '<script type="application/json" id="initial-data"></script>', `<script type="application/json" id="initial-data">${JSON.stringify(initialData)}</script>` );
Create val
// In frontend/index.tsx const initialData = JSON.parse( document.getElementById('initial-data')?.textContent || '{}' );

6. Shared Types (Browser-Safe)

shared/types.ts:

Create val
export interface PageView { site: string; path: string; timestamp: string; referrer: string; userAgent: string; browser: string; os: string; screenSize: string; language: string; country?: string; customData?: Record<string, unknown>; } export interface PathStats { views: number; referrers: Record<string, number>; browsers: Record<string, number>; os: Record<string, number>; screenSizes: Record<string, number>; languages: Record<string, number>; countries: Record<string, number>; lastUpdated: string; viewTimestamps: string[]; } export interface SiteStats { totalViews: number; paths: Record<string, PathStats>; } export type AnalyticsData = Record<string, SiteStats>;

7. Database Layer

backend/database/migrations.ts:

  • Keep CREATE TABLE IF NOT EXISTS (idempotent)
  • Run once per warm instance via module-level lazy init
  • Use sqlite.batch([...], "write") for atomic operations

backend/database/queries.ts:

  • Export typed query functions
  • Consistent error handling (don't mix return values and throws)
  • Use parameterized queries for all user input

Migration Steps

Phase 1: Directory Structure (30 min)

  1. Create backend/, frontend/, shared/ directories
  2. Move files to appropriate locations
  3. Update all import paths

Phase 2: Backend Modernization (1-2 hours)

  1. Create Hono app in backend/index.http.ts
  2. Split routes into separate modules
  3. Implement serveFile for static assets
  4. Add SPA fallback route

Phase 3: Frontend Modernization (1-2 hours)

  1. Create frontend/deps.ts with pinned React 18.2.0
  2. Update all components to use local imports
  3. Extract HTML to frontend/index.html
  4. Update to createRoot API

Phase 4: Hardening (30 min)

  1. Add SSE heartbeat and longer intervals
  2. Implement XSS-safe data injection
  3. Add proper TypeScript types throughout

Estimated Effort

ScopeTime
File reorganization only1-3 hours
Full modernization1-2 days

Risks & Mitigations

RiskSeverityMitigation
React version mismatchHighCentralize pins in deps.ts
SSE connection dropsMediumAdd heartbeat, longer intervals
XSS in data injectionMediumUse JSON script tag pattern
Breaking existing trackingHighTest with allowedOrigins before deploying

Testing Checklist

  • Dashboard loads at /
  • Site-specific view loads at /:sitename
  • SSE stream connects and receives updates
  • Tracking script loads from /trackingScript
  • Page views are recorded via /trackPageView
  • Pixel tracking works via /pixel.gif
  • Dark/light theme toggle works
  • All charts render correctly
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
© 2026 Val Town, Inc.