Hello Service
The simplest possible Aster service: a greeting RPC. Three files, no credentials, no infrastructure. This example is Python-specific.
The service definition
Section titled “The service definition”Create hello_service.py with the shared type and service definitions:
from dataclasses import dataclassfrom aster import service, rpc
@dataclassclass HelloRequest: name: str = ""
@dataclassclass HelloResponse: message: str = ""
@serviceclass 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
Section titled “The producer”Create producer.py to host the service:
import asynciofrom hello_service import HelloServicefrom 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
Section titled “The consumer”Create consumer.py to call the service:
import asynciofrom hello_service import HelloService, HelloRequestfrom 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
Section titled “Running it”Open two terminals. No credentials or configuration files are needed.
Terminal 1 – producer:
pip install aster-rpcpython producer.pyOutput:
Endpoint address: g6Jpa...kNQ==Waiting for connections... (Ctrl+C to stop)Terminal 2 – consumer:
# Option A: pass the address inline in the codepython consumer.py
# Option B: use an environment variableexport ASTER_ENDPOINT_ADDR=g6Jpa...kNQ==python consumer.pyOutput:
Hello, World!What is happening under the hood
Section titled “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
Section titled “Next steps”- Add
@wire_typetags for production stability: Define a Service - Connect with credentials: Dial a Service
- Add streaming methods: Define a Service – Streaming