Single-Binary Architecture
Temps is one Rust binary that replaces seven SaaS tools: Vercel (git-push deployments), PostHog (analytics and session replay), Sentry (error tracking), Pingdom (uptime monitoring), Datadog (OpenTelemetry traces, metrics and logs), Resend (transactional email) and E2B (AI agent sandboxes). It runs on a server you control, next to Docker for your app containers and PostgreSQL with TimescaleDB for its data. This page explains why it is built this way and how the pieces fit together.
The problem with tool sprawl
A small team shipping a web application usually ends up with a separate service for each of these jobs:
- Git-push deployments — Vercel, Netlify, Railway or Render
- Web and product analytics, with session replay — PostHog, Plausible, FullStory
- Error tracking — Sentry
- Uptime monitoring — Pingdom, UptimeRobot
- Traces, metrics and logs — Datadog
- Transactional email — Resend, Postmark, SendGrid
- Sandboxes for AI agents — E2B
That is seven accounts, seven bills, seven privacy policies covering your users' data, and data split across seven providers that don't share an ID for the same request. Temps puts these jobs in one self-hosted binary, so a request, its trace and the error it raised appear on one timeline, and the error links to its trace by trace_id (see Observe).
What one binary replaces
| Capability | Tool it replaces | Temps component |
|---|---|---|
| Git-push deployments | Vercel | Deployment pipeline: autopack or your Dockerfile, run as Docker containers behind the Pingora proxy |
| Web analytics and session replay | PostHog | Built-in analytics and rrweb-based session replay (off by default) |
| Error tracking | Sentry | Sentry-compatible DSN ingestion |
| Uptime monitoring | Pingdom | Uptime monitors with incidents and alerts |
| Traces, metrics and logs | Datadog | OpenTelemetry ingestion over OTLP/HTTP |
| Transactional email | Resend | Email API that sends through SES, Scaleway or an SMTP relay |
| AI agent sandboxes | E2B | Sandboxes: isolated containers for running untrusted code |
The same binary also provides capabilities that aren't counted in the seven:
| Capability | Instead of | Temps component |
|---|---|---|
| Reverse proxy with TLS | nginx + certbot, Caddy | Pingora-based proxy with automatic Let's Encrypt certificates (request flow) |
| Managed databases and storage | Neon, Supabase, Upstash, AWS S3 | Docker-managed PostgreSQL, Redis, MongoDB and S3-compatible storage (RustFS) |
| KV and blob storage | Upstash Redis, Vercel Blob | KV backed by Redis, blob storage backed by RustFS |
| LLM gateway | A separate LLM proxy | OpenAI-compatible AI gateway |
| Multi-server scheduling | Docker Swarm, a small Kubernetes cluster | Worker nodes joined over a private network you provide (a VPC, a VPN or Tailscale) |
| Performance monitoring | PageSpeed Insights | Core Web Vitals collection |
By default, observability data (analytics events, traces, metrics and proxy logs) is stored in the same PostgreSQL database, using TimescaleDB for time-series queries. Operators who need columnar-scale throughput can move those four domains to ClickHouse without rebuilding the binary — see opt-in columnar storage below.
How it works internally
Temps is a Rust workspace of 88 crates at v0.1.0-beta.56, each responsible for one domain. A few of them:
temps (binary)
├── temps-core # Shared types, error handling, retry logic
├── temps-config # Configuration management
├── temps-database # PostgreSQL/Sea-ORM connection and migrations
├── temps-entities # Database models
├── temps-routes # HTTP router (Axum)
├── temps-auth # Authentication, permissions, API keys
├── temps-proxy # Pingora reverse proxy
├── temps-deployments # Deployment pipeline and job execution
├── temps-deployer # Docker container management
├── temps-environments # Environment and env var management
├── temps-providers # Managed services (PostgreSQL, Redis, etc.)
├── temps-domains # Domain management and TLS/ACME
├── temps-analytics # Page views, visitors, events
├── temps-error-tracking# Sentry-compatible error ingestion
├── temps-monitoring # Outage detection, disk space monitoring
├── temps-status-page # Health checks, incidents, monitors
├── temps-backup # S3 backups, schedules, restore
├── temps-notifications # Email, Slack, webhook alerts
├── temps-logs # Structured deployment logging
├── temps-git # Git provider integration
├── temps-queue # Background job processing
├── temps-otel # OpenTelemetry ingest and trace storage (TimescaleDB or ClickHouse)
├── temps-metrics # Resource metrics store (TimescaleDB or ClickHouse)
├── temps-email # Transactional email API and providers
├── temps-sandbox # Isolated sandboxes for agents and one-off runs
├── ... # And more
At startup, the binary:
- Reads configuration from environment variables (including the optional
TEMPS_CLICKHOUSE_*settings) - Connects to PostgreSQL and runs pending migrations
- Initializes the plugin system — each crate registers its services
- Starts the HTTP server (Axum) for the API and dashboard
- Starts the reverse proxy (Pingora) for routing traffic to deployed containers
- Starts background workers for health checks, backups, and notifications
Two processes
Temps has two process roles:
temps serve— The API server, dashboard, background workers, and all application logictemps proxy— The Pingora-based reverse proxy that routes external traffic to the correct container
Both are subcommands of the same temps binary. The proxy can run as its own process because Pingora manages its own event loop and worker threads. What the proxy does with each request is described in Request Flow.
Combined mode (default)
By default, temps serve launches both roles in a single OS process. The proxy and console share memory and communicate directly. This is the simplest setup and the right choice for most self-hosted installations.
Split-process mode
Pass --role=console to temps serve to run the console without the embedded proxy. The console must bind a stable address in this mode so the sibling proxy has a fixed target to forward console traffic to — temps serve refuses to start without it, because the default is a random localhost port the proxy could never discover:
# Terminal 1 — console process (API, dashboard, workers)
temps serve --role=console --console-address 127.0.0.1:8081
# Terminal 2 — proxy process (Pingora, TLS, routing)
temps proxy --console-address 127.0.0.1:8081
--role accepts all (the default, both halves in one process) or console. There is deliberately no proxy role — the proxy half is the existing standalone temps proxy command. Both flags also read from the environment: TEMPS_ROLE and TEMPS_CONSOLE_ADDRESS.
In split mode, the two processes coordinate through the shared PostgreSQL database:
- Route table propagation — When a new deployment goes live or a domain is updated, the console publishes a
NOTIFYon theroute_table_changesPostgreSQL channel. The proxy process listens for this notification and reloads its in-memory routing table without restarting. - On-demand wake — Waking scaled-to-zero environments is a proxy-side concern: the proxy process owns the on-demand manager, watches request activity, and wakes containers on the first request. A console process started with
--role=consoledoes not construct one at all; when the console's own wake API is called, it reaches the proxy over the same PostgreSQLNOTIFYchannel. - Version-skew detection — The console records its binary version on startup. At startup the proxy compares its own version against that record and, if they differ, logs a warning advising you to upgrade the proxy to match — during a rolling upgrade the proxy may otherwise read tables a newer console has already migrated. It is a log-only, best-effort check: it never aborts proxy startup.
The primary production use case for split mode is zero-downtime console upgrades: you can restart temps serve (rolling out a new version of the dashboard or API) while temps proxy continues serving traffic uninterrupted. The proxy keeps routing based on the last known route table until the console comes back up and publishes any changes.
Opt-in columnar storage
The ClickHouse storage backends are compiled into the binary but stay dormant until you configure a ClickHouse connection. When they are on, they take over four high-volume observability domains, all in one ClickHouse database:
| Domain | Default backend | With ClickHouse enabled |
|---|---|---|
| Distributed traces | TimescaleDB | ClickHouse |
| Metrics | TimescaleDB | ClickHouse |
| Proxy access logs | TimescaleDB | ClickHouse |
| Analytics events | TimescaleDB | Written to PostgreSQL first, then copied to ClickHouse, which serves the reads |
To turn it on, set TEMPS_CLICKHOUSE_URL, TEMPS_CLICKHOUSE_USER and TEMPS_CLICKHOUSE_PASSWORD (and optionally TEMPS_CLICKHOUSE_DATABASE, which defaults to temps), then restart temps serve. The variables are read when temps serve starts. If any of the three required values is missing or empty, the configuration counts as off and Temps keeps using PostgreSQL, so check all three are set. All other data (deployments, domains, users, service configuration) stays in PostgreSQL.
The variables, the dual write for analytics events and the behaviour differences are in the Environment Variables Reference; the enable, backfill and rollback steps are in Migrate to ClickHouse.
Swapping the storage engine this way needs a restart but no rebuild, which is the kind of pluggable backend single-binary designs usually give up.
The plugin system
Each domain crate registers its services through a type-safe plugin system:
impl TempsPlugin for BackupPlugin {
fn register_services(&self, ctx: &ServiceRegistrationContext) -> Result<()> {
let db = ctx.require_service::<Arc<DatabaseConnection>>();
let encryption = ctx.require_service::<Arc<EncryptionService>>();
ctx.register_service(Arc::new(BackupService::new(db, encryption)));
Ok(())
}
}
This pattern means:
- Each crate is self-contained with its own service layer, error types, and API handlers
- Dependencies between crates are explicit — a backup service depends on the database and encryption service, not on the deployment service
- Services are initialized in two phases: registration (declare what you provide) then initialization (resolve dependencies)
- New features can be added as new crates without modifying existing ones
Why Rust
Rust is chosen for specific technical reasons:
- Memory safety without garbage collection — The proxy handles thousands of concurrent connections. GC pauses would introduce latency spikes.
- One self-contained binary — The platform compiles to one executable with no language runtime to install (no JVM, no Node.js, no Python). The host still needs Docker, which runs your apps and the TimescaleDB database; the installer sets both up.
- Compile-time error checking — The typed error system (thiserror + RFC 7807 Problem Details) catches error handling mistakes at compile time, not in production.
- Performance — Analytics ingestion, log streaming, and proxy routing are all hot paths where microseconds matter.
- Ecosystem — Sea-ORM for database, Axum for HTTP, Pingora for proxying, Bollard for Docker — all mature Rust libraries.
Trade-offs
The single-binary approach has deliberate trade-offs:
Advantages:
- One thing to install, update, and monitor
- No inter-service networking to configure
- All data in one database (simpler backups, simpler queries)
- No vendor lock-in — you own the server and the data
Disadvantages:
- Starts as a single server by default, though multi-node mode allows distributing containers across worker nodes
- All features share server resources (a CPU-heavy build affects analytics ingestion)
- Most application logic is fixed to what Temps implements; operators can swap the storage engine for observability domains (traces, metrics, proxy logs, analytics) to ClickHouse with a restart, without rebuilding
For the target audience — solo developers and small teams running 1-10 applications on a single VPS — these trade-offs strongly favor simplicity. If you outgrow a single server, you can add worker nodes to distribute containers, move the database to a managed instance, or run the binary on a larger machine.
Split-process operation as a production HA option:
For teams that need rolling restarts without dropping traffic, the split-process mode (temps serve --role=console + temps proxy) is the recommended production topology. The proxy process stays alive and keeps routing while the console process is upgraded, restarted, or redeployed. This is a lightweight alternative to a full active-active cluster when the main availability concern is console upgrade downtime rather than hardware failure.