Skip to main content

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:

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.