Data Dashboard
This example builds a full-stack customer analytics dashboard. It demonstrates the complete data layer: reading CSV files, cleaning data, computing aggregations, enriching with AI, and rendering an interactive UI.
The Full Application
shared {
type Customer {
id: Int
name: String
email: String
spend: Float
country: String
active: Bool
}
type Sentiment { Positive, Negative, Neutral }
}
data {
source raw_customers: Table<Customer> = read("customers.csv")
pipeline customers = raw_customers
|> drop_nil(.email)
|> fill_nil(.spend, 0.0)
|> derive(
.name = .name |> trim(),
.email = .email |> lower()
)
|> where(.spend > 0)
|> sort_by(.spend, desc: true)
pipeline by_country = customers
|> group_by(.country)
|> agg(
count: count(),
total_spend: sum(.spend),
avg_spend: mean(.spend)
)
|> sort_by(.total_spend, desc: true)
validate Customer {
.email |> contains("@"),
.name |> len() > 0,
.spend >= 0
}
refresh raw_customers every 10.minutes
}
server {
ai "fast" {
provider: "anthropic"
model: "claude-haiku"
api_key: env("ANTHROPIC_API_KEY")
}
fn get_customers() { customers }
fn get_summary() { by_country }
fn get_top_customers(n: Int) {
customers |> limit(n)
}
fn search_customers(query: String) {
customers
|> where(.name |> lower() |> contains(query |> lower()))
}
fn get_insights() {
customers
|> limit(100)
|> derive(
.segment = fast.classify(
"Customer spend={.spend}, country={.country}. Classify: budget/mid/premium",
["budget", "mid", "premium"]
)
)
}
route GET "/api/customers" => get_customers
route GET "/api/summary" => get_summary
}
browser {
state customers: Table<Customer> = Table([])
state summary = []
state search = ""
state loading = true
computed filtered = customers
|> where(.name |> lower() |> contains(search |> lower()))
computed total_spend = customers
|> agg(total: sum(.spend))
effect {
customers = server.get_customers()
summary = server.get_summary()
loading = false
}
component SearchBar {
<input
type="text"
bind:value={search}
placeholder="Search customers..."
/>
}
component StatsCard(label, value) {
<div class="stat-card">
<div class="stat-label">{label}</div>
<div class="stat-value">{value}</div>
</div>
}
component App {
<div class="dashboard">
<h1>"Customer Dashboard"</h1>
<div class="stats">
<StatsCard label="Total Customers" value={customers.rows} />
<StatsCard label="Countries" value={len(summary)} />
</div>
<SearchBar />
<h2>"Customers"</h2>
<table>
<thead>
<tr>
<th>"Name"</th>
<th>"Email"</th>
<th>"Country"</th>
<th>"Spend"</th>
</tr>
</thead>
<tbody>
{for customer in filtered {
<tr>
<td>{customer.name}</td>
<td>{customer.email}</td>
<td>{customer.country}</td>
<td>{"${customer.spend}"}</td>
</tr>
}}
</tbody>
</table>
</div>
}
}What This Demonstrates
Data Block
The data {} block centralizes all data definitions:
source raw_customersloads the CSV file, cached and lazily initializedpipeline customerscleans the raw data: drops nil emails, fills nil spend, trims names, lowercases emails, filters out zero-spend rowspipeline by_countryaggregates the cleaned data by country with count, total, and averagevalidate Customerdeclares validation rules for the Customer typerefreshreloads the CSV every 10 minutes
AI Integration
The get_insights() function uses a named AI provider (fast) to classify customer segments using the classify() method inside a derive() pipeline step.
Server Functions
Server functions reference pipelines by name (customers, by_country). The search_customers function shows how to apply dynamic filters using column expressions.
Client Reactivity
The client uses computed values that automatically update when the search filter changes. The filtered computed reruns the where() query whenever search changes.
Running It
- Create a
customers.csvfile with columns:id,name,email,spend,country,active - Set the
ANTHROPIC_API_KEYenvironment variable (only needed for the insights endpoint) - Run the application:
tova run app.tovaKey Patterns
Data block for centralization. All data logic lives in data {}. Server functions just reference pipeline names.
Column expressions in pipelines. .name |> trim() and .email |> lower() compile to row-level lambdas automatically.
AI in derive. fast.classify(...) inside derive() enriches each row with AI-generated classifications.
Reactive filtering. The computed filtered value re-evaluates automatically when search changes, giving instant client-side search without server round-trips.
Type safety across blocks. The Customer type in shared {} is used in data {} for schema validation, in server {} for return types, and in browser {} for state typing.