First Application
Time to build something real. We're making a todo list — and I know, I know, every framework tutorial does a todo list. But there's a reason: it exercises state, per-user data, privacy, and CRUD in a way that actually reveals how the system works. By the end of this, you'll have a working app with user-specific tasks, privacy controls, and the ability to share lists.
Project Setup
The fastest way to start is kickstart:
java -jar adama.jar kickstart
When prompted:
- Template: Type
webappand press Enter - Space name: Pick something unique like
mytodo-yourname(spaces are globally unique)
This creates a directory with your space name containing:
| Path | Description |
|---|---|
backend.adama |
Main Adama specification file |
backend/ |
Additional Adama files (for organization) |
frontend/*.rx.html |
RxHTML templates for the web UI |
local.verse.json |
Local development configuration |
README.md |
Notes from the template |
Running the Development Server
Fire up the devbox:
cd mytodo-yourname
java -jar ../adama.jar devbox
Navigate to http://localhost:8080 in your browser. You should see the template's default interface.
Changes to .adama files reload instantly. Changes to .rx.html files need a browser refresh (F5).
Understanding the Data Model
Before writing code, let's think about what we need:
- Tasks: Each one has a title, completion status, and owner
- Users: Track who created each task
- Privacy: Users see only their own tasks (by default)
- Sharing: Optional ability to share lists with others
Here's the data model in backend.adama:
// A single todo item
record Task {
// Unique identifier (auto-assigned)
public int id;
// Who owns this task
private principal owner;
// Task content
public string title;
// Completion status
public bool done;
// When it was created
public datetime created;
}
// Store all tasks
table<Task> _tasks;
A few things to notice:
idis public so we can reference tasks in the UIowneris private — the raw principal value never leaks to clientstitle,done, andcreatedare public within the task- The table itself (
_tasks) is always private in Adama
Prefixing table names with underscore (_tasks) is a convention that reminds you tables are inherently private. Not required, but it helps when you're scanning code.
Implementing Channels
Users interact with the document through messages and channels. Define the messages first:
// Create a new task
message CreateTask {
string title;
}
// Mark a task as done or not done
message ToggleTask {
int task_id;
}
// Delete a task
message DeleteTask {
int task_id;
}
Now the channels that handle them:
message CreateTask { string title; }
message ToggleTask { int task_id; }
message DeleteTask { int task_id; }
record Task {
public int id;
public principal owner;
public string title;
public bool done;
public datetime created;
}
table<Task> _tasks;
// Handle task creation
channel create_task(CreateTask msg) {
_tasks <- {
owner: @who,
title: msg.title,
done: false,
created: Time.datetime()
};
}
// Handle toggling completion
channel toggle_task(ToggleTask msg) {
// Find the task - only allow toggling your own tasks
if ((iterate _tasks where id == msg.task_id && owner == @who)[0] as task) {
task.done = !task.done;
}
}
// Handle deletion
channel delete_task(DeleteTask msg) {
// Delete only if you own the task
(iterate _tasks where id == msg.task_id && owner == @who).delete();
}
The pattern is the same every time:
- Query the table with conditions (
where id == ... && owner == @who) - Use
if (... as task)to safely unwrap the optional result - Modify or delete the record
The owner == @who check means users can only touch their own tasks. Privacy enforcement at the query level — no middleware, no authorization layer bolted on after the fact.
Privacy Controls
Now expose tasks to users. Each user should see only their tasks:
record Task {
public int id;
public principal owner;
public string title;
public bool done;
public datetime created;
}
table<Task> _tasks;
// Each user sees their own tasks, sorted by creation date
bubble my_tasks = iterate _tasks where owner == @who order by created asc;
The bubble creates a per-viewer computation. User A's my_tasks contains only User A's tasks. User B sees a completely different list. The server does all the filtering; clients just render what they get.
Connection Handling
Allow users to connect:
@connected {
return true;
}
For a production app, you might want restrictions:
@connected {
// Example: Only allow authenticated users
return @who != @no_one;
}
Document Creation Policy
Control who can create documents in your space:
@static {
// Anyone can create a new todo list document
create {
return true;
}
// Allow creating documents on-demand via connect
invent {
return true;
}
}
The invent policy is interesting — it lets documents spring into existence when someone connects to a key that doesn't exist yet. Handy for per-user documents where you don't want to pre-create anything.
Complete Backend Code
Here's the full backend.adama:
@static {
create {
return true;
}
invent {
return true;
}
}
@connected {
return true;
}
// Data model
record Task {
public int id;
private principal owner;
public string title;
public bool done;
public datetime created;
}
table<Task> _tasks;
// Messages
message CreateTask {
string title;
}
message ToggleTask {
int task_id;
}
message DeleteTask {
int task_id;
}
// Channels
channel create_task(CreateTask msg) {
_tasks <- {
owner: @who,
title: msg.title,
done: false,
created: Time.datetime()
};
}
channel toggle_task(ToggleTask msg) {
if ((iterate _tasks where id == msg.task_id && owner == @who)[0] as task) {
task.done = !task.done;
}
}
channel delete_task(DeleteTask msg) {
(iterate _tasks where id == msg.task_id && owner == @who).delete();
}
// Exposed data
bubble my_tasks = iterate _tasks where owner == @who order by created asc;
// Stats
public formula total_tasks = _tasks.size();
bubble my_task_count = (iterate _tasks where owner == @who).size();
bubble my_completed_count = (iterate _tasks where owner == @who && done).size();
Adding the Frontend
Edit frontend/initial.rx.html to create the UI:
<page uri="/todo">
<connection use-domain name="todo">
<h1>My Todo List</h1>
<!-- Task creation form -->
<form rx:action="send:create_task">
<input type="text" name="title" placeholder="What needs to be done?" />
<button type="submit">Add Task</button>
</form>
<!-- Task list -->
<ul rx:iterate="my_tasks">
<li>
<input
type="checkbox"
rx:checked="done"
rx:action="toggle:toggle_task"
rx:click="set:task_id={view:id}"
/>
<span rx:if="done" style="text-decoration: line-through;">
<lookup path="title" />
</span>
<span rx:ifnot="done">
<lookup path="title" />
</span>
<button rx:action="send:delete_task" rx:click="set:task_id={view:id}">
Delete
</button>
</li>
</ul>
<!-- Stats -->
<p>
<lookup path="my_completed_count" /> of <lookup path="my_task_count" /> tasks completed
</p>
</connection>
</page>
The RxHTML bits worth knowing:
rx:iterateloops over a collectionrx:action="send:channel_name"sends a message on form submitlookup path="field"displays data from the current scoperx:ifandrx:ifnotfor conditional rendering
Testing Locally
With the devbox running, go to http://localhost:8080/todo .
- Create a few tasks
- Toggle them complete/incomplete
- Delete some
- Open a second browser window (or incognito)
- Notice that tasks are separate per user session
The debugger (wifi icon in the bottom right) lets you inspect the document state and send messages directly. It's worth poking around in there.
Deployment
When you're ready for production:
Deploy the Backend
java -jar adama.jar spaces deploy --space mytodo-yourname --file backend.adama
This uploads your Adama code to the space, making it live for production documents.
Verify Deployment
java -jar adama.jar space list
You should see your space in the list with storage usage updating as documents get created.
Extending the Application
Some things to try if you want to keep going:
Due Dates
record Task {
// ... existing fields ...
public maybe<datetime> due_date;
}
message CreateTask {
string title;
maybe<datetime> due_date;
}
The maybe type makes the field optional — no null pointer exceptions, no sentinel values.
Task Priorities
record Task {
public int id;
public principal owner;
public string title;
public bool done;
public datetime created;
public int priority;
}
table<Task> _tasks;
bubble my_tasks = iterate _tasks
where owner == @who
order by priority desc, created asc;
Sharing Lists
record Task {
public int id;
public principal owner;
public string title;
public bool done;
public datetime created;
}
table<Task> _tasks;
// Track who can see this list
record SharedUser {
public principal who;
}
table<SharedUser> _shared_with;
// Message to share with someone
message ShareWith {
principal user;
}
channel share_list(ShareWith msg) {
_shared_with <- {who: msg.user};
}
// Modified bubble to include shared tasks
bubble visible_tasks = iterate _tasks
where owner == @who || (iterate _shared_with where who == @who).size() > 0;
All of this — state, privacy, real-time sync, per-user views — in under 100 lines of Adama code. No database setup, no WebSocket plumbing, no state synchronization logic.
If you want to go deeper from here:
- Language Fundamentals — the full syntax
- Tables and Queries — more data operations
- Privacy — complex access control patterns
- State Machines — workflows and game logic