Common Patterns

These are the patterns I keep coming back to -- the ones that show up in virtually every Adama application regardless of what it does. Authentication, authorization, data access, user experience. The fundamentals.

Authentication and Session Management

Basic Connection Authentication

Every Adama application needs to decide who can connect. The @connected handler is your first line of defense.

Problem: Allow only authenticated users to access the document.

@connected {
  // Reject anonymous connections
  if (@who == @no_one) {
    return false;
  }
  return true;
}

Owner-Based Access

Problem: Only the document creator should have full access.

private principal owner;

@construct {
  owner = @who;  // Whoever creates the document becomes owner
}

@connected {
  return @who == owner;
}

Multi-User with Invitation

Problem: The owner can invite specific users to access the document.

private principal owner;

record AllowedUser {
  public int id;
  private principal who;
  public string invitedBy;
  public datetime invitedAt;
}

table<AllowedUser> _allowed;

@construct {
  owner = @who;
  // Owner is automatically allowed
  _allowed <- {who: @who, invitedBy: "self", invitedAt: Time.datetime()};
}

@connected {
  // Check if user is in the allowed list
  return (iterate _allowed where who == @who).size() > 0;
}

message InviteUser {
  principal user;
}

channel invite(InviteUser msg) {
  // Only owner can invite
  if (@who != owner) {
    return;
  }
  // Check if already invited
  if ((iterate _allowed where who == msg.user).size() > 0) {
    return;
  }
  _allowed <- {who: msg.user, invitedBy: owner.agent(), invitedAt: Time.datetime()};
}

message RevokeUser {
  principal user;
}

channel revoke(RevokeUser msg) {
  // Only owner can revoke, and cannot revoke self
  if (@who != owner || msg.user == owner) {
    return;
  }
  (iterate _allowed where who == msg.user).delete();
}

Session Tracking

Problem: Track which users are currently connected and their last activity.

record Session {
  public int id;
  private principal who;
  public string displayName;
  public bool online;
  public datetime lastSeen;

  index who;
}

table<Session> _sessions;

// Expose online users to everyone
public formula onlineUsers = iterate _sessions where online;
public formula onlineCount = (iterate _sessions where online).size();

@connected {
  // Find or create session
  list<Session> existing = iterate _sessions where who == @who;
  if (existing.size() == 0) {
    _sessions <- {
      who: @who,
      displayName: @who.agent(),
      online: true,
      lastSeen: Time.datetime()
    };
  } else {
    existing.online = true;
    existing.lastSeen = Time.datetime();
  }
  return true;
}

@disconnected {
  (iterate _sessions where who == @who).online = false;
}

// Let users update their display name
message UpdateProfile {
  string displayName;
}

channel updateProfile(UpdateProfile msg) {
  (iterate _sessions where who == @who).displayName = msg.displayName;
  (iterate _sessions where who == @who).lastSeen = Time.datetime();
}

Activity Heartbeat

Problem: Detect idle users who are connected but not active.

record UserPresence {
  public int id;
  private principal who;
  public bool online;
  public bool active;
  public datetime lastActivity;

  index who;
}

table<UserPresence> _presence;

message Heartbeat {}

channel heartbeat(Heartbeat h) {
  (iterate _presence where who == @who).lastActivity = Time.datetime();
  (iterate _presence where who == @who).active = true;
}

// Check for idle users every minute
@cron idle_check daily 0:00 {
  datetime threshold = Time.datetime().past(@timespan 5 min);
  (iterate _presence where active && lastActivity < threshold).active = false;
}

Role-Based Access Control

Simple Role System

Problem: Implement distinct permission levels (admin, moderator, member).

enum Role { Admin, Moderator, Member, Guest }

record User {
  public int id;
  private principal who;
  public string name;
  public Role role;

  index who;

  // Only admins and the user themselves see their role
  policy can_see_role {
    return @who == who || isAdmin(@who);
  }
}

table<User> _users;

procedure isAdmin(principal p) -> bool {
  list<User> found = iterate _users where who == p;
  if (found.size() > 0) {
    if (found[0] as u) {
      return u.role == Role::Admin;
    }
  }
  return false;
}

procedure isModerator(principal p) -> bool {
  list<User> found = iterate _users where who == p;
  if (found.size() > 0) {
    if (found[0] as u) {
      return u.role == Role::Admin || u.role == Role::Moderator;
    }
  }
  return false;
}

procedure canEdit(principal p) -> bool {
  list<User> found = iterate _users where who == p;
  if (found.size() > 0) {
    if (found[0] as u) {
      return u.role != Role::Guest;
    }
  }
  return false;
}

Permission-Based Access

Problem: Fine-grained permissions rather than broad roles.

record Permission {
  public int id;
  private principal who;
  public string permission;

