Privacy

I hate the way most systems handle privacy. You build your data model, expose it through an API, and then try to bolt on access control after the fact. Maybe you add middleware, maybe you write a bunch of if (user.isAdmin()) checks scattered across your codebase, maybe you pray that nobody forgets one. The result is always the same — bugs, data leaks, and a constant low-grade anxiety about what you might have missed.

Adama does it differently. Privacy is in the data model itself, checked at compile time. Every field has a visibility rule. Every connected client sees a different projection of the same document. If you try to expose private data through a public field, the compiler rejects your code.

Is it perfect? No. But it's a hell of a lot better than the alternative.

Privacy Philosophy

In Adama, privacy is about ensuring that each viewer receives exactly the data they should see — nothing more, nothing less.

Compile-Time Enforcement

Privacy rules are checked by the Adama compiler. If you try to expose private data through a public field, the compiler will reject your code:

private int secret = 42;
public int exposed = secret;  // ERROR: Cannot expose private data

This means privacy bugs are caught during development, not discovered in production. I cannot overstate how much sleep this has saved me.

Differential Privacy Per Viewer

Each connected client sees a different view of the document. Consider a card game where players should only see their own hand:

record Card {
  private principal owner;
  use_policy<can_see> int value;

  policy can_see {
    return owner == @who;  // Only the card owner sees the value
  }
}

When Alice connects, she sees her cards' values but not Bob's. When Bob connects, he sees his cards but not Alice's. The underlying document is the same, but each viewer receives a tailored projection. This is the whole trick — one document, many views.

Private by Default

Fields without an explicit privacy modifier are private. I made this the default because secure-by-default is the only sane choice. You have to consciously decide to expose data:

int implicitly_private;       // Private - no viewer can see this
private int explicitly_private;  // Same as above, just explicit
public int everyone_sees;     // Must opt-in to public visibility

Privacy Modifiers Reference

Modifier Effect Use Case
public Anyone connected can see Shared game state, public counters
private No viewer can see (default) Internal calculations, secrets
viewer_is<field> Only the principal in field can see Player-specific data, personal info
use_policy<name> Visibility determined by code Complex conditional logic

The public Modifier

The public modifier makes a field visible to all connected clients:

public int score = 0;           // Everyone sees the score
public string gameName = "Chess";  // Everyone sees the game name
public bool gameOver = false;   // Everyone sees if game ended

Use public sparingly and intentionally. Ask yourself: "Does every connected user need to see this?" If the answer is no, don't make it public.

The private Modifier

The private modifier hides data from all viewers. Since private is the default, these declarations are equivalent:

private int internal_counter = 0;
int also_private = 0;  // Default is private

Private fields are perfect for:

  • Internal state tracking
  • Calculations that drive other fields
  • Sensitive data that should never leave the server
private int moves_made = 0;     // Track internally
private string admin_notes;      // Server-side only
private double internal_score;   // Used in formulas

The viewer_is Modifier

When you want a specific principal to see data, use viewer_is:

private principal owner;
viewer_is<owner> int secret_balance = 1000;
viewer_is<owner> string private_notes = "";

The field inside the angle brackets must be of type principal. Only when @who (the current viewer) matches that principal will the data be visible. Simple, declarative, no boilerplate.

Multiple Owner Fields

You can have different fields visible to different principals:

record SharedDocument {
  private principal author;
  private principal editor;

  public string title;
  viewer_is<author> string author_notes;
  viewer_is<editor> string editor_notes;
}

In this example:

  • Everyone sees the title
  • Only the author sees author_notes
  • Only the editor sees editor_notes

The use_policy Modifier

For visibility rules that can't be expressed with a simple principal check, use use_policy:

use_policy<can_see_balance> int balance = 1000;

policy can_see_balance {
  return @who != @no_one;  // Any authenticated user
}

Policies can access any field in scope and perform arbitrary logic.

Combining with Other State

Policies can reference other fields to make dynamic decisions:

private bool game_over = false;
private principal winner;
use_policy<reveal_winner> principal the_winner;

policy reveal_winner {
  return game_over;  // Only show winner when game ends
}

The @who Constant

The @who constant represents the current viewer — the principal who is observing the document. It's fundamental to privacy logic:

private principal owner;

policy is_me {
  return @who == owner;  // True if viewer owns this record
}

Common @who Patterns

private principal document_owner;

record Admin {
  public principal who;
}

table<Admin> admins;

// Check if viewer is authenticated
policy authenticated {
  return @who != @no_one;
}

// Check if viewer is the document owner
policy is_owner {
  return @who == document_owner;
}

// Check if viewer is in a list of admins
policy is_admin {
  return (iterate admins where who == @who).size() > 0;
}

