惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
Darknet – Hacking Tools, Hacker News & Cyber Security
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
阮一峰的网络日志
阮一峰的网络日志
G
Google Developers Blog
宝玉的分享
宝玉的分享
爱范儿
爱范儿
Last Week in AI
Last Week in AI
U
Unit 42
B
Blog RSS Feed
Microsoft Azure Blog
Microsoft Azure Blog
D
DataBreaches.Net
Recent Commits to openclaw:main
Recent Commits to openclaw:main
雷峰网
雷峰网
T
The Exploit Database - CXSecurity.com
L
LangChain Blog
C
CERT Recently Published Vulnerability Notes
S
Schneier on Security
C
Cisco Blogs
MongoDB | Blog
MongoDB | Blog
G
GRAHAM CLULEY
Hacker News - Newest:
Hacker News - Newest: "LLM"
大猫的无限游戏
大猫的无限游戏
L
LINUX DO - 最新话题
D
Docker
K
Kaspersky official blog
Security Latest
Security Latest
博客园 - 【当耐特】
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
The Hacker News
The Hacker News
P
Privacy International News Feed
Microsoft Security Blog
Microsoft Security Blog
V2EX - 技术
V2EX - 技术
The Last Watchdog
The Last Watchdog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Martin Fowler
Martin Fowler
Latest news
Latest news
Project Zero
Project Zero
TaoSecurity Blog
TaoSecurity Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
T
Threat Research - Cisco Blogs
H
Heimdal Security Blog
N
News and Events Feed by Topic
N
News | PayPal Newsroom
Help Net Security
Help Net Security
A
Arctic Wolf
Cisco Talos Blog
Cisco Talos Blog
Engineering at Meta
Engineering at Meta
M
MIT News - Artificial intelligence

Hacker News: Show HN

