OddSockets Elixir SDK
Official Elixir SDK for OddSockets real-time messaging platform
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:
def deps do
[
{:oddsockets, "~> 1.0.0"}
]
end
Then run:
mix deps.get
def deps do
[
{:oddsockets, git: "https://github.com/jyswee/oddsockets-elixir-sdk.git"}
]
end
def deps do
[
{:oddsockets, path: "../oddsockets"}
]
end
Quick Start
Basic Usage
# 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
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
# 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
{: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
: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
# 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
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
# 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}.
- Typing —
start_typing,stop_typing→user_typing,user_stopped_typing - Reactions —
add_reaction,remove_reaction,get_reactions→reaction_added,reaction_removed - Threads —
thread_reply,get_thread,subscribe_thread,follow_thread,mark_thread_read→thread_reply,thread_subscribed,thread_followed,thread_read_updated - Read receipts —
mark_read,mark_all_read,get_unread_counts→user_read,unread_count_updated,all_marked_read - Messages —
edit_message,delete_message,pin_message,unpin_message,get_pinned_messages,search_messages→message_edited,message_deleted,message_pinned,message_unpinned - Presence & status —
set_status,set_custom_status,set_dnd,get_user_presence→user_status_changed,custom_status_updated,dnd_status_changed - Channels —
create_channel,update_channel,archive_channel,invite_to_channel,join_channel,leave_channel→channel_created,channel_updated,user_invited,user_joined_channel,user_left_channel - DMs —
create_dm,send_dm,get_dm_conversations→dm_created,dm_received - Notifications —
subscribe_notifications,get_notifications,mark_notification_read,clear_notifications→notification,notification_read,notifications_cleared - Search —
search_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
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. Ackchallenge_create_success.report_progress— fire-and-forget metric progress. (no ack)complete_challenge— finalize with anoutcome. Ackchallenge_complete_success.unlock_achievement— fire-and-forget; passpercentComplete(0–100). (no ack)get_standings— request top-N + caller rank. Ackchallenge_standings_success.get_achievements— query achievement state. Ackachievement_state.send_challenge_invite— directed invite to another user. Ackchallenge_invite_success.reply_challenge_invite— accept/decline an invite. Ackchallenge_reply_success.cancel_challenge_invite— cancel a sent invite. Ackchallenge_invite_cancel_success.get_challenge_invites— list pending invites. Ackchallenge_invites.
Outcome vocabulary
The outcome passed to complete_challenge is one of:
completed— win (rank 1)failed— losstied— drawconceded— resign / concedeexpired— 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 broadcasts —
challenge_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,
0–1
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:
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
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
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
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
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