OddSockets Elixir SDK

Official Elixir SDK for OddSockets real-time messaging platform

Hex ready Elixir/OTP GenServer High Performance Fault Tolerant

Overview & Features

The OddSockets Elixir SDK provides a powerful, fault-tolerant interface for real-time messaging built on Elixir/OTP principles.

Elixir/OTP Native

Built with GenServers and supervision trees for maximum fault tolerance.

JavaScript Compatible

Full API compliance with JavaScript SDK pattern while maintaining Elixir idioms.

High Performance

Leverages Elixir's lightweight processes for concurrent message handling.

Fault Tolerant

Automatic reconnection and supervision for 99.9% uptime.

Cost Effective

No per-message pricing, industry-standard 32KB message limits.

Presence Tracking

Real-time user presence and state management with OTP patterns.

Installation

Add oddsockets to your list of dependencies in mix.exs:

elixir
def deps do
  [
    {:oddsockets, "~> 1.0.0"}
  ]
end

Then run:

bash
mix deps.get
elixir
def deps do
  [
    {:oddsockets, git: "https://github.com/jyswee/oddsockets-elixir-sdk.git"}
  ]
end
elixir
def deps do
  [
    {:oddsockets, path: "../oddsockets"}
  ]
end

Quick Start

Basic Usage

elixir
# Start a client
{:ok, client} = OddSockets.start_link(api_key: "ak_live_1234567890abcdef")

# Get a channel
channel = OddSockets.channel(client, "my-channel")

# Subscribe to messages
:ok = OddSockets.Channel.subscribe(channel, fn message ->
  IO.inspect(message, label: "Received")
end)

# Publish a message
{:ok, result} = OddSockets.Channel.publish(channel, %{text: "Hello World!"})

GenServer Integration

elixir
defmodule MyApp.ChatServer do
  use GenServer

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  def init(_opts) do
    {:ok, client} = OddSockets.start_link(api_key: "your-api-key")
    channel = OddSockets.channel(client, "chat-room")
    
    :ok = OddSockets.Channel.subscribe(channel, &handle_message/1)
    
    {:ok, %{client: client, channel: channel}}
  end

  defp handle_message(message) do
    IO.puts("Chat message: #{inspect(message)}")
  end
end

Event Handling

elixir
# Subscribe to client events
:ok = OddSockets.subscribe_events(client)

# Handle events in your process
receive do
  {:oddsockets_event, :connected} ->
    IO.puts("Connected to OddSockets!")
  
  {:oddsockets_event, {:error, reason}} ->
    IO.puts("Connection error: #{inspect(reason)}")
  
  {:oddsockets_event, {:worker_assigned, info}} ->
    IO.puts("Assigned to worker: #{info.worker_id}")
end

Configuration

Client Options

elixir
{:ok, client} = OddSockets.start_link(
  api_key: "your-api-key",           # Required: Your OddSockets API key
  user_id: "user-id",                # Optional: User identifier
  auto_connect: true,                # Optional: Auto-connect on creation
  options: %{
    timeout: 10_000                  # Optional: Connection timeout (ms)
  }
)

Channel Options

elixir
:ok = OddSockets.Channel.subscribe(channel, callback, %{
  max_history: 100,                  # Maximum history messages to retain
  retain_history: true,              # Whether to retain message history
  enable_presence: false             # Enable presence tracking
})

{:ok, result} = OddSockets.Channel.publish(channel, message, %{
  ttl: 3600,                         # Time to live (seconds)
  metadata: %{priority: "high"}      # Additional metadata
})

Application Configuration

elixir
# config/config.exs
config :oddsockets,
  api_key: "your-api-key",
  manager_url: "https://connect.oddsockets.tyga.network"

Examples

Explore comprehensive examples demonstrating the OddSockets Elixir SDK in action:

Enhanced Features

