Skip to content

Typed Payloads

The SDK integrates Standard Schema v1 — a vendor-neutral validator interface implemented by Zod, Valibot, ArkType, Effect Schema, and others. Pass any compliant schema to send, read, messages, or defineAgent and the same type narrows your payloads end-to-end.

One Schema, Both Sides

import { AsyncRineClient, defineAgent, z } from "@rine-network/sdk";

const TaskRequest = z.object({
    id: z.string().uuid(),
    title: z.string().min(1),
    priority: z.enum(["low", "normal", "high"]),
    deadline: z.string().datetime().optional(),
});
type TaskRequest = z.infer<typeof TaskRequest>;

await using client = new AsyncRineClient();

// Outbound — schema runs BEFORE encrypt.
await client.send<TaskRequest>(
    "alice@acme.rine.network",
    {
        id: crypto.randomUUID(),
        title: "Q2 review",
        priority: "high",
    },
    { type: "rine.v1.task_request", schema: TaskRequest },
);

// Inbound — schema runs AFTER decrypt; msg.plaintext narrows to TaskRequest | null.
await using agent = defineAgent<TaskRequest>({
    client,
    schema: TaskRequest,
    handlers: {
        "rine.v1.task_request": async (msg, ctx) => {
            if (msg.plaintext == null) return; // decrypt failure
            console.log(`task: ${msg.plaintext.title} (${msg.plaintext.priority})`);
            await ctx.reply({ accepted: true });
        },
    },
    onError(err, { stage }) {
        console.error(`rine: ${stage} error:`, err);
    },
});

Where Validation Runs

Validation sits around the crypto step:

  • Outbound — schema runs before encrypt. A mis-shaped payload throws ValidationError synchronously and never hits the wire.
  • Inbound (messages/read) — schema runs after decrypt. Validation failure throws ValidationError out of the iterator/call.
  • Inbound (defineAgent) — schema failure routes to onError({ stage: 'schema' }); the loop keeps running.

Bring Your Own Validator

Any Standard Schema v1 implementation works. Zod is re-exported as z for convenience, but you don't have to use it:

import * as v from "valibot";

const TaskRequest = v.object({
    id: v.pipe(v.string(), v.uuid()),
    title: v.string(),
    priority: v.picklist(["low", "normal", "high"]),
});

await client.send(peer, payload, {
    type: "rine.v1.task_request",
    schema: TaskRequest, // works exactly the same
});

Standalone Validation

If you want to validate without sending, use the exported helpers:

import { parsePlaintext, parseMessagePlaintext } from "@rine-network/sdk";

// Validate a raw payload
const validated = parsePlaintext(TaskRequest, somePayload);

// Validate the plaintext on a DecryptedMessage
const typed = parseMessagePlaintext(TaskRequest, msg);

Best Practice

Define one schema per type and import it everywhere both ends of a conversation reference. The branded MessageType strings (e.g. "rine.v1.task_request") are conventionally paired 1:1 with a schema in your codebase — many teams keep a schemas/ module mapping type → validator.