{% extends "base.html" %}
{% block title %}Pipey{% endblock %}
{% block styles %}
<style>
.hero{
display:grid;
grid-template-columns:1.05fr 0.95fr;
gap:40px;
align-items:center;
padding:40px 0 8px;
}
.hero h1{
font-size:36px;
line-height:1.16;
font-weight:600;
margin-bottom:16px;
letter-spacing:-0.02em;
}
.hero h1 .accent{ color:var(--accent); }
.hero p.lede{
color:var(--muted);
font-size:15.5px;
max-width:46ch;
margin-bottom:26px;
}
.cta-row{ display:flex; gap:10px; flex-wrap:wrap; }
.btn{
display:inline-flex; align-items:center; gap:9px;
padding:11px 20px;
border-radius:9px;
font-size:14px;
font-weight:600;
font-family:var(--font-body);
border:1px solid transparent;
transition:transform .12s ease, box-shadow .12s ease, background .12s ease, border-color .12s ease;
}
.btn-primary{ background:var(--accent); color:var(--accent-ink); box-shadow:0 6px 16px -6px #ffab2e66; }
.btn-primary:hover{ background:var(--accent-dark); transform:translateY(-1px); }
.btn-ghost{ background:transparent; border-color:var(--border-2); color:var(--ink); }
.btn-ghost:hover{ background:var(--surface-2); transform:translateY(-1px); }
.preview-wrap{ position:relative; }
.dispatch{
background:#08090a;
border:1px solid var(--border);
border-radius:var(--radius);
box-shadow:var(--shadow-md);
overflow:hidden;
}
.dispatch-bar{
display:flex; align-items:center; justify-content:space-between;
padding:11px 16px;
border-bottom:1px solid var(--border);
background:var(--surface);
}
.dispatch-bar .dots{ display:flex; gap:6px; }
.dispatch-bar .dots span{ width:8px; height:8px; border-radius:50%; background:var(--border-2); }
.dispatch-bar .name{ font-family:var(--font-mono); font-size:11.5px; color:var(--muted); }
.dispatch-body{ padding:16px 18px 18px; font-family:var(--font-mono); font-size:12.5px; line-height:2.05; min-height:196px; }
.dispatch-row{ display:flex; align-items:center; gap:10px; opacity:0; animation:rowIn .4s ease forwards; white-space:nowrap; overflow:hidden; }
.dispatch-row .t{ color:var(--muted-2); flex-shrink:0; }
.dispatch-row .ev{ color:var(--ink); flex-shrink:0; }
.dispatch-row .arrow{ color:var(--muted-2); flex-shrink:0; }
.dispatch-row .uid{ color:var(--muted); flex-shrink:0; }
.dispatch-row .stat{ margin-left:auto; flex-shrink:0; padding:1px 8px; border-radius:5px; font-weight:600; font-size:11px; }
.stat.ok{ color:var(--live); background:var(--live-soft); }
.stat.pending{ color:var(--muted); background:var(--surface-2); }
@keyframes rowIn{ from{ opacity:0; transform:translateY(3px); } to{ opacity:1; transform:translateY(0); } }
.dispatch-row:nth-child(1){ animation-delay:.1s; }
.dispatch-row:nth-child(2){ animation-delay:.5s; }
.dispatch-row:nth-child(3){ animation-delay:.9s; }
.dispatch-row:nth-child(4){ animation-delay:1.3s; }
.dispatch-row:nth-child(5){ animation-delay:1.7s; }
.live-badge{
position:absolute;
bottom:-16px;
right:18px;
display:flex;
align-items:center;
gap:10px;
background:var(--surface);
border:1px solid var(--border-2);
color:var(--ink);
padding:10px 15px;
border-radius:10px;
box-shadow:var(--shadow-lg);
}
.live-dot-wrap{
position:relative;
width:8px; height:8px;
}
.live-dot-wrap .core{
position:absolute; inset:0;
background:var(--live);
border-radius:50%;
}
@media (prefers-reduced-motion: no-preference){
.live-dot-wrap .ring{
position:absolute; inset:-5px;
border-radius:50%;
background:var(--live);
opacity:.5;
animation:pulse-ring 2s ease-out infinite;
}
}
@keyframes pulse-ring{
0%{ transform:scale(0.5); opacity:.5; }
100%{ transform:scale(2.4); opacity:0; }
}
.live-badge .count{ font-family:var(--font-mono); font-weight:700; font-size:14px; }
.live-badge .label{ font-size:11.5px; color:var(--muted); }
.section-title{ font-size:19px; font-weight:600; margin-bottom:4px; }
.section-sub{ font-size:13.5px; color:var(--muted); margin-bottom:18px; }
.steps-row{
display:grid;
grid-template-columns:repeat(3, 1fr);
gap:0;
}
.step{ padding:4px 18px; position:relative; }
.step + .step{ border-left:1px solid var(--border); }
.step .n{ font-family:var(--font-mono); color:var(--accent); font-size:12px; margin-bottom:8px; }
.step .t{ font-weight:600; font-size:14.5px; margin-bottom:4px; }
.step .d{ font-size:13px; color:var(--muted); }
.feature-grid{
display:grid;
grid-template-columns:repeat(2, 1fr);
gap:12px;
}
.feature{
background:var(--surface);
border:1px solid var(--border);
border-radius:var(--radius-sm);
padding:18px;
transition:border-color .15s ease, transform .15s ease;
}
.feature:hover{ border-color:var(--border-2); transform:translateY(-2px); }
.feature .t{ font-weight:600; font-size:14.5px; margin-bottom:4px; }
.feature .d{ font-size:13px; color:var(--muted); margin-bottom:12px; }
.mini{
background:#08090a;
border:1px solid var(--border);
border-radius:7px;
padding:10px 12px;
font-family:var(--font-mono);
font-size:11.5px;
line-height:1.85;
color:var(--muted);
overflow:hidden;
}
.mini .hi{ color:var(--live); }
.mini .warn{ color:var(--warn); }
.mini .accent{ color:var(--accent); }
.mini .dim{ color:var(--muted-2); text-decoration:line-through; }
.mini-row{ display:flex; justify-content:space-between; gap:8px; }
.mini-tags{ display:flex; flex-wrap:wrap; gap:6px; }
.mini-tags span{
font-size:11px; font-family:var(--font-mono);
color:var(--muted); background:var(--surface-2);
border:1px solid var(--border); padding:3px 8px; border-radius:5px;
}
.mini-instances{ display:flex; align-items:center; gap:8px; justify-content:center; padding:6px 0; }
.mini-box{ font-size:10.5px; color:var(--muted); background:var(--surface-2); border:1px solid var(--border); border-radius:5px; padding:5px 8px; }
.mini-instances .to{ color:var(--muted-2); }
.mini-box.redis{ color:var(--accent); border-color:var(--accent-soft); }
.usecase-list{ display:flex; flex-wrap:wrap; gap:8px; }
.usecase-list span{
font-size:13px;
font-weight:500;
color:var(--ink);
background:var(--surface-2);
border:1px solid var(--border);
padding:7px 14px;
border-radius:8px;
}
@media (max-width:760px){
.hero{ grid-template-columns:1fr; padding-top:20px; }
.feature-grid{ grid-template-columns:1fr; }
.steps-row{ grid-template-columns:1fr; }
.step + .step{ border-left:none; border-top:1px solid var(--border); padding-top:16px; }
.hero h1{ font-size:27px; }
.preview-wrap{ margin-top:8px; }
}
.sponsor-card{
background:var(--surface);
border:1px solid var(--border);
border-radius:var(--radius);
padding:30px;
margin-bottom:18px;
scroll-margin-top:88px;
}
.sponsor-head{ display:flex; align-items:flex-start; gap:14px; margin-bottom:22px; }
.sponsor-head .icon{
flex:none;
width:38px; height:38px;
border-radius:9px;
background:var(--accent-soft);
color:var(--accent);
display:flex; align-items:center; justify-content:center;
font-size:15px;
}
.sponsor-head h2{ font-size:18px; font-weight:600; margin-bottom:4px; }
.sponsor-head p{ color:var(--muted); font-size:13.5px; max-width:52ch; }
.sponsor-grid{
display:grid;
grid-template-columns:repeat(3, 1fr);
gap:12px;
}
.sponsor-option{
background:var(--surface-2);
border:1px solid var(--border);
border-radius:var(--radius-sm);
padding:18px;
display:flex;
flex-direction:column;
gap:10px;
transition:border-color .15s ease;
}
.sponsor-option:hover{ border-color:var(--border-2); }
.sponsor-option .top{ display:flex; align-items:center; gap:10px; }
.sponsor-option .badge{
width:30px; height:30px;
border-radius:8px;
display:flex; align-items:center; justify-content:center;
font-size:13px;
flex:none;
}
.badge.github{ background:var(--border); color:var(--ink); }
.badge.kofi{ background:#ff5e5b22; color:#ff8583; }
.badge.mpesa{ background:#43b02a22; color:#5fd444; }
.sponsor-option .name{ font-weight:600; font-size:14px; }
.sponsor-option .desc{ font-size:12.5px; color:var(--muted); flex:1; }
.sponsor-option .action{
display:inline-flex; align-items:center; justify-content:center; gap:7px;
padding:9px 14px;
border-radius:8px;
font-size:13px;
font-weight:600;
text-align:center;
cursor:pointer;
border:none;
font-family:var(--font-body);
transition:opacity .12s ease, transform .12s ease;
}
.sponsor-option .action:hover{ transform:translateY(-1px); }
.action.github{ background:var(--accent); color:var(--accent-ink); }
.action.kofi{ background:#ff5e5b; color:#fff; }
.action.mpesa{ background:#43b02a; color:#fff; }
.mpesa-detail{
background:#08090a;
border:1px dashed var(--border-2);
border-radius:7px;
padding:9px 11px;
font-family:var(--font-mono);
font-size:12px;
color:var(--ink);
line-height:1.5;
}
.mpesa-detail .label{ color:var(--muted-2); font-size:10.5px; text-transform:uppercase; letter-spacing:.04em; }
@media (max-width:760px){
.sponsor-grid{ grid-template-columns:1fr; }
}
</style>
{% endblock %}
{% block content %}
<div class="hero">
<div>
<div class="eyebrow">Open source, self-hosted</div>
<h1>Deliver real-time events with a <span class="accent">simple HTTP request.</span></h1>
<p class="lede">
Tired of managing socket connections in every service. Your backend simply sends an HTTP request to Pipey. Pipey routes the event to the correct connected user in real time
</p>
<div class="cta-row">
<a href="/docs" class="btn btn-primary"><i class="fa-solid fa-book"></i>Read the docs</a>
<a href="/client" class="btn btn-ghost"><i class="fa-solid fa-terminal"></i>Open test client</a>
</div>
</div>
</<br>
><br>
<div class="preview-wrap">
<div class="dispatch">
<div class="dispatch-bar">
<span class="dots">
<span></span><span></span><span></span>
</span>
<span class="name">pipey · delivery log</span>
</div>
<div class="dispatch-body">
<div class="dispatch-row">
<span class="t">10:41:02</span>
<span class="method">POST</span>
<span class="route">/api/v1/pipey/publish</span>
<span class="arrow">→</span>
<span class="uid">user_8f2a</span>
<span class="stat ok">WS DELIVERED</span>
</div>
<div class="dispatch-row">
<span class="t">10:41:05</span>
<span class="method">POST</span>
<span class="route">/api/v1/pipey/publish</span>
<span class="arrow">→</span>
<span class="uid">user_11c9</span>
<span class="stat ok">WS DELIVERED</span>
</div>
<div class="dispatch-row">
<span class="t">10:41:09</span>
<span class="method">POST</span>
<span class="route">/api/v1/pipey/publish</span>
<span class="arrow">→</span>
<span class="uid">user_04ee</span>
<span class="stat pending">USER OFFLINE</span>
</div>
<div class="dispatch-row">
<span class="t">10:41:14</span>
<span class="method">POST</span>
<span class="route">/api/v1/pipey/publish</span>
<span class="arrow">→</span>
<span class="uid">user_8f2a</span>
<span class="stat ok">WS DELIVERED</span>
</div>
<div class="dispatch-row">
<span class="t">10:41:20</span>
<span class="method">POST</span>
<span class="route">/api/v1/pipey/publish</span>
<span class="arrow">→</span>
<span class="uid">user_c02f</span>
<span class="stat ok">WS DELIVERED</span>
</div>
</div>
</div>
<div class="live-badge">
<span class="live-dot-wrap">
<span class="ring"></span>
<span class="core"></span>
</span>
<span class="count">{{ online_users }}</span>
<span class="label">connected now</span>
</div>
</div>
</div>
<div class="card" style="margin-top:28px;">
<div class="steps-row">
<div class="step">
<div class="n">01</div>
<div class="t">Your backend sends</div>
<div class="d">POST a user id and a JSON payload to Pipey. No socket code on your end.</div>
</div>
<div class="step">
<div class="n">02</div>
<div class="t">Pipey looks up the user</div>
<div class="d">Connection state is checked in Redis, so it works no matter which instance is running.</div>
</div>
<div class="step">
<div class="n">03</div>
<div class="t">The browser receives it</div>
<div class="d">If the user is connected, the payload lands on their open socket immediately.</div>
</div>
</div>
</div>
<div class="card" style="margin-top:24px;">
<h2 class="section-title">Why Pipey</h2>
<p class="section-sub">Six things it actually does, not a features list for its own sake.</p>
<div class="feature-grid">
<div class="feature">
<div class="t">One HTTP call</div>
<div class="d">No client library and no channel to set up. Send a POST request with a user id and Pipey handles the rest.</div>
<div class="mini">curl -X POST /pipey/publish \<br> -d '{"recipient":"user_8f2a", ...}'</div>
</div>
<div class="feature">
<div class="t">Targets one user</div>
<div class="d">Every event goes to a specific connected user. There is no room or topic to subscribe to.</div>
<div class="mini">
<div class="mini-row"><span>user_8f2a</span><span class="hi">online</span></div>
<div class="mini-row"><span>user_11c9</span><span class="hi">online</span></div>
<div class="mini-row"><span>user_04ee</span><span class="warn">offline</span></div>
</div>
</div>
<div class="feature">
<div class="t">Redis backed routing</div>
<div class="d">Connection state lives in Redis, so delivery still works when you run more than one instance.</div>
<div class="mini mini-instances">
<span class="mini-box">pipey-1</span><span class="to">→</span>
<span class="mini-box redis">redis</span><span class="to">←</span>
<span class="mini-box">pipey-2</span>
</div>
</div>
<div class="feature">
<div class="t">Heartbeats and cleanup</div>
<div class="d">Pipey checks for dead connections and clears them on its own. You do not need to watch for this.</div>
<div class="mini">
<div class="mini-row"><span>user_8f2a</span><span class="hi">ping ok</span></div>
<div class="mini-row"><span class="dim">user_9a31</span><span class="warn">cleared</span></div>
</div>
</div>
<div class="feature">
<div class="t">Built on Actix Web</div>
<div class="d">A small Rust service that runs alongside your backend and does one job.</div>
<div class="mini">$ cargo run --release<br><span class="hi">listening on :8080</span></div>
</div>
<div class="feature">
<div class="t">Publish from anywhere</div>
<div class="d">Any backend that can make an HTTP request can publish through Pipey. Nothing to install on that side.</div>
<div class="mini mini-tags">
<span>curl</span><span>node</span><span>python</span><span>go</span><span>rust</span><span>php</span>
</div>
</div>
</div>
</div>
<div class="card">
<h2 class="section-title">Example</h2>
<p class="section-sub">The entire integration, start to finish.</p>
<pre><code>POST /api/v1/pipey/publish
Content-Type: application/json
{
"recipient": "550e8400-e29b-41d4-a716-446655440000",
"payload": {
"message": "Hello Pipey!"
}
}</code></pre>
<p style="color:var(--muted); font-size:14px; margin-top:12px;">
If <code>recipient</code> is connected, the payload is delivered over their WebSocket right away.
If not, Pipey responds with <code>404</code> and it is up to your backend to store the event, retry later, or drop it.
</p>
</div>
<div class="card">
<h2 class="section-title">Where it fits</h2>
<p class="section-sub">Anywhere your backend needs to reach a specific, currently online user.</p>
<div class="usecase-list">
<span>Realtime notifications</span>
<span>Chat delivery</span>
<span>Live dashboards</span>
<span>Ride tracking</span>
<span>Payment status</span>
<span>IoT telemetry</span>
<span>Collaborative apps</span>
<span>Gaming events</span>
</div>
</div>
<div class="sponsor-card" id="sponsor">
<div class="sponsor-head">
<span class="icon"><i class="fa-solid fa-heart"></i></span>
<div>
<h2>Support Pipey</h2>
<p>Pipey is free and open source. If it is saving you time, a small contribution helps cover hosting and keeps the project maintained.</p>
</div>
</div>
<div class="sponsor-grid">
<div class="sponsor-option">
<div class="top">
<span class="badge github"><i class="fa-brands fa-github"></i></span>
<span class="name">GitHub Sponsors</span>
</div>
<p class="desc">One-off or monthly, billed straight through GitHub.</p>
<a class="action github" href="https://github.com/sponsors/anomalous254" target="_blank" rel="noopener noreferrer">
<i class="fa-solid fa-arrow-up-right-from-square"></i> Sponsor
</a>
</div>
<div class="sponsor-option">
<div class="top">
<span class="badge kofi"><i class="fa-solid fa-mug-hot"></i></span>
<span class="name">Ko-fi</span>
</div>
<p class="desc">Buy a coffee, no account needed.</p>
<a class="action kofi" href="https://ko-fi.com/nyando" target="_blank" rel="noopener noreferrer">
<i class="fa-solid fa-arrow-up-right-from-square"></i> ko-fi.com/nyando
</a>
</div>
<div class="sponsor-option">
<div class="top">
<span class="badge mpesa"><i class="fa-solid fa-mobile-screen-button"></i></span>
<span class="name">M-Pesa</span>
</div>
<p class="desc">Send directly, Kenya only.</p>
<div class="mpesa-detail">
<div class="label">Send money to</div>
0116611118, Peter Nyando
</div>
<button class="action mpesa" id="copy-mpesa" type="button">
<i class="fa-regular fa-copy"></i> Copy number
</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const copyBtn = document.getElementById("copy-mpesa");
if (copyBtn) {
copyBtn.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText("0116611118");
const original = copyBtn.innerHTML;
copyBtn.innerHTML = '<i class="fa-solid fa-check"></i> Copied';
setTimeout(() => { copyBtn.innerHTML = original; }, 1800);
} catch (err) {
console.error("Copy failed:", err);
}
});
}
</script>
{% endblock %}