<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Konstantin Kai]]></title><description><![CDATA[Thoughts on frontend, backend, and the craft of building software.]]></description><link>https://konstantinkai.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/69b02d2eabc0d95001728753/c18501db-7563-42af-b5de-af0a15a25251.png</url><title>Konstantin Kai</title><link>https://konstantinkai.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 01 Sep 2026 16:19:00 GMT</lastBuildDate><atom:link href="https://konstantinkai.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[ReelKit: A Virtualized TikTok-Style Slider Engine]]></title><description><![CDATA[You're building a vertical feed — TikTok-style swipe, full-screen slides, thousands of items. You reach for a carousel library and hit the wall: it renders all slides to the DOM, chokes on touch gestu]]></description><link>https://konstantinkai.hashnode.dev/reelkit-a-virtualized-tiktok-style-slider-engine</link><guid isPermaLink="true">https://konstantinkai.hashnode.dev/reelkit-a-virtualized-tiktok-style-slider-engine</guid><category><![CDATA[React]]></category><category><![CDATA[webdev]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Konstantin Kai]]></dc:creator><pubDate>Mon, 16 Mar 2026 11:13:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69b02d2eabc0d95001728753/b9029f90-9fb3-4df2-9feb-afd7694a5bae.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You're building a vertical feed — TikTok-style swipe, full-screen slides, thousands of items. You reach for a carousel library and hit the wall: it renders all slides to the DOM, chokes on touch gestures, and bundles half the internet.</p>
<p><a href="https://reelkit.dev">ReelKit</a> renders <strong>3 DOM nodes</strong> at any time — previous, current, next — whether you have 4 slides or 40,000. Zero dependencies in core. Touch-first with momentum and snap. ~3.7 kB gzipped.</p>
<p><a href="https://stackblitz.com/github/KonstantinKai/reelkit-react-starter"><strong>Try it live on StackBlitz</strong></a> — no setup needed.</p>
<h2>Quick start</h2>
<pre><code class="language-shell">npm install @reelkit/react
</code></pre>
<pre><code class="language-typescript">import { useState } from 'react';
import { Reel, ReelIndicator } from '@reelkit/react';

const slides = [
  { title: 'Discover', color: '#6366f1' },
  { title: 'Trending', color: '#8b5cf6' },
  { title: 'Following', color: '#ec4899' },
  { title: 'For You', color: '#14b8a6' },
];

export default function App() {
  const [index, setIndex] = useState(0);

  return (
    &lt;Reel
      count={slides.length}
      style={{ width: '100%', height: '100dvh' }}
      direction="vertical"
      enableWheel
      useNavKeys
      afterChange={setIndex}
      itemBuilder={(i, _inRange, size) =&gt; (
        &lt;div
          style={{
            width: size[0],
            height: size[1],
            background: slides[i].color,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            color: 'white',
            fontSize: '2rem',
          }}
        &gt;
          {slides[i].title}
        &lt;/div&gt;
      )}
    &gt;
      &lt;ReelIndicator count={slides.length} active={index} /&gt;
    &lt;/Reel&gt;
  );
}
</code></pre>
<p>Swipe, keyboard arrows, mouse wheel — all work out of the box. The <code>size</code> prop is optional: omit it and ReelKit auto-measures via <code>ResizeObserver</code>.</p>
<h3>Programmatic navigation</h3>
<pre><code class="language-typescript">const apiRef = useRef&lt;ReelApi&gt;(null);

&lt;Reel count={100} apiRef={apiRef} itemBuilder={(i) =&gt; &lt;Slide index={i} /&gt;} /&gt;

