Generated Bindings
Type-safe frontend bindings generated from your Rust code and Forge macros.
The Code
Run codegen after adding or modifying functions:
forge generate
forge generate auto-detects the frontend target from frontend/ and can also be forced with --target sveltekit or --target dioxus.
SvelteKit Output
Your Rust models, input DTOs, and function signatures become TypeScript:
// types.ts - generated from #[forge::model] and input DTOs
export interface User {
id: string;
email: string;
created_at: string;
}
// api.ts - generated from #[forge::query] and #[forge::mutation]
export const getUser = (args: { id: string }): Promise<User> =>
getForgeClient().call("get_user", args);
export const createUser = (args: {
name: string;
email: string;
}): Promise<User> => getForgeClient().call("create_user", args);
// Reactive subscription (Svelte store)
export const getUserStore$ = (args: { id: string }) =>
createSubscriptionStore<{ id: string }, User>("get_user", args);
What Happens
The forge generate command:
- Scans your
src/directory for models, enums, and functions - Extracts type information from Rust macros
- Generates TypeScript interfaces with correct nullability
- Creates API bindings that call your functions with type safety
- Produces subscription factories for queries with real-time support
Output goes to frontend/src/lib/forge/ by default for SvelteKit projects.
Dioxus Output
For Dioxus projects, Forge generates Rust bindings under frontend/src/forge/:
// api.rs - generated from #[forge::query] and #[forge::mutation]
pub struct GetUserParams {
pub id: String,
}
impl GetUserParams {
pub fn new(id: impl Into<String>) -> Self {
Self { id: id.into() }
}
}
pub async fn get_user(client: &ForgeClient, args: GetUserParams) -> Result<User, ForgeClientError> {
client.call("get_user", args).await
}
// Default: one-shot query
pub fn use_get_user(args: GetUserParams) -> QueryState<User> {
use_forge_query("get_user", args)
}
// Opt-in: real-time subscription
pub fn use_get_user_live(args: GetUserParams) -> SubscriptionState<User> {
use_forge_subscription("get_user", args)
}
// Mutations return a typed Mutation handle
pub fn use_create_user() -> Mutation<CreateUserParams, User> {
use_forge_mutation("create_user")
}
The generated mod.rs also re-exports ForgeProvider, ForgeClient, error types, and the Dioxus hooks from forge-dioxus.
use_<name>() is a one-shot query. Append _live for real-time subscriptions.
Mutations return a Mutation<Args, Result> with a .call(args) method.
Generated params and DTO structs include new(...) constructors plus builder methods for
optional fields, so common calls avoid verbose Rust struct literals.
Type Mapping
Rust to TypeScript
| Rust Type | TypeScript Type |
|---|---|
String, &str | string |
Uuid | string |
i32, i64, f32, f64 | number |
bool | boolean |
DateTime<Utc> | string |
NaiveDate, NaiveTime | string |
Option<T> | T | null |
Vec<T> | T[] |
HashMap<K, V> | Record<string, V> |
Value (JSON) | unknown |
Upload | File | Blob |
Bytes (return) | Blob |
Bytes (arg) | Uint8Array |
Enums to Union Types
Rust enums become TypeScript union types:
#[forge::forge_enum]
pub enum TaskStatus {
Draft,
Active,
Completed,
}
export type TaskStatus = "draft" | "active" | "completed";
Query Bindings
One-Shot RPC
Call a query once and get the result:
import { getUser, listTodos } from "$lib/forge";
// With arguments
const user = await getUser({ id: userId });
// No arguments
const todos = await listTodos();
Returns a Promise<T> that resolves with the data or rejects with a ForgeClientError.
Reactive Subscriptions
Subscribe to query changes with automatic updates:
import { getUserStore$ } from "$lib/forge";
// Create a subscription store
const userStore = getUserStore$({ id: userId });
// Use in Svelte component
$: ({ loading, data, error, stale } = $userStore);
Svelte 5 Runes
For Svelte 5 runes-based reactivity:
import { getUser$ } from '$lib/forge';
// Returns a reactive object
const user = getUser$({ id: userId });
// Access properties directly
{#if user.loading}
Loading...
{:else if user.error}
Error: {user.error.message}
{:else}
Welcome, {user.data.name}
{/if}
The $ suffix functions use $state internally for fine-grained reactivity.
Subscription State
All subscription stores expose the same shape:
interface SubscriptionResult<T> {
loading: boolean; // true until first data arrives
data: T | null; // the query result
error: Error | null;
stale: boolean; // reserved for reconnect-aware UIs
}
| Field | When True |
|---|---|
loading | Initial fetch in progress |
stale | Currently reserved in the Svelte runtime; use createConnectionStore() for connection status |
error | Initial query or subscription registration failed |
The current Svelte runtime keeps stale at false and exposes connection state separately through createConnectionStore().
Mutations
Mutations return Promises; subscriptions are not required for mutation calls:
import { createUser, updateTodo, deleteTodo } from "$lib/forge";
// Create
const user = await createUser({ name: "Alice", email: "alice@example.com" });
// Update
await updateTodo({ id: todoId, completed: true });
// Delete
await deleteTodo({ id: todoId });
On success, all active subscriptions that depend on affected tables re-evaluate automatically via server-side change detection.
File Uploads
The Upload type maps to File | Blob on the client:
#[forge::mutation]
pub async fn upload_avatar(
ctx: &MutationContext,
file: Upload,
user_id: Uuid,
) -> Result<String> {
let bytes = file.bytes();
let filename = file.name();
let content_type = file.content_type();
// Store file, return URL
}
import { uploadAvatar } from "$lib/forge";
// From file input
const url = await uploadAvatar({
file: inputElement.files[0],
userId: currentUserId,
});
// From Blob
const blob = new Blob([data], { type: "application/pdf" });
await uploadDocument({ file: blob, name: "report.pdf" });
When arguments contain File or Blob, the client automatically:
- Switches to
multipart/form-dataencoding - Encodes non-file arguments into the
_jsonmultipart field - Extracts filename and content type from the
Fileobject
Auto-Reconnect
The client handles connection drops automatically:
| Behavior | Value |
|---|---|
| Max attempts | 10 |
| Initial delay | 1 second |
| Backoff | Exponential with jitter |
| Max delay | 30 seconds |
During reconnection:
- The SSE transport reconnects with backoff
- Active query subscriptions re-register on reconnect
- One-shot RPC calls do not retry automatically
Connection Lifecycle
import { createConnectionStore } from "$lib/forge";
const connection = createConnectionStore();
$: state = $connection; // 'disconnected' | 'connecting' | 'connected'
Connection IDs prevent race conditions. If a reconnect starts while another is in progress, the stale attempt is cancelled.
Token Change Detection
When the auth token changes, the client detects it and reconnects:
// In auth.svelte.ts
auth.setAuth(newToken, user); // Triggers reconnect
auth.clearAuth(); // Triggers reconnect
The client hashes the current token and compares it before each subscription registration. If the hash differs from the connected token, it triggers a full reconnect to re-authenticate subscriptions.
This handles:
- User login (token acquired)
- Token refresh (token replaced)
- User logout (token cleared)
Generated Files
SvelteKit files
After forge generate, a SvelteKit target gets:
| File | Contents |
|---|---|
types.ts | Interfaces from #[forge::model], input DTOs, and union types from #[forge::forge_enum] |
api.ts | Query, mutation, job, and workflow bindings |
stores.ts | Re-exports from @forge-rs/svelte |
runes.svelte.ts | Svelte 5 toReactive() helper |
reactive.svelte.ts | Runes-based subscription functions (getUser$), generated when queries exist |
auth.svelte.ts | Auth store (when auth configured) |
index.ts | Barrel export |
Dioxus files
After forge generate --target dioxus, a Dioxus target gets:
| File | Contents |
|---|---|
types.rs | Rust structs for models and input DTOs, plus enums from #[forge::forge_enum] |
api.rs | Query hooks, _live subscription hooks, Mutation handles, job/workflow hooks |
mod.rs | Re-exports generated modules plus forge-dioxus runtime types |
CLI Options
forge generate [OPTIONS]
| Option | Description |
|---|---|
--force | Regenerate even if files exist |
--output, -o | Output directory (defaults to the target's standard binding path) |
--target | Frontend target (sveltekit or dioxus) |
--src, -s | Source directory to scan (default: src) |
-y, --yes | Auto-accept prompts (for CI) |
Usage in SvelteKit Components
<script lang="ts">
import { listTodos$, createTodo, toggleTodo } from '$lib/forge';
const todos = listTodos$();
async function addTodo(title: string) {
await createTodo({ title });
// Refetches are handled by the subscription updates
}
async function toggle(id: string, completed: boolean) {
await toggleTodo({ id, completed });
}
</script>
{#if todos.loading}
<p>Loading...</p>
{:else if todos.error}
<p>Error: {todos.error.message}</p>
{:else}
<ul>
{#each todos.data as todo}
<li>
<input
type="checkbox"
checked={todo.completed}
on:change={() => toggle(todo.id, !todo.completed)}
/>
{todo.title}
</li>
{/each}
</ul>
{/if}
For Dioxus, the same backend contract is consumed through generated Rust hooks:
use dioxus::prelude::*;
use crate::forge::{CreateTodoInput, use_create_todo, use_list_todos_live};
#[component]
fn TodoList() -> Element {
let create_todo = use_create_todo();
let todos = use_list_todos_live();
rsx! {
button {
onclick: move |_| {
let create_todo = create_todo.clone();
spawn(async move {
let _ = create_todo.call(CreateTodoInput::new("Ship it")).await;
});
},
"Add"
}
ul {
for todo in todos.data.clone().unwrap_or_default() {
li { "{todo.title}" }
}
}
}
}
Under the Hood
SSE Transport
Subscriptions use Server-Sent Events with a dedicated connection. The client:
- Opens an EventSource to
/_api/eventswith the auth token - Receives a session ID and session secret on connection
- Registers each subscription via POST to
/_api/subscribe - Receives updates via the SSE channel with
targetrouting
SSE was chosen over WebSocket for:
- HTTP/2 multiplexing support
- Compatible with standard proxy configuration
- Automatic reconnection built into EventSource
- Simpler firewall traversal
Subscription Re-registration
On reconnect, the client re-registers all active query subscriptions. Each subscription tracks:
- Function name and arguments
- Failed attempt count
- Callback reference
After 3 consecutive re-registration failures, the subscription emits a MAX_RETRIES_EXCEEDED error and stops retrying.
Connection ID Pattern
Each connect() call increments a connection ID. All async operations check this ID before modifying state:
const currentConnectionId = ++this.connectionId;
// ... async work ...
if (currentConnectionId !== this.connectionId) return; // Stale, abort
This prevents race conditions when rapid reconnects overlap.