Skip to content

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 ​

tova
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
    |> dropNil(.email)
    |> fillNil(.spend, 0.0)
    |> derive(
      .name = .name |> trim(),
      .email = .email |> lower()
    )
    |> where(.spend > 0)
    |> sortBy(.spend, desc: true)

  pipeline by_country = customers
    |> groupBy(.country)
    |> agg(
      count: count(),
      total_spend: sum(.spend),
      avg_spend: mean(.spend)
    )
    |> sortBy(.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_customers loads the CSV file, cached and lazily initialized
  • pipeline customers cleans the raw data: drops nil emails, fills nil spend, trims names, lowercases emails, filters out zero-spend rows
  • pipeline by_country aggregates the cleaned data by country with count, total, and average
  • validate Customer declares validation rules for the Customer type
  • refresh reloads 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 ​

  1. Create a customers.csv file with columns: id, name, email, spend, country, active
  2. Set the ANTHROPIC_API_KEY environment variable (only needed for the insights endpoint)
  3. Run the application:
bash
tova run app.tova

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

Released under the MIT License.