Architecture
Overview
Section titled “Overview”Pinchy is not a fork of OpenClaw. It’s a governance layer on top of it. OpenClaw handles agent execution, tool use, and model communication. Pinchy adds authentication, provider management, agent permissions, cryptographic audit trails, and (soon) team management.
Request flow
Section titled “Request flow”When a user sends a message, it flows through three layers:
- Browser → connects via WebSocket to
/api/wson Pinchy, sending a JSON message withtype,content, andagentId - Pinchy → authenticates the user, verifies agent access, resolves the session key from the in-memory session cache, generates a message ID, and forwards the message to OpenClaw Gateway with the
agentIdandsessionKey - OpenClaw → routes the message to the correct agent from its config, processes it through the configured model, and streams the response back
- Pinchy → attaches the message ID and forwards each chunk to the browser
- Browser → renders the streaming response in real time
Each browser connection gets its own ClientRouter instance that manages agent access checks and session resolution. Pinchy acts as a bridge — it never interprets or modifies the AI response content.
Agent routing
Section titled “Agent routing”When a message arrives, the ClientRouter passes the agentId from the browser message to OpenClaw via chatOptions. OpenClaw uses this to select the matching agent from its agents.list[] config — each agent can have its own model, system prompt, tools, and workspace.
Chat sessions
Section titled “Chat sessions”Pinchy derives a deterministic session key for each (agentId, userId) pair using the format agent:<agentId>:direct:<userId>. This gives each user their own conversation per agent — even for shared agents. Session keys are resolved in-memory via a SessionCache that periodically syncs with OpenClaw’s session list — no database table is needed. The session key is opaque and never leaves the server — the browser only sends an agentId, and Pinchy resolves the session internally.
The browser can request conversation history by sending a { type: "history", agentId } message. Pinchy fetches the history from OpenClaw via openclaw-node and strips internal metadata (timestamps, thinking blocks) before returning it.
Session key naming convention
Section titled “Session key naming convention”Session keys follow the format agent:<agentId>:<scope>. OpenClaw validates that the agentId segment matches the agentId parameter passed to chat() — a mismatch causes silent failures (history not loaded, messages not sent).
Current format:
| Scope | Key format | Example |
|---|---|---|
| Per-user sessions | agent:<agentId>:direct:<userId> | agent:83aa6035-...:direct:8c0953d2-... |
This format is used for both personal and shared agents. Each user always gets their own session, which means converting a personal agent to a shared agent is seamless — the original user keeps their session, and other users get new ones.
Planned formats (not yet implemented):
| Scope | Key format | Example | Use case |
|---|---|---|---|
| Cron jobs | agent:<agentId>:cron-<jobId> | agent:83aa6035-...:cron-daily-check | Scheduled agent actions |
| Webhook triggers | agent:<agentId>:hook-<hookId> | agent:83aa6035-...:hook-slack-alert | Event-driven agent actions |
The agentId segment must always be the Pinchy agent UUID. The scope segment is free-form but should follow the patterns above for consistency.
Connection resilience
Section titled “Connection resilience”The browser-to-Pinchy WebSocket connection includes several layers of protection against network instability:
Keep-alive heartbeats — Once the first response chunk arrives, Pinchy sends a { type: "thinking" } message to the browser every 15 seconds for the duration of the request. This defeats browser and proxy idle-timeout disconnects during long pauses between agent turns (e.g. slow local Ollama models running a tool-use loop). Heartbeats are intentionally deferred until the first chunk — starting them earlier would prevent the stuck-request timeout from firing when OpenClaw is unresponsive before it sends anything.
OpenClaw disconnect propagation — If Pinchy loses its connection to OpenClaw while a stream is in progress, it immediately closes all active browser WebSockets. This triggers the browser’s existing disconnect-recovery path (disconnect error + auto-reconnect) without waiting for the server-side stream to time out. openclaw-node’s chat generator hangs indefinitely when OpenClaw disappears, so the close is the only reliable way to surface the error quickly.
Auto-reconnect with exponential backoff — If the connection drops, the browser automatically reconnects. Delays start at 1 second and double with each attempt, capped at 5 seconds, up to 10 attempts total. On reconnect, conversation history is re-fetched from the server and any partial streamed message is replaced with the canonical server version.
Disconnect recovery — If the WebSocket closes while a response is being streamed, an inline error appears in the chat so the user knows the response may have been interrupted. If OpenClaw finished processing the request before the disconnect, the error is automatically replaced by the complete response when history reloads after reconnect.
Stuck-request timeout — If 60 seconds pass without any response activity (no chunks and no thinking heartbeats), the spinner stops and an error is shown prompting the user to send the message again. Heartbeats reset the timer between turns, so a slow-but-alive agent is never killed prematurely.
Reconnect exhausted — After 10 failed reconnect attempts, a persistent banner prompts the user to reload the page.
Session lifecycle and compaction
Section titled “Session lifecycle and compaction”Sessions grow with each message. OpenClaw tracks the full conversation in JSONL files and sends the relevant context to the LLM provider with each request. Over time, this increases token usage and cost.
OpenClaw handles this via compaction — automatic summarization of older messages when the context window approaches its limit. Pinchy’s OpenClaw config uses "compaction": { "mode": "safeguard" }, which means OpenClaw compacts automatically as a safety measure. There is also an explicit sessions.compact(key, { maxLines }) API for manual compaction.
Sessions track a compactionCount to record how many compactions have occurred. The impact on sessions.history() output after compaction (whether summaries appear as regular messages or a special type) is an open question that will be addressed when sessions grow large enough to trigger it.
Authentication
Section titled “Authentication”Pinchy uses Better Auth with email/password authentication:
- Passwords are hashed with scrypt before storage (with bcrypt legacy fallback for migrated accounts)
- Sessions are stored in the database (server-side session store in PostgreSQL)
- The user’s role (
adminoruser) is managed by the Better Auth Admin Plugin - The first user created via the setup wizard becomes the admin
- All app routes require authentication — unauthenticated requests redirect to
/login
Pinchy has two roles:
- Admin — can manage agents, users, invites, and settings
- User — can chat with agents and update their own profile
Invite system
Section titled “Invite system”Admins invite new users by generating an invite token:
- Admin creates an invite for an email address via Settings → Users
- Pinchy generates a random token, stores its SHA-256 hash in the database, and returns the plaintext token as a one-time invite link
- The invite recipient opens the link, sets their name and password, and their account is created
- A personal Smithers agent is automatically created for the new user
- Invite tokens expire after 7 days and are single-use
Permission layer
Section titled “Permission layer”Pinchy uses an allow-list model for agent permissions: agents have no tools by default. Admins explicitly enable tools for each agent via the Permissions tab in Agent Settings.
Tools are organized into two categories:
- Safe tools — sandboxed directory access (
pinchy_ls,pinchy_read). Only work within directories the admin has approved. - Powerful tools — direct server access (shell commands, unrestricted file access, web access). Only for trusted use cases.
When an agent has safe tools enabled, the pinchy-files plugin validates every file access request against the agent’s allowed directories, with symlink resolution to prevent escapes.
In addition to the allow-list, every agent has built-in PDF and image readers that are scoped to its own workspace — they cannot reach files outside the workspace, regardless of permissions. This is what lets chat-attached files (uploaded into uploads/<filename>) reach the agent without granting it broader file access.
Smithers agents also use the pinchy-context plugin, which provides save_user_context and save_org_context tools. These tools call Pinchy’s internal API (authenticated via a shared gateway token) to save context from the onboarding interview directly to the database and sync it to agent workspaces.
All agent-accessible files live under /data/ in the container, mounted via Docker volumes. This means even if all software layers failed, the agent can only see files that were explicitly mounted.
Agent access control
Section titled “Agent access control”Pinchy restricts which agents a user can see:
- Admins can access all agents (personal and shared)
- Users can access shared agents and their own personal agent
- Only admins can view and modify agent permissions
For the full details, see Agent Permissions.
Database
Section titled “Database”PostgreSQL 17, accessed via Drizzle ORM. The schema includes:
- Auth tables —
user,account,session,verification(managed by Better Auth) agents— Agent configuration (name, model, template, allowed tools, plugin config, owner)invites— Invite tokens (SHA-256 hashed token, email, expiry, status)settings— Key-value store for app configuration (provider keys, onboarding state)auditLog— Append-only audit trail with HMAC-SHA256 signed rows, protected by PostgreSQL triggers preventing UPDATE and DELETE
Migrations are generated with drizzle-kit generate and applied automatically on container startup via drizzle-kit migrate.
Encryption
Section titled “Encryption”Provider API keys are encrypted at rest using AES-256-GCM:
- A 256-bit encryption key is either provided via the
ENCRYPTION_KEYenvironment variable or auto-generated and persisted in thepinchy-secretsDocker volume - Each encrypted value stores the IV, auth tag, and ciphertext together
- Decryption happens on-demand when Pinchy writes the OpenClaw configuration file
Audit Trail
Section titled “Audit Trail”Pinchy includes a cryptographic audit trail for compliance and security. Every significant action is logged to the auditLog table with an HMAC-SHA256 signature.
Design principles
Section titled “Design principles”- Append-only — PostgreSQL triggers prevent UPDATE and DELETE operations on audit rows. Once written, entries are immutable.
- Cryptographically signed — Each row is signed with HMAC-SHA256 using a server-side secret (auto-generated if not provided via
AUDIT_HMAC_SECRET). - Fire-and-forget — Audit logging never blocks or breaks the main operation. If logging fails, the original action still succeeds.
- Admin-only access — Only admins can view, verify, or export the audit log.
What gets logged
Section titled “What gets logged”Pinchy logs events across seven categories: authentication, agent management, user management, group management, channel management, configuration changes, and tool execution. Chat message content is not logged — only tool calls and permission-relevant actions. See Audit Trail for the complete list of event types.
Integrity verification
Section titled “Integrity verification”Admins can verify the integrity of audit entries via the admin UI or the /api/audit/verify endpoint. Verification recomputes HMAC signatures and reports any tampered rows.
CSV export
Section titled “CSV export”The audit log can be exported as CSV for compliance reporting via the admin UI or the /api/audit/export endpoint.
For the full details, see Audit Trail.
OpenClaw integration
Section titled “OpenClaw integration”OpenClaw runs as a separate Docker container. Pinchy communicates with it via openclaw-node (the official Node.js client) over WebSocket on port 18789. The browser never connects to OpenClaw directly.
Config generation
Section titled “Config generation”Pinchy owns the OpenClaw configuration file (openclaw.json). A single function, regenerateOpenClawConfig(), handles every write — from the first config during setup to every subsequent change when agents, permissions, or providers are modified. It rebuilds the config from current database state, preserving only the gateway block (auth token and OpenClaw-generated fields).
Because the database is the source of truth, regeneration is idempotent and self-healing: deleted providers and agents are cleaned up automatically, and calling it twice produces the same file. To avoid unnecessary restarts, Pinchy short-circuits the write when the new content is identical to what’s already on disk.
An inotify-based wrapper script inside the OpenClaw container detects config file changes and restarts the gateway automatically, with a 30-second grace period between restarts to prevent restart loops.
Authentication
Section titled “Authentication”Pinchy authenticates to OpenClaw Gateway using a bearer token. The token is auto-generated on first setup via crypto.randomBytes(24) and stored in the gateway.auth block of openclaw.json. It never appears in source control.
Channels
Section titled “Channels”Users don’t have to reach their agents through the web UI. Pinchy supports external channels that connect agents to messaging platforms. Telegram is the first implemented channel; additional channels (email, Slack) are planned.
Per-agent bots
Section titled “Per-agent bots”Each agent can be connected to its own Telegram bot via Agent Settings → Telegram. The main Pinchy bot (the Smithers bot) is set up once globally via Settings → Telegram and acts as the entry point for user account linking. Additional agent bots require the main bot to be configured first — users link their Telegram account via the main bot and then gain access to any bot they have permission to use.
Identity linking
Section titled “Identity linking”Users link their Telegram account to their Pinchy account via a one-time pairing code: the user messages the bot, receives a code, and enters it in Pinchy. The link is stored in the identityLinks table. All inbound Telegram messages are resolved to the linked Pinchy user before being routed to the agent — so the agent always knows who it’s talking to.
Session unification
Section titled “Session unification”Messages from Telegram and the web UI flow through the same session cache using the same agent:<agentId>:direct:<userId> key format. This means switching channels doesn’t fork conversation history — a user who chats with an agent on the web and then messages it on Telegram sees the combined conversation.
Config propagation
Section titled “Config propagation”Telegram bot tokens and channel settings live in the database as the source of truth. Changes trigger a regenerateOpenClawConfig() write, and OpenClaw picks up the new config via the file watcher. There’s no WebSocket RPC for channel configuration — the config file is the contract.
For the user-facing setup, see Set Up Telegram.
Integrations
Section titled “Integrations”Integrations let agents work with external business data — email accounts, ERPs, search engines — without giving them direct system access. Each integration is a connection stored in the database, referenced by agents via per-agent permissions.
| Integration | Connection method | Stored credentials |
|---|---|---|
| Gmail | OAuth 2.0 | Access token + refresh token |
| Odoo | API key | URL, database, login, API key |
| Web Search | API key | Brave Search API key |
All credentials are encrypted at rest with AES-256-GCM and isolated per-row: a single decryption failure no longer hides every integration. Credentials never reach the OpenClaw config file — plugins fetch them on-demand via Pinchy’s internal API, authenticated with the shared gateway token.
Each integration enforces its own permission model on the agent side:
- Gmail — checkbox-based permissions (Read / Create drafts / Send) map to specific tool IDs
- Odoo — access levels (Read-only / Read & Write / Full / Custom) with optional per-model restrictions
- Web Search — tool checkboxes plus per-agent filters (Domain allow/deny, Freshness, Language, Region)
For the conceptual overview, see Integrations. For per-integration setup, see the guides: Connect Email, Connect Odoo, Set Up Web Search.
Domain lock and insecure mode
Section titled “Domain lock and insecure mode”Pinchy’s security profile is controlled by a single runtime setting: the locked domain. When a domain is locked via Settings → Security, Pinchy:
- Rejects requests whose
Hostheader doesn’t match the locked domain (403 Access Denied) - Issues cookies with
SecureandSameSite=Lax - Advertises HSTS
- Enforces origin checks on state-changing requests
When unlocked, Pinchy shows an insecure-mode warning banner to admins in the UI. This flips security-relevant middleware off as a package — convenient for local development, unsafe for production. A docker exec pinchy pnpm domain:reset CLI recovers access if HTTPS goes down after a lock. See HTTPS & Domain Lock for the full flow.
Tech stack
Section titled “Tech stack”| Layer | Technology |
|---|---|
| Frontend | Next.js 16, React 19, Tailwind CSS v4, shadcn/ui |
| Chat UI | assistant-ui (React) |
| Auth | Better Auth (email/password, DB sessions) |
| Database | PostgreSQL 17, Drizzle ORM |
| Agent runtime | OpenClaw Gateway (WebSocket) |
| Encryption | AES-256-GCM (Node.js crypto) |
| Testing | Vitest, React Testing Library, Playwright (E2E) |
| CI/CD | GitHub Actions, ESLint, Prettier |
| Deployment | Docker Compose, pre-built images on GHCR |
| Reverse proxy | Caddy (recommended) or nginx |
| SBOM | Syft via anchore/sbom-action |
| License | AGPL-3.0 |