pipey 0.2.1

A lightweight HTTP-to-WebSocket event delivery service.
Documentation
{% extends "base.html" %}

{% block title %}SDK | Pipey{% endblock %}

{% block styles %}
    /* ---------- SDK page ---------- */
    .sdk-hero{ margin-bottom:8px; }
    .sdk-hero h1{
        font-size:clamp(28px, 4vw, 38px);
        font-weight:700;
        line-height:1.15;
        margin-bottom:12px;
    }
    .sdk-hero p{
        color:var(--muted);
        font-size:15.5px;
        max-width:60ch;
    }
    .sdk-hero p code{ font-size:0.86em; }

    .section-label{
        font-family:var(--font-mono);
        font-size:11.5px;
        font-weight:600;
        color:var(--muted-2);
        text-transform:uppercase;
        letter-spacing:0.06em;
        margin-bottom:10px;
    }

    /* Tabs */
    .tabs{
        display:flex;
        flex-wrap:wrap;
        gap:4px;
        background:var(--surface-2);
        border:1px solid var(--border);
        border-radius:999px;
        padding:4px;
        width:max-content;
        max-width:100%;
        margin-bottom:0;
    }
    .tab-btn{
        display:inline-flex; align-items:center; gap:7px;
        border:none;
        background:transparent;
        color:var(--muted);
        font-family:var(--font-body);
        font-size:13px;
        font-weight:600;
        padding:7px 15px;
        border-radius:999px;
        cursor:pointer;
        white-space:nowrap;
        transition:background .15s ease, color .15s ease;
    }
    .tab-btn:hover{ color:var(--ink); }
    .tab-btn.active{
        background:var(--accent);
        color:var(--accent-ink);
    }
    .tab-panels{ margin-top:14px; }
    .tab-panel{ display:none; }
    .tab-panel.active{ display:block; }

    .panel-card{
        background:var(--surface);
        border:1px solid var(--border);
        border-radius:var(--radius);
        box-shadow:var(--shadow-sm);
        padding:24px;
        margin-bottom:18px;
    }
    .panel-card h2{
        font-size:19px;
        font-weight:700;
        margin-bottom:6px;
    }
    .panel-card > .panel-desc{
        color:var(--muted);
        font-size:14px;
        margin-bottom:18px;
    }

    /* Status table */
    .status-table{
        width:100%;
        border-collapse:collapse;
        font-size:13.5px;
    }
    .status-table th{
        text-align:left;
        font-family:var(--font-mono);
        font-size:11px;
        text-transform:uppercase;
        letter-spacing:0.04em;
        color:var(--muted-2);
        font-weight:600;
        padding:0 0 10px;
        border-bottom:1px solid var(--border);
    }
    .status-table td{
        padding:12px 0;
        border-bottom:1px solid var(--border);
        vertical-align:top;
    }
    .status-table tr:last-child td{ border-bottom:none; }
    .status-pill{
        display:inline-flex;
        font-family:var(--font-mono);
        font-size:12px;
        font-weight:700;
        padding:2px 9px;
        border-radius:999px;
    }
    .status-200{ background:var(--live-soft); color:var(--live); }
    .status-404{ background:var(--warn-soft); color:var(--warn); }
    .status-4xx{ background:var(--error-soft); color:var(--error); }
    .status-table td.desc{ color:var(--muted); padding-right:12px; }

    .feature-grid{
        display:grid;
        grid-template-columns:repeat(2, 1fr);
        gap:14px;
        margin-top:4px;
    }
    .feature-grid .item{
        display:flex; gap:10px;
        font-size:13.5px;
        color:var(--muted);
    }
    .feature-grid .item i{
        color:var(--accent);
        margin-top:3px;
        font-size:12px;
        flex-shrink:0;
    }
    .feature-grid .item strong{ color:var(--ink); }

    .next-links{
        display:flex; flex-wrap:wrap; gap:10px;
        margin-top:6px;
    }
    .next-links a{
        display:inline-flex; align-items:center; gap:8px;
        background:var(--accent);
        color:var(--accent-ink);
        font-size:13.5px;
        font-weight:600;
        padding:10px 16px;
        border-radius:10px;
        transition:background .15s ease, transform .15s ease;
    }
    .next-links a:hover{ background:var(--accent-dark); color:var(--accent-ink); transform:translateY(-1px); }
    .next-links a.secondary{
        background:var(--surface-2);
        color:var(--ink);
        border:1px solid var(--border-2);
    }
    .next-links a.secondary:hover{ background:var(--border); transform:translateY(-1px); }

    @media (max-width:640px){
        .feature-grid{ grid-template-columns:1fr; }
        .tabs{ width:100%; overflow-x:auto; }
    }
{% endblock %}

