A Unified Approach

Stately is a framework for building Rust backends and TypeScript/React frontends, with an emphasis on configuration-driven applications. It provides a unified approach to state management, API generation, and UI rendering that spans your entire stack.

The Problem

Building full-stack applications often involves significant boilerplate:

  • Defining data models in multiple places (backend structs, API schemas, frontend types)
  • Writing CRUD endpoints by hand for every entity, often inconsistent and ad-hoc
  • Building forms that match your data structures
  • Keeping everything in sync as your application evolves
  • Designing around denotational semantics for scalability and composability

Changes to your data model cascade through layers of code, each requiring manual updates. Poor decisions up front can escalate problems later on as your application's capabilities grow.

NOTE

In more cases than I can count, I needed to build an application that a user could install themself. The common method was to provide a configuration file argument that users could supply a path to a local file which would change the way the application behaved. Over time I realized providing users with a simple UI they could run and access locally was a much better experience. What I needed was a framework I could leverage to cut down on the boilerplate inherent to these types of applications. This is just one use case where state and configuration align. In reality, there are countless possible.

The Stately Approach

Stately takes a different approach: define your entities, derive everything else.

On the backend, you define your entities as Rust structs with derive macros, and compose your state:

#[stately::entity]
pub struct Pipeline {
    pub name: String,
    pub description: Option<String>,
    pub source: Link<Source>,
    pub enabled: bool,
}

#[stately::state]
pub struct AppState {
    pipelines: Pipeline,
}

From this definition, Stately generates:

  • Type-safe state management supporting both collections and singletons
  • Complete CRUD API endpoints through the unified state structure
  • OpenAPI documentation
  • Serialization/Deserialization and validation

On the frontend, you consume the generated OpenAPI spec:

type AppSchemas = Schemas<
  DefineConfig<components, paths, operations, ParsedSchemas>,
  readonly [/** Plugin Schema Factories */]
>;

const runtime = statelyUi<AppSchemas, readonly [/** Plugin UI Factories */]>({
  // Provide openapi-fetch client
  client: createClient({ baseUrl: '/api' }),
  // Provide stately schema 
  schema: stately<AppSchemas>(openapiSpec, PARSED_SCHEMAS),
  // Options and configuration
  options: { /** Configure Stately options */ },
  core: { /** Configure Stately's core plugin */ },
});

This gives you:

  • Fully typed API client, accessible anywhere in the application
  • Schema-driven form rendering
  • React hooks for data fetching and mutations
  • Automatic page layouts and app navigation
  • Pre-built views and pages
  • Access to pre-defined or app-defined plugins

Key Concepts

Entities

Entities are the core data structures in your application. They have friendly names, can be referenced by ID, and are managed in collections.

#[stately::entity]
pub struct Source {
    pub name: String,
    pub endpoint: String,
}

State

State is a container for your entity collections. The #[stately::state] macro generates the infrastructure for managing entities:

#[stately::state(openapi)]
pub struct AppState {
    pipelines: Pipeline,
    sources: Source,
    #[singleton]
    settings: Settings,
}

Links represent relationships between entities. They can be references (by ID) or inline (embedded):

pub struct Pipeline {
    pub source: Link<Source>,  // Reference or inline
}

API

Stately currently supports axum (support for different libraries planned) and provides a simple way to automatically construct everything needed to manage the Stately state over api endpoints:

#[axum_api(AppState, openapi(components = [Settings, Source, Pipeline]))]
pub struct ApiState {
    // Your additional dependencies
    pub db_pool: PgPool,
    pub config: SomeAppConfig,
}

Plugins

Plugins extend Stately with additional capabilities. Each plugin provides coordinated backend and frontend packages.

Currently Stately provides two plugins out of the box.

Files

File upload, automatic versioning using time-sortable unique ids, and file management.

Implement axum::extract::FromRef from FileState to your defined and annotated api state, and simply include the file plugin's routes in your root router:

