Hello Service
The simplest possible Aster service: a greeting RPC. Three files, no credentials, no infrastructure. This example is Python-specific.
The service definition
Create hello_service.py with the shared type and service definitions:
# hello_service.py
from dataclasses import dataclass
from aster.decorators import service, rpc
@dataclass
class HelloRequest:
name: str = ""
@dataclass
class HelloResponse:
message: str = ""
@service
class HelloService:
"""Simple greeting service."""
@rpc
async def say_hello(self, req: HelloRequest) -> HelloResponse:
return HelloResponse(message=f"Hello, {req.name}!")
Line by line:
@dataclassdefines plain data types for the request and response. Fields have defaults so Fory can construct them during deserialization.@servicemarks the class as an Aster RPC service. The service name defaults to the class name (HelloService), version defaults to1.@rpcmarkssay_helloas a unary RPC method. The framework inspects the type annotations (HelloRequestandHelloResponse) to handle serialization automatically.
Since no @wire_type is applied, the @service decorator auto-tags the types using their module-qualified names. This is fine for development; production services should use explicit @wire_type tags.
The producer
Create producer.py to host the service:
# producer.py
import asyncio
from hello_service import HelloService
from aster import AsterServer
async def main():
async with AsterServer(services=[HelloService()]) as srv:
print(f"Endpoint address: {srv.endpoint_addr_b64}")
print("Waiting for connections... (Ctrl+C to stop)")
await srv.serve()
asyncio.run(main())
Line by line:
AsterServer(services=[HelloService()])creates a producer that hosts one service. The server builds an iroh node, registers theaster/1ALPN for RPC, and runs the consumer admission ALPN for discovery.async withcallsstart()(which creates the QUIC endpoint) andserve()(which starts the accept loop). On exit, it callsclose().srv.endpoint_addr_b64is the base64-encodedNodeAddrthat consumers need to connect.
The consumer
Create consumer.py to call the service:
# consumer.py
import asyncio
from hello_service import HelloService, HelloRequest
from aster import AsterClient
async def main():
async with AsterClient(endpoint_addr="<paste address from producer>") as client:
hello = await client.client(HelloService)
resp = await hello.say_hello(HelloRequest(name="World"))
print(resp.message)
asyncio.run(main())
Line by line:
AsterClient(endpoint_addr=...)takes the base64 address printed by the producer. Alternatively, setASTER_ENDPOINT_ADDRas an environment variable and omit the argument.async withcallsconnect(), which creates a QUIC endpoint and runs the admission handshake to discover available services.client.client(HelloService)returns a typed stub. The stub'ssay_hellomethod serializes the request, sends it over QUIC, deserializes the response, and returns it.
Running it
Open two terminals. No credentials or configuration files are needed.
Terminal 1 -- producer:
pip install aster-python
python producer.py
Output:
Endpoint address: g6Jpa...kNQ==
Waiting for connections... (Ctrl+C to stop)
Terminal 2 -- consumer:
# Option A: pass the address inline in the code
python consumer.py
# Option B: use an environment variable
export ASTER_ENDPOINT_ADDR=g6Jpa...kNQ==
python consumer.py
Output:
Hello, World!
What is happening under the hood
-
QUIC endpoint creation. Both producer and consumer create iroh QUIC endpoints. Each gets an ed25519 identity key (ephemeral by default) and connects to a relay server for NAT traversal.
-
ALPN negotiation. The consumer first connects on the
aster.consumer_admissionALPN. The producer responds with the list of available services (in this case,HelloServicev1) and its RPC channel address. -
Admission. In dev mode (no root key configured), the producer auto-opens the consumer gate. The consumer is admitted without credentials.
-
RPC connection. The consumer opens a second QUIC connection on the
aster/1ALPN. This connection carries RPC traffic. -
Call dispatch. The consumer sends a
StreamHeader(service name, method name, serialization mode), followed by the serializedHelloRequest. The producer deserializes it, callssay_hello, serializes theHelloResponse, and sends it back. -
Cleanup. When the
async withblocks exit, both sides close their QUIC connections and endpoints.
Next steps
- Add
@wire_typetags for production stability: Define a Service - Connect with credentials: Dial a Service
- Add streaming methods: Define a Service -- Streaming