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. See docs/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