PurrrrrFocus: Pomodoro Timer App - App Store Workflow Engine — Multi-Step Orchestration for Bun RapidPhoto: Pro Photo Editor App - App Store GitHub - DheerG/swarms: Achieve extraordinary results with claude code across a variety of tasks SPICE simulation → oscilloscope → verification with Claude Code — Lucas Gerads Show HN: VCoding – A 5 MB native Windows IDE with no dynamic dependencies Show HN: LLMs don't hallucinate because they're bad at math, it's the format GitHub - Agent-FM/agentfm-core: AgentFM is a peer-to-peer network that turns everyday computers into a decentralized AI supercomputer. AgentFM lets you run massive AI workloads directly across a global mesh of idle CPUs and GPUs. Show HN: Tracking Top US Science Olympiad Alumni over Last 25 Years GitHub - Potarix/agent-hub: One place to talk to all your agents Show HN: Runtime security for AI agents(injection,tool abuse, data exfiltration) GitHub - dubeyKartikay/lazyspotify: Terminal Spotify client for macOS and Linux GitHub - the-banana-tool/king-louie: Easy to use GUI Personal AI Assistant. Win/Linux/Mac. Show HN I made my vacation rental bookable by AI agents–no Airbnb, 0% commission GitHub - basteez/jsf-autoreload: maven plugin to enable hot reload on jsf projects uvm32/hosts/host-gdbstub at main · ringtailsoftware/uvm32 GitHub - labsai/EDDI: Config-driven engine that turns JSON into production-grade AI agents. Multi-agent orchestration, 12+ LLM providers, MCP/A2A protocols, RAG, persistent memory, and enterprise compliance (EU AI Act, GDPR, HIPAA). Built on Quarkus. GitHub - glitchnsec/fortyone-oss: AI Executive Assistant Platform Quickstart | Alien GitHub - muxshed/shed: One stream in, or many. Every destination, simultaneously. No cloud middleman, no per-channel fees, no limits. GitHub - ocrbase-hq/ocrbase: 📄 PDF/IMG ->.MD/JSON Document OCR API for PaddleOCR and GLMOCR. Self-hostable. GitHub - impactjo/home-memory: MCP server that lets your AI assistant remember everything about your home. GitHub - Sets88/dbcls: DbCls is a powerful terminal database client that supports various databases GitHub - neptun2000/heor-agent-mcp GitHub - SeanFDZ/macmind: Single-layer transformer in HyperTalk for the classic Macintosh RollQuation: Math Puzzles - Apps on Google Play GitHub - dropbox/witchcraft Show HN: Agent-cache – Multi-tier LLM/tool/session caching for Valkey and Redis GitHub - opentalon/opentalon: OpenTalon is an open-source platform built from the ground up in Go as a robust alternative to OpenClaw LinkedIn™ 职位抓取工具 - Chrome 应用商店 GitHub - EdoardoBambini/Agent-Armor-Iaga: AI agents are getting tool access — shell, file system, databases, APIs, secrets. But **nobody is governing what they actually do with it**. Frameworks like LangChain, CrewAI, AutoGen, and Claude Code give agents the power to execute. Agent Armor gives you the power to control, audit, and approve every single action before it happens. HN Vibes — Week 15, Apr 7–13 2026 GitHub - chojs23/ec: Easy terminal-native 3-way git mergetool vim-like workflow GitHub - SethPyle376/hiraeth: Local AWS emulator focused on fast integration testing, with SQS support, SQLite-backed state, and a debug-friendly web UI. GitHub - JakOb-dotcom/cloud-sandbox-security-analysis: Technical analysis and Proof of Concept (PoC) regarding environment variable exfiltration in containerized cloud sandboxes via side-channel data leaks. Springboards - Flint Alpha Show HN: A simpler coding agent harness GitHub - audiodude/sudomake-friends GitHub - 256thFission/mini-mythos: OSS clone of Anthropic’s Mythos harness to locate C/C++ memory vulnerabilities Show HN: OpenParallax: OS-level privilege separation for AI agent execution Hacker News Sorted - Chrome 应用商店 Show HN: How to Install Docker on Ubuntu 24.04 LTS: Complete 2026 Guide GitHub - himanshudongre/smriti GitHub - sverrirsig/claude-control: macOS desktop dashboard for monitoring and managing multiple Claude Code sessions GitHub - ory/dockertest: Write better integration tests! Dockertest helps you boot up ephermal docker images for your Go tests with minimal work. Chiral - Chrome 应用商店 Show HN: Two Claudes collaborating through shared memory on a $100 mini-PC GitHub - pmichaillat/latex-cv: Minimalist LaTeX template for academic CVs GitHub - oguzbilgic/posse: A web UI for Anthropic Managed Agents. GitHub - sshiraz/depsly: Dependency risk analysis tool for npm packages ABI Add safari/agent-harness — Safari browser automation via safari-mcp by achiya-automation · Pull Request #212 · HKUDS/CLI-Anything GitHub - Halfblood-Prince/trustcheck: Verify PyPI package attestations and improve Python supply-chain security GitHub - oguzbilgic/kern-ai: Agents that do the work and show it. GitHub - bruits/satteri: High-performance Markdown and MDX processing for the JavaScript ecosystem GitHub - tylergibbs1/feedstock: High-performance web crawler and scraper for TypeScript, powered by Bun and Playwright GitHub - Grimm67123/grimmbot: The self-improving sandboxed and open-source AI agent. With persistent memory and scheduling. GitHub - whitevanillaskies/whitebloom: Local whiteboard that blooms. GitHub - hwdsl2/docker-whisper: Docker image for a self-hosted Whisper speech-to-text server with speaker diarization and OpenAI-compatible transcription and translation APIs. Powered by faster-whisper. Supports all Whisper models, NVIDIA GPU (CUDA) acceleration, JSON/SRT/VTT output, SSE streaming, offline mode, and multi-arch (amd64, arm64). GitHub - yisding/reviewwiggum GitHub - MarwanAlsoltany/serrors: Structured errors for Go: sentinel hierarchies, typed data, custom formatting, and slog integration. GitHub - soatok/age-php GitHub - Luthiraa/markitme GitHub - stagas/rtdiff: realtime git diff gui and AI-assisted commits GitHub - tombedor/excalicharts GitHub - wh1le/excalidraw-edit: Open and edit .excalidraw files from the terminal. Offline, auto-saves to disk. MalExt Sentry - Malicious Extension Scanner - Chrome 应用商店 GitHub - syi0808/asciianimesvg: Generate animated ASCII art SVGs from text. CLI, Rust library, WASM, and web editor. GitHub - zaina-ml/ml_forge: A visual-based graph node editor for training computer vision models. GitHub - anakin87/llm-rl-environments-lil-course: 🌱 A little course on Reinforcement Learning Environments for evaluating and training Language Models GitHub - takaakit/superpowers-uml: Superpowers-UML modifies Superpowers to ensure a software development workflow in which AI agents design through UML modeling. AdriByte Studio - Sviluppo Web e Soluzioni Digitali GitHub - chouligi/angel-copilot: Your personalized Angel Investment Advisor Show HN: MoodSense AI (ML and FastAPI and Gradio, Deployed on Hugging Face) Moodsense Ai - a Hugging Face Space by aman179102 GitHub - agenteractai/lodmem: Level Of Detail Context Management for Agents GitHub - ostefani/subnetlens: A fast, concurrent network scanner with a TUI and plain-text CLI, built in Go. It discovers live hosts on your network, scans their open ports, resolves hostnames, and fingerprints operating systems—delivered. Cyber Pulse: Agentic Intel - Apps on Google Play Whisper API: Self-Hostable Speech to Text Transcription The Agent-Web Protocol Stack: A Research Thesis GitHub - msmarkgu/RelayFreeLLM: A restful API designed to route user prompts to various AI model providers. Show HN: Provepy – A Python decorator that proves your code using Lean and LLMs Show HN: Pardonned.com – A searchable database of US Pardons GitHub - patrickdappollonio/dux: Dux is a terminal UI that lets you run multiple AI coding agents side by side, each in its own git worktree, with full companion terminals, macros, commit generation, and a command palette that knows more tricks than you do. kMC Crystal Simulator Show HN: HyperFlow – A self-improving agent framework built on LangGraph GitHub - stef41/vibescore: 🎵 Grade your vibe-coded project. One command, instant letter grade across security, quality, dependencies, and testing. GitHub - stef41/lmscan: 🔍 Detect AI-generated text and fingerprint which LLM wrote it. Open-source GPTZero alternative. Zero dependencies, works offline. imgur.com GitHub - visionscaper/collabmem: Enabling long-term collaboration with Agentic AI - building up episodic and world model memory over time with in-context awareness 在 Steam 上购买 FriedrichAI: Offline AI 立省 10% GitHub - atripati/ark: AI Runtime Kernel — a context operating system for AI agents. Eliminates tool bloat, loads only what’s needed, and gives LLMs their reasoning space back. GitHub - nowork-studio/toprank: Open-source Claude Code skills for SEO, SEM, Google Ads GitHub - tacomanator/sash: Lightweight macOS menu bar app for reliably cycling through windows of the current application. Appents | Social Media Management for Product-First Teams GitHub - pnhoang/youtube-spam-blocker: Automatically detects and hides spam messages in YouTube Live chat. Set rate limits, keyword filters, and block repeat offenders. GitHub - decisionnode/DecisionNode: CLI + Local MCP - A shared structured memory store across Claude Code, Cursor, Windsurf, Antigravity, and every MCP client. Semantically queryable. GitHub - AvaCodeSolutions/django-email-learning: An open source Django app for creating email-based learning platforms with IMAP integration and React frontend components. The $100K Gap in Kubernetes Security Tooling Function Calling Harness: From 6.75% to 100%
From 17 minutes to 2 minutes: how we made bulk credential imports 6x faster
rajatrv · 2026-05-13 · via Hacker News: Show HN