{% block content %}

<div class="sdk-hero">
    <span class="eyebrow">SDK &amp; CODE EXAMPLES</span>
    <h1>There's no library to install.<br>Just an HTTP call.</h1>
    <p>Pipey doesn't ship a client SDK because it doesn't need one. Publishing an event is one <code>POST</code> request from whatever backend you already have. Pick a language below and copy it in.</p>
</div>
</br>

<!-- Publish examples -->
<div class="panel-card">
    <div class="section-label">01 · Publish an event</div>
    <h2>Send from your backend</h2>
    <p class="panel-desc">Every example below does the same thing: sends a <code>payload</code> to a <code>recipient</code> user_id via <code>POST /api/v1/pipey/publish</code>.</p>

    <div class="tabs" role="tablist" aria-label="Language">
        <button class="tab-btn active" data-tab="curl">cURL</button>
        <button class="tab-btn" data-tab="node">Node.js</button>
        <button class="tab-btn" data-tab="python">Python</button>
        <button class="tab-btn" data-tab="go">Go</button>
        <button class="tab-btn" data-tab="rust">Rust</button>
        <button class="tab-btn" data-tab="php">PHP</button>
    </div>

    <div class="tab-panels">
        <div class="tab-panel active" data-panel="curl">
<pre><code>curl -X POST https://pipey.up.railway.app/api/v1/pipey/publish \
  -H "Content-Type: application/json" \
  -d '{
    "recipient": "550e8400-e29b-41d4-a716-446655440000",
    "payload": { "message": "Hello Pipey!" }
  }'</code></pre>
        </div>

        <div class="tab-panel" data-panel="node">
<pre><code>const res = await fetch("https://pipey.up.railway.app/api/v1/pipey/publish", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    recipient: "550e8400-e29b-41d4-a716-446655440000",
    payload: { message: "Hello Pipey!" }
  })
});

if (res.status === 404) {
  // recipient isn't connected right now. Store it, retry it, or drop it
} else if (res.ok) {
  // delivered over their socket immediately
}</code></pre>
        </div>

        <div class="tab-panel" data-panel="python">
<pre><code>import requests

res = requests.post(
    "https://pipey.up.railway.app/api/v1/pipey/publish",
    json={
        "recipient": "550e8400-e29b-41d4-a716-446655440000",
        "payload": {"message": "Hello Pipey!"},
    },
)

if res.status_code == 404:
    # recipient isn't connected right now. Store it, retry it, or drop it
    ...
res.raise_for_status()</code></pre>
        </div>

        <div class="tab-panel" data-panel="go">
<pre><code>body, _ := json.Marshal(map[string]any{
    "recipient": "550e8400-e29b-41d4-a716-446655440000",
    "payload":   map[string]string{"message": "Hello Pipey!"},
})

resp, err := http.Post(
    "https://pipey.up.railway.app/api/v1/pipey/publish",
    "application/json",
    bytes.NewReader(body),
)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusNotFound {
    // recipient isn't connected right now. Store it, retry it, or drop it
}</code></pre>
        </div>

        <div class="tab-panel" data-panel="rust">
<pre><code>let client = reqwest::Client::new();

let res = client
    .post("https://pipey.up.railway.app/api/v1/pipey/publish")
    .json(&serde_json::json!({
        "recipient": "550e8400-e29b-41d4-a716-446655440000",
        "payload": { "message": "Hello Pipey!" }
    }))
    .send()
    .await?;

if res.status() == reqwest::StatusCode::NOT_FOUND {
    // recipient isn't connected right now. Store it, retry it, or drop it
}</code></pre>
        </div>

        <div class="tab-panel" data-panel="php">