&lt;button onClick={() =&gt; apiRef.current?.prev()}&gt;Prev&lt;/button&gt;
&lt;button onClick={() =&gt; apiRef.current?.next()}&gt;Next&lt;/button&gt;
&lt;button onClick={() =&gt; apiRef.current?.goTo(50)}&gt;Jump to 50&lt;/button&gt;
</code></pre>
<p>Navigation methods return promises — <code>await apiRef.current!.next()</code> resolves when the animation completes, so you can chain transitions sequentially.</p>
<h2>How it works</h2>
<h3>Virtualization</h3>
<p>Only render what's visible. ReelKit computes a visible range from the current index:</p>
<pre><code class="language-plaintext">Current index: 50, count: 10,000
Visible: [49, 50, 51]  ← 3 DOM nodes, always
</code></pre>
<p>With loop mode, it wraps at boundaries:</p>
<pre><code class="language-plaintext">Current index: 0, loop: true
Visible: [9999, 0, 1]  ← seamless wrap
</code></pre>
<p>When you call <code>goTo(5000)</code> from index 0, it doesn't animate through 5,000 slides. It temporarily swaps the adjacent slide with the target, animates a single step, and resolves. One smooth transition — the virtualization handles the rest.</p>
<h3>Signals, not React state</h3>
<p>ReelKit implements its own reactive system:</p>
<ul>
<li><p><strong>Signal</strong> — writable observable value</p>
</li>
<li><p><strong>ComputedSignal</strong> — lazy derived value (zero cost when unobserved)</p>
</li>
<li><p><strong>batch()</strong> — groups multiple updates into a single notification pass</p>
</li>
</ul>
<p>The React <code>&lt;Reel&gt;</code> component subscribes to these signals in effects and uses <code>flushSync()</code> on each animation frame to apply transforms synchronously — bypassing React's default batching. Your React tree doesn't re-render during swipes — transforms update at 60fps without touching component state.</p>
<p>When an animation completes, the index and transform value update in a single <code>batch()</code> call — observers never see an intermediate state. Navigation methods (<code>next()</code>, <code>goTo()</code>) return promises that resolve on completion, so you can chain transitions or await before taking the next action.</p>
<h3>Touch-first gestures</h3>
<p>The gesture controller detects the dominant axis from the initial touch vector and locks to it. It tracks per-frame delta, cumulative distance, and velocity. A fast swipe (&gt; 1400 px/s) or drag past the threshold triggers a slide change with snap-back animation.</p>
<h2>Packages</h2>
<table>
<thead>
<tr>
<th>Package</th>
<th>What it does</th>
<th>Size (gzip)</th>
</tr>
</thead>
<tbody><tr>
<td><code>@reelkit/core</code></td>
<td>Framework-agnostic engine</td>
<td>3.7 kB</td>
</tr>
<tr>
<td><code>@reelkit/react</code></td>
<td>React components + hooks</td>
<td>2.6 kB</td>
</tr>
<tr>
<td><code>@reelkit/react-reel-player</code></td>
<td>Full-screen video reel player</td>
<td>3.8 kB</td>
</tr>
<tr>
<td><code>@reelkit/react-lightbox</code></td>
<td>Image &amp; video gallery lightbox</td>
<td>3.4 kB</td>
</tr>
</tbody></table>
<p><strong>@reelkit/core</strong> is the engine — all slider logic, gesture detection, keyboard/wheel controllers, and the signal system. Zero dependencies. Framework-agnostic. Vue bindings are in progress.</p>
<p>Everything in core is factory functions, not classes — <code>createSliderController</code>, <code>createGestureController</code>, <code>createKeyboardController</code>, <code>createWheelController</code>. Plain closures, no <code>this</code> binding issues, better tree-shaking.</p>
<p><strong>@reelkit/react</strong> bridges the core to React. The <code>&lt;Reel&gt;</code> component creates a <code>SliderController</code> once via <code>useState</code> initializer and never recreates it. <code>&lt;ReelIndicator&gt;</code> renders Instagram-style scrollable dot indicators.</p>
<h3>Reel Player</h3>
<p>A ready-made TikTok/Instagram Reels overlay:</p>
<pre><code class="language-typescript">import { ReelPlayerOverlay } from '@reelkit/react-reel-player';
import '@reelkit/react-reel-player/styles.css';

&lt;ReelPlayerOverlay
  isOpen={isOpen}
  onClose={() =&gt; setIsOpen(false)}
  content={items}
  initialIndex={0}
/&gt;
</code></pre>
<p>Videos autoplay when the slide becomes active, pause when swiped away. A shared video element is reused across slides for iOS sound continuity.</p>
<h3>Lightbox</h3>
<p>Full-screen image gallery with three transition modes (slide, fade, zoom-in), swipe-to-close, keyboard navigation, and fullscreen API:</p>
<pre><code class="language-typescript">import { LightboxOverlay } from '@reelkit/react-lightbox';
import '@reelkit/react-lightbox/styles.css';