Engineering

·By Rajat Ravinder Varuni, Founder

A walkthrough of seven patterns that took our worst-case onboarding from 17 minutes down to 2, plus the observability we built to prove it worked.

The setup

CertScore lets users import their entire Credly portfolio in one shot. A user pastes their profile URL, the server fetches all their badges, verifies ownership cryptographically (Open Badges Infrastructure email-hash matching), and saves the verified credentials.

Two real users, four days apart, told the story:

UserDateCredentialsEnd-to-end time
User AMay 41617 minutes 2 seconds
User BMay 8122 minutes 7 seconds

Per-cert throughput went from 64 seconds to 10.6 seconds. A 6x improvement.

This post walks through each optimization that landed between those two signups, plus the observability layer we built so we could actually see the win. Most of the patterns are generic. The CertScore-specific bits are flagged.

Pattern 1: parallelize wherever wall-clock matters more than server load

The problem

For ownership verification, the server fetches the OBI badge assertion for the first 5 badges and SHA-256-hashes the recipient identity against each of the user's verified emails. Originally these ran sequentially in a loop:

typescript

for (const badge of badgesToCheck) {
  const obiData = await fetch(
    `https://api.credly.com/v1/obi/v2/badge_assertions/${badge.id}`
  );
  // hash + compare
  if (matched) break;
}

