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

推荐订阅源

H
Help Net Security
G
Google Developers Blog
aimingoo的专栏
aimingoo的专栏
博客园 - 聂微东
酷 壳 – CoolShell
酷 壳 – CoolShell
小众软件
小众软件
Stack Overflow Blog
Stack Overflow Blog
美团技术团队
博客园_首页
T
Tailwind CSS Blog
博客园 - 三生石上(FineUI控件)
B
Blog
D
DataBreaches.Net
腾讯CDC
C
Check Point Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
U
Unit 42
月光博客
月光博客
V
V2EX
Vercel News
Vercel News
T
The Blog of Author Tim Ferriss
The Cloudflare Blog
博客园 - 叶小钗
Y
Y Combinator Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
We built a real psql terminal in the browser. Here’s what...
Alex Zhdanko · 2026-05-14 · via DEV Community

A PTY-backed PostgreSQL console running in the browser using reverse WebSockets, Redis Streams, and xterm.js — built under NAT constraints and production realities.

We needed a real PostgreSQL terminal in the browser.

Not a SQL editor.
Not a query API.
A real psql session with full terminal semantics.

That immediately meant:

  • PTY required
  • stateful process required
  • bidirectional streaming required

And three constraints made the architecture non-obvious:

  • agents behind NAT
  • xterm.js only supports WebSocket
  • we cannot emulate psql

High-level architecture

This system only makes sense if you read it as a dataflow graph, not components.

Browser (xterm.js)
    │
    │ WebSocket (user input/output)
    ▼
Control Plane ───────────────────────────────────────────────┐
    │                                                        │
    │ session management + auth                              │
    │                                                        │
    ▼                                                        │
Redis Streams (PTY output buffer)                            │
    │                                                        │
    │ pub/sub control events                                 │
    ▼                                                        │
Agent Control Channel (reverse WebSocket)                    │
    │                                                        │
    │ writes stdin / reads stdout                            │
    ▼                                                        │
PTY process (real psql)

Enter fullscreen mode Exit fullscreen mode

Now the important part:

The agent initiates the entire connection graph.

Everything else is just message routing.

Step 1 — Browser creates a session

Browser
  │
  │ WebSocket connect
  ▼
Control Plane
  ├── creates session_id
  ├── registers browser handler
  └── starts auth timeout

Enter fullscreen mode Exit fullscreen mode

At this point:

  • there is no agent
  • no database
  • no PTY Only a logical session exists.

Step 2 — Control plane triggers the agent

Control Plane
  │
  │ HTTP POST /terminal?session_id
  ▼
Agent (behind NAT)

Enter fullscreen mode Exit fullscreen mode

This is intentional.

We do not open inbound connections.

Instead:

we push a signal, not a connection

The signal means:

“open a reverse WebSocket for this session”

Step 3 — Reverse WebSocket is established

Agent
  │
  │ WebSocket connect (outbound)
  ▼
Control Plane (Client Handler)

Enter fullscreen mode Exit fullscreen mode

Now we have:

  • browser WS → control plane
  • agent WS → control plane

But they are still isolated.

The system is now in a half-connected state.

Step 4 — Session stitching

Browser Handler ───────┐
                       ├── session_id → Redis → logical binding
Agent Handler ─────────┘

Enter fullscreen mode Exit fullscreen mode

At this moment:

the control plane stops being a transport and becomes a router

It now forwards:

  • browser input → agent
  • agent output → browser

But not directly.

Everything goes through buffering and coordination layers.

Step 5 — PTY + psql is spawned

Agent
  │
  │ forkpty()
  ▼
PTY master fd
  │
  ├── child → psql process
  └── parent → I/O threads

Enter fullscreen mode Exit fullscreen mode

From here on:

the system stops being “web architecture” and becomes “process supervision”

We now manage:

  • file descriptors
  • blocking reads
  • backpressure
  • OS signals

Step 6 — The real data pipeline

This is the most important flow in the system.