  index who;
  index permission;
}

table<Permission> _permissions;

procedure hasPermission(principal p, string perm) -> bool {
  return (iterate _permissions where who == p && permission == perm).size() > 0;
}

// Guard content with policies
record ProtectedContent {
  public int id;
  public string content;

  use_policy<can_view> string sensitiveData;

  policy can_view {
    return hasPermission(@who, "view_sensitive");
  }
}

message GrantPermission {
  principal user;
  string permission;
}

channel grantPermission(GrantPermission msg) {
  // Only users with grant permission can grant
  if (!hasPermission(@who, "grant_permissions")) {
    return;
  }
  // Avoid duplicates
  if ((iterate _permissions where who == msg.user && permission == msg.permission).size() == 0) {
    _permissions <- {who: msg.user, permission: msg.permission};
  }
}

Resource-Level Access Control

Problem: Different users have different access to different resources.

enum AccessLevel { None, Read, Write, Admin }

record Resource {
  public int id;
  public string name;
  public string content;
  private principal owner;

  // Use policy to check access level
  use_policy<can_read> string protected_content;

  policy can_read {
    return getAccessLevel(@who, id) != AccessLevel::None;
  }
}

record ResourceAccess {
  public int id;
  private principal user;
  public int resourceId;
  public AccessLevel level;

  index user;
  index resourceId;
}

table<Resource> _resources;
table<ResourceAccess> _access;

procedure getAccessLevel(principal p, int resourceId) -> AccessLevel {
  // Owner always has admin access
  list<Resource> res = iterate _resources where id == resourceId;
  if (res.size() > 0) {
    if (res[0] as r) {
      if (r.owner == p) {
        return AccessLevel::Admin;
      }
    }
  }

  // Check explicit access grants
  list<ResourceAccess> grants = iterate _access where user == p && resourceId == resourceId;
  if (grants.size() > 0) {
    if (grants[0] as grant) {
      return grant.level;
    }
  }

  return AccessLevel::None;
}

message UpdateResource {
  int resourceId;
  string content;
}

channel updateResource(UpdateResource msg) {
  AccessLevel level = getAccessLevel(@who, msg.resourceId);
  if (level != AccessLevel::Write && level != AccessLevel::Admin) {
    return;  // No write access
  }
  (iterate _resources where id == msg.resourceId).content = msg.content;
}

Optimistic Updates

Immediate Feedback Pattern

Problem: Provide instant visual feedback while ensuring eventual consistency.

Adama's reactive data model naturally supports this. When a client sends a message, they can optimistically show the change locally; the server's authoritative state then syncs back and either confirms or corrects the client's view. Most of the time, the optimistic guess was right.

record Task {
  public int id;
  public string title;
  public bool completed;
  public datetime updatedAt;
}

table<Task> _tasks;

public formula tasks = iterate _tasks order by updatedAt desc;

message ToggleTask {
  int taskId;
}

channel toggleTask(ToggleTask msg) {
  list<Task> found = iterate _tasks where id == msg.taskId;
  if (found.size() > 0) {
    // Toggle the completion status
    bool current = false;
    if (found[0] as t) {
      current = t.completed;
    }
    found.completed = !current;
    found.updatedAt = Time.datetime();
  }
}

The client-side pattern:

  1. User clicks to toggle a task
  2. Client immediately shows the toggled state (optimistic)
  3. Client sends toggleTask message
  4. Server processes and broadcasts updated state
  5. All clients receive authoritative state (which matches the optimistic update)

Optimistic Adds with Server ID

Problem: Show new items immediately, but let the server assign the real ID.

record Message {
  public int id;
  public string text;
  public principal author;
  public datetime createdAt;
}

table<Message> _messages;

public formula messages = iterate _messages order by createdAt asc;

message SendMessage {
  string text;
  string clientTempId;  // Client's temporary ID for correlation
}

record MessageConfirmation {
  public string clientTempId;
  public int serverId;
}

// Clients can see confirmations for their own messages
bubble myConfirmations = iterate _confirmations where forUser == @who;

record Confirmation {
  public int id;
  private principal forUser;
  public string clientTempId;
  public int serverId;
}

table<Confirmation> _confirmations;

channel sendMessage(SendMessage msg) {
  _messages <- {
    text: msg.text,
    author: @who,
    createdAt: Time.datetime()
  } as newId;

  // Store confirmation so client can correlate temp ID with real ID
  _confirmations <- {
    forUser: @who,
    clientTempId: msg.clientTempId,
    serverId: newId
  };

  // Clean up old confirmations (keep last 10 per user)
  list<Confirmation> userConfs = iterate _confirmations
    where forUser == @who
    order by id desc
    offset 10;
  userConfs.delete();
}

Conflict Resolution