<pre><code>$ch = curl_init("https://pipey.up.railway.app/api/v1/pipey/publish");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
    CURLOPT_POSTFIELDS => json_encode([
        "recipient" => "550e8400-e29b-41d4-a716-446655440000",
        "payload"   => ["message" => "Hello Pipey!"],
    ]),
]);

curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status === 404) {
    // recipient isn't connected right now. Store it, retry it, or drop it
}</code></pre>
        </div>
    </div>
</div>

<!-- Response codes -->
<div class="panel-card">
    <div class="section-label">02 · What comes back</div>
    <h2>Response codes</h2>
    <p class="panel-desc">Pipey answers synchronously, so your backend knows the outcome before it moves on.</p>

    <table class="status-table">
        <thead>
            <tr><th style="width:90px;">Status</th><th>Meaning</th></tr>
        </thead>
        <tbody>
            <tr>
                <td><span class="status-pill status-200">200</span></td>
                <td class="desc">Recipient was connected and the payload was delivered over their WebSocket immediately.</td>
            </tr>
            <tr>
                <td><span class="status-pill status-404">404</span></td>
                <td class="desc">Recipient isn't connected right now. Nothing was delivered. Store it, retry it, or drop it, it's your call.</td>
            </tr>
            <tr>
                <td><span class="status-pill status-4xx">400 / 422</span></td>
                <td class="desc">Malformed request. Check that <code>recipient</code> and <code>payload</code> are present and valid JSON.</td>
            </tr>
        </tbody>
    </table>
</div>

<!-- Receiving on the client -->
<div class="panel-card">
    <div class="section-label">03 · Receive on the client</div>
    <h2>Connect from the browser</h2>
    <p class="panel-desc">The recipient just opens a WebSocket keyed to their <code>user_id</code>. There is no auth handshake and no channel to subscribe to. Pipey routes by id.</p>

<pre><code>const socket = new WebSocket(
  "wss://pipey.up.railway.app/ws/550e8400-e29b-41d4-a716-446655440000"
);

socket.onmessage = (event) => {
  const { message } = JSON.parse(event.data);
  console.log("Received:", message);
};

socket.onclose = () => {
  // reconnect with backoff if you need the connection to stay warm
};</code></pre>
    
 </br>

    <div class="feature-grid">
        <div class="item"><i class="fa-solid fa-heart-pulse"></i><span><strong>Heartbeats</strong> keep the connection alive and let Pipey clear it if it goes dead.</span></div>
        <div class="item"><i class="fa-solid fa-database"></i><span><strong>Redis backed routing</strong> means this works the same whether Pipey is one instance or ten.</span></div>
        <div class="item"><i class="fa-solid fa-arrows-rotate"></i><span><strong>Reconnects</strong> are on you. A simple exponential backoff on <code>onclose</code> is usually enough.</span></div>
        <div class="item"><i class="fa-solid fa-user-check"></i><span><strong>One socket per user</strong> is the assumption. Open a fresh one per session or device as needed.</span></div>
    </div>
</div>

<!-- Next steps -->
<div class="panel-card">
    <div class="section-label">04 · Next</div>
    <h2>Try it before you wire it up</h2>
    <p class="panel-desc">Open the test client to watch an event land in real time, or read the full docs for the details above plus error handling and deployment notes.</p>
    <div class="next-links">
        <a href="/client"><i class="fa-solid fa-terminal"></i> Open test client</a>
        <a href="/docs" class="secondary"><i class="fa-solid fa-book"></i> Read the docs</a>
        <a href="https://github.com/anomalous254/pipey" target="_blank" rel="noopener noreferrer" class="secondary"><i class="fa-brands fa-github"></i> View source</a>
    </div>
</div>

{% endblock %}

{% block scripts %}
<script>
(() => {
    const buttons = document.querySelectorAll(".tab-btn");
    const panels = document.querySelectorAll(".tab-panel");
    buttons.forEach((btn) => {
        btn.addEventListener("click", () => {
            const target = btn.dataset.tab;
            buttons.forEach((b) => b.classList.toggle("active", b === btn));
            panels.forEach((p) => p.classList.toggle("active", p.dataset.panel === target));
        });
    });
})();
</script>
{% endblock %}