Beyond core pub/sub, OddSockets ships a Slack-like enhanced surface: typing indicators, reactions, threads, read receipts, presence/status, notifications, DMs, channel management, message editing and search. It lives in the OddSockets.EnhancedFeatures module. Actions are sent with a OddSockets.EnhancedFeatures.* function (the client pid is always the first argument) and the worker broadcasts back as events that surface on any process registered with OddSockets.subscribe_events/1 — you match {:oddsockets_event, {"<event>", payload}} in your mailbox.

Typing & Reactions

elixir
alias OddSockets.EnhancedFeatures

{:ok, client} = OddSockets.start_link(api_key: "YOUR_API_KEY", user_id: "alice")
:ok = OddSockets.connect(client)

# Enhanced broadcasts surface on the public event stream
:ok = OddSockets.subscribe_events(client)

channel = OddSockets.channel(client, "room-42")
:ok = OddSockets.Channel.subscribe(channel, fn _msg -> :ok end, %{enable_presence: true})

# Send-path: enhanced actions over the live socket (client pid first)
:ok = EnhancedFeatures.start_typing(client, "alice", "room-42")
:ok = EnhancedFeatures.add_reaction(client, "msg-1", "room-42", ":thumbsup:", "alice", "Alice")

# Receive-path: broadcasts from other users on the channel
receive do
  {:oddsockets_event, {"user_typing", payload}} ->
    IO.puts("#{payload["userId"]} is typing")

  {:oddsockets_event, {"reaction_added", payload}} ->
    IO.puts("#{payload["userId"]} reacted #{payload["emoji"]}")
end

Threads

elixir
# Send a threaded reply
:ok = EnhancedFeatures.thread_reply(client, "room-42", "msg-1", "Replying in the thread", "alice", "Alice")

# Query functions block and return {:ok, data}
{:ok, thread} = EnhancedFeatures.get_thread(client, "thread-1")

# Receive the broadcast on the event stream
receive do
  {:oddsockets_event, {"thread_reply", _payload}} -> IO.puts("New reply")
end

Enhanced surface

Each area exposes functions on OddSockets.EnhancedFeatures; the worker broadcasts the paired events which surface via OddSockets.subscribe_events/1. Query functions (get_*, search_*) block and return {:ok, data}.

  • Typingstart_typing, stop_typinguser_typing, user_stopped_typing
  • Reactionsadd_reaction, remove_reaction, get_reactionsreaction_added, reaction_removed
  • Threadsthread_reply, get_thread, subscribe_thread, follow_thread, mark_thread_readthread_reply, thread_subscribed, thread_followed, thread_read_updated
  • Read receiptsmark_read, mark_all_read, get_unread_countsuser_read, unread_count_updated, all_marked_read
  • Messagesedit_message, delete_message, pin_message, unpin_message, get_pinned_messages, search_messagesmessage_edited, message_deleted, message_pinned, message_unpinned
  • Presence & statusset_status, set_custom_status, set_dnd, get_user_presenceuser_status_changed, custom_status_updated, dnd_status_changed
  • Channelscreate_channel, update_channel, archive_channel, invite_to_channel, join_channel, leave_channelchannel_created, channel_updated, user_invited, user_joined_channel, user_left_channel
  • DMscreate_dm, send_dm, get_dm_conversationsdm_created, dm_received
  • Notificationssubscribe_notifications, get_notifications, mark_notification_read, clear_notificationsnotification, notification_read, notifications_cleared
  • Searchsearch_messages, search_in_channel, search_by_user, filter_messages{:ok, data} results

For any worker event not wrapped above, it still surfaces as {:oddsockets_event, {"<event>", payload}} once you have called OddSockets.subscribe_events/1 — all enhanced broadcasts are forwarded to the event stream.

Challenges & Leaderboards

Challenges, leaderboards and achievements build on the same live socket. The send side lives on the OddSockets.EnhancedFeatures surface (the client pid is always the first argument); request/query functions block and resolve with the worker's reply as {:ok, data}, while progress and achievement calls are fire-and-forget. Inbound broadcasts arrive on the client event surface — subscribe with OddSockets.subscribe_events/1 and match {:oddsockets_event, {"<event>", payload}} in your mailbox.

Run a challenge

elixir
alias OddSockets.EnhancedFeatures