Five round-trips at 1 to 3 seconds each = 5 to 15 seconds of opaque waiting. The "break on first match" optimization felt clever but it was actually hurting us. Because the work was sequential, every additional badge added latency.

The fix

Wrap the work in Promise.all and run all five in parallel. Wall-clock time becomes O(slowest) instead of O(sum):

typescript

const ownershipResults = await Promise.all(
  badgesToCheck.map(async (badge) => {
    const obiData = await fetchObi(badge.id);
    const { matched, matchedEmail } = await checkEmailMatch(
      allEmails,
      obiData.recipient.identity,
      obiData.recipient.salt,
    );
    return matched
      ? { badgeId: badge.id, matchedEmail, recipient: obiData.recipient }
      : null;
  })
);

const ownershipMatch = ownershipResults.find((r) => r !== null);

Result: 1 to 3 seconds instead of 5 to 15. The "break on first match" optimization we used to do sequentially is moot. The third-party API is cheap and we care about UX latency, not server load.

When this pattern applies

Any time you have N independent network calls and the user is waiting. The mental cost of Promise.all is near zero. The wall-clock win is large. The case where it doesn't apply: when each call is expensive on the third party's side and you have a contractual obligation not to fan out.

Pattern 2: INSERT ON CONFLICT collapses N transactions into one

The problem

After ownership verification, we save N selected credentials. The original client did this:

typescript

for (const badge of selectedBadges) {
  await saveCredential(badge);  // ~1 to 2s per round-trip
  setProgress({ current: i + 1, total });
}

For 16 badges: about 30 seconds of save time, plus the overhead of each individual transaction. Smart-duplicate detection added more pain. Each save had to fetch existing credentials, pattern-match by issuer + name (with abbreviation awareness), decide insert vs update, run the write, and wait for the trigger that recomputes total points. Repeated 16 times.

The fix: a single Postgres function with INSERT ON CONFLICT

sql

CREATE OR REPLACE FUNCTION bulk_save_credentials(
  p_user_id uuid,
  p_badges jsonb
) RETURNS TABLE (cred_id uuid, action text, matched_input_index int)
LANGUAGE plpgsql SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
BEGIN
  RETURN QUERY
  INSERT INTO credentials (
    user_id, issuer, credential_name, tier, base_points,
    issued_date, expiry_date, verification_method, verification_status,
    badge_url, image_url, raw_extraction_data
  )
  SELECT
    p_user_id,
    b->>'issuer',
    b->>'credential_name',
    b->>'tier',
    (b->>'base_points')::int,
    NULLIF(b->>'issued_date','')::date,
    NULLIF(b->>'expiry_date','')::date,
    b->>'verification_method',
    b->>'verification_status',
    b->>'badge_url',
    b->>'image_url',
    b->'raw_extraction_data'
  FROM jsonb_array_elements(p_badges) WITH ORDINALITY AS t(b, ord)
  ON CONFLICT (user_id, lower(trim(issuer)), lower(trim(credential_name)))
  DO UPDATE SET
    expiry_date = EXCLUDED.expiry_date,
    verification_status = EXCLUDED.verification_status,
    updated_at = now()
  RETURNING credentials.id,
    CASE WHEN xmax = 0 THEN 'inserted' ELSE 'updated' END,
    (ord - 1)::int;
END;
$$;

