## Why These Choices Matter

At Altertable, we're building an AI-native analytics platform that needs to handle massive data volumes while staying fast and developer-friendly. Our stack isn't just about individual technologies—it's about how they work together to create something greater than the sum of their parts.

Here's the story of our architecture, following the journey from raw data ingestion to beautiful, responsive dashboards.

## The Big Picture: How It All Fits Together

Before diving into specifics, here's how our stack works as a unified system:

1. **Data flows in** through our Rust-powered ingestion pipeline into Apache Iceberg
2. **Query engines** (Trino, DuckDB) provide fast access to lakehouse data
3. **Rails + GraphQL** serves as our API layer and business logic hub
4. **React + Urql** creates responsive, type-safe user interfaces
5. **AI agents** access everything through the same APIs via our internal MCP server
6. **Background jobs** (Faktory) orchestrate AI agents and async workflows

Each piece serves a specific purpose, but they're designed to work seamlessly together.

## 1. Data Ingestion with Rust + Apache Iceberg

Our data journey starts with [**Rust**](https://rust-lang.org/). We chose Rust for ingestion because when you're processing terabytes of data, performance and memory safety aren't luxuries. They're requirements.

We write directly to [**Apache Iceberg**](https://iceberg.apache.org/), giving us:

- **Transactional writes** (no more corrupt data from failed jobs)
- **Schema evolution** (add columns without breaking existing queries)
- **Time travel** (query data as it existed at any point in time)

We use [**axum**](https://github.com/tokio-rs/axum) for our ingestion APIs, which gives us async performance with minimal overhead. The Rust layer focuses on one thing: getting data into our lakehouse as quickly and reliably as possible.

## 2. Query Layer - Trino for Federation, DuckDB for Speed

Here's where our query strategy gets interesting. We use different engines for different needs:

### Trino for Federated Queries

[**Trino**](https://trino.io/) lets us join our lakehouse data with customer databases in real-time—no ETL required:

```sql
-- Join internal events with customer's PostgreSQL users
SELECT
  e.user_id,
  u.email,
  COUNT(*) as event_count
FROM iceberg.events e
JOIN postgres.customer_users u ON e.user_id = u.id
GROUP BY e.user_id, u.email;
```

We extend Trino with custom Java plugins using its [SPI system](https://trino.io/docs/current/develop/spi-overview.html). While Java might not be trendy in 2025, it's proven itself for data infrastructure. Elasticsearch, Kafka, and Solr all use it, and Trino's plugin architecture makes extending it straightforward.

### DuckDB for Interactive Analytics

For real-time dashboards that need sub-second response times, we're integrating [**DuckDB**](https://duckdb.org/). Its columnar storage and aggressive optimizations make it perfect for the kind of time-series analytics our users expect.

## 3. API Layer with Rails + GraphQL

[**Ruby on Rails**](https://rubyonrails.org/) serves as our API backbone. After 15+ years with Rails, we know how to move fast without sacrificing quality. [RSpec](https://rspec.info/) keeps our test coverage high, and Rails' conventions let us focus on business logic instead of boilerplate.

We expose everything through [**GraphQL**](https://graphql.org/), which gives us a clean, consistent API surface:

```ruby
# app/graphql/types/chart_type.rb
module Types
  class ChartType < Types::BaseObject
    field :id, ID, null: false
    field :title, String, null: false
    field :data_points, [Types::DataPointType], null: false

    def data_points
      # Use DataLoader to prevent N+1 queries
      dataloader.with(AssociationLoader, Chart, :data_points).load(object)
    end
  end
end
```

GraphQL's type system and introspection capabilities are crucial for the next layer: our frontend.

## 4. Frontend with React + TypeScript + Urql

Our frontend is [**React**](https://react.dev/) + [**TypeScript**](https://www.typescriptlang.org/), styled with [**Tailwind CSS**](https://tailwindcss.com/), and powered by [**Urql**](https://nearform.com/open-source/urql/) for GraphQL.

The key to our frontend architecture is **fragment composition**. Each component defines exactly what data it needs:

```tsx
// components/ChartWidget.tsx
import { DataPoint } from './DataPoint';

export const ChartWidget = ({ chart }: Props) => {
  return (
    <div className="p-4 border rounded-lg bg-white">
      <h3 className="text-lg font-semibold">{chart.title}</h3>
      <div className="mt-4">
        {chart.data_points.map(point => (
          <DataPoint key={point.id} point={point} />
        ))}
      </div>
    </div>
  );
};

ChartWidget.fragments = {
  chart: gql`
    fragment ChartWidget_chart on Chart {
      id
      title
      data_points {
        ...DataPoint_point
      }
    }
    ${DataPoint.fragments.point}
  `,
};
```

Parent components compose these fragments into complete queries. **GraphQL Codegen** ensures end-to-end type safety. If the backend schema changes, TypeScript catches the mismatch at compile time.

This approach keeps our frontend modular and ensures we only fetch the data we actually render.

## 5. AI-Native Architecture with MCP

Here's what makes Altertable truly AI-native: our internal [**MCP**](https://modelcontextprotocol.io/docs/2026-07-28/getting-started/intro) server mirrors our GraphQL API, giving AI agents the same access to data and actions that our frontend has.

No special endpoints. No duplicate logic. Agents can explore data, propose insights, and trigger actions through the same authenticated, structured interface that powers our web app.

This shared contract means humans and agents collaborate through identical interfaces. It's a foundational pattern for AI-native platforms.

Building with AI at this level fundamentally changes how we approach software development. <Link to="/blog/2025-06-17-relearning-the-craft">Read about how AI-first development transforms the craft</Link>.

## 6. Background Orchestration with Faktory

[**Faktory**](https://contribsys.com/faktory/) serves as our async job orchestration system, handling everything from AI agent workflows to background analytics tasks. We chose Faktory because it's language-agnostic. We can have Ruby workers handling business logic and Rust workers running AI agents, all in the same queue.

```ruby
# Ruby worker for orchestrating AI workflows
class AgentWorkflowWorker
  include Faktory::Job

  def perform(workflow_id, context)
    workflow = AgentWorkflow.find(workflow_id)
    workflow.execute_with_context(context)
  end
end
```

This separation keeps our real-time APIs responsive while AI agents and heavy background processing happen asynchronously. It's the glue that lets our AI-native features work seamlessly without blocking user interactions.

## Why This Stack Works

Our technology choices aren't arbitrary: they're the result of careful consideration of what it takes to build a truly AI-native analytics platform:

- **Speed of iteration**: Rails lets us ship features fast
- **Performance at scale**: Rust handles our heaviest compute workloads
- **Query flexibility**: Trino federates across data sources, DuckDB delivers interactive speed
- **Developer experience**: TypeScript + GraphQL Codegen catches errors at compile time
- **AI-first design**: Shared APIs mean agents and humans use the same interfaces
- **Reliability**: Each layer has a focused responsibility with clear boundaries

The magic happens in how these layers work together, from ingestion to insight, with AI as a first-class citizen throughout.

If building the future of AI-native analytics sounds like your kind of challenge, <Link to="/careers">we're hiring</Link>.