Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

Your first app

In this tutorial you will build QuickNotes — a simple notes app where users sign up, create notes stored in Postgres, get a greeting from a serverless function, and browse the app from a deployed static site.

Estimated time: 20–30 minutes.

LayerTechnologyReactor capability
AuthEmail + password sign-upIdentity
Databasenotes table with RLSData
API logicsummarize-note functionFunctions
FrontendStatic HTML/JS in sites/webSites

Final architecture:

Browser → Sites (static) → Identity (login)
→ Data (notes CRUD)
→ Functions (summarize-note)
Terminal window
reactor init quicknotes
cd quicknotes
reactor dev

Keep reactor dev running in one terminal. All remaining commands run in a second terminal from quicknotes/.

Replace the sample migration with a notes schema. Create data/migrations/001_notes.sql:

-- Notes table with row-level security
CREATE TABLE notes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
title TEXT NOT NULL,
body TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX notes_user_id_idx ON notes (user_id);
ALTER TABLE notes ENABLE ROW LEVEL SECURITY;
-- Users can only see and modify their own notes
CREATE POLICY notes_select ON notes
FOR SELECT USING (user_id = current_setting('reactor.user_id', true)::uuid);
CREATE POLICY notes_insert ON notes
FOR INSERT WITH CHECK (user_id = current_setting('reactor.user_id', true)::uuid);
CREATE POLICY notes_update ON notes
FOR UPDATE USING (user_id = current_setting('reactor.user_id', true)::uuid);
CREATE POLICY notes_delete ON notes
FOR DELETE USING (user_id = current_setting('reactor.user_id', true)::uuid);

Apply the migration:

Terminal window
reactor data migrate
reactor data inspect notes

Step 3 — Register the function in reactor.toml

Section titled “Step 3 — Register the function in reactor.toml”

Open reactor.toml and add your function and site entries:

project_id = "quicknotes-xxxxxxxx"
name = "QuickNotes"
default_context = "local"
[data]
migrations_dir = "data/migrations"
[[functions]]
name = "summarize-note"
source = "functions/summarize-note"
runtime = "wasm"
entry = "index.ts"
[[sites]]
name = "web"
source = "sites/web"
framework = "static"

Create functions/summarize-note/index.ts:

// Summarize a note body — invoked at POST /fn/v1/summarize-note
export async function handler(request: Request): Promise<Response> {
const { body } = await request.json();
const text = typeof body === 'string' ? body : body?.text ?? '';
if (!text.trim()) {
return new Response(JSON.stringify({ error: 'text is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
// Simple summary: first sentence or first 120 characters
const sentence = text.split(/[.!?]/)[0]?.trim() || text;
const summary = sentence.length > 120 ? sentence.slice(0, 117) + '...' : sentence;
return new Response(JSON.stringify({ summary, length: text.length }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}

Test it locally:

Terminal window
reactor functions invoke summarize-note \
--body '{"text": "Reactor makes backend development simple. You get auth, data, and functions in one place."}'

Create a minimal static site at sites/web/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>QuickNotes</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 640px; margin: 2rem auto; padding: 0 1rem; }
input, textarea, button { display: block; width: 100%; margin: 0.5rem 0; padding: 0.5rem; }
.note { border: 1px solid #ddd; border-radius: 8px; padding: 1rem; margin: 1rem 0; }
.hidden { display: none; }
</style>
</head>
<body>
<h1>QuickNotes</h1>
<section id="auth">
<h2>Sign in</h2>
<input id="email" type="email" placeholder="Email" />
<input id="password" type="password" placeholder="Password" />
<button id="signup">Sign up</button>
<button id="login">Log in</button>
</section>
<section id="app" class="hidden">
<p>Signed in as <strong id="user-email"></strong>
<button id="logout">Log out</button></p>
<h2>New note</h2>
<input id="title" placeholder="Title" />
<textarea id="body" rows="4" placeholder="Body"></textarea>
<button id="save">Save note</button>
<h2>Your notes</h2>
<div id="notes"></div>
</section>
<script type="module">
import { createClient } from 'https://esm.sh/@reactor/client';
const reactor = createClient(window.location.origin, {
key: 'rk_pub_local_dev', // replace with your project key in production
});
const authEl = document.getElementById('auth');
const appEl = document.getElementById('app');
const notesEl = document.getElementById('notes');
async function refreshUI() {
const session = await reactor.auth.getSession();
if (session?.user) {
authEl.classList.add('hidden');
appEl.classList.remove('hidden');
document.getElementById('user-email').textContent = session.user.email;
await loadNotes();
} else {
authEl.classList.remove('hidden');
appEl.classList.add('hidden');
}
}
async function loadNotes() {
const { data } = await reactor.from('notes').select('*').order('created_at', { ascending: false });
notesEl.innerHTML = '';
for (const note of data ?? []) {
const div = document.createElement('div');
div.className = 'note';
div.innerHTML = `<h3>${note.title}</h3><p>${note.body}</p>`;
notesEl.appendChild(div);
}
}
document.getElementById('signup').onclick = async () => {
await reactor.auth.signUp({
email: document.getElementById('email').value,
password: document.getElementById('password').value,
});
await refreshUI();
};
document.getElementById('login').onclick = async () => {
await reactor.auth.signIn({
email: document.getElementById('email').value,
password: document.getElementById('password').value,
});
await refreshUI();
};
document.getElementById('logout').onclick = async () => {
await reactor.auth.signOut();
await refreshUI();
};
document.getElementById('save').onclick = async () => {
const session = await reactor.auth.getSession();
await reactor.from('notes').insert({
user_id: session.user.id,
title: document.getElementById('title').value,
body: document.getElementById('body').value,
});
document.getElementById('title').value = '';
document.getElementById('body').value = '';
await loadNotes();
};
refreshUI();
</script>
</body>
</html>

Sign up and create a note using the SDK in a small script, or use reactor login after creating a user through the UI.

Open the site in your browser. During local dev, static files are typically served at the project site path — check reactor dev output for the exact URL.

Add a production context if you have not already:

Terminal window
reactor context add production \
--endpoint https://api.reactor.cloud \
--org my-org
reactor context use production
reactor login

Build and deploy:

Terminal window
reactor build
reactor deploy

Reactor will:

  1. Validate reactor.toml and bundle functions, migrations, and site assets.
  2. Apply migrations on the target database.
  3. Publish summarize-note to the Functions runtime.
  4. Upload sites/web to Sites hosting.

Check deployment status:

Terminal window
reactor sites show web
reactor functions show summarize-note
Terminal window
reactor functions invoke summarize-note \
--context production \
--body '{"text": "Deployed and working!"}'
reactor sites domains list web

Visit your site URL (printed by reactor sites show web) and confirm sign-up, note creation, and listing work end to end.

Once the basics work, try these additions:

IdeaCapabilityStarting point
Auto-summarize on saveJobsTrigger summarize-note after note insert
Avatar uploadsStoragereactor.storage.from('avatars').upload(...)
AI-powered summariesGatewayRoute to an LLM instead of truncating text
Slack notificationsConnectWebhook on new note creation
ProblemSolution
Notes not visible after insertConfirm RLS policies and that user_id matches the authenticated user
Function 404Check [[functions]] name in reactor.toml matches the directory
Site shows blank pageVerify sites/web/index.html exists and framework = "static"
Deploy auth errorRun reactor login and reactor whoami against the target context
  • Concepts — understand contexts, adapters, and deployment grades.
  • Identity — OAuth, MFA, and organization setup.
  • Jobs — schedule a nightly digest of new notes.