Two tricks worth knowing:

  1. The unique constraint is what enables ON CONFLICT. You need an index on the conflict-target columns, otherwise Postgres rejects the statement:

    sql

    CREATE UNIQUE INDEX credentials_user_issuer_name_unique
      ON credentials (user_id, lower(trim(issuer)), lower(trim(credential_name)));
  2. xmax = 0 distinguishes insert vs update on the same row. After an upsert, you can't tell whether each row was inserted or updated unless you check xmax. Postgres sets xmax = 0 for new inserts and a non-zero transaction id for updates.

Now 16 badges save in one round-trip, in one transaction, in one statement. Atomicity is free.

Watch out: WITH ORDINALITY returns bigint

A subtle gotcha that cost me 20 minutes of debugging. jsonb_array_elements WITH ORDINALITY returns (element jsonb, ordinality bigint). If your function declares matched_input_index int and you return ord directly:

ERROR: 42804: structure of query does not match function result type
DETAIL: Returned type bigint does not match expected type integer in column 3.

Cast it explicitly: (ord - 1)::int. Postgres won't auto-narrow.

Pattern 3: statement-level triggers, not row-level, for aggregations

The problem

User points are aggregated from credentials. Every credential write fires a row-level trigger that recomputes users.total_points:

sql

CREATE TRIGGER trigger_update_points
AFTER INSERT OR UPDATE OR DELETE ON credentials
FOR EACH ROW
EXECUTE FUNCTION update_user_total_points();

For 16 inserts in a bulk save, this trigger fires 16 times. Each time, it re-aggregates the same user's credentials. The Nth firing produces the same total once the dust settles, but the N-1 intermediate aggregations are wasted work.

The fix: convert to statement-level using transition tables

sql

CREATE OR REPLACE FUNCTION update_user_total_points_statement()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
  WITH affected_users AS (
    SELECT DISTINCT user_id FROM new_table
    UNION
    SELECT DISTINCT user_id FROM old_table
  )
  UPDATE users
  SET total_points = (
    SELECT COALESCE(SUM(base_points), 0)
    FROM credentials
    WHERE user_id = users.id
      AND verification_status IN ('verified', 'expired')
  )
  WHERE id IN (SELECT user_id FROM affected_users);
  RETURN NULL;
END;
$$;

-- One trigger per event type, because transition table availability differs:
CREATE TRIGGER trigger_update_points_insert
AFTER INSERT ON credentials
REFERENCING NEW TABLE AS new_table
FOR EACH STATEMENT
EXECUTE FUNCTION update_user_total_points_statement();

CREATE TRIGGER trigger_update_points_update
AFTER UPDATE ON credentials
REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table
FOR EACH STATEMENT
EXECUTE FUNCTION update_user_total_points_statement();

CREATE TRIGGER trigger_update_points_delete
AFTER DELETE ON credentials
REFERENCING OLD TABLE AS old_table
FOR EACH STATEMENT
EXECUTE FUNCTION update_user_total_points_statement();

Saves 1 to 3 seconds on a 16-row bulk save. The trigger now fires once per statement, not once per row.

Why three separate triggers?

You'd expect one trigger handling all three event types, but transition table availability differs:

  • INSERT only has NEW TABLE (no rows existed before)
  • DELETE only has OLD TABLE (no rows exist after)
  • UPDATE has both

Postgres rejects a single trigger that references unavailable transition tables. Three triggers, one per event, sharing one function.

Pattern 4: single batched audit event for batch operations

Every credential creation writes to audit_log. With a 16-cert import: 16 audit_log inserts, 16 trigger firings that post 16 messages to Slack, a wall of repetitive entries that drown out signal.

When the bulk path runs, fire ONE batched event:

typescript

logAuditEvent(supabase, {
  actorId: userId,
  action: 'credential.bulk_create',
  category: 'credential',
  entityType: 'user',
  entityId: userId,
  metadata: {
    source: 'bulk-save-selected-credentials',
    total: 16,
    inserted: 12,
    updated: 4,
    skill_links: 102,
    points_before: 0,
    points_after: 970,
    points_gained: 970,
  },
});

One row in audit_log, one Slack post, one cleaner timeline. Audit_log is a story for humans. Don't make humans read 16 lines that describe the same story.

Pattern 5: NDJSON streaming for long-running operations