&lt;LightboxOverlay
  isOpen={index !== null}
  images={images}
  initialIndex={index ?? 0}
  onClose={() =&gt; setIndex(null)}
  transition="fade"
/&gt;
</code></pre>
<p>Video support is opt-in and tree-shakeable — image-only usage pays zero extra cost.</p>
<p>Both packages expose render props for controls, navigation, and slide content — replace anything you need.</p>
<h2>Links</h2>
<ul>
<li><p><a href="https://reelkit.dev">Documentation &amp; demos</a></p>
</li>
<li><p><a href="https://github.com/KonstantinKai/reelkit">GitHub</a></p>
</li>
<li><p><a href="https://www.npmjs.com/package/@reelkit/core">npm: @reelkit/core</a> | <a href="https://www.npmjs.com/package/@reelkit/react">@reelkit/react</a> | <a href="https://www.npmjs.com/package/@reelkit/react-reel-player">@reelkit/react-reel-player</a> | <a href="https://www.npmjs.com/package/@reelkit/react-lightbox">@reelkit/react-lightbox</a></p>
</li>
<li><p><a href="https://stackblitz.com/github/KonstantinKai/reelkit-react-starter">StackBlitz starter</a></p>
</li>
</ul>
<p>If you're building a vertical feed, a reel player, or a gallery lightbox in React — give ReelKit a try. MIT licensed, open source.</p>
<p>Feedback, suggestions, and bug reports are welcome — <a href="https://github.com/KonstantinKai/reelkit/issues">open an issue</a> or drop a comment below. And if ReelKit saved you some time, a <a href="https://github.com/KonstantinKai/reelkit">GitHub star</a> would mean a lot — it's a small thing, but it really helps the project get noticed.</p>
]]></content:encoded></item><item><title><![CDATA[Managing Flutter & Dart SDK Versions with Proto]]></title><description><![CDATA["It works on my machine." Three Flutter projects, three different SDK versions, and flutter downgrade is your most-used command. There has to be a better way — and there is.
Proto is a universal versi]]></description><link>https://konstantinkai.hashnode.dev/managing-flutter-dart-sdk-versions-with-proto</link><guid isPermaLink="true">https://konstantinkai.hashnode.dev/managing-flutter-dart-sdk-versions-with-proto</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Dart]]></category><category><![CDATA[tools]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[Konstantin Kai]]></dc:creator><pubDate>Tue, 10 Mar 2026 16:59:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69b02d2eabc0d95001728753/e0970ad7-369b-46eb-acc1-fdf6b85881aa.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>"It works on my machine." Three Flutter projects, three different SDK versions, and <code>flutter downgrade</code> is your most-used command. There has to be a better way — and there is.</p>
<p><a href="https://moonrepo.dev/proto">Proto</a> is a universal version manager (think <code>nvm</code> or <code>asdf</code>, but faster and cross-platform) — and now it supports Flutter and Dart through community WASM plugins.</p>
<h2>Why proto over FVM or asdf?</h2>
<p><strong>FVM</strong> is Flutter-specific. It works, but it's one more tool in your stack. If you already manage Node, Go, Rust, or Python versions, that's yet another version manager with its own config format.</p>
<p><strong>asdf</strong> supports many tools via plugins, but it's historically been shell-script based, Unix-only, and can be slow.</p>
<p><strong>proto</strong> gives you:</p>
<ul>
<li><p>One tool for all your SDKs (Node, Go, Rust, Python, Flutter, Dart, PHP, and more)</p>
</li>
<li><p>Blazing fast — plugins are compiled to WASM, not shell scripts</p>
</li>
<li><p>Cross-platform — Linux, macOS, and Windows</p>
</li>
<li><p>Per-project version pinning via <code>.prototools</code> (one file, all tools)</p>
</li>
<li><p>Automatic version detection from <code>pubspec.yaml</code></p>
</li>
<li><p>Auto-switching — proto picks the right version when you <code>cd</code> into a project</p>
</li>
</ul>
<h2>Getting started</h2>
<h3>Install proto</h3>
<pre><code class="language-shell"># macOS / Linux
curl -fsSL https://moonrepo.dev/install/proto.sh | bash