Browser
  │
  │ keystroke
  ▼
Control Plane
  │
  │ forward event
  ▼
Agent WS handler
  │
  │ write(fd)
  ▼
PTY → psql
  │
  │ stdout
  ▼
PTY reader thread
  │
  │ Redis XADD  ←--- decoupling boundary
  ▼
Redis Streams
  │
  │ consumer (async)
  ▼
Control Plane
  │
  │ WebSocket push
  ▼
Browser (xterm.js)

Enter fullscreen mode Exit fullscreen mode

The most important design boundary

This line:
PTY reader → Redis XADD → consumer → WebSocket
is the entire stability model.

Without it:

  • WebSocket backpressure freezes psql output
  • terminal becomes non-deterministic under load

With it:

  • PTY is isolated from network behavior
  • system becomes resilient to slow clients

Why Redis Streams are not optional

We originally tried:
PTY → WebSocket directly

This failed in production because:

  • WebSocket writes block under load
  • PTY reader is synchronous
  • blocking IO propagates backwards

So the failure mode was:

network slowdown → frozen terminal → stuck psql session

Redis Streams break that chain:

  • PTY write becomes O(1)
  • network becomes asynchronous
  • failure is localized

The real system is actually two loops

This is the part most designs hide.

Loop 1 — Input loop
Browser → Control Plane → Agent → PTY

Loop 2 — Output loop
PTY → Redis → Control Plane → Browser

They are completely independent.
This is why the system survives partial failure.

Why two WebSocket handlers exist

We deliberately split responsibilities:

Browser Handler:

  • auth
  • session lifecycle
  • user events

Agent Handler:

  • PTY lifecycle
  • process management
  • reconnect logic

Reason:

browser failures and agent failures are fundamentally different systems problems

Merging them creates coupled failure modes.

Failure model (what actually breaks)

A. Redis failure

Impact:

  • output pipeline breaks
  • PTY continues running

Mitigation:

  • bounded memory
  • retention limits
  • monitoring

B. Agent disconnect

Impact:

  • WS breaks
  • PTY may still run

Mitigation:

  • reconnect window
  • session reattachment
  • delayed teardown

C. Process explosion

Impact:

  • memory exhaustion
  • DB connection storm

Mitigation:
BoundedSemaphore(max_sessions=10)
This is the simplest and most effective safety boundary in the system.

D. xterm resize storm

Impact:
ioctl(TIOCSWINSZ) × 100/sec

Mitigation:

  • 200ms debounce
  • agent-side throttling

Scaling reality

Each session is not lightweight.

It includes:

  • psql process
  • PTY
  • two threads
  • Redis stream
  • WS pair
  • DB connection

So scaling is bounded by:

number of real database sessions you can sustain

Not by WebSockets.
Not by Redis.
Not by CPU.

Why HTTP/SSE architecture fails

We evaluated:

HTTP polling

  • stateless
  • no streaming
  • no cancellation
  • no session continuity

SSE

  • one-directional
  • incompatible with terminal interaction model

Conclusion:

terminals are inherently bidirectional state machines → WebSocket is the only fit

What this system really is

If you strip all abstractions:

It is a distributed process supervisor for a PTY running psql

Everything else is just transport.

Final architecture insight

The system is defined by three separations:

  1. Connection separation
    Reverse WebSocket removes NAT from the problem space.

  2. Process separation
    PTY isolates PostgreSQL from the web layer.

  3. Flow separation
    Redis decouples terminal I/O from network I/O.

Final mental model

If you understand only one thing, understand this:

Browser ↔ Control Plane ↔ Agent ↔ PTY ↔ psql
                     ↑
              Redis is the buffer

Enter fullscreen mode Exit fullscreen mode

Everything else is failure handling around this chain.

Final thought

We didn’t build a “web UI for PostgreSQL”.

We built a distributed, fault-tolerant terminal runtime for a stateful OS process.

PostgreSQL just happened to be the process we attached to it.