Your First App
Create a real-time todo list using Forge queries, mutations, and PostgreSQL-backed subscriptions.
Install Forge
curl -fsSL https://tryforge.dev/install.sh | sh
Requires Docker (with Compose v2).
Create and Run
forge new my-app --template with-svelte/minimal
cd my-app
forge dev
This template starts with a SvelteKit frontend. To start with Dioxus instead, use forge new my-app --template with-dioxus/minimal.
This runs Docker Compose with the frontend and backend exposed on your host:
| Service | URL |
|---|---|
| Frontend | http://localhost:5173 |
| Backend | http://localhost:8080 |
PostgreSQL runs inside the Docker network by default and is not published on localhost:5432. Use docker compose exec db psql ... if you want to inspect it directly.
Backend restarts are scoped to backend files (src/, migrations/, Cargo.toml, Cargo.lock, build.rs, .env, forge.toml) via an .ignore allowlist so unrelated files do not trigger rebuilds.
Define the Model
Create src/schema/todo.rs:
use forge::prelude::*;
#[forge::model]
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct Todo {
pub id: Uuid,
pub title: String,
pub completed: bool,
pub created_at: DateTime<Utc>,
}
Export it in src/schema/mod.rs:
pub mod todo;
pub use todo::*;
Add a Query
Create src/functions/todos.rs:
use forge::prelude::*;
use crate::schema::Todo;
#[forge::query(public)]
pub async fn list_todos(ctx: &QueryContext) -> Result<Vec<Todo>> {
sqlx::query_as!(Todo, "SELECT * FROM todos ORDER BY created_at DESC")
.fetch_all(ctx.db())
.await
.map_err(Into::into)
}
Export it in src/functions/mod.rs:
pub mod todos;
pub use todos::*;
Register it in src/main.rs:
Forge::builder()
.register_query::<functions::ListTodosQuery>()
// ... other registrations
Add the Table
Create migrations/0001_todos.sql:
-- @up
CREATE TABLE todos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
completed BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- @down
DROP TABLE IF EXISTS todos;
Test It
curl -X POST http://localhost:8080/_api/rpc/list_todos \
-H "Content-Type: application/json" \
-d '{}'
Returns []. The query works.
Add a Mutation
Add to src/functions/todos.rs:
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateTodoInput {
pub title: String,
}
#[forge::mutation(public)]
pub async fn create_todo(ctx: &MutationContext, input: CreateTodoInput) -> Result<Todo> {
let mut conn = ctx.conn().await?;
sqlx::query_as!(Todo, "INSERT INTO todos (title) VALUES ($1) RETURNING *", input.title)
.fetch_one(&mut *conn)
.await
.map_err(Into::into)
}
Register it in src/main.rs:
Add to the builder chain in src/main.rs:
Forge::builder()
.register_query::<functions::ListTodosQuery>()
.register_mutation::<functions::CreateTodoMutation>()
// ...
Test it:
curl -X POST http://localhost:8080/_api/rpc/create_todo \
-H "Content-Type: application/json" \
-d '{"args": {"title": "Learn Forge"}}'
Make It Real-Time
Two changes needed:
1. Enable table reactivity:
Create migrations/0002_todos_reactivity.sql:
-- @up
SELECT forge_enable_reactivity('todos');
-- @down
SELECT forge_disable_reactivity('todos');
This sets up PostgreSQL triggers that emit NOTIFY on changes.
2. Use a subscription on the frontend.
The query itself doesn't need to change. Forge extracts table dependencies from the SQL at compile time.
If you change SQL or migrations and want to refresh the offline cache used by sqlx macros and forge check, run:
forge migrate prepare
Use from Frontend
Generate frontend bindings:
forge generate
This creates type-safe bindings in the default output for your target:
frontend/src/lib/forge/for SvelteKitfrontend/src/forge/for Dioxus
SvelteKit example
The with-svelte/minimal template uses SvelteKit, so you can use the generated TypeScript bindings like this:
<script lang="ts">
import { listTodos$, createTodo } from '$lib/forge';
const todos = listTodos$();
let title = '';
async function add() {
await createTodo({ title });
title = '';
}
</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>{todo.title}</li>
{/each}
</ul>
{/if}
<input bind:value={title} />
<button onclick={add}>Add</button>
The $ suffix subscribes to real-time updates through the Forge SSE client, which relays
PostgreSQL LISTEN/NOTIFY changes. Create a todo in one browser tab, it appears in all others.
Dioxus example
With --target dioxus, Forge generates Rust bindings and hooks instead:
use dioxus::prelude::*;
use crate::forge::use_list_todos_live;
#[component]
fn TodoList() -> Element {
let todos = use_list_todos_live();
rsx! {
ul {
for todo in todos.data.clone().unwrap_or_default() {
li { "{todo.title}" }
}
}
}
}
use_list_todos() fetches once. use_list_todos_live() subscribes to real-time changes.
What You Built
- Query with compile-time SQL validation
- Mutation with automatic type generation
- Real-time subscriptions via PostgreSQL
LISTEN/NOTIFY - Type-safe frontend bindings for SvelteKit or Dioxus
Next: Project Anatomy to understand the file structure.