The problem

Even after all the speedups, the user still sees an opaque spinner for 2 to 3 seconds during profile fetch + ownership verification. We have rich progress information server-side. We just don't expose it to the UI.

The fix: stream progress events as newline-delimited JSON

Why NDJSON over Server-Sent Events (SSE)? NDJSON is just newline-delimited JSON. Easier to parse. Easier to cross-platform. SSE has the data: and event: framing which adds nothing meaningful when both ends are your own code.

Server-side wrapper:

typescript

serve(async (req) => {
  const isStreaming = new URL(req.url).searchParams.get('stream') === '1';

  if (!isStreaming) {
    return runImport(req, () => {});  // existing flow, no progress
  }

  const stream = new ReadableStream({
    async start(controller) {
      const send = (event) =>
        controller.enqueue(new TextEncoder().encode(JSON.stringify(event) + '\n'));

      try {
        const response = await runImport(req, send);
        const body = await response.json();
        send({ type: 'complete', status: response.status, data: body });
      } catch (e) {
        send({ type: 'error', message: e.message });
      }
      controller.close();
    },
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'application/x-ndjson',
      'Cache-Control': 'no-cache',
      'X-Accel-Buffering': 'no',  // tells nginx-style proxies not to buffer
    },
  });
});

Inside runImport, the existing logic gets send() calls at milestones:

typescript

send({ type: 'progress', stage: 'fetching_profile' });
const profile = await fetch(profileUrl);
send({
  type: 'progress',
  stage: 'profile_loaded',
  totalBadges: profile.length,
});

let done = 0;
const results = await Promise.all(
  badges.map(async (b) => {
    const result = await verifyOwnership(b);
    done++;
    send({
      type: 'progress',
      stage: 'verifying_ownership',
      current: done,
      total: badges.length,
    });
    return result;
  })
);

Note the closure-captured counter. Each parallel promise increments and emits as it finishes. Progress events arrive in completion order, not input order. For UX this is exactly what you want: "verified 1 out of 5", "verified 2 out of 5", as fast as the server can confirm.

Web client

typescript

const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  const lines = buffer.split('\n');
  buffer = lines.pop() ?? '';  // keep incomplete trailing line
  for (const line of lines) {
    if (line.trim()) onProgress(JSON.parse(line));
  }
}

if (buffer.trim()) onProgress(JSON.parse(buffer));

The buffer-and-split-on-newline pattern is critical. Chunks rarely arrive on JSON-line boundaries. Hold the trailing partial line until the next chunk completes it.

Mobile (React Native): use XMLHttpRequest, not fetch

fetch().body.getReader() works on RN 0.81 in theory, but its chunking behavior varies across Hermes vs JSC and across iOS vs Android. The reliable cross-platform pattern is XHR with onprogress:

typescript

const xhr = new XMLHttpRequest();
xhr.open('POST', url);
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
xhr.setRequestHeader('Content-Type', 'application/json');

let lastIndex = 0;
let buffer = '';

xhr.onprogress = () => {
  const newText = xhr.responseText.substring(lastIndex);
  lastIndex = xhr.responseText.length;
  buffer += newText;
  const lines = buffer.split('\n');
  buffer = lines.pop() ?? '';
  for (const line of lines) {
    if (line.trim()) onProgress(JSON.parse(line));
  }
};

xhr.onload = () => {
  if (buffer.trim()) onProgress(JSON.parse(buffer));
  resolve();
};

xhr.send(JSON.stringify(body));

XHR has been stable since RN 0.20 and onprogress fires incrementally with partial responseText on every modern RN runtime. It's the closest thing to a guaranteed cross-platform streaming primitive.

Prove streaming actually streams

Bare HTTP can buffer at any layer (the runtime, the proxy, the CDN). After deploying, build a probe that records t_ms of each chunk. For a 5-badge profile, we captured 11 events across 12 chunks over 1.9 seconds, with distinct timestamps for each event:

chunk  t_ms   event
1      470    fetching_profile
3      1418   profile_loaded (totalBadges: 5)
4      1487   verifying_ownership 0/5
5      1634   verifying_ownership 1/5
6      1634   verifying_ownership 2/5
7      1636   verifying_ownership 3/5
9      1637   verifying_ownership 4/5
10     1819   verifying_ownership 5/5
10     1819   ownership_verified
11     1875   processing_badges
12     1943   complete (status 200, full payload)

