Your First App
Build a real-time todo list with PostgreSQL. Nothing else required.
Install Forge
curl -fsSL https://tryforge.dev/install.sh | sh
Requires PostgreSQL 18+ (or use embedded mode for development).
Create and Run
forge new my-app --minimal
cd my-app
forge dev
Three services start:
- Frontend: http://localhost:5173
- Backend: http://localhost:8080
- Embedded PostgreSQL: localhost:5432
Define the Model
Create src/schema/todo.rs:
use forge::prelude::*;
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
#[forge::model]
pub struct Todo {
pub id: Uuid,
pub title: String,
pub completed: bool,
}
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("SELECT id, title, completed FROM todos ORDER BY id")
.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:
builder
.function_registry_mut()
.register_query::<functions::ListTodosQuery>();
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
);
-- @down
DROP TABLE IF EXISTS todos;
Test It
curl http://localhost:8080/_api/rpc/list_todos
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> {
sqlx::query_as("INSERT INTO todos (title) VALUES ($1) RETURNING *")
.bind(input.title)
.fetch_one(ctx.db())
.await
.map_err(Into::into)
}
Register it in src/main.rs:
builder
.function_registry_mut()
.register_mutation::<functions::CreateTodoMutation>();
Test it:
curl -X POST http://localhost:8080/_api/rpc/create_todo \
-H "Content-Type: application/json" \
-d '{"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.
Use from Frontend
Generate the TypeScript client:
forge generate
This creates type-safe bindings in frontend/src/lib/forge/.
Now use them in your Svelte component:
<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. No WebSocket setup. No polling. Create a todo in one browser tab, it appears in all others.
What You Built
- Query with compile-time SQL validation
- Mutation with automatic type generation
- Real-time subscriptions via PostgreSQL
LISTEN/NOTIFY - Type-safe frontend client
Next: Project Anatomy to understand the file structure.