# Create a ranked challenge (ack: challenge_create_success)
{:ok, _} = EnhancedFeatures.create_challenge(client, %{challengeId: "daily-sprint", metric: "score", ranked?: true, channel: "room-42"})

# Report progress toward the metric (fire-and-forget)
:ok = EnhancedFeatures.report_progress(client, %{challengeId: "daily-sprint", value: 1200})

# Pull server-ordered standings (awaited; ack: challenge_standings_success)
{:ok, standings} = EnhancedFeatures.get_standings(client, %{challengeId: "daily-sprint", limit: 10})

# Finalize with an outcome (ack: challenge_complete_success)
{:ok, _} = EnhancedFeatures.complete_challenge(client, %{challengeId: "daily-sprint", outcome: "completed"})

Send-path functions

Every function lives on OddSockets.EnhancedFeatures with the client pid as the first argument. Request/query calls block until the ack arrives and return {:ok, data}; fire-and-forget calls return :ok.

  • create_challenge — create a challenge/leaderboard. Ack challenge_create_success.
  • report_progress — fire-and-forget metric progress. (no ack)
  • complete_challenge — finalize with an outcome. Ack challenge_complete_success.
  • unlock_achievement — fire-and-forget; pass percentComplete (0–100). (no ack)
  • get_standings — request top-N + caller rank. Ack challenge_standings_success.
  • get_achievements — query achievement state. Ack achievement_state.
  • send_challenge_invite — directed invite to another user. Ack challenge_invite_success.
  • reply_challenge_invite — accept/decline an invite. Ack challenge_reply_success.
  • cancel_challenge_invite — cancel a sent invite. Ack challenge_invite_cancel_success.
  • get_challenge_invites — list pending invites. Ack challenge_invites.

Outcome vocabulary

The outcome passed to complete_challenge is one of:

  • completed — win (rank 1)
  • failed — loss
  • tied — draw
  • conceded — resign / concede
  • expired — timed out

Progressive achievements

unlock_achievement always emits the wire event achievement_unlock; the worker is authoritative and derives the outbound broadcast from percentComplete. A value < 100 broadcasts achievement_progress (status in_progress); >= 100 or an omitted value broadcasts achievement_unlock (status unlocked). You never emit achievement_progress yourself.

Inbound events

Subscribe with OddSockets.subscribe_events/1 and match {:oddsockets_event, {"<event>", payload}} in your mailbox.

  • Room broadcastschallenge_progress, leaderboard_rank_change, challenge_complete, achievement_unlock, achievement_progress
  • Directed (per-user)challenge_invited, challenge_reply_received, challenge_invite_cancelled

Usage Analytics

Pull your tenant's headline usage tiles — monthly active users, daily active users, total messages published, and error rate — straight from the SDK, without hand-rolling a REST call. OddSockets.get_usage_stats/1 resolves the same four tiles the developer dashboard renders.

{:ok, stats} = OddSockets.get_usage_stats(client)
# stats => %{
#   mau: 1240,             # monthly active users, or nil
#   dau: 210,              # daily active users, or nil
#   total_messages: 84213, # messages published, or nil
#   error_rate: 0.002,     # 0-1, or nil
#   owner_scope: "ak_...owner",
#   detail: nil,
#   timestamp: "2026-09-04T12:00:00.000Z"
# }

# Render an em-dash when a tile is nil
IO.puts("MAU: #{stats.mau || "\u2014"}")

The four tiles

  • mau — monthly active users for your owner scope
  • dau — daily active users
  • total_messages — total messages published
  • error_rate — publish error rate, 01

Honesty rule — nil, never a fake zero

Each tile is a number or nil. A nil means that leg of the analytics pipeline is not live yet for your tenant — the SDK returns nil verbatim and never coerces it to 0. Render an em-dash () for a nil tile so you never show a fabricated zero.

Requires an API key

OddSockets.get_usage_stats/1 reads your owner-scoped analytics, so it needs an api_key. Keyless / token-only clients have no owner scope to query and return {:error, :requires_api_key}.

Performance & Compatibility

