
Public
Like1
vtrr
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.
Viewing readonly version of main branch: v93View latest version
A fullstack community message board built with the vtrr framework — demonstrating nested routes, server loaders/actions, SQLite persistence, search, and client-side navigation.
- Topics — browse, create, and view discussion threads
- Messages — post messages in topics with optimistic form resets
- Search — full-text search across topics and messages with deep-link to individual messages
- SSR + Hydration — pages render on the server, then hydrate for instant client-side transitions
- SQLite persistence — data lives in Val Town's built-in SQLite, seeded with sample data on first run
Rendering mermaid diagram...
🟦 Routes 🟩 Loaders (read data) 🟪 Actions (write data)
demos/message-board/
├── index.tsx # Entry point — route definitions + createApp()
├── types.ts # Shared types (Topic, Message, SearchResults)
│
├── database/
│ ├── migrations.ts # Schema creation + seed data
│ └── queries.ts # All SQL queries (CRUD + search)
│
├── components/
│ ├── LoadingSpinner.tsx # Reusable loading indicator
│ ├── MessageForm.tsx # Post a new message (uses useFetcher)
│ ├── MessageList.tsx # Render messages with highlight support
│ └── SearchForm.tsx # Search input with navigation
│
└── routes/
├── App.tsx # Root layout — <Outlet>, loading state, footer
├── Home.tsx # Topic list + "Create Topic" form
├── Home.loader.ts # Loads all topics from DB
├── Topic.tsx # Topic detail view + message list + post form
├── Topic.loader.ts # Loads topic + its messages
├── Topic.action.ts # Creates a new message in a topic
├── TopicMessage.loader.ts # Same as Topic.loader (with message highlight)
├── Topics.action.ts # Creates a new topic → redirects to it
├── Search.tsx # Search results (topics + messages)
└── Search.loader.ts # Runs search query against DB
Rendering mermaid diagram...
Rendering mermaid diagram...
The database is auto-initialized on cold start via initializeDatabase() and seeded with 3 sample topics and 5 messages if empty.
This demo showcases the key patterns of the vtrr framework:
export const routes = defineRoutes([
{
path: "/",
Component: App,
action: "./routes/Topics.action.ts", // server-only, never shipped to client
children: [
{ index: true, Component: Home, loader: "./routes/Home.loader.ts" },
{ path: "topics/:topicId", Component: Topic, loader: "...", action: "..." },
],
},
]);
Loaders and actions are specified as relative file paths — the server dynamically imports them, while the client only gets lightweight stubs.
// Home.loader.ts
export async function loader() {
return { topics: await getAllTopics() };
}
// Topics.action.ts
export async function action({ request }) {
const formData = await request.formData();
const topic = await createTopic(formData.get("title"));
return redirect(`/topics/${topic.id}`);
}
export default createApp({
routes,
head: `
<title>Community Message Board</title>
<script src="https://cdn.twind.style" crossorigin></script>
`,
});
Uses Twind (Tailwind-in-JS) loaded from CDN — zero build step, just write Tailwind classes directly in JSX.