P2P Quickstart
This quickstart gets two actors talking peer-to-peer: actor A publishes a
property change, and actor B – who has established trust with actor A and
subscribed to its "properties" target – receives that change through a
subscription data hook. Both actors are served by the same ActingWeb app;
the trust handshake between them is a real HTTP call regardless of whether
the two actors happen to share a process.
The full application code below is
examples/p2p_quickstart.py,
included directly by this page (not copied), so what you read here is
exactly what runs.
Install
# pip
pip install 'actingweb[fastapi]'
# or with Poetry
poetry add actingweb -E fastapi
You also need a database. For local development, start DynamoDB Local:
docker compose -f docker-compose.test.yml up dynamodb-test
See Quickstart for PostgreSQL and other backend options.
Both Sides in One App
Peer-to-peer subscriptions require .with_subscription_processing() –
without it, @app.subscription_data_hook never fires (only the raw
@app.subscription_hook does; see Hooks Reference).
auto_sequence=True is what makes the receiving hook below get already-
sequenced, deduplicated, stored data instead of raw callback payloads.
The trust_request_received lifecycle hook auto-approves an incoming
trust request. It has to: create_relationship() (below) only approves
the relationship on the side that initiates it – the peer’s side stays
unapproved until the peer approves it too, and a subscription request is
rejected with 403 unless both sides are approved. This is demo-only
auto-approval; see the Security Note near the bottom of this page before
using this pattern for real trust requests.
app = (
ActingWebApp(
aw_type="urn:actingweb:example.com:p2p",
database="dynamodb",
fqdn=os.getenv("APP_HOST_FQDN", "localhost:5000"),
# Defaults to http:// for local development. Subscription callbacks
# are outbound HTTP calls the library makes to the URL it recorded
# for the peer at trust time -- if that URL's scheme doesn't match
# what the server actually speaks, callbacks fail silently (an SSL
# handshake error against a plain-HTTP port, or the reverse). Set
# APP_HOST_PROTO=https:// in production, matching your real proto.
proto=os.getenv("APP_HOST_PROTO", "http://"),
)
.with_web_ui(False)
.with_devtest(enable=False)
.with_subscription_processing(
auto_sequence=True,
auto_storage=True,
auto_cleanup=True,
)
)
@app.subscription_data_hook("properties")
def on_properties_changed(
actor: ActorInterface,
peer_id: str,
target: str,
data: dict,
sequence: int,
callback_type: str,
) -> None:
# Data is already sequenced, deduplicated, and stored in RemotePeerStore
# by the time this fires -- see docs/guides/subscriptions.rst.
print(f"[{actor.id}] update from {peer_id} ({callback_type} #{sequence}): {data}")
@app.lifecycle_hook("trust_request_received")
def on_trust_request_received(actor: ActorInterface, peer_id: str, **kwargs) -> None:
# Fires on the actor RECEIVING a trust request -- here, actor A (the
# publisher) when actor B (the subscriber) calls create_relationship().
# Auto-approving here is what makes subscribe_to_peer() below succeed:
# a subscription request is rejected with 403 until both sides of the
# relationship are approved, not just the side that initiated it.
# Demo-only: never auto-approve an unverified peer in a real
# application -- see the Security Note in docs/guides/p2p-quickstart.rst.
actor.trust.approve_relationship(peer_id=peer_id)
Actor A – Publish
Create actor A the same way Getting Started does –
passing hooks=app.hooks so lifecycle hooks fire on creation – then write
a property. Any subscriber to "properties" sees this change.
config = app.get_config()
actor_a = ActorInterface.create(creator="a@example.com", config=config, hooks=app.hooks)
def publish_status(actor: ActorInterface, status: str) -> None:
"""Actor A: write a property. Subscribers to "properties" see this."""
actor.properties.status = status
publish_status(actor_a, "active")
Actor B – Establish Trust and Subscribe
Actor B initiates trust with actor A’s URL, approves the relationship, then
subscribes. create_relationship() returns None on failure – check it
before reading .peer_id off the result, unlike the magic-string
peer_id="peer123" you may see in older examples.
def establish_trust_and_subscribe(subscriber: ActorInterface, publisher_url: str):
"""
Actor B: establish trust with actor A, approve it, then subscribe.
create_relationship() implicitly approves the relationship on the
*initiating* (subscriber) side only -- the peer's side stays unapproved
until the peer approves it too, which is what the on_trust_request_received
hook above does. subscribe_to_peer() below would otherwise get a 403:
subscription requests require an approved relationship on *both* sides.
Approving a trust relationship grants the peer whatever the trust type
permits. Do not auto-approve trust with an unverified peer in a real
application -- see docs/guides/access-control.rst.
"""
rel = subscriber.trust.create_relationship(
peer_url=publisher_url, relationship="friend"
)
if rel is None:
raise RuntimeError("Failed to create trust relationship")
subscriber.trust.approve_relationship(peer_id=rel.peer_id)
subscriber.subscriptions.subscribe_to_peer(peer_id=rel.peer_id, target="properties")
return rel
actor_b = ActorInterface.create(creator="b@example.com", config=config, hooks=app.hooks)
rel = establish_trust_and_subscribe(actor_b, publisher_url=f"http://localhost:5000/{actor_a.id}")
Actor B – Receive
The receiving hook is already registered above, in the same app-setup
block:
@app.subscription_data_hook("properties")
def on_properties_changed(actor, peer_id, target, data, sequence, callback_type):
print(f"[{actor.id}] update from {peer_id} ({callback_type} #{sequence}): {data}")
Run It
# examples/p2p_quickstart.py runs a FastAPI server directly:
APP_HOST_FQDN=localhost:5000 python examples/p2p_quickstart.py
# or with uvicorn against your own app module:
uvicorn myapp:api --reload --port 5000
Verify
Create both actors, establish trust, subscribe, and publish – all over the
REST API. Actor creation is unauthenticated, but every request after that
requires HTTP Basic auth (creator:passphrase) – capture each actor’s
passphrase from its creation response rather than discarding it (this
needs jq):
# Create actor A -- capture id and passphrase, both needed below
actor_a=$(curl -s -X POST http://localhost:5000/ -d '{"creator":"a@example.com"}' \
-H 'Content-Type: application/json')
actor_a_id=$(echo "$actor_a" | jq -r .id)
actor_a_pass=$(echo "$actor_a" | jq -r .passphrase)
# Create actor B
actor_b=$(curl -s -X POST http://localhost:5000/ -d '{"creator":"b@example.com"}' \
-H 'Content-Type: application/json')
actor_b_id=$(echo "$actor_b" | jq -r .id)
actor_b_pass=$(echo "$actor_b" | jq -r .passphrase)
# Actor B initiates trust with actor A, authenticated as actor B's owner.
# The running server's on_trust_request_received hook (see "Both Sides in
# One App" above) auto-approves actor A's side as this request arrives --
# no separate approval step needed.
curl -s -u "b@example.com:$actor_b_pass" -X POST "http://localhost:5000/$actor_b_id/trust" \
-H 'Content-Type: application/json' \
-d "{\"url\": \"http://localhost:5000/$actor_a_id\", \"relationship\": \"friend\"}"
# Actor B subscribes to actor A's properties, authenticated as actor B's owner
curl -s -u "b@example.com:$actor_b_pass" -X POST "http://localhost:5000/$actor_b_id/subscriptions" \
-H 'Content-Type: application/json' \
-d "{\"peerid\": \"$actor_a_id\", \"target\": \"properties\", \"granularity\": \"high\"}"
# Actor A publishes a change, authenticated as actor A's owner
curl -s -u "a@example.com:$actor_a_pass" -X POST "http://localhost:5000/$actor_a_id/properties" \
-H 'Content-Type: application/json' -d '{"name": "status", "value": "active"}'
Actor B’s server log should show the on_properties_changed hook firing
with {"status": "active"}.
Security Note
Approving a trust relationship grants the peer whatever the trust type
permits – for the built-in "friend" type used above, that’s broad
read/write access (see ActingWeb Unified Access Control System). Do not auto-approve trust
requests from unverified peers in a real application; require an explicit
human or policy decision before calling approve_relationship().
If you use a custom trust type instead of a built-in one, note that
acl_rules is a silent hard dependency for subscriptions and callbacks: a
custom trust type without acl_rules covering the subscriptions and
callbacks/subscriptions HTTP paths will have its subscribe/callback
requests denied with no other symptom. See ActingWeb Access Control (Simple Guide) for
add_trust_type(..., acl_rules=...).
Production Notes
On Lambda/serverless, call
.with_sync_callbacks()so subscription callbacks complete before the function freezes – async fire-and-forget callbacks can otherwise be lost. Seedocs/quickstart/deployment.rst.For back-pressure, gap detection, circuit-breaker behavior, and fan-out tuning, see Subscription Manager – this quickstart uses the library’s defaults throughout.
Where to Go Next
See a complete application:
examples/demo/in this repository wires up OAuth2 login and the full hook system – including trust and subscription hooks – in one worked example, version-locked to the release you have checked out.Trust Manager – the full trust lifecycle, including per-relationship permission overrides
Subscription Manager – subscription processing configuration, gap handling, and resync
ActingWeb Unified Access Control System – the full permission and trust-type system