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 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.address}")
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.addressis theaster1...connection address that 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(address="<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(address=...)takes theaster1...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-rpc
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