impl axum::extract::FromRef<ApiState> for stately_files::FileState {
    fn from_ref(_: &ApiState) -> Self {
        let app_dirs = crate::AppDirs::get();
        let base = 
            stately_files::settings::Dirs::new(app_dirs.cache.clone(), app_dirs.data.clone());
        FileState { base: Some(base) }
    }
}

fn api(state: &ApiState) -> Router<ApiState> {
    axum::Router::new()
        .nest("/entity", ApiState::router(state.clone()))
        .nest("/files", stately_files::router::router::<ApiState>(state.clone()))
        .nest("/other", some_other_endpoints::router())
        .layer(CorsLayer::new().allow_headers(Any).allow_methods(Any).allow_origin(Any))
}

Arrow

Data connectivity with SQL query execution, powered by the battle-tested Datafusion.

Implement axum::extract::FromRef from QueryState to your defined and annotated api state

impl axum::extract::FromRef<ApiState> for stately_arrow::QueryState<DefaultQuerySessionContext> {
    // Extract query context from api state
    fn from_ref(state: &ApiState) -> Self { Self::new(state.query_context.clone()) }
}

// Include the generated api endpoints in your app router
fn api(state: &ApiState) -> Router<ApiState> {
    axum::Router::new()
        .nest("/entity", ApiState::router(state.clone()))
        .nest("/arrow", stately_arrow::api::router(state.clone()))
        .layer(CorsLayer::new().allow_headers(Any).allow_methods(Any).allow_origin(Any))
}

When to Use Stately

Stately is a good fit when:

  • You're building a configuration-heavy application
  • Entity-driven or concept-driven design guides your app development
  • Denotational design provides entities and their relationships up front
  • You want type safety across your full stack
  • You need CRUD operations across different entity types
  • You want schema-driven UIs that stay in sync with your backend
  • You plan to extend functionality with plugins
  • You want a consistent framework that AI can understand and work with. learn more

Stately may not be the best choice when:

  • Your API is primarily highly custom designs that don't follow CRUD patterns
  • Your frontend has complex state management needs that don't align with your backend
  • You're building ad-hoc applications where boilerplate or cross-API consistency is not needed

Architecture Overview

Your Application

┌────────────────────────────────────────────────────────────────────┐
│  Frontend (React)                                                  │
└────────────────────────────────────────────────────────────────────┘

    ⬆ Automatic Form Generation
   ╭────────────────────╮   ╭───────────────╮   ╭─────────╮          
   │ @statelyjs/stately │ + │ @statelyjs/ui │ + │ plugins │
   ╰────────────────────╯   ╰───────────────╯   ╰─────────╯
    ⬇ OpenAPI Driven Schema and API 

┌────────────────────────────────────────────────────────────────────┐
│  Backend (Rust)                                                    │
└────────────────────────────────────────────────────────────────────┘

    ⬆ End to End Type-safe State Management
   ╭─────────────╮   ╭─────────────╮
   │   stately   │ + │   plugins   │
   ╰─────────────╯   ╰─────────────╯
    ⬇ CRUD Updates and Events                                                  

┌────────────────────────────────────────────────────────────────────┐               
│  Your State + Entities                                             │
└────────────────────────────────────────────────────────────────────┘

    ⬆ Merge State Changes
    ⬇ Persist State Updates

┌────────────────────────────────────────────────────────────────────┐               
│  Backing Store                                                     │
└────────────────────────────────────────────────────────────────────┘

Plugins are vertical slices of functionality, spanning frontend to backend:

╭─────────╮   ╭──────────────────╮ ╭──────────────────╮     ╭─────────────────╮
│ stately │   │ @statelyjs/files │ │ @statelyjs/arrow │     │ plugin-frontend │
│    ↑    │   │        ↑         │ │        ↑         │     │        ↑        │
│         │ + │                  │ │                  │ ... │                 │
│    ↓    │   │        ↓         │ │        ↓         │     │        ↓        │
│ stately │   │  stately-files   │ │  stately-arrow   │     │  plugin-backend │
╰─────────╯   ╰──────────────────╯ ╰──────────────────╯     ╰─────────────────╯