# Windows
irm https://moonrepo.dev/install/proto.ps1 | iex
</code></pre>
<h3>Add the Flutter plugin</h3>
<pre><code class="language-shell">proto plugin add flutter "github://KonstantinKai/proto-flutter-plugin"
</code></pre>
<p>One command. No git clones, no compiling from source. Proto downloads a tiny WASM plugin and you're ready to go.</p>
<h3>Install Flutter</h3>
<pre><code class="language-shell"># Install the latest stable
proto install flutter

# Install a specific version
proto install flutter 3.29

# Install a beta version
proto install flutter beta
</code></pre>
<p>Proto downloads the official Flutter SDK archive from Google's servers, extracts it, and makes it available immediately.</p>
<h3>Pin a version per project</h3>
<pre><code class="language-shell">cd my-flutter-project
proto pin flutter 3.29
</code></pre>
<p>This creates (or updates) a <code>.prototools</code> file in your project root:</p>
<pre><code class="language-plaintext">flutter = "3.29"
</code></pre>
<p>Now every teammate gets the exact same Flutter version on <code>proto install</code>. No surprises.</p>
<h2>Automatic version detection</h2>
<p>The plugin reads your <code>pubspec.yaml</code> out of the box. If you have:</p>
<pre><code class="language-yaml">environment:
  flutter: "&gt;=3.22.0 &lt;4.0.0"
</code></pre>
<p>Proto will detect and resolve the appropriate Flutter version — no extra configuration needed.</p>
<h2>Managing Dart alongside Flutter</h2>
<p>Flutter bundles Dart, so in most cases you don't need a separate Dart plugin. But if you have pure Dart projects (CLI tools, server apps, packages), there's a <a href="https://github.com/KonstantinKai/proto-dart-plugin">Dart plugin</a> too:</p>
<pre><code class="language-shell">proto plugin add dart "github://KonstantinKai/proto-dart-plugin"
proto install dart 3.7
</code></pre>
<p>It reads <code>environment.sdk</code> from <code>pubspec.yaml</code> and supports the same version pinning and auto-detection workflow.</p>
<h2>One config file for your entire stack</h2>
<p>Here's where proto really shines. A single <code>.prototools</code> file manages everything:</p>
<pre><code class="language-toml">flutter = "3.29"
node = "22"
go = "1.24"

[plugins.tools]
flutter = "github://KonstantinKai/proto-flutter-plugin"
</code></pre>
<p>Your whole team gets consistent SDK versions across Flutter, Dart, Node, Go — whatever your project needs. One file, committed to git, no ambiguity.</p>
<h2>How it works under the hood</h2>
<p>Curious about the internals?</p>
<p>Proto plugins are compiled to WebAssembly (WASM) and run in a sandboxed environment. The Flutter plugin:</p>
<ol>
<li><p>Fetches the official Flutter release manifest from Google's servers</p>
</li>
<li><p>Filters versions by your OS and architecture</p>
</li>
<li><p>Downloads and verifies the SDK archive (with SHA-256 checksum)</p>
</li>
<li><p>Extracts it to proto's tool directory</p>
</li>
<li><p>Exposes both <code>flutter</code> and <code>dart</code> executables</p>
</li>
</ol>
<p>There are no shell scripts, no git clones, no channel management. Just clean, deterministic version management.</p>
<h2>Supported platforms</h2>
<p>The Flutter plugin covers all officially supported platforms:</p>
<table>
<thead>
<tr>
<th>Platform</th>
<th>Architecture</th>
<th>Notes</th>
</tr>
</thead>
<tbody><tr>
<td>Linux</td>
<td>x64</td>
<td>All versions</td>
</tr>
<tr>
<td>macOS</td>
<td>x64</td>
<td>All versions</td>
</tr>
<tr>
<td>macOS</td>
<td>arm64 (Apple Silicon)</td>
<td>Stable &gt;= 3.0.0, beta &gt;= 2.12.0</td>
</tr>
<tr>
<td>Windows</td>
<td>x64</td>
<td>All versions</td>
</tr>
</tbody></table>
<h2>Useful commands</h2>
<pre><code class="language-shell"># List all available Flutter versions
proto versions flutter

# Check which version is active
proto run flutter -- --version

# Switch between versions
proto install flutter 3.22
proto pin flutter 3.22

