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:
$regionis the deployment region (e.g.,us-east-1)$spaceis your Adama space name (the deployed script)$keyis the document key (the specific document instance)$pathis 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.