12 chunks for 11 events with separated timestamps means chunks ARE flushing. Not buffered. Verified. If your probe shows all events arriving at the same timestamp at the end, something between you and the client is buffering. Common culprits: nginx without proxy_buffering off, Cloudflare without Cache-Control: no-cache, RN's response handling on certain Android builds.

Pattern 6: don't trust over-the-air updates for critical paths

After shipping the streaming code, we pushed it to mobile via Expo OTA. Both iOS and Android builds had runtimeVersion: "1" baked in. The OTA published successfully. Then we tested on a real device. The mobile call hit:

POST | 200 | https://api.example.com/functions/v1/import-credly-profile (1632ms)

No ?stream=1 query parameter. The app was running the OLD bundle.

Why

Expo's default OTA behavior:

  1. App opens with cached (old) bundle
  2. New bundle downloads silently in the background
  3. New bundle activates on the NEXT app launch

So your "first relaunch after publishing" still uses the cached bundle. You need to relaunch twice (once to download, once to apply). And there's no UI feedback telling you which state you're in. The user flow looks identical between "OTA hasn't reached you yet" and "OTA reached you and is broken."

The lesson

For critical paths, ship a native build. The certainty is worth the 15 to 25 minute build cycle. Native binary. Streaming and bulk-save baked in. No cache to fight. No "next launch" lottery. OTA is great for low-stakes JS-only fixes that aren't on the critical path. For anything you're going to eyeball-verify on a device, ship native.

Pattern 7: build observability before you need it

The optimizations above are mechanical. They are true even without measurement. But without observability you'd be hoping, not knowing. Here's the layered approach we built.

Layer 1: a last_seen_at column with a trigger

sql

ALTER TABLE users ADD COLUMN last_seen_at TIMESTAMPTZ;

CREATE INDEX users_last_seen_at_idx
  ON users (last_seen_at DESC NULLS LAST);

CREATE OR REPLACE FUNCTION bump_user_last_seen()
RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER AS $$
BEGIN
  IF NEW.actor_id IS NOT NULL THEN
    UPDATE users
       SET last_seen_at = GREATEST(
         COALESCE(last_seen_at, '-infinity'::timestamptz),
         NEW.created_at
       )
     WHERE id = NEW.actor_id;
  END IF;
  RETURN NEW;
END;
$$;

CREATE TRIGGER trg_bump_user_last_seen
AFTER INSERT ON audit_log
FOR EACH ROW
EXECUTE FUNCTION bump_user_last_seen();

-- Backfill from history
UPDATE users u
   SET last_seen_at = a.last_event
  FROM (
    SELECT actor_id, MAX(created_at) AS last_event
    FROM audit_log
    WHERE actor_id IS NOT NULL
    GROUP BY actor_id
  ) a
 WHERE a.actor_id = u.id;

Now last_seen_at is always current. Five minutes of work. Unlocks "active in last 7 days," "dormant 14 to 30 days," "churned 30+ days" with a single column scan.

Layer 2: a daily_user_metrics table + cron

Aggregations get expensive. Pre-compute them once a day and cache the result in a daily metrics table. Schedule via pg_cron, hit an edge function via net.http_post, post a Slack digest. Result: every morning, you get the previous day's snapshot in Slack without thinking about it.

sql

SELECT cron.schedule(
  'daily-metrics-summary',
  '30 14 * * *',  -- 14:30 UTC
  $$
  SELECT net.http_post(
    url := 'https://your-project.supabase.co/functions/v1/daily-metrics-summary',
    headers := jsonb_build_object('x-cron-secret', 'YOUR_SECRET'),
    body := '{}'::jsonb
  );
  $$
);

Layer 3: track explicit logins via a database trigger

Most apps don't track "logins." They track "activity." To distinguish, hook into Supabase's auth.sessions table:

sql

CREATE OR REPLACE FUNCTION log_auth_session_start()
RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER AS $$
DECLARE
  v_public_user_id uuid;
