Architecture Overview
Stately is built around a few core principles that shape its architecture. Understanding these principles helps you work effectively with the framework.
Design Principles
1. Define Once, Derive Everything
The central idea in Stately is that your entity definitions are the single source of truth. From a Rust struct with derive macros, the framework generates:
- State management infrastructure (collections, CRUD operations)
- API endpoints with proper HTTP semantics
- OpenAPI documentation
- TypeScript types for the frontend
- Schema definitions for form rendering
This eliminates the drift that occurs when you maintain separate definitions in multiple places.
2. Vertical Plugin Architecture
Plugins in Stately are "vertical" - they span the entire stack from backend to frontend. A plugin like files provides:
- Rust crate (
stately-files) with API endpoints and utilities
- TypeScript package (
@statelyjs/files) with components and hooks
This ensures that backend capabilities have corresponding frontend support, and vice versa.
3. Schema-Driven UI
The frontend doesn't just consume API types - it uses parsed schema information to render appropriate form controls. A string field renders a text input. An enum renders a select. An object renders a nested form. This happens automatically based on the schema structure.
4. Composition Over Configuration
Stately favors composable building blocks over monolithic configuration. You can use:
- Individual hooks without pre-built views
- Views without full pages
- Schema parsing without UI
- Backend macros without frontend integration
Each layer is useful independently.
Core Architecture
Backend Layer
┌─────────────────────────────────────────────────┐
│ Your Application │
├─────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ #[stately::axum_api] │ │
│ │ Generates HTTP handlers, router, │ │
│ │ OpenAPI docs, request/response types │ │
│ └─────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────┐ │
│ │ #[stately::state] │ │
│ │ Generates StateEntry enum, Entity │ │
│ │ wrapper, collection types, state impl │ │
│ └─────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────┐ │
│ │ #[stately::entity] │ │
│ │ Implements HasName trait │ │
│ └─────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────┐ │
│ │ stately crate │ │
│ │ Core types: │ │
│ │ EntityId, Link, Collection, Singleton, │ │
│ │ traits, error types, etc. │ │
│ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────┘
Key components:
- EntityId: UUID v7-based identifiers that are time-sortable
- Link: Enum for entity references (by ID) or inline embedding
- Collection: HashMap-backed entity collection with CRUD operations
- Singleton: Single-instance container for settings-like entities
- StateEntity trait: Connects entities to their collection type
- Entity enum: Type-erased entity container
Frontend Layer
┌─────────────────────────────────────────────────┐
│ Your Application │
├─────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ Your Components: │ │
│ │ Use hooks, views, and pages │ │
│ └─────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────┐ │
│ │ @statelyjs/stately │ │
│ │ Runtime, hooks, views, pages, codegen │ │
│ │ Core plugin with entity management │ │
│ └─────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────┐ │
│ │ @statelyjs/ui │ │
│ │ Base components, layout, plugin system,│ │
│ │ component registry, theming │ │
│ └─────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────┐ │
│ │ @statelyjs/schema │ │
│ │ Schema type system, node parsing, │ │
│ │ validation, runtime creation │ │
│ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────┘
Key components:
- Schema Runtime: All of the types and interfaces powering the Stately UIs
- Codegen: Parses OpenAPI schemas into typed node trees
- UI Runtime: Combines schema with API client and plugins
- Component Registry: Maps node types to React components
- Hooks: React Query-based data fetching and mutations
- Views/Pages: Pre-built UI for common entity operations
OpenAPI Bridge
The OpenAPI specification serves as the contract between backend and frontend:
Backend OpenAPI Frontend
──────── ─────── ────────
#[state(openapi)] → openapi.json → stately generate
#[state(axum_api)] ↓
┌─────────────────┐
│ types.ts │ TypeScript types
│ schemas.ts │ Parsed schema nodes
└─────────────────┘
↓
┌─────────────────┐
│ stately │ Schema runtime
│ statelyUi │ UI runtime
└─────────────────┘
The codegen step ensures frontend types exactly match backend definitions.
Plugin Architecture
Plugins extend Stately at both layers:
Backend Plugin Pattern
// Plugin provides a router factory
pub fn router<S>(state: S) -> Router<S>
where
S: Clone + Send + Sync + 'static,
MyPluginState: FromRef<S>,
{
Router::new()
.route("/my-endpoint", get(handler))
.with_state(state)
}
// State extraction via FromRef
impl FromRef<AppState> for MyPluginState {
fn from_ref(state: &AppState) -> Self {
// Extract plugin-specific state
}
}
Frontend Plugin Pattern
// Schema plugin - extends type system
const mySchemaPlugin = createSchemaPlugin<MyPlugin>({ name: 'my-plugin' });
// UI plugin - registers components and API
export const myUiPlugin = createUiPlugin<MyUiPlugin>({
name: 'myPlugin',
operations: MY_OPERATIONS,
setup: (ctx, options) => {
// Register custom components
ctx.registerComponent('myNodeType', 'edit', MyEditComponent);
return {};
};
}
Data Flow
Creating an Entity
1. User fills in form
↓
2. useCreateEntity hook
↓
3. API client sends PUT /api/entity
↓
4. Axum handler deserializes request
↓
5. State.create_entity() adds to collection
↓
6. Response with new EntityId
↓
7. React Query invalidates cache
↓
8. UI re-renders with new entity
1. Schema codegen and runtime parses OpenAPI spec
↓
2. Node trees created (object → properties → other node types)
↓
3. FieldEdit component routes node
↓
4. Registry lookup by nodeType
↓
5. Specific component rendered (string → Input, enum → Select, etc.)
↓
6. onChange propagates up to form state, gated at complex layers
Key Types
Backend
Frontend