# Remove a version
proto uninstall flutter 3.22
</code></pre>
<h2>Links</h2>
<p>The plugins are open source and available on GitHub:</p>
<ul>
<li><p><a href="https://github.com/KonstantinKai/proto-flutter-plugin">proto-flutter-plugin</a></p>
</li>
<li><p><a href="https://github.com/KonstantinKai/proto-dart-plugin">proto-dart-plugin</a></p>
</li>
</ul>
<p>If you hit any issues or have feature requests, open an issue on GitHub.</p>
<hr />
<p><em>If you also work with PHP, check out</em> <a href="https://github.com/KonstantinKai/proto-php-plugin"><em>proto-php-plugin</em></a> <em>and</em> <a href="https://github.com/KonstantinKai/proto-composer-plugin"><em>proto-composer-plugin</em></a> <em>— same approach, same workflow.</em></p>
]]></content:encoded></item><item><title><![CDATA[Stop Building API Dashboards From Scratch]]></title><description><![CDATA[Every API developer has been there. You ship an API, someone starts using it, and the questions begin:

"How many requests are we getting?"

"Who's our heaviest consumer?"

"Why did error rates spike ]]></description><link>https://konstantinkai.hashnode.dev/stop-building-api-dashboards-from-scratch</link><guid isPermaLink="true">https://konstantinkai.hashnode.dev/stop-building-api-dashboards-from-scratch</guid><category><![CDATA[api]]></category><category><![CDATA[webdev]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[Middleware]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Konstantin Kai]]></dc:creator><pubDate>Tue, 10 Mar 2026 15:57:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69b02d2eabc0d95001728753/5344e0c5-1b10-4993-97d6-0220ce27d837.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every API developer has been there. You ship an API, someone starts using it, and the questions begin:</p>
<ul>
<li><p>"How many requests are we getting?"</p>
</li>
<li><p>"Who's our heaviest consumer?"</p>
</li>
<li><p>"Why did error rates spike at 3am?"</p>
</li>
</ul>
<p>So you write a few SQL queries. Maybe stand up a Grafana instance. Add some log parsing. Before you know it, you've spent two days on infrastructure that has nothing to do with your actual product.</p>
<h2>The pattern I kept repeating</h2>
<p>For every API project, I built some version of the same system:</p>
<ol>
<li><p>Log request metadata to a database</p>
</li>
<li><p>Write queries to aggregate it</p>
</li>
<li><p>Build a dashboard to visualize it</p>
</li>
<li><p>Set up alerts when things go wrong</p>
</li>
</ol>
<p>The fourth time I did this, I realized it should be a product.</p>
<h2>PeekAPI: one middleware call, full API analytics</h2>
<p>PeekAPI is a middleware you add to your API server. Here's the full setup:</p>
<p><strong>Node.js (Express):</strong></p>
<pre><code class="language-typescript">import { peekapi } from "@peekapi/sdk-node";
app.use(peekapi({ apiKey: "pk_..." }));
</code></pre>
<p><strong>Python (FastAPI):</strong></p>
<pre><code class="language-python">from peekapi import PeekAPIMiddleware

app.add_middleware(PeekAPIMiddleware, api_key="pk_...")
</code></pre>
<p><strong>Go (net/http):</strong></p>
<pre><code class="language-go">handler := peekapi.Middleware(mux, peekapi.Config{
    APIKey: "pk_...",
})
http.ListenAndServe(":8080", handler)
</code></pre>
<p><strong>Rust (Actix Web):</strong></p>
<pre><code class="language-rust">App::new()
    .wrap(PeekApi::new("pk_..."))
    .service(/* your routes */)
</code></pre>
<p><strong>Ruby (Rails):</strong></p>
<pre><code class="language-ruby"># config/application.rb
config.middleware.use PeekAPI::Middleware, api_key: "pk_..."
</code></pre>
<p><strong>PHP (Laravel):</strong></p>
<pre><code class="language-php">// bootstrap/app.php
-&gt;withMiddleware(function (Middleware $middleware) {
    $middleware-&gt;append(\PeekAPI\Laravel\PeekApiMiddleware::class);
})
</code></pre>
<p><strong>Java (Spring Boot):</strong></p>
<pre><code class="language-properties"># application.properties
peekapi.api-key=pk_...
</code></pre>
<p><strong>Dart (Shelf)</strong></p>
<pre><code class="language-java">final client = await PeekApiClient.create(PeekApiOptions(
    apiKey: 'ak_live_...',
  ));

  final handler = const Pipeline()
      .addMiddleware(peekApiMiddleware(client))
      .addHandler(_router);
</code></pre>
<p>That's the entire integration for each language. No agents, no config files, no infrastructure to manage.</p>
<h2>What you get</h2>
<p>Once the middleware is running, your dashboard shows:</p>
<p><strong>Real-time request stream</strong> — every API call as it happens, with method, path, status, latency, and consumer identity.</p>
<p><strong>Endpoint analytics</strong> — request volume, error rates, and average latency for each route. Spot which endpoints are most used, which are failing, and which are slow.</p>
<p><strong>Consumer tracking</strong> — PeekAPI automatically identifies who's calling your API from authorization headers or API keys. Consumers are identified by a SHA-256 hash — raw credentials never leave your server.</p>
<p><strong>Smart alerts</strong> — get notified when error rates spike, latency exceeds thresholds, or an endpoint goes silent. Notifications via email, Slack, Discord, Telegram, or generic webhook.</p>
<h2>What it captures (and what it doesn't)</h2>
<p><strong>Captures:</strong> HTTP method, path, status code, response time, request/response size, and a hashed consumer identifier.</p>
<p><strong>Does NOT capture:</strong> request/response bodies, query parameters, or raw authentication credentials.</p>
<p>This is a deliberate design choice. PeekAPI answers "who, what, when, how fast" — not "what data was in the request." If you need payload inspection, you need a different tool.</p>
<h2>Zero dependencies, by design</h2>
<p>Every SDK is zero-dependency. The Node SDK uses only built-in modules (<code>https</code>, <code>crypto</code>, <code>fs</code>, <code>os</code>). Python uses only stdlib. Same for Go, Rust, Ruby, PHP, Java and Dart.</p>
<p>Why? Two reasons:</p>
<ol>
<li><p><strong>No supply chain risk.</strong> Your API middleware shouldn't pull in a tree of transitive dependencies.</p>
</li>
<li><p><strong>No conflicts.</strong> The SDK will never clash with your existing dependency versions.</p>
</li>
</ol>
<h2>Built for reliability</h2>
<p>The SDKs are designed to never affect your API's performance or reliability:</p>
<ul>
<li><p><strong>Async buffering</strong> — events are collected in memory and flushed in batches (configurable interval and batch size)</p>
</li>
<li><p><strong>Exponential backoff</strong> — if the analytics server is down, the SDK backs off automatically (max 5 consecutive failures)</p>
</li>
<li><p><strong>Disk persistence</strong> — after max flush failures, on non-retryable errors, or on process shutdown, undelivered events are saved to a JSONL file. Recovered automatically every 60 seconds and on startup</p>
</li>
<li><p><strong>Graceful shutdown</strong> — SIGTERM/SIGINT handlers persist buffered events to disk, recovered automatically on restart</p>
</li>
</ul>
<p>If PeekAPI's servers are unreachable, your API keeps running normally. Analytics are best-effort — they should never be a single point of failure.</p>
<h2>8 SDKs, 20+ frameworks</h2>
<table>
<thead>
<tr>
<th>Language</th>
<th>Frameworks</th>
</tr>
</thead>
<tbody><tr>
<td>Node.js</td>
<td>Express, Fastify, Koa, Hapi, NestJS</td>
</tr>
<tr>
<td>Python</td>
<td>ASGI, WSGI, Django</td>
</tr>
<tr>
<td>Go</td>
<td>net/http, Gin, Echo, Fiber, Chi</td>
</tr>
<tr>
<td>Rust</td>
<td>Actix Web, Axum, Rocket</td>
</tr>
<tr>
<td>Ruby</td>
<td>Rack, Rails</td>
</tr>
<tr>
<td>PHP</td>
<td>PSR-15, Laravel</td>
</tr>
<tr>
<td>Java</td>
<td>Spring Boot, Jakarta Servlet</td>
</tr>
<tr>
<td>Dart</td>
<td>Shelf</td>
</tr>
</tbody></table>
<h2>Try it</h2>
<p>Free tier at <a href="http://peekapi.dev">peekapi.dev</a> — 500K events/month, no credit card required. SDKs are MIT licensed.</p>
<p>If you have questions about the architecture or feature requests, I'd love to hear them in the comments.</p>
]]></content:encoded></item></channel></rss>