The @no_one Principal

@no_one represents the absence of a principal — an anonymous or unauthenticated user:

private principal assigned_to;

@construct {
  assigned_to = @no_one;  // Not yet assigned to anyone
}

policy has_owner {
  return assigned_to != @no_one;
}

Policy Definitions

Policies are named boolean expressions that determine visibility. Define them with the policy keyword:

policy policy_name {
  return boolean_expression;
}

Policy Syntax

record Card {
  private bool face_up = false;
  private principal holder;

  use_policy<is_visible> int rank;
  use_policy<is_visible> string suit;

  policy is_visible {
    // Card is visible if face up OR viewer holds it
    return face_up || holder == @who;
  }
}

Policies Can Access Fields

Policies have full access to the record's fields:

record BankAccount {
  private principal owner;
  private bool account_public = false;
  private int balance;

  use_policy<can_view_balance> int visible_balance;

  policy can_view_balance {
    // Owner always sees, others only if account is public
    if (@who == owner) {
      return true;
    }
    return account_public && balance > 0;
  }
}

Policies Must Return Boolean

Every policy must return a boolean value. The compiler will reject policies that don't:

policy valid_policy {
  return @who == owner;  // Returns bool - OK
}

policy invalid_policy {
  return 42;  // ERROR: Must return boolean
}

Multiple Policies

Apply multiple policies to a field by listing them with commas. The field is visible only if ALL policies return true:

record BannedUser {
  public principal who;
}

table<BannedUser> banned_users;

int permission_level;

use_policy<is_authenticated, is_not_banned, has_permission> int sensitive_data;

policy is_authenticated {
  return @who != @no_one;
}

policy is_not_banned {
  return (iterate banned_users where who == @who).size() == 0;
}

policy has_permission {
  return permission_level >= 2;
}

This is equivalent to an AND operation:

// Conceptually: is_authenticated && is_not_banned && has_permission

The require Keyword

While policies control field visibility, require controls the visibility of an entire record. If the required policy returns false, the record appears not to exist — not just empty or redacted, but genuinely absent from query results:

record PrivateNote {
  private principal owner;
  public string content;

  policy is_owner {
    return @who == owner;
  }

  require is_owner;  // Entire record hidden if not owner
}

table<PrivateNote> notes;

When Alice queries the notes table, she only sees her own notes. Bob's notes are completely invisible to her — not just their content, but their very existence.

Multiple require Statements

You can require multiple policies:

record SensitiveRecord {
  private principal owner;
  private bool is_active = true;

  public string data;

  policy is_owner {
    return @who == owner;
  }

  policy is_visible {
    return is_active;
  }

  require is_owner;
  require is_visible;
}

The record is only visible if ALL required policies pass.

require vs use_policy

Feature require use_policy
Scope Entire record Single field
When false Record does not exist Field value is hidden
Use case Hide records from unauthorized users Hide specific sensitive fields

Document-Level Access Control

Beyond field and record privacy, Adama provides document-level access control through special handlers.

The @connected Event

The @connected event determines who can connect to a document. Return true to allow the connection, false to reject it:

private principal owner;

@construct {
  owner = @who;  // Creator becomes owner
}

@connected {
  // Only owner and anonymous users can connect
  if (@who == owner) {
    return true;
  }
  if (@who.isAnonymous()) {
    return true;
  }
  return false;
}

If @connected returns false, the user cannot establish a connection and receives no data from the document. Full stop.

The @disconnected Event

Clean up when users disconnect:

public int active_users = 0;

@connected {
  active_users++;
  return true;
}

@disconnected {
  active_users--;
}

Static Creation Policies

Control who can create documents using the @static block:

@static {
  // Control explicit document creation
  create {
    return @who != @no_one;  // Must be authenticated
  }

  // Control document invention (create-on-connect)
  invent {
    return true;  // Anyone can invent
  }
}

The create policy controls direct document creation via the API. The invent policy controls whether documents can be created automatically when someone connects to a non-existent document.

Advanced Static Policies

Access context information in static policies:

@static {
  create {
    // Only allow creation from specific origins
    return @context.origin == "https://myapp.com" ||
           @who.isAdamaDeveloper();
  }

  invent {
    // Allow anonymous users to create ephemeral documents
    return @who.isAnonymous();
  }

  // Document configuration
  maximum_history = 100;
  delete_on_close = true;  // Delete when all users disconnect
}

Channel-Level Access Control

Policies can be applied to channels using the requires<policy_name> keyword. This is a declarative guard that rejects messages from principals who don't satisfy the policy — the handler body never even executes:

record Person {
  public int id;
  public principal account;
  public bool is_admin;
  public bool is_staff;
  public string name;
}

table<Person> _people;

record Bulletin {
  public string title;
  public string content;
}

table<Bulletin> _bulletins;

message JustId {
  int id;
}

message DraftCoords {
  string title;
  string content;
}

policy is_admin {
  if ((iterate _people where account == @who)[0] as person) {
    return person.is_admin;
  }
  return false;
}

policy is_staff {
  if ((iterate _people where account == @who)[0] as person) {
    return person.is_staff;
  }
  return false;
}

// Only admins can delete
channel person_delete(JustId jid) requires<is_admin> {
  (iterate _people where id == jid.id).delete();
}

// Staff and above can create bulletins
channel bulletin_create(DraftCoords dc) requires<is_staff> {
  _bulletins <- dc as new_id;
}

The requires<policy_name> guard runs before the channel body. If the policy returns false, the message is rejected and the handler never executes. This is the pattern I recommend for production — it's cleaner than scattering authorization checks throughout your handler bodies.

Privacy on Formulas

Policies can also be applied to formulas using use_policy<P>, controlling who can see computed values:

record Person {
  public principal account;
  public bool is_admin;
  public bool is_staff;
  public bool is_coach;
  public string name;
}

table<Person> _people;

formula compute_authors_edit = iterate _people where is_staff;

policy is_admin {
  if ((iterate _people where account == @who)[0] as person) {
    return person.is_admin;
  }
  return false;
}

policy is_admin_or_staff {
  if ((iterate _people where account == @who)[0] as person) {
    return person.is_admin || person.is_staff;
  }
  return false;
}

use_policy<is_admin> formula coaches = iterate _people where is_coach;
use_policy<is_admin> string notes_markdown;
use_policy<is_admin_or_staff> formula authors_edit = compute_authors_edit;

This restricts the formula result to viewers who satisfy the policy, just like use_policy on regular fields.

Privacy Bubbles

While policies filter shared data, bubbles compute viewer-specific data. A bubble is a formula that incorporates @who:

record Card {
  private principal holder;
  public int id;
}

table<Card> deck;

// Each viewer sees only their own cards
bubble my_hand = iterate deck where holder == @who;

Policy-Gated Bubbles

Bubbles can be restricted to viewers who satisfy a policy using the bubble<policy_name> syntax:

record Person {
  public principal account;
  public bool is_admin;
  public bool is_staff;
  public string name;
}

table<Person> _people;

formula compute_messaging_teams = iterate _people where is_staff;

policy is_admin {
  if ((iterate _people where account == @who)[0] as person) {
    return person.is_admin;
  }
  return false;
}

policy is_staff {
  if ((iterate _people where account == @who)[0] as person) {
    return person.is_staff;
  }
  return false;
}

policy is_admin_or_staff {
  if ((iterate _people where account == @who)[0] as person) {
    return person.is_admin || person.is_staff;
  }
  return false;
}

// Only admins see the full people list
bubble<is_admin> people = iterate _people;

// Only staff see this view
bubble<is_staff> staff_view = iterate _people where is_staff;

// Staff and above see messaging teams
bubble<is_admin_or_staff> messaging_teams = compute_messaging_teams;

This is the production-recommended pattern for role-based data access. More declarative, less error-prone.

Bubbles are covered in more detail in the Formulas chapter, but they're inseparable from Adama's privacy model — they allow efficient, viewer-dependent queries without exposing underlying data.

Complete Privacy Example

Here's a bigger example showing multiple privacy features working together:

// Static policies for document creation
@static {
  create { return @who != @no_one; }
  invent { return true; }
}

// Document-level access
private principal document_owner;

@construct {
  document_owner = @who;
}

@connected {
  return @who == document_owner || @who.isAnonymous();
}

// User record with layered privacy
record User {
  private principal identity;
  public string display_name;
  viewer_is<identity> string email;
  viewer_is<identity> int private_score;
  use_policy<can_see_stats> int public_score;

  policy can_see_stats {
    return show_leaderboard || identity == @who;
  }
}

// Control leaderboard visibility
public bool show_leaderboard = false;

table<User> users;

// Each user sees their own profile
bubble my_profile = iterate users where identity == @who limit 1;

// Leaderboard uses policy-controlled field
public formula leaderboard = iterate users
                             order by public_score desc
                             limit 10;

In this example:

  • Document creation requires authentication
  • Only the owner and anonymous users can connect
  • Each user's email and private score are visible only to themselves
  • Public scores are visible to everyone when leaderboard is enabled
  • Each user has a personal bubble showing their own profile

That's defense in depth. Static policies, connection control, field-level privacy, record-level require, policy-gated bubbles — layer upon layer. The compiler checks all of it. You sleep better at night.

Previous Formulas
Next Channels