Public
An elegant template for an elegant single-person full stack app.
elegantfull-stackminimalquickstartreacttemplateasteryx
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.

Val Town Quickstart

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).


What it demos

PageWhat you'll see
/ (Home)Demo-first landing: fork CTA, live-demo cards, then the building blocks
/remindersFlagship demo. Shared list any signed-in user can add to / check off / delete
/emailAI demo. Your App's real inbound address; a free GPT-mini model triages spam and summarises the rest
/anatomyBlock diagram of Val Town's infra layer, this App's routing, and its core stack
/httpHTTP routes and live request demos
/sqliteVisit log and admin controls
/cronScheduled task history — this is what wipes demo data every 24h
/blobSign-in-gated Blob read/write/list demo, keys namespaced per user
/uiAstryx theming explainer + component showcase

Create Your Own Fork

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.

Application Shape

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:

MethodPathAccess
GET/api/healthPublic
GET/api/mePublic session context
GET/api/visitsPublic
POST/api/prune-visitsAdmin
GET/api/cron-runsPublic
GET/api/remindersPublic read
POST/api/remindersSigned-in
POST/api/reminders/doneSigned-in
POST/api/reminders/deleteSigned-in
GET/api/emailsPublic read
GET/api/blob-keysSigned-in, per-user
POST/api/blob-writeSigned-in, per-user
GET/api/blob-readSigned-in, per-user
GET/api/admin/usersSuper-admin
POST/api/admin/set-adminSuper-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.

Runtime Configuration

Set these in the created val's Val Town environment UI, never in Git or GitHub Actions:

VariableKindPurpose
APP_NAMEOptionalDisplay name override
VAL_TOWN_USERNAMEOptionalYour Val Town username — powers "view source"/Fork links and the inbound email address
DAILY_HOUR_UTCOptionalDaily task hour, 023 UTC
AUTH_SUPER_ADMINSOptionalComma-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.

Trigger Contract

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.

Architecture

  • 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 use Cxt (e.g. UserCxt, RouteCxt) — the more common short form for "context" — not Ctx.
  • 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 free ValTownOpenAI proxy, 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 a PAGES map (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.tsx leads with the live demos and a Fork CTA; page-anatomy.tsx is the code deep-dive for visitors who want it.
  • frontend/pages/kit.tsx — shared page pieces: SourceLink/SourceLinkList ("view source") and ForkButton — both degrade gracefully when VAL_TOWN_USERNAME is 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.

Key Files

PathPurpose
index.http.tsHTTP, page routing, GET /theme, API table
backend/auth.tsVal Town OAuth and roles
backend/db.tsVal-scoped SQLite repositories
backend/ai.tsFree-tier LLM spam/summary email triage
backend/storage.tsAccount-scoped Blob adapter, per-user namespacing
frontend/app.tsxReact SSR root, per-route page dispatch
frontend/astryx/Pinned Astryx imports, SSR shim, nav shell
frontend/pages/home.tsxDemo-first landing: Fork CTA + live-demo cards
frontend/pages/page-reminders.tsxFlagship shared Reminders demo
frontend/pages/page-anatomy.tsxInfra diagram, routing flow, core stack
frontend/pages/Interactive reference pages
frontend/client.tsNon-hydrated demo API wiring
cron.tsScheduled tasks, incl. the 24h demo-data wipe
email.tsInbound trigger (AI triage) + outbound helpers
val.ymlIdentity, triggers, and environment contract
tests/Pure-function unit tests (slug/mask/triage parsing)
../../_specs/astryx-template.mdApproved Astryx migration plan