BEGIN
  SELECT id INTO v_public_user_id
    FROM users
   WHERE supabase_auth_id = NEW.user_id;

  IF v_public_user_id IS NOT NULL THEN
    INSERT INTO audit_log (actor_id, actor_type, action, category, entity_type, entity_id, metadata)
    VALUES (
      v_public_user_id, 'user', 'auth.session.start', 'auth', 'session', NEW.id,
      jsonb_build_object('aal', NEW.aal::text, 'ip', NEW.ip::text)
    );
  END IF;
  RETURN NEW;
END;
$$;

CREATE TRIGGER trg_log_auth_session_start
AFTER INSERT ON auth.sessions
FOR EACH ROW
EXECUTE FUNCTION log_auth_session_start();

Now every actual sign-in (not session refresh, not background activity) gets logged. Zero client changes.

Layer 4: funnel time-to-value

For users whose first verified credential was on this date, what's the median signup-to-first-cred time?

sql

WITH first_creds AS (
  SELECT user_id, MIN(created_at) AS first_cred_at
    FROM credentials
   WHERE verification_status IN ('verified','expired')
   GROUP BY user_id
),
cohort AS (
  SELECT EXTRACT(EPOCH FROM (fc.first_cred_at - u.created_at))::int AS seconds_to_first
    FROM users u
    JOIN first_creds fc ON fc.user_id = u.id
   WHERE fc.first_cred_at >= v_day_start
     AND fc.first_cred_at < v_day_end
     AND fc.first_cred_at >= u.created_at
)
SELECT
  PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY seconds_to_first) AS p50,
  PERCENTILE_DISC(0.9) WITHIN GROUP (ORDER BY seconds_to_first) AS p90,
  COUNT(*) AS cohort_size
FROM cohort;

Now you can detect activation regressions automatically. If p50 jumps from 127s to 600s, something broke.

The receipts

User B's audit_log entry confirms the path:

json

{
  "action": "credential.bulk_create",
  "metadata": {
    "source": "bulk-save-selected-credentials",
    "total": 12,
    "inserted": 12,
    "updated": 0,
    "skill_links": 102,
    "points_before": 0,
    "points_after": 970,
    "points_gained": 970
  }
}

The source: "bulk-save-selected-credentials" is the new edge function. The inserted: 12 shows the ON CONFLICT path firing as inserts. The single audit row replaced what would have been 12 individual credential.create events plus 12 Slack posts.

Takeaways

  1. Parallelize wherever wall-clock matters more than server load.
  2. INSERT ... ON CONFLICT with a unique index collapses N transactions into one.
  3. Statement-level triggers fire once per statement, not once per row.
  4. NDJSON over chunked HTTP is the simplest streaming protocol. Use XHR for React Native.
  5. Don't trust OTA for critical paths. Native build for anything you'll eyeball-verify on a device.
  6. Observability has tiers. Start cheap. A last_seen_at column unlocks 80% of retention questions.
  7. Build the dashboard before you need it. Without a daily metrics table, you have anecdotes.
  8. Prove streaming actually streams. Bare HTTP can buffer at any layer.

What I'd do differently

  • Add observability earlier. I built the perf improvements, then realized I couldn't measure them. Layer 1 takes five minutes. Worth doing before any optimization work, not after.
  • Skip OTA entirely for major changes. The 30 minutes I spent debugging why streaming wasn't working on the first device test would have been a native build. Save OTA for fixes you wouldn't even mention in a release note.
  • Backfill aggressively. When you ship a new metric, immediately compute it for the last 14 to 30 days. The historical view turns "interesting numbers" into a real trend you can act on.
  • One probe per claim. When I said "OTA is published, mobile streaming should work," I didn't actually verify it. The probe pattern (mint a token via auth.admin.generateLink + verifyOtp, hit the endpoint server-to-server, validate the chunked response) is universal and cheap. Build it the moment you make a claim that's hard to falsify by clicking around.

About CertScore

CertScore verifies professional certifications cryptographically (Open Badges email-hash matching) and ranks them on a global leaderboard. Hiring managers can ask any candidate to verify their stack in two minutes, not seventeen.

Verify my certifications