# Datonow SDK — AI Agent Integration Guide

> Optimized for AI agents (Claude, GPT, Cursor, Copilot, etc.) to quickly understand and implement Datonow.
> Datonow is a **self-hosted** realtime messaging + push notification platform.
> Source: https://github.com/datonow/datonow

## Quick Reference

| Package | Purpose | Runtime |
|---------|---------|---------|
| `@datonow/sdk` | Client SDK — WebSocket connections, channels, notification inbox | Browser, Node.js, Bun, Deno |
| `@datonow/sdk-server` | Server SDK — send notifications, trigger events, sign channel auth | Node.js, Bun, Deno (server-only) |

---

## Installation

```bash
npm install @datonow/sdk @datonow/sdk-server
# or
pnpm add @datonow/sdk @datonow/sdk-server
```

---

## Client SDK (`@datonow/sdk`)

### Import

```typescript
import { DatonowClient } from "@datonow/sdk";
import type { ConnectionState, NotificationMessage } from "@datonow/sdk";
```

### Two Connection Modes

The client operates in **one of two modes** based on which credential you provide:

| Mode | Option | Use case |
|------|--------|---------|
| **Token mode** | `token: "..."` (64-char hex) | Notification delivery — receives push messages for a specific device/user |
| **AppKey mode** | `appKey: "..."` (40-char hex) | Pub/sub channels — subscribes to realtime events (like Pusher) |

### Initialize & Connect

```typescript
// ── Token mode (notification client) ──────────────────────────────
const client = new DatonowClient({
  token: process.env.DATONOW_CLIENT_TOKEN!,
  wsHost:   "wss://rt.yourdomain.com",   // your self-hosted server
  httpHost: "https://rt.yourdomain.com", // same host, HTTP
});

// ── AppKey mode (channel client) ──────────────────────────────────
const client = new DatonowClient({
  appKey:       process.env.DATONOW_APP_KEY!,
  wsHost:       "wss://rt.yourdomain.com",
  httpHost:     "https://rt.yourdomain.com",
  authEndpoint: "/api/datonow/auth", // required for private/presence channels
});

client.connect();
```

> **Self-hosted defaults**: `wsHost` defaults to `wss://rt.datonow.com` and `httpHost` to `https://rt.datonow.com`.
> Override both when running your own server.

### Notification Inbox (token mode only)

```typescript
// Receive realtime push notifications
const unsub = client.notifications.onNotification((msg: NotificationMessage) => {
  console.log(`[P${msg.priority}] ${msg.title}: ${msg.message}`);
  console.log("Extras:", msg.extras);
  // msg shape: { id, appId, title, message, payload, priority, extras, expiresAt, createdAt }
});

// Fetch history (cursor-based pagination)
const { messages, nextCursor } = await client.notifications.getHistory(
  undefined, // cursor (number) — omit for first page
  20         // limit
);

// Mark as read
await client.notifications.markAsRead(messages[0].id);
await client.notifications.markAllRead();

// Delete a message
await client.notifications.delete(messages[0].id);

// Unregister handler
unsub();
```

### Realtime Channels (appKey mode only)

```typescript
// Public channel (no auth required)
const ch = client.subscribe("my-channel");
await ch.subscribe();

// Private channel (auth required — sends to authEndpoint)
const privateCh = client.subscribe("private-orders");
await privateCh.subscribe();

// Presence channel (auth required — tracks online members)
const presenceCh = client.subscribe("presence-room-42");
await presenceCh.subscribe({
  userId:   "user-123",
  userInfo: { name: "Alice", avatarUrl: "https://..." },
});

// Bind to events
ch.bind("order_created", (data) => {
  console.log("New order:", data);
});

// Presence member events
presenceCh.bind("datonow:member_added",   (member) => console.log("Joined:", member));
presenceCh.bind("datonow:member_removed", (member) => console.log("Left:", member));

// Unbind
ch.unbind("order_created", handler); // specific handler
ch.unbind("order_created");          // all handlers for event

// Unsubscribe
await client.unsubscribe("my-channel");
```

### Connection State

```typescript
// Listen for state changes
const removeLis = client.onConnectionStateChange((state: ConnectionState) => {
  // "initialized" | "connecting" | "connected" | "disconnected" | "failed"
  console.log("Connection:", state);
});

// Current state and socket ID
const state    = client.getState();
const socketId = client.getSocketId(); // available after "connected"

// Remove listener
removeLis();
```

### Cleanup

```typescript
client.disconnect(); // closes socket, stops reconnect
```

> **Auto-reconnect**: The client retries up to 5 times with exponential backoff (max 30 s). On reconnect it automatically re-subscribes all channels.

---

## Server SDK (`@datonow/sdk-server`)

### Import

```typescript
import { DatonowServer } from "@datonow/sdk-server";
import type { NotificationMessage, ClientToken } from "@datonow/sdk-server";
```

### Initialize

```typescript
const datonow = new DatonowServer({
  appId:   process.env.DATONOW_APP_ID!,
  key:     process.env.DATONOW_APP_KEY!,
  secret:  process.env.DATONOW_APP_SECRET!, // app secret OR a client token
  apiHost: "https://rt.yourdomain.com",     // optional — defaults to https://rt.datonow.com
});
```

> **`secret` is dual-purpose**: Pass the **app secret** for broadcast notifications and token management. Pass a **user's client token** to scope delivery to that specific user only (see SaaS Integration below).

### Send Notifications

```typescript
// Broadcast to all tokens in the app (auth with app secret)
const msg = await datonow.sendNotification({
  title:    "Deploy Complete",
  message:  "v2.4.0 shipped to production.",
  priority: 8,       // 1 (low) – 10 (critical)
  ttl:      3600,    // seconds until expiry (0 = never expire)
  extras: {          // arbitrary JSON, passed through to the client
    version: "2.4.0",
    url:     "https://ci.example.com/builds/512",
  },
});
// → NotificationMessage { id, appId, title, message, priority, createdAt, ... }

// Targeted (auth with a specific user's client token — see SaaS pattern below)
const targeted = new DatonowServer({
  appId:  process.env.DATONOW_APP_ID!,
  key:    process.env.DATONOW_APP_KEY!,
  secret: user.datonowToken,  // ← the user's personal client token
});
await targeted.sendNotification({ title: "Your export is ready", priority: 7 });
```

### Message Management

```typescript
// List messages (cursor-based pagination)
const { messages, nextCursor } = await datonow.getMessages({ cursor: 0, limit: 20 });

// Delete one
await datonow.deleteMessage(42);

// Delete all
await datonow.deleteAllMessages();
```

### Trigger Channel Events

```typescript
// Single channel
await datonow.trigger("live-scores", "goal_scored", { team: "Home", score: "2-1" });

// Multiple channels
await datonow.trigger(["room-1", "room-2"], "announcement", { text: "Maintenance in 5 min" });

// Exclude sender socket (avoids echo on client-originated events)
await datonow.trigger("chat", "message_sent", data, { socketId: senderSocketId });
```

### Authenticate Private Channels

```typescript
// In your auth endpoint (e.g. POST /api/datonow/auth)
const { socket_id, channel_name } = await req.json();

const auth = datonow.authenticatePrivateChannel(socket_id, channel_name);
// → { auth: "<app-key>:<hmac-sha256-hex>" }
return Response.json(auth);
```

### Authenticate Presence Channels

```typescript
const auth = datonow.authenticatePresenceChannel(
  socket_id,
  channel_name,
  user.id,                                    // user ID
  { name: user.name, avatarUrl: user.avatar } // arbitrary user info (shown to other members)
);
// → { auth: "<app-key>:<hmac-sha256-hex>", channel_data: "{"user_id":...}" }
return Response.json(auth);
```

### Client Token Management

```typescript
// Create a token (e.g. for a new end-user in your SaaS)
const token: ClientToken = await datonow.createClientToken("user-42");
// → { id, appId, name, token: "<64-char-hex>", lastSeenAt, createdAt }

// List all tokens
const tokens = await datonow.listClientTokens();

// Delete
await datonow.deleteClientToken(token.id);
```

---

## Channel Naming Convention

| Prefix | Type | Auth Required | Notes |
|--------|------|--------------|-------|
| *(none)* | Public | No | Anyone with the App Key can subscribe |
| `private-` | Private | Yes (HMAC) | Server-signed auth required |
| `presence-` | Presence | Yes (HMAC) | Like private + member list tracking |

---

## Auth Model — Delivery Scoping

This is the most important concept to understand:

| `secret` passed to `DatonowServer` | Delivery scope |
|------------------------------------|---------------|
| **App secret** (`DATONOW_APP_SECRET`) | Broadcast — delivered to **all** client tokens in the app |
| **Client token** (`<64-char hex>`) | Targeted — delivered **only** to that specific token |

The server automatically detects which mode to use based on the credential format.

---

## Common Patterns

### React Hook

```typescript
import { useEffect, useRef } from "react";
import { DatonowClient } from "@datonow/sdk";
import type { NotificationMessage } from "@datonow/sdk";

export function useDatonowNotifications(token: string, onMessage: (msg: NotificationMessage) => void) {
  const clientRef = useRef<DatonowClient | null>(null);

  useEffect(() => {
    const client = new DatonowClient({
      token,
      wsHost:   process.env.NEXT_PUBLIC_DATONOW_WS_HOST,
      httpHost: process.env.NEXT_PUBLIC_DATONOW_HTTP_HOST,
    });

    const unsub = client.notifications.onNotification(onMessage);
    client.connect();
    clientRef.current = client;

    return () => {
      unsub();
      client.disconnect();
    };
  }, [token]); // reconnects if token changes

  return clientRef;
}
```

### Next.js App Router — Auth Endpoint

```typescript
// app/api/datonow/auth/route.ts
import { NextRequest, NextResponse } from "next/server";
import { DatonowServer } from "@datonow/sdk-server";
import { getSession } from "@/lib/session"; // your own auth helper

const datonow = new DatonowServer({
  appId:  process.env.DATONOW_APP_ID!,
  key:    process.env.DATONOW_APP_KEY!,
  secret: process.env.DATONOW_APP_SECRET!,
});

export async function POST(req: NextRequest) {
  const session = await getSession();
  if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

  const { socket_id, channel_name } = await req.json();

  if (channel_name.startsWith("presence-")) {
    const auth = datonow.authenticatePresenceChannel(
      socket_id, channel_name,
      session.userId, { name: session.userName }
    );
    return NextResponse.json(auth);
  }

  if (channel_name.startsWith("private-")) {
    const auth = datonow.authenticatePrivateChannel(socket_id, channel_name);
    return NextResponse.json(auth);
  }

  return NextResponse.json({ error: "Unknown channel type" }, { status: 400 });
}
```

### SaaS Integration — User-Targeted Notifications

```typescript
// ── Step 1: Provision a token when the user signs up in YOUR app ──

const mgmt = new DatonowServer({
  appId:  process.env.DATONOW_APP_ID!,
  key:    process.env.DATONOW_APP_KEY!,
  secret: process.env.DATONOW_APP_SECRET!, // use app secret for management
});

const token = await mgmt.createClientToken(`user-${user.id}`);
// Store token.id and token.token in your DB
await db.users.update({ where: { id: user.id }, data: { datonowToken: token.token } });

// ── Step 2: Send to one user from a background job ────────────────

const user = await db.users.findUnique({ where: { id: jobResult.userId } });

const targeted = new DatonowServer({
  appId:  process.env.DATONOW_APP_ID!,
  key:    process.env.DATONOW_APP_KEY!,
  secret: user.datonowToken,  // ← user's personal token, not the app secret
});

await targeted.sendNotification({
  title:   "Your file is ready",
  message: "The CSV export you requested has finished processing.",
  priority: 7,
  extras:  { downloadUrl: "https://..." },
});

// ── Step 3: Connect from the browser (each user uses their own token) ─

// Backend endpoint that returns the token for the current session:
// GET /api/me/notifications-token → { token: "<64-char-hex>" }

const { token } = await fetch("/api/me/notifications-token").then(r => r.json());

const client = new DatonowClient({
  token,
  wsHost:   process.env.NEXT_PUBLIC_DATONOW_WS_HOST,
  httpHost: process.env.NEXT_PUBLIC_DATONOW_HTTP_HOST,
});
client.connect();
client.notifications.onNotification((msg) => showToast(msg.title, msg.message));
```

### Express.js

```typescript
import { DatonowServer } from "@datonow/sdk-server";

const datonow = new DatonowServer({
  appId:  process.env.DATONOW_APP_ID!,
  key:    process.env.DATONOW_APP_KEY!,
  secret: process.env.DATONOW_APP_SECRET!,
  apiHost: process.env.DATONOW_API_HOST,
});

// Auth endpoint for private/presence channels
app.post("/api/datonow/auth", async (req, res) => {
  const { socket_id, channel_name } = req.body;
  const auth = datonow.authenticatePrivateChannel(socket_id, channel_name);
  res.json(auth);
});

// Send from any route or background job
app.post("/api/orders", async (req, res) => {
  const order = await createOrder(req.body);
  await datonow.trigger("orders", "order_created", { orderId: order.id });
  res.json(order);
});
```

---

## Environment Variables

```env
# Server-side (never expose to browser)
DATONOW_APP_ID=your-app-id
DATONOW_APP_KEY=<your-40-char-hex-app-key>
DATONOW_APP_SECRET=your-app-secret
DATONOW_API_HOST=https://rt.yourdomain.com

# Client-side (safe to expose)
NEXT_PUBLIC_DATONOW_WS_HOST=wss://rt.yourdomain.com
NEXT_PUBLIC_DATONOW_HTTP_HOST=https://rt.yourdomain.com
```

---

## REST API Reference

Base URL: your server address (e.g. `https://rt.yourdomain.com`)

### Notification API

Auth headers: `X-Datonow-App-Id: <appId>` + `X-Datonow-Token: <clientToken|appSecret>`

| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/api/message` | Send a notification |
| `GET` | `/api/messages?cursor=&limit=` | List messages (cursor-paginated) |
| `DELETE` | `/api/messages/{id}` | Delete one message |
| `DELETE` | `/api/messages` | Delete all messages |
| `POST` | `/api/messages/{id}/delivered` | Mark as delivered |
| `POST` | `/api/messages/{id}/read` | Mark as read |
| `POST` | `/api/messages/read-all` | Mark all as read |
| `GET` | `/api/apps/{appId}/tokens` | List client tokens |
| `POST` | `/api/apps/{appId}/tokens` | Create a client token |
| `DELETE` | `/api/apps/{appId}/tokens/{id}` | Delete a client token |

### POST `/api/message` — Request Body

```json
{
  "title":    "string (required)",
  "message":  "string",
  "priority": 5,
  "ttl":      86400,
  "extras":   {}
}
```

### Realtime API

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `GET` | `/ws?token=` | Client token | WebSocket — notification client |
| `GET` | `/ws?appKey=` | App key | WebSocket — channel client |
| `POST` | `/api/trigger` | HMAC signature | Trigger event on one or more channels |
| `POST` | `/api/subscribe` | — | Subscribe a socket to a channel (SDK-managed) |
| `POST` | `/api/unsubscribe` | — | Unsubscribe a socket (SDK-managed) |

### POST `/api/trigger` — Request Body + Signing

```typescript
const body = JSON.stringify({
  appKey:    "<your-40-char-hex>",
  channel:   "my-channel",     // or ["ch-1", "ch-2"]
  event:     "event-name",
  data:      { any: "payload" },
  socketId:  "optional-exclude-socket",
  requestId: crypto.randomUUID(),
});

const signature = "sha256=" + createHmac("sha256", APP_SECRET).update(body).digest("hex");
// Header: X-Datonow-Signature: sha256=...
```

---

## Rate Limits

| Limit | Scope |
|-------|-------|
| 10 connections/sec | Per IP address (WebSocket) |
| 600 requests/min | Per app (trigger + message API) |

---

## Troubleshooting

| Issue | Likely cause | Solution |
|-------|-------------|---------|
| WebSocket fails to connect | Wrong `wsHost` | Verify URL starts with `wss://` and server is running |
| Auth returns 403 | Wrong credentials | Check `X-Datonow-App-Id` + `X-Datonow-Token` headers |
| Channel auth fails | Signature mismatch | Ensure `authenticatePrivateChannel` uses the **app secret**, not a client token |
| Notifications not received | Wrong client mode | Use `token:` option (not `appKey:`) when connecting as a notification client |
| Presence members empty | Missing `channel_data` | Ensure `authenticatePresenceChannel` is called (not the private variant) |
| Message broadcast instead of targeted | Wrong `secret` | Pass the user's **client token** as `secret`, not the app secret |

---

## Type Definitions

```typescript
// @datonow/sdk
interface DatonowOptions {
  token?:        string; // client token — use for notification mode
  appKey?:       string; // app key — use for channel mode
  wsHost?:       string; // WebSocket URL (defaults to wss://rt.datonow.com)
  httpHost?:     string; // HTTP base URL (defaults to https://rt.datonow.com)
  authEndpoint?: string; // your auth route for private/presence channels
}

type ConnectionState = "initialized" | "connecting" | "connected" | "disconnected" | "failed";

interface NotificationMessage {
  id:         number;
  appId?:     string;
  title:      string;
  message:    string;
  payload?:   Record<string, unknown>;
  priority:   number;   // 1–10
  extras?:    Record<string, unknown>;
  expiresAt?: string;
  createdAt:  string;
}

// @datonow/sdk-server
interface DatonowServerOptions {
  appId:    string;
  key:      string;
  secret:   string; // app secret (broadcast) OR client token (targeted)
  apiHost?: string; // defaults to https://rt.datonow.com
}

interface ClientToken {
  id:          string;
  appId:       string;
  name:        string;
  token:       string;
  lastSeenAt?: string;
  createdAt:   string;
}
```
