Security
Adama approaches security differently than most systems. Rather than bolting security onto an API layer and hoping nothing slips through, Adama embeds privacy and access control directly into the language. The compiler enforces security policies before your code runs. I think this is one of the most important design decisions in the whole system -- and the one I'm most proud of.
This chapter covers authentication mechanisms, privacy enforcement, policy best practices, and secure configuration patterns.
Security Architecture
Adama's security model operates at multiple layers:
| Layer | Mechanism | Enforcement |
|---|---|---|
| Document creation | Static policies | Compile time |
| Connection | @connected handler | Runtime |
| Record visibility | require policies | Runtime |
| Field visibility | Privacy modifiers | Compile time |
| Message handling | Channel guards | Runtime |
| External access | Web handlers | Runtime |
Each layer provides defense in depth. An attacker would have to bypass multiple controls to access unauthorized data. That's the point.
Authentication Mechanisms
Authentication in Adama establishes who is connecting to a document. The system supports multiple patterns.
The Principal Type
Every connected user is represented by a principal -- a typed identity that cannot be forged:
private principal document_owner;
@connected {
// @who is the connecting principal
return @who == document_owner;
}
Principals come from the authentication layer. The Adama runtime validates principals before they reach your code, so by the time you see @who, you know it's legit.
Anonymous Access
Anonymous users connect with a special anonymous principal:
@connected {
if (@who.isAnonymous()) {
// Allow read-only anonymous access
return true;
}
// Require authentication for full access
return @who != @no_one;
}
Anonymous access is useful for public-facing features -- read-only dashboards, game spectating, that sort of thing.
Adama Developer Authentication
Users authenticated through the Adama platform have verified identities:
@connected {
if (@who.isAdamaDeveloper()) {
// Trusted platform user
return true;
}
return false;
}
Custom Authentication
For integrating with external identity providers, use authorization channels:
message AuthRequest {
string email;
}
@authorization (AuthRequest req) {
// Look up user and validate credentials
return {
agent: req.email,
hash: "password_hash_here"
};
}
The authorization handler receives a message with credentials and returns a response containing an agent identifier and a password hash for validation.
Compile-Time Privacy Enforcement
This is where things get interesting. Adama's most powerful security feature is compile-time privacy checking. The compiler prevents you from accidentally exposing private data. Not at runtime. Not in testing. At compile time.
Privacy Modifiers
Every field must have a privacy modifier:
private principal owner;
policy can_see {
return @who == owner;
}
private int internal_state; // Never exposed
public int shared_state; // Visible to all viewers
viewer_is<owner> int personal_data; // Visible only to owner
use_policy<can_see> int controlled_data; // Visibility determined by policy
Compile-Time Guarantees
The compiler enforces privacy rules:
private int secret = 42;
// ERROR: Cannot assign private value to public field
public int exposed = secret; // Compiler rejects this
// OK: Formula can compute from private data
public formula safe = secret > 0; // Boolean result doesn't leak value
This means privacy bugs are caught during development, not discovered in production when someone reports that they can see data they shouldn't. I cannot overstate how much better this is than the traditional approach.
Data Flow Analysis
The compiler tracks data flow to prevent leaks:
message UpdateMsg {
string newDisplay;
}
private string password;
public string display;
channel updateDisplay(UpdateMsg msg) {
// ERROR: Cannot assign private data to public field
display = password; // Compiler rejects this
// OK: Assign message data
display = msg.newDisplay;
}
Policy Best Practices
Policies control access to documents, records, and fields. Well-designed policies are the foundation of a secure application.
Document-Level Policies
Control who can create and access documents:
@static {
// Who can explicitly create documents
create {
// Require authenticated user
return @who != @no_one;
}
// Who can create documents by connecting to non-existent key
invent {
// Only allow invention for specific patterns
return @context.key.startsWith("public-");
}
}
Connection Policies
The @connected handler is your first line of defense:
private principal owner;
record MemberRecord {
public principal who;
}
table<MemberRecord> members;
@connected {
// Owner always allowed
if (@who == owner) {
return true;
}
// Members allowed
if ((iterate members where who == @who).size() > 0) {
return true;
}
// Everyone else rejected
return false;
}
Returning false from @connected prevents the user from seeing any document data. Full stop.
Record-Level Policies
Use require to hide entire records:
record PrivateNote {
private principal author;
public string content;
policy is_author {
return @who == author;
}
require is_author; // Record invisible to non-authors
}
table<PrivateNote> notes;
Users can't see records they aren't authorized to view -- not even their existence. The record simply doesn't appear in their view of the data.
Field-Level Policies
Apply policies to specific fields:
record AdminRecord {
public principal who;
}
table<AdminRecord> admins;
record UserProfile {
public string name;
use_policy<is_self> string email;
use_policy<is_admin> string internal_notes;
private principal user_id;
policy is_self {
return @who == user_id;
}
policy is_admin {
return (iterate admins where who == @who).size() > 0;
}
}
Policy Composition
Combine policies for complex rules:
record BannedUser {
public principal who;
}
table<BannedUser> banned_users;
public int permission_level;
use_policy<is_authenticated, is_not_banned, has_permission> int sensitive;
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 >= 3;
}
The field is visible only if ALL policies return true. Chain as many as you need.
Channel Security
Channels are how clients send messages to documents. Secure them carefully -- they're your input surface.
Channel Guards
Use guards to restrict who can send to a channel:
message AdminRequest {
string action;
}
record AdminRecord {
public principal who;
}
table<AdminRecord> admins;
channel adminAction(AdminRequest req) {
if ((iterate admins where who == @who).size() == 0) {
return; // Silently ignore non-admin requests
}
// Process admin action
}
Message Validation
Validate message content before processing. Trust nothing from the client.
message Transfer {
int amount;
principal recipient;
}
record UserRecord {
public principal who;
}
table<UserRecord> users;
int MAX_TRANSFER = 10000;
procedure processTransfer(principal from, principal to, int amount) {
// transfer logic
}
channel transfer(Transfer msg) {
// Validate amount
if (msg.amount <= 0 || msg.amount > MAX_TRANSFER) {
return; // Invalid amount
}
// Validate recipient exists
if ((iterate users where who == msg.recipient).size() == 0) {
return; // Unknown recipient
}
// Safe to process
processTransfer(@who, msg.recipient, msg.amount);
}
Rate Limiting
Protect against abuse by tracking message frequency:
message ActionMsg {
string action;
}
record RateLimit {
private principal user;
private datetime last_action;
private int action_count;
}
table<RateLimit> rate_limits;
channel sensitiveAction(ActionMsg msg) {
// Find or create rate limit record
list<RateLimit> limit = iterate rate_limits where user == @who;
if (limit.size() > 0) {
if (limit[0] as record) {
timespan elapsed = record.last_action.between(Time.datetime());
double elapsedSec = elapsed.seconds();
if (elapsedSec < 60.0 && record.action_count >= 10) {
return; // Rate limited
}
}
if (limit[0] as record) {
timespan elapsed = record.last_action.between(Time.datetime());
if (elapsed.seconds() >= 60.0) {
limit.action_count = 1;
} else {
limit.action_count = record.action_count + 1;
}
}
limit.last_action = Time.datetime();
} else {
rate_limits <- { user: @who, last_action: Time.datetime(), action_count: 1 };
}
// Process action
}
Web Endpoint Security
HTTP endpoints need explicit security configuration.
Authentication in Web Handlers
Web handlers receive authentication context:
record ProfileRecord {
public principal owner;
public string name;
}
table<ProfileRecord> profiles;
@web get /api/profile {
if (@who == @no_one) {
return {
json: { error: "Authentication required" },
status: 401
};
}
if ((iterate profiles where owner == @who)[0] as found) {
return {
json: { name: found.name },
status: 200
};
}
return {
json: { error: "Not found" },
status: 404
};
}
Input Validation
Always validate request input:
message UpdateSettings {
string theme;
}
record Settings {
public string theme;
}
Settings settings;
@web put /api/settings (UpdateSettings body) {
// Validate required fields
if (body.theme == "") {
return { json: { error: "Theme required" }, status: 400 };
}
// Validate allowed values
if (body.theme != "light" && body.theme != "dark") {
return { json: { error: "Invalid theme" }, status: 400 };
}
// Safe to apply
settings.theme = body.theme;
return { json: { success: true }, status: 200 };
}
CORS Configuration
Control cross-origin access:
@web options /api {
return {cors: true};
}
Secure Configuration
Protect your deployment configuration.
Secrets Management
Never hardcode secrets in Adama code. I know this seems obvious, but I'm saying it anyway:
// WRONG - Secret in code
private string api_key = "sk_live_abc123";
// RIGHT - Reference configured service
service payment {
class = "http";
// Credentials configured externally
}
Configure secrets through the service layer or environment:
java -jar adama.jar service config set --space myapp --name payment --key apikey --value $API_KEY
Configuration Security
Protect configuration files:
# Restrict configuration file permissions
chmod 600 ~/.adama
chmod 600 config/production.json
# Use environment variables for sensitive values
export ADAMA_CONFIG=/secure/path/config.json
Network Security
Secure network configuration:
- Use TLS: Always terminate TLS at the load balancer or reverse proxy
- Restrict ports: Only expose necessary ports
- Firewall rules: Limit source IPs where possible
- Internal networks: Keep Adama on private networks when possible
# Example nginx TLS termination
server {
listen 443 ssl;
ssl_certificate /etc/ssl/certs/myapp.crt;
ssl_certificate_key /etc/ssl/private/myapp.key;
location / {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Common Security Patterns
Owner-Based Access
Documents owned by a specific user:
private principal owner;
@construct {
owner = @who;
}
@connected {
return @who == owner;
}
Role-Based Access
Multiple roles with different permissions:
enum Role { Admin, Editor, Viewer }
record Member {
private principal user;
private Role role;
}
table<Member> members;
policy is_admin {
return (iterate members where user == @who && role == Role::Admin).size() > 0;
}
policy is_editor {
return (iterate members where user == @who && (role == Role::Admin || role == Role::Editor)).size() > 0;
}
policy is_viewer {
return (iterate members where user == @who).size() > 0;
}
Shared with Specific Users
Documents shared with an explicit list:
private principal owner;
record SharedUser {
public principal who;
}
table<SharedUser> shared_with;
message ShareMsg {
principal user;
}
@connected {
if (@who == owner) return true;
return (iterate shared_with where who == @who).size() > 0;
}
channel share(ShareMsg msg) {
if (@who != owner) return; // Only owner can share
shared_with <- { who: msg.user };
}
Time-Based Access
Access that expires:
private datetime access_expires;
@connected {
if (Time.datetime() > access_expires) {
return false; // Access expired
}
return true;
}
Audit Logging
Track security-relevant events:
message ActionMsg {
string action;
}
record AuditLog {
private datetime timestamp;
private principal actor;
private string action;
private string details;
}
table<AuditLog> audit_log;
procedure logAction(principal who, string action, string details) {
audit_log <- {
timestamp: Time.datetime(),
actor: who,
action: action,
details: details
};
}
channel sensitiveAction(ActionMsg msg) {
logAction(@who, "sensitive_action", "Performed by " + @who.agent());
// ... perform action
}
Security Checklist
Before deploying to production:
Code Review
- [ ] All fields have appropriate privacy modifiers
- [ ]
@connectedhandler restricts access appropriately - [ ] Static create/invent policies are configured
- [ ] Channels validate input before processing
- [ ] No secrets hardcoded in Adama code
Configuration
- [ ] TLS enabled in production
- [ ] Configuration files have restricted permissions
- [ ] Secrets stored securely, not in version control
- [ ] Unnecessary ports blocked by firewall
Testing
- [ ] Privacy policies tested with multiple users
- [ ] Unauthorized access attempts fail appropriately
- [ ] Edge cases in policies verified
Monitoring
- [ ] Audit logging enabled for sensitive actions
- [ ] Alerting configured for authentication failures
- [ ] Rate limiting configured for sensitive operations
At the end of the day, Adama's approach means security isn't an afterthought -- it's a property of your application that the compiler helps you maintain. The compiler catches accidental data exposure. The privacy system enforces who sees what. Combined with careful configuration and monitoring, this gets you to a strong security posture with less effort than the traditional "bolt it on and pray" approach.