OddSockets Elixir SDK delivers superior performance with Elixir/OTP advantages:

<50ms
Latency
99.9%
Uptime
32KB
Max Message
1M+
Processes

Elixir Support

  • Elixir 1.12+ (OTP 24+)
  • Phoenix Framework
  • LiveView Integration
  • GenServer Patterns

OTP Features

  • Supervision Trees
  • Fault Tolerance
  • Hot Code Reloading
  • Distributed Systems

OTP Patterns & Integration

The OddSockets Elixir SDK is designed to work seamlessly with Elixir/OTP patterns and popular frameworks:

Phoenix Framework

elixir
defmodule MyAppWeb.ChatLive do
  use MyAppWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, client} = OddSockets.start_link(api_key: "your-api-key")
    channel = OddSockets.channel(client, "chat-room")
    
    :ok = OddSockets.Channel.subscribe(channel, fn message ->
      send(self(), {:new_message, message})
    end)

    {:ok, assign(socket, client: client, channel: channel, messages: [])}
  end

  def handle_info({:new_message, message}, socket) do
    messages = [message | socket.assigns.messages]
    {:noreply, assign(socket, messages: messages)}
  end

  def handle_event("send_message", %{"message" => text}, socket) do
    {:ok, _} = OddSockets.Channel.publish(socket.assigns.channel, %{
      text: text,
      user: "current_user"
    })
    
    {:noreply, socket}
  end
end

Supervision Tree

elixir
defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      # Start OddSockets client as part of supervision tree
      {OddSockets, api_key: "your-api-key", name: MyApp.OddSocketsClient},
      
      # Other supervised processes
      MyApp.Repo,
      MyAppWeb.Endpoint,
      {MyApp.ChatManager, []}
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

GenServer Pattern

elixir
defmodule MyApp.NotificationManager do
  use GenServer

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  def init(_opts) do
    {:ok, client} = OddSockets.start_link(api_key: "your-api-key")
    
    # Subscribe to multiple channels
    channels = ["notifications", "alerts", "updates"]
    |> Enum.map(fn name ->
      channel = OddSockets.channel(client, name)
      :ok = OddSockets.Channel.subscribe(channel, &handle_notification/1)
      {name, channel}
    end)
    |> Map.new()

    {:ok, %{client: client, channels: channels}}
  end

  def broadcast_notification(type, message) do
    GenServer.cast(__MODULE__, {:broadcast, type, message})
  end

  def handle_cast({:broadcast, type, message}, state) do
    case Map.get(state.channels, type) do
      nil -> 
        {:noreply, state}
      channel ->
        {:ok, _} = OddSockets.Channel.publish(channel, message)
        {:noreply, state}
    end
  end

  defp handle_notification(message) do
    # Process incoming notifications
    IO.puts("Notification received: #{inspect(message)}")
  end
end

Task and Agent Integration

elixir
defmodule MyApp.MessageProcessor do
  def start_processing(api_key) do
    {:ok, client} = OddSockets.start_link(api_key: api_key)
    channel = OddSockets.channel(client, "processing-queue")
    
    # Use Agent to store state
    {:ok, agent} = Agent.start_link(fn -> %{processed: 0, errors: 0} end)
    
    # Subscribe with async processing
    :ok = OddSockets.Channel.subscribe(channel, fn message ->
      Task.start(fn ->
        process_message_async(message, agent)
      end)
    end)
    
    {:ok, {client, channel, agent}}
  end

  defp process_message_async(message, agent) do
    try do
      # Process the message
      result = process_message(message)
      
      # Update success counter
      Agent.update(agent, fn state ->
        %{state | processed: state.processed + 1}
      end)
      
      result
    rescue
      error ->
        # Update error counter
        Agent.update(agent, fn state ->
          %{state | errors: state.errors + 1}
        end)
        
        {:error, error}
    end
  end

  defp process_message(message) do
    # Your message processing logic here
    IO.puts("Processing: #{inspect(message)}")
    :ok
  end

  def get_stats(agent) do
    Agent.get(agent, & &1)
  end
end