Last-Write-Wins

Problem: Multiple users editing the same field -- accept the most recent change.

This is the simplest approach. It's also the one that loses data. But sometimes that's the right tradeoff.

record Document {
  public int id;
  public string content;
  public principal lastEditor;
  public datetime lastEdited;
}

table<Document> _docs;

message UpdateDocument {
  int docId;
  string newContent;
}

channel updateDocument(UpdateDocument msg) {
  list<Document> doc = iterate _docs where id == msg.docId;
  doc.content = msg.newContent;
  doc.lastEditor = @who;
  doc.lastEdited = Time.datetime();
}

Versioned Updates (Optimistic Locking)

Problem: Detect and reject conflicting updates.

record VersionedDocument {
  public int id;
  public string content;
  public int version;
  public principal lastEditor;
  public datetime lastEdited;
}

table<VersionedDocument> _docs;

message UpdateVersioned {
  int docId;
  string newContent;
  int expectedVersion;  // Client sends the version they are editing
}

record UpdateResult {
  bool success;
  string error;
  int currentVersion;
}

// Per-user result storage
record UserResult {
  public int id;
  private principal forUser;
  public bool success;
  public string error;
  public int currentVersion;
}

table<UserResult> _results;

bubble lastResult = (iterate _results where forUser == @who order by id desc limit 1);

channel updateVersioned(UpdateVersioned msg) {
  list<VersionedDocument> doc = iterate _docs where id == msg.docId;

  if (doc.size() == 0) {
    _results <- {forUser: @who, success: false, error: "Document not found", currentVersion: 0};
    return;
  }

  int currentVersion = 0;
  if (doc[0] as d) {
    currentVersion = d.version;
  }

  if (currentVersion != msg.expectedVersion) {
    // Conflict detected - someone else edited since client loaded
    _results <- {
      forUser: @who,
      success: false,
      error: "Version conflict - please reload and try again",
      currentVersion: currentVersion
    };
    return;
  }

  // No conflict - apply update
  doc.content = msg.newContent;
  doc.version = currentVersion + 1;
  doc.lastEditor = @who;
  doc.lastEdited = Time.datetime();

  _results <- {forUser: @who, success: true, error: "", currentVersion: currentVersion + 1};
}

Field-Level Merge

Problem: Allow concurrent edits to different fields without conflict.

The idea here is straightforward -- if two users are editing different fields of the same record, there's no conflict. Separate channels for separate fields means the updates don't step on each other.

record Profile {
  public int id;
  private principal owner;
  public string name;
  public string bio;
  public string location;
  public datetime nameUpdatedAt;
  public datetime bioUpdatedAt;
  public datetime locationUpdatedAt;
}

table<Profile> _profiles;

message UpdateName {
  string name;
}

message UpdateBio {
  string bio;
}

message UpdateLocation {
  string location;
}

// Each field can be updated independently
channel updateName(UpdateName msg) {
  list<Profile> p = iterate _profiles where owner == @who;
  p.name = msg.name;
  p.nameUpdatedAt = Time.datetime();
}

channel updateBio(UpdateBio msg) {
  list<Profile> p = iterate _profiles where owner == @who;
  p.bio = msg.bio;
  p.bioUpdatedAt = Time.datetime();
}

channel updateLocation(UpdateLocation msg) {
  list<Profile> p = iterate _profiles where owner == @who;
  p.location = msg.location;
  p.locationUpdatedAt = Time.datetime();
}

Pagination Patterns

Offset-Based Pagination

Problem: Display large lists in pages of fixed size.

record Post {
  public int id;
  public string title;
  public string content;
  public datetime createdAt;
}

table<Post> _posts;

public formula totalPosts = _posts.size();
public int pageSize = 10;

// View-dependent pagination
view int currentPage;

bubble currentPagePosts =
  iterate _posts
  order by createdAt desc
  offset (@viewer.currentPage * pageSize)
  limit pageSize;

bubble totalPages = (_posts.size() + pageSize - 1) / pageSize;
bubble hasNextPage = @viewer.currentPage * pageSize + pageSize < _posts.size();
bubble hasPrevPage = @viewer.currentPage > 0;

Cursor-Based Pagination

Problem: Handle pagination when items can be added or removed between page loads.

Offset-based pagination breaks when the underlying data changes -- you end up skipping or duplicating items. Cursor-based pagination avoids this by anchoring to a specific point in the data.

record Item {
  public int id;
  public string content;
  public datetime createdAt;

  index createdAt;
}

table<Item> _items;

view datetime cursor;
view int limit;

// Get items after the cursor timestamp
bubble nextItems =
  iterate _items
  where createdAt > @viewer.cursor
  order by createdAt asc
  limit (@viewer.limit > 0 ? @viewer.limit : 20);

// Get items before the cursor (for loading previous)
bubble prevItems =
  iterate _items
  where createdAt < @viewer.cursor
  order by createdAt desc
  limit (@viewer.limit > 0 ? @viewer.limit : 20);

Infinite Scroll

Problem: Load more items as the user scrolls down.

record FeedItem {
  public int id;
  public string content;
  public int score;
  public datetime createdAt;
}

table<FeedItem> _feed;

// Client tracks how many items they have loaded
view int loadedCount;

bubble feedItems =
  iterate _feed
  order by createdAt desc
  limit (@viewer.loadedCount > 0 ? @viewer.loadedCount : 10);

bubble hasMore = _feed.size() > @viewer.loadedCount;
bubble remainingCount = _feed.size() - @viewer.loadedCount;

Search and Filtering

Problem: Filter items by a search term.

record Product {
  public int id;
  public string name;
  public string description;
  public double price;
  public string category;

  index category;
}

table<Product> _products;

view string searchTerm;
view string categoryFilter;

bubble filteredProducts =
  iterate _products
  where_as p:
    (@viewer.categoryFilter == "" || p.category == @viewer.categoryFilter) &&
    (@viewer.searchTerm == "" ||
     p.name.contains(@viewer.searchTerm) ||
     p.description.contains(@viewer.searchTerm));

Multi-Filter with Dynamic Sort

Problem: Combine multiple filters with user-selected sorting.

record Listing {
  public int id;
  public string title;
  public double price;
  public string category;
  public string location;
  public int bedrooms;
  public bool available;
  public datetime listedAt;

  index category;
  index location;
  index available;
}

table<Listing> _listings;

view string category;
view string location;
view int minBedrooms;
view double maxPrice;
view bool showOnlyAvailable;
view string sortBy;  // "price", "-price", "listedAt", "-listedAt"

bubble filteredListings =
  iterate _listings
  where_as l:
    (@viewer.category == "" || l.category == @viewer.category) &&
    (@viewer.location == "" || l.location == @viewer.location) &&
    l.bedrooms >= @viewer.minBedrooms &&
    (@viewer.maxPrice == 0.0 || l.price <= @viewer.maxPrice) &&
    (!@viewer.showOnlyAvailable || l.available)
  order_dyn (@viewer.sortBy != "" ? @viewer.sortBy : "-listedAt");

bubble resultCount = (iterate _listings
  where_as l:
    (@viewer.category == "" || l.category == @viewer.category) &&
    (@viewer.location == "" || l.location == @viewer.location) &&
    l.bedrooms >= @viewer.minBedrooms &&
    (@viewer.maxPrice == 0.0 || l.price <= @viewer.maxPrice) &&
    (!@viewer.showOnlyAvailable || l.available)
).size();

Problem: Show available filter options based on current results.

record Article {
  public int id;
  public string title;
  public string author;
  public string category;
  public string tags;
  public datetime publishedAt;

  index category;
  index author;
}

table<Article> _articles;

// Get unique categories with counts
public formula categoryCounts =
  (iterate _articles)
  reduce on category via @lambda x: x.size();

// Get unique authors with counts
public formula authorCounts =
  (iterate _articles)
  reduce on author via @lambda x: x.size();

view string selectedCategory;
view string selectedAuthor;

bubble filteredArticles =
  iterate _articles
  where_as a:
    (@viewer.selectedCategory == "" || a.category == @viewer.selectedCategory) &&
    (@viewer.selectedAuthor == "" || a.author == @viewer.selectedAuthor)
  order by publishedAt desc;

Saved Filters

Problem: Let users save and reuse filter configurations.

record SavedFilter {
  public int id;
  private principal owner;
  public string name;
  public string category;
  public string location;
  public int minBedrooms;
  public double maxPrice;
  public string sortBy;
}

table<SavedFilter> _savedFilters;

bubble myFilters = iterate _savedFilters where owner == @who order by name asc;

message SaveFilter {
  string name;
  string category;
  string location;
  int minBedrooms;
  double maxPrice;
  string sortBy;
}

channel saveFilter(SaveFilter msg) {
  _savedFilters <- {
    owner: @who,
    name: msg.name,
    category: msg.category,
    location: msg.location,
    minBedrooms: msg.minBedrooms,
    maxPrice: msg.maxPrice,
    sortBy: msg.sortBy
  };
}

message DeleteFilter {
  int filterId;
}

channel deleteFilter(DeleteFilter msg) {
  (iterate _savedFilters where id == msg.filterId && owner == @who).delete();
}

These patterns form the backbone of most Adama applications. The key insight is how privacy policies, reactive formulas, and the @connected/@who mechanism work together -- once you internalize that combination, you can adapt these patterns to fit almost anything.

Previous Patterns