Getting Started
This is a longer, step‑by‑step tutorial that builds on the short Quickstart. If you’re new to ActingWeb, do the Quickstart first (minimal Flask/FastAPI app, create an actor), then come back here for deeper patterns, hooks, and integration details.
The easiest way to get started is to use the modern ActingWeb interface that provides a clean, fluent API for building ActingWeb applications. For a complete example, see the actingwebdemo mini-application at http://acting-web-demo.readthedocs.io/.
Quick Start
Create a basic ActingWeb application:
from actingweb.interface import ActingWebApp, ActorInterface
# Create app with fluent configuration
app = ActingWebApp(
aw_type="urn:actingweb:example.com:myapp",
database="dynamodb",
fqdn="myapp.example.com"
).with_web_ui().with_devtest()
# Initialize actors after creation
@app.lifecycle_hook("actor_created")
def on_actor_created(actor: ActorInterface, **kwargs):
# Set the creator email as a property
actor.properties.email = actor.creator
# Add property hooks
@app.property_hook("email")
def handle_email(actor, operation, value, path):
if operation == "put":
# value may be non-string or None on delete — guard before using it
if isinstance(value, str) and "@" in value:
return value.lower()
return None
return value
# Run the application
app.run(port=5000)
Flask Integration
For production applications, integrate with Flask:
from flask import Flask
from actingweb.interface import ActingWebApp
# Create Flask app
flask_app = Flask(__name__)
# Create ActingWeb app
aw_app = ActingWebApp(
aw_type="urn:actingweb:example.com:myapp",
database="dynamodb",
fqdn="myapp.example.com"
).with_oauth(
provider="google",
client_id="your-client-id",
client_secret="your-client-secret"
).with_web_ui()
# Integrate with Flask (auto-generates all routes)
aw_app.integrate_flask(flask_app)
if __name__ == "__main__":
flask_app.run()
FastAPI Integration (non‑MCP)
If you prefer FastAPI and do not need MCP features:
from fastapi import FastAPI
from actingweb.interface import ActingWebApp
app = FastAPI()
aw = ActingWebApp(
aw_type="urn:actingweb:example.com:myapp",
database="dynamodb",
fqdn="myapp.example.com",
).with_web_ui(enable=True)
# Explicitly disable MCP exposure for this app
aw.with_mcp(enable=False)
# Auto-generate all ActingWeb routes under the FastAPI app.
# templates_dir is optional — ActingWeb ships default web-UI templates, so
# the UI works out of the box. Pass templates_dir="templates" only to
# override individual templates with your own.
aw.integrate_fastapi(app)
# Run with: uvicorn main:app --reload
How it works
An ActingWeb mini-application exposes an endpoint to create a new actor representing one instance on behalf of one person or entity. This could for example be the location of a mobile phone, and the app is thus a location app. The ActingWeb actor representing one mobile phone’s location can be reached on https://app-url.a-domain.io/actor-id and all the ActingWeb endpoints to get the location, subscribe to location updates and so on can be found below this actor root URL.
The modern interface automatically generates all the necessary routes and handles request/response transformation. You no longer need to manually define routes or handle complex request parsing.
Actor Management
Creating and managing actors is straightforward:
# Create a new actor (config comes from your ActingWebApp instance).
# Pass hooks=app.hooks so lifecycle hooks (e.g. "actor_created") fire on
# creation — without it, ActorInterface.create() creates the actor but runs
# no lifecycle hooks. (Creating over HTTP via the factory endpoint always
# runs the hooks.)
config = app.get_config()
actor = ActorInterface.create(
creator="user@example.com", config=config, hooks=app.hooks
)
# Access properties
actor.properties.email = "user@example.com"
actor.properties.status = "active"
# Manage trust relationships
peer = actor.trust.create_relationship(
peer_url="https://peer.example.com/actor123",
relationship="friend"
)
# Handle subscriptions
actor.subscriptions.subscribe_to_peer(
peer_id="peer123",
target="properties"
)
# Notify subscribers of changes
actor.subscriptions.notify_subscribers(
target="properties",
data={"status": "active"}
)
Note
Actors can also be created over REST via the factory endpoint
(POST / with a JSON {"creator": "..."} body), which is how a browser
sign-up flow or an external client creates them. See
Quickstart for the HTTP request/response. The factory path runs the
actor_created lifecycle hook automatically.
Configuration
The modern interface uses a fluent configuration API that’s much simpler than the old approach:
app = ActingWebApp(
aw_type="urn:actingweb:example.com:myapp",
database="dynamodb",
fqdn="myapp.example.com",
proto="https://"
)
# Chain configuration methods
app.with_oauth(
client_id="your-client-id",
client_secret="your-client-secret",
scope="read write"
).with_web_ui(enable=True
).with_devtest(enable=True
).with_bot(
token="bot-token",
email="bot@example.com"
).with_unique_creator(enable=True
).add_actor_type("myself", relationship="friend")
All Configuration Options
The ActingWebApp constructor accepts these parameters:
aw_type: The ActingWeb type URI (required)database: Database backend (“dynamodb”, default)fqdn: Fully qualified domain name (required)proto: Protocol (”https://”, default)
Configuration methods:
.with_oauth(provider, client_id, client_secret, scope, ...)- Configure OAuth authentication (call multiple times for multiple providers).with_web_ui(enable=True)- Enable/disable web UI at /www.with_devtest(enable=True)- Enable/disable development endpoints (MUST be False in production).with_bot(token, email, secret, admin_room)- Configure bot integration.with_unique_creator(enable=True)- Enforce unique creator field.with_email_as_creator(enable=True)- Use email property as creator.add_actor_type(name, factory, relationship)- Add known actor type
Customizing Behavior with Hooks
The modern interface uses a hook system instead of the old OnAWBase class. Hooks are focused functions that handle specific events:
Property Hooks
Handle property access and validation:
@app.property_hook("email")
def handle_email_property(actor, operation, value, path):
if operation == "get":
# Reads/transforms happen here (runs for every accessor).
# Restrict who may read via the permission system / authenticated
# views, not from inside the hook — see the SDK docs.
return value
elif operation == "put":
# Validate email format (value may be non-string or None)
if isinstance(value, str) and "@" in value:
return value.lower()
return None
return value
@app.property_hook("settings")
def handle_settings_property(actor, operation, value, path):
if operation == "put" or operation == "post":
# Ensure settings is always a dict
if isinstance(value, str):
import json
try:
return json.loads(value)
except:
return None
return value if isinstance(value, dict) else {}
return value
Callback Hooks
Handle custom endpoints and bot integration:
@app.callback_hook("bot")
def handle_bot_callback(actor, name, data):
if data.get("method") == "POST":
# Process bot request
body = data.get("body", {})
# Handle bot integration logic
return True
return False
@app.callback_hook("status")
def handle_status_callback(actor, name, data):
if data.get("method") == "GET":
return {
"status": "active",
"actor_id": actor.id,
"last_seen": str(datetime.now())
}
return False
Subscription Hooks
Handle subscription callbacks from other actors:
@app.subscription_hook
def handle_subscription_callback(actor, subscription, peer_id, data):
print(f"Received subscription callback from {peer_id}: {data}")
# Process the subscription data
if subscription.get("target") == "properties":
# Handle property changes from peer
if "status" in data:
actor.properties[f"peer_{peer_id}_status"] = data["status"]
return True
Lifecycle Hooks
Handle actor lifecycle events:
@app.lifecycle_hook("actor_created")
def on_actor_created(actor, **kwargs):
# Initialize new actor
actor.properties.created_at = str(datetime.now())
actor.properties.version = "1.0"
@app.lifecycle_hook("actor_deleted")
def on_actor_deleted(actor, **kwargs):
# Cleanup before deletion
print(f"Actor {actor.id} is being deleted")
@app.lifecycle_hook("oauth_success")
def on_oauth_success(actor, **kwargs):
token = kwargs.get("token")
if token:
actor.properties.oauth_token = token
Migration from Legacy Interface
Warning
Breaking Change in v3.1: The legacy OnAWBase interface has been completely removed.
If you’re migrating from the old interface, all code using OnAWBase must be updated
to use the modern hook system. See ActingWeb v3.1 Migration Guide for detailed migration instructions.
The modern hook system provides better organization, type safety, and testing capabilities compared to the legacy interface.
Database Configuration
ActingWeb supports two production-ready database backends: DynamoDB (default)
and PostgreSQL. Select one with the database argument (or the
DATABASE_BACKEND environment variable, which takes precedence):
app = ActingWebApp(
aw_type="urn:actingweb:example.com:myapp",
database="dynamodb", # or "postgresql"
fqdn="localhost:5000"
)
For the full backend comparison, PostgreSQL setup, and migration guidance, see Configuration Reference and Database Backends Reference.
For production with DynamoDB, ensure your AWS credentials are properly configured and DynamoDB tables are created with the appropriate permissions.
For DynamoDB Local, set the following environment variables before running your app:
export AWS_ACCESS_KEY_ID=local
export AWS_SECRET_ACCESS_KEY=local
export AWS_DEFAULT_REGION=us-east-1
export AWS_DB_HOST=http://localhost:8000
Testing
The modern interface makes testing much easier:
import unittest
from actingweb.interface import ActingWebApp, ActorInterface
class TestMyApp(unittest.TestCase):
def setUp(self):
self.app = ActingWebApp(
aw_type="urn:test:example.com:test",
database="dynamodb"
)
def test_actor_creation(self):
actor = ActorInterface.create(
creator="test@example.com",
config=self.app.get_config()
)
self.assertIsNotNone(actor.id)
self.assertEqual(actor.creator, "test@example.com")
def test_property_hook(self):
@self.app.property_hook("email")
def handle_email(actor, operation, value, path):
return value.lower() if operation == "put" else value
# Test hook directly
actor = ActorInterface.create(
creator="test@example.com",
config=self.app.get_config()
)
result = handle_email(actor, "put", "TEST@EXAMPLE.COM", [])
self.assertEqual(result, "test@example.com")
Deployment
For production deployment, use standard Python deployment practices:
Docker:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
AWS Lambda (Serverless):
import serverless_wsgi
from flask import Flask
from actingweb.interface import ActingWebApp
flask_app = Flask(__name__)
aw_app = ActingWebApp(...).with_web_ui()
aw_app.integrate_flask(flask_app)
def handler(event, context):
return serverless_wsgi.handle_request(flask_app, event, context)
Kubernetes:
apiVersion: apps/v1
kind: Deployment
metadata:
name: actingweb-app
spec:
replicas: 3
selector:
matchLabels:
app: actingweb-app
template:
metadata:
labels:
app: actingweb-app
spec:
containers:
- name: actingweb-app
image: myapp:latest
ports:
- containerPort: 5000
Next Steps
See the Developer API guide for detailed API documentation
Check out the actingwebdemo application for a complete working example
Read the ActingWeb specification for protocol details
Join the ActingWeb community for support and discussion
The modern ActingWeb interface makes it easy to build distributed, actor-based applications with minimal boilerplate code while maintaining full compatibility with the ActingWeb protocol.