Web Endpoints

Every Adama document is a webserver. I mean that literally — beyond real-time subscriptions via WebSocket, each document can handle standard HTTP requests directly. You can build REST APIs, serve HTML pages, handle webhooks, and integrate with external services, all from within your Adama document. No separate server needed.

Documents as Webservers

Each document is addressable via HTTP using a predictable URL structure:

https://$region.adama-platform.com/$space/$key/$path

Where:

  • $region is the deployment region (e.g., us-east-1)
  • $space is your Adama space name (the deployed script)
  • $key is the document key (the specific document instance)
  • $path is the path handled by your web handlers

So if you have a space called myapp with a document keyed user-123, accessing /profile would hit:

https://us-east-1.adama-platform.com/myapp/user-123/profile

GET Endpoints

Define GET endpoints to serve content when users or services request data:

@web get / {
  return {
    html: "<h1>Welcome to my document!</h1>"
  };
}

@web get /status {
  return {
    json: { alive: true, version: "1.0" }
  };
}

GET handlers are good for serving HTML pages, providing JSON APIs, delivering static assets like CSS and JavaScript, or returning document state in whatever format you need.

PUT and POST Endpoints

PUT and POST endpoints receive data from clients. Adama normalizes both methods and converts url-encoded bodies into JSON objects, treating POST as PUT internally.

Define a message type for the incoming data, then use it in your handler:

message ContactForm {
  string name;
  string email;
  string message;
}

@web put /contact (ContactForm form) {
  // Process the form data
  // You could store it in a table, send notifications, etc.
  return {
    json: { received: true, name: form.name }
  };
}

Webhook Integration

PUT endpoints are perfect for receiving webhooks from external services:

message TwilioSMS {
  string From;
  string Body;
}

@web put /webhook/sms (TwilioSMS sms) {
  // Store the incoming SMS
  // sms.From contains the sender's phone number
  // sms.Body contains the message text
  return {
    xml: "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Response><Message>Thanks for your message!</Message></Response>"
  };
}

Form Submissions

Handle HTML form submissions directly:

message RegistrationForm {
  string username;
  string email;
  string password;
}

@web put /register (RegistrationForm data) {
  // Validate and process registration
  if (data.username.length() < 3) {
    return {
      json: { error: "Username too short" },
      cors: true
    };
  }
  // Create user, etc.
  return {
    json: { success: true },
    cors: true
  };
}

DELETE Endpoints

DELETE endpoints handle resource deletion requests:

@web delete /session {
  // Clear session data, logout logic, etc.
  return {
    json: { logged_out: true }
  };
}

@web delete /cache {
  // Clear cached data
  return {
    json: { cleared: true }
  };
}

OPTIONS (CORS Preflight)

Browsers send OPTIONS requests before cross-origin requests. Handle these to enable CORS:

@web options / {
  return {
    cors: true
  };
}

@web options /api {
  return {
    cors: true
  };
}

When cors: true is set, Adama adds the appropriate CORS headers to allow browser-based cross-origin requests. It is worth noting that CORS is one of those things that's annoying to deal with in every framework, so I tried to make it as painless as possible here.

Combining CORS with Other Endpoints

For a complete CORS-enabled API, pair OPTIONS handlers with your GET/PUT handlers:

// Handle preflight
@web options /api/data {
  return { cors: true };
}

// Handle actual request
@web get /api/data {
  return {
    json: { items: [1, 2, 3] },
    cors: true
  };
}

Path Parameters

Capture dynamic segments of the URL using path parameters. Prefix the segment with $ and add the type annotation with a colon:

@web get /user/$id:int {
  return {
    json: { user_id: id }
  };
}

@web get /article/$slug:string {
  return {
    json: { article: slug }
  };
}

Wildcard Path Parameters

Capture the remainder of the URL path using the * suffix:

@web get /attachment/$bid:int/$aid:int/$vanity* {
  // bid : int, aid : int
  // vanity : string (captures rest of path, e.g., "photo.jpg" or "folder/photo.jpg")
  return { json: { bid: bid, aid: aid, vanity: vanity } };
}

@web get /preview/$bid:int/$aid:int/$transform:string/$vanity* {
  // transform : string, vanity captures remaining segments
  return { json: { transform: transform } };
}

The $name* wildcard captures everything after it in the URL path as a single string.

Multiple Path Parameters

Capture multiple segments in a single path:

@web get /org/$orgId:int/user/$userId:int {
  return {
    json: {
      organization: orgId,
      user: userId
    }
  };
}

Using Path Parameters with Data

Combine path parameters with document state:

record Product {
  public int id;
  public string name;
  public double price;
}

table<Product> products;

@web get /product/$id:int {
  if ((iterate products where id == id limit 1)[0] as product) {
    return {
      json: { name: product.name, price: product.price },
      cors: true
    };
  }
  return {
    json: { error: "Not found" },
    cors: true
  };
}

Response Fields Reference

The return value of a @web handler is a message that controls the response. You can include at most one body field plus optional modifiers.

Body Fields

Field Value Type Content-Type Description
json message application/json Converts the message to JSON
html string text/html Sends raw HTML
xml string application/xml Sends XML (useful for webhooks)
csv string text/csv Sends CSV data
asset asset varies Downloads an attached asset file
asset_transform string - Transform to apply to the asset (e.g., "stamp")
error string text/error Sends an error response
sign string - Creates a signed JWT token for the agent
identity string - Yields a pre-signed identity
forward string - HTTP 302 redirect (temporary)
redirect string - HTTP 301 redirect (permanent)

Modifier Fields

Field Value Type Description
cors bool When true, adds CORS headers allowing cross-origin access
cache_ttl_seconds int Enables caching for the specified number of seconds

Response Examples

JSON API Response:

@web get /api/status {
  return {
    json: { status: "ok", timestamp: Time.datetime() },
    cors: true,
    cache_ttl_seconds: 60
  };
}

HTML Page:

@web get /page {
  return {
    html: "<html><body><h1>Hello World</h1></body></html>"
  };
}

Asset Download:

private asset document_file;

@web get /download {
  return {
    asset: document_file
  };
}

// With transformation (e.g., image processing)
@web get /thumbnail {
  return {
    asset: document_file,
    asset_transform: "stamp"
  };
}

Redirect:

@web get /old_path {
  return {
    redirect: "/new_path"
  };
}

@web get /external {
  return {
    forward: "https://example.com"
  };
}

Cached Response:

@web get /static_data {
  return {
    json: { data: "This rarely changes" },
    cache_ttl_seconds: 3600  // Cache for 1 hour
  };
}

Special Constants

Web handlers have access to special constants for accessing request metadata.

@parameters

The @parameters constant gives you typed access to URL query parameters:

@web get /search {
  // Typed access with default values
  var query = @parameters.str("q", "");      // string param with default
  var page = @parameters.i("page", 0);       // int param with default

  return {
    json: { query: query, page: page }
  };
}
Method Return Type Description
.str(name, default) string Get string parameter with default value
.i(name, default) int Get integer parameter with default value

You can also access parameters dynamically:

@web get /flexible {
  // Access parameter using str method
  let raw = @parameters.str("q", "");
  return { json: { raw: raw } };
}

@headers

The @headers constant gives you access to HTTP headers as a map<string, string>:

@web get /info {
  let userAgent = @headers["User-Agent"];
  let contentType = @headers["Content-Type"];
  return {
    json: {
      user_agent: userAgent,
      content_type: contentType
    }
  };
}

Using Headers for Authentication

@web get /protected {
  let authHeader = @headers["Authorization"];
  if (authHeader == "") {
    return {
      json: { error: "Unauthorized" }
    };
  }
  // Validate the auth header...
  return {
    json: { data: "Secret stuff" }
  };
}

Complete API Example

Here's a REST-like API for a simple notes application. Nothing fancy — just showing how all the pieces fit together:

record Note {
  public int id;
  public string title;
  public string content;
  public datetime created_at;
}

table<Note> notes;
private int next_id = 1;

// CORS preflight for all API routes
@web options /api/notes {
  return { cors: true };
}

@web options /api/notes/$id:int {
  return { cors: true };
}

// List all notes
@web get /api/notes {
  return {
    json: { notes: iterate notes },
    cors: true
  };
}

// Get a specific note
@web get /api/notes/$id:int {
  if ((iterate notes where id == id limit 1)[0] as note) {
    return {
      json: { title: note.title, content: note.content },
      cors: true
    };
  }
  return {
    json: { error: "Note not found" },
    cors: true
  };
}

// Create a new note
message CreateNote {
  string title;
  string content;
}

@web put /api/notes (CreateNote data) {
  notes <- {
    id: next_id,
    title: data.title,
    content: data.content,
    created_at: Time.datetime()
  };
  next_id++;
  return {
    json: { created: true, id: next_id - 1 },
    cors: true
  };
}

// Delete a note
@web delete /api/notes/$id:int {
  (iterate notes where id == id).delete();
  return {
    json: { deleted: true },
    cors: true
  };
}

The thing I find satisfying about this is that the same document serving these HTTP endpoints can also have WebSocket subscribers getting real-time delta updates. You get REST and real-time from the same source of truth, with no synchronization headaches.

Previous State Machines
Next Services