A live, forkable full-stack Val Town starter — disguised as a working app. Sign in, add a reminder to the shared list, email its inbox and watch AI triage it as spam or not. Then fork it and swap in your own idea. Sized for a solo project or a family's shared tools (<10 people), not an enterprise platform.
Try it live, then fork: open this App, click Fork this App on the home page. vtx new (see
below) is for the CLI-first workflow inside this monorepo; forking on val.town works standalone too.
All demo data (the shared Reminders list, the AI email inbox) is public and is permanently deleted
24 hours after creation — see backend/db.ts's pruneOld* functions and cron.ts. Don't put
anything sensitive in it.
The UI is built on Astryx, Meta's open-source React + StyleX design
system. See _specs/astryx-template.md for the migration
history, version pins, and the Deno SSR workarounds.
_template is excluded from main-branch deployment because its directory starts with _.
Since every Val Town HTTP val is browsable source, the deployed copy of this App is readable at
https://www.val.town/x/<owner>/<val-name>/code/<path> — e.g.
https://www.val.town/x/pikachu/Val-Town-Quickstart/code/backend/auth.ts. In-app "view source" and
"Fork" links use this same pattern (see VAL_TOWN_USERNAME below).
| Page | What you'll see |
|---|---|
/ (Home) | Demo-first landing: fork CTA, live-demo cards, then the building blocks |
/reminders | Flagship demo. Shared list any signed-in user can add to / check off / delete |
/email | AI demo. Your App's real inbound address; a free GPT-mini model triages spam and summarises the rest |
/anatomy | Block diagram of Val Town's infra layer, this App's routing, and its core stack |
/http | HTTP routes and live request demos |
/sqlite | Visit log and admin controls |
/cron | Scheduled task history — this is what wipes demo data every 24h |
/blob | Sign-in-gated Blob read/write/list demo, keys namespaced per user |
/ui | Astryx theming explainer + component showcase |
On val.town: open the deployed App → Fork this App on the home page → rename → done.
Inside this monorepo, from any directory after sourcing the uApps shell plugin:
vtx new my-app cd vals/my-app vtx status vtx push --dry-run vtx push vtx url
vtx new copies the template, replaces the application name and account-scoped Blob prefix, writes
the new val.yml name, and links the new remote val. Existing destinations are refused.
The HTTP entry wraps the app in Val Town OAuth, routes /api/* through typed auth boundaries, and
renders the selected SSR page for every other path.
API routes:
| Method | Path | Access |
|---|---|---|
GET | /api/health | Public |
GET | /api/me | Public session context |
GET | /api/visits | Public |
POST | /api/prune-visits | Admin |
GET | /api/cron-runs | Public |
GET | /api/reminders | Public read |
POST | /api/reminders | Signed-in |
POST | /api/reminders/done | Signed-in |
POST | /api/reminders/delete | Signed-in |
GET | /api/emails | Public read |
GET | /api/blob-keys | Signed-in, per-user |
POST | /api/blob-write | Signed-in, per-user |
GET | /api/blob-read | Signed-in, per-user |
GET | /api/admin/users | Super-admin |
POST | /api/admin/set-admin | Super-admin |
"Signed-in" means any authenticated Val Town user — see backend/auth.ts's getAuthedUc — not just
admins. This App is deliberately open: it's a shared demo, not a private tool.
Set these in the created val's Val Town environment UI, never in Git or GitHub Actions:
| Variable | Kind | Purpose |
|---|---|---|
APP_NAME | Optional | Display name override |
VAL_TOWN_USERNAME | Optional | Your Val Town username — powers "view source"/Fork links and the inbound email address |
DAILY_HOUR_UTC | Optional | Daily task hour, 0–23 UTC |
AUTH_SUPER_ADMINS | Optional | Comma-separated Val Town usernames |
No API key is needed for the AI email triage — backend/ai.ts uses Val Town's free-tier
ValTownOpenAI proxy.
The repository-level VAL_TOWN_API_KEY secret is only for CI deployment. See the root
README.md.
val.yml.files declares the intended remote types:
files: index.http.ts: http cron.ts: interval email.ts: email
Run vtx types after linking. Use vtx apply-types only when deliberately repairing remote trigger
metadata.
index.http.ts— HTTP composition root: per-route SSR page serving,GET /theme(zero-JS mode switch), and the API route table.backend/auth.ts— OAuth context and role enforcement (getUc/getAuthedUc/getAdminUc/getSuperAdminUc). Auth types useCxt(e.g.UserCxt,RouteCxt) — the more common short form for "context" — notCtx.backend/handlers.ts— focused API handlers, including Reminders CRUD and the email inbox list.backend/db.ts— retryable migration, visits, Cron runs, users, Reminders, and the inbound email log — the latter two carry a 24h retention window baked into every query.backend/ai.ts— spam/summary triage via Val Town's freeValTownOpenAIproxy, using Structured Outputs so the response is guaranteed valid JSON.backend/storage.ts— val-namespaced account Blob access, with per-user key sub-prefixing.frontend/app.tsx— React SSR composition root; dispatches one page per request via aPAGESmap (no client-side router — navigation is real<a href>).frontend/astryx/— pinned Astryx component barrel (components.ts), the RAF/CAF SSR shim (ssr-shims.ts), and the side-nav/theme-form shell (shell.tsx).frontend/pages/— one file per route, composed from Astryx components.home.tsxleads with the live demos and a Fork CTA;page-anatomy.tsxis the code deep-dive for visitors who want it.frontend/pages/kit.tsx— shared page pieces:SourceLink/SourceLinkList("view source") andForkButton— both degrade gracefully whenVAL_TOWN_USERNAMEis unset.frontend/client.ts— non-hydrated demo API wiring only (no router, no theme JS — both are server-driven). Dynamic table rows (Reminders, the email inbox) are cloned from a hidden, real Astryx-rendered<template>row rather than built from raw DOM APIs — this keeps every row's Button/Badge chrome pixel-identical to the rest of the App with zero React hydration.
This template does not hydrate React: client.ts is inlined via dangerouslySetInnerHTML and only
does fetch + textContent/template-clone DOM updates by element id. Components that need
hydration (Astryx's CodeBlock copy button, TextInput/Field) are either disabled or avoided —
see _specs/astryx-template.md for the full list and reasoning.
| Path | Purpose |
|---|---|
index.http.ts | HTTP, page routing, GET /theme, API table |
backend/auth.ts | Val Town OAuth and roles |
backend/db.ts | Val-scoped SQLite repositories |
backend/ai.ts | Free-tier LLM spam/summary email triage |
backend/storage.ts | Account-scoped Blob adapter, per-user namespacing |
frontend/app.tsx | React SSR root, per-route page dispatch |
frontend/astryx/ | Pinned Astryx imports, SSR shim, nav shell |
frontend/pages/home.tsx | Demo-first landing: Fork CTA + live-demo cards |
frontend/pages/page-reminders.tsx | Flagship shared Reminders demo |
frontend/pages/page-anatomy.tsx | Infra diagram, routing flow, core stack |
frontend/pages/ | Interactive reference pages |
frontend/client.ts | Non-hydrated demo API wiring |
cron.ts | Scheduled tasks, incl. the 24h demo-data wipe |
email.ts | Inbound trigger (AI triage) + outbound helpers |
val.yml | Identity, triggers, and environment contract |
tests/ | Pure-function unit tests (slug/mask/triage parsing) |
../../_specs/astryx-template.md | Approved Astryx migration plan |