Skip to main content

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:

  1. Scans your src/ directory for models, enums, and functions
  2. Extracts type information from Rust macros
  3. Generates TypeScript interfaces with correct nullability
  4. Creates API bindings that call your functions with type safety
  5. 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 TypeTypeScript Type
String, &strstring
Uuidstring
i32, i64, f32, f64number
boolboolean
DateTime<Utc>string
NaiveDate, NaiveTimestring
Option<T>T | null
Vec<T>T[]
HashMap<K, V>Record<string, V>
Value (JSON)unknown
UploadFile | 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
}
FieldWhen True
loadingInitial fetch in progress
staleCurrently reserved in the Svelte runtime; use createConnectionStore() for connection status
errorInitial 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-data encoding
  • Encodes non-file arguments into the _json multipart field
  • Extracts filename and content type from the File object

Auto-Reconnect

The client handles connection drops automatically:

BehaviorValue
Max attempts10
Initial delay1 second
BackoffExponential with jitter
Max delay30 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:

FileContents
types.tsInterfaces from #[forge::model], input DTOs, and union types from #[forge::forge_enum]
api.tsQuery, mutation, job, and workflow bindings
stores.tsRe-exports from @forge-rs/svelte
runes.svelte.tsSvelte 5 toReactive() helper
reactive.svelte.tsRunes-based subscription functions (getUser$), generated when queries exist
auth.svelte.tsAuth store (when auth configured)
index.tsBarrel export

Dioxus files

After forge generate --target dioxus, a Dioxus target gets:

FileContents
types.rsRust structs for models and input DTOs, plus enums from #[forge::forge_enum]
api.rsQuery hooks, _live subscription hooks, Mutation handles, job/workflow hooks
mod.rsRe-exports generated modules plus forge-dioxus runtime types

CLI Options

forge generate [OPTIONS]
OptionDescription
--forceRegenerate even if files exist
--output, -oOutput directory (defaults to the target's standard binding path)
--targetFrontend target (sveltekit or dioxus)
--src, -sSource directory to scan (default: src)
-y, --yesAuto-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:

  1. Opens an EventSource to /_api/events with the auth token
  2. Receives a session ID and session secret on connection
  3. Registers each subscription via POST to /_api/subscribe
  4. Receives updates via the SSE channel with target routing

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.