Managed Services
Temps provides fully managed database and storage services that you can provision and link to your projects in seconds. Unlike platforms like Vercel that require external services, Temps includes everything you need.
Competitive Advantage: While platforms like Vercel require third-party database providers (Vercel Postgres, Vercel KV), Temps includes managed PostgreSQL, Redis, and S3 storage out of the box—no additional accounts or billing required.
Available Services
Temps offers three types of managed services that integrate seamlessly with your projects:
- PostgreSQL — Production-ready relational database
- Redis — High-performance caching and pub/sub
- S3 Storage — Object storage compatible with MinIO and AWS S3
All services are provisioned on your infrastructure, giving you complete control over your data.
PostgreSQL, Redis, and S3 services are provisioned through the dashboard or the REST API. The temps CLI does not yet support managed service provisioning directly.
PostgreSQL Database
Creating a PostgreSQL Instance
Provision a production-ready PostgreSQL database with automatic backups and high availability:
Via Dashboard:
- Navigate to Services → Create Service
- Select PostgreSQL
- Choose version (13, 14, 15, or 16)
- Configure storage size and instance resources
- Click Create Database
Configuration Options:
| Option | Description | Default |
|---|---|---|
version | PostgreSQL version (13-16) | 16 |
storage | Disk space allocation | 10GB |
memory | RAM allocation | 1GB |
replicas | Number of read replicas | 1 |
backups | Automatic daily backups | Enabled |
Connecting to PostgreSQL
Once created, Temps generates a connection string you can use in your applications:
Node.js/TypeScript
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: {
rejectUnauthorized: true
}
});
// Query example
const result = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);
Python
import psycopg2
conn = psycopg2.connect(
os.environ['DATABASE_URL'],
sslmode='require'
)
cursor = conn.cursor()
cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))
Connection String Format: postgresql://username:password@host:5432/database?sslmode=require
The connection string is automatically injected as DATABASE_URL when you link the service to a project.
Linking to Projects
Link your PostgreSQL database to one or more projects:
Via Dashboard:
- Open the service details page for your PostgreSQL instance.
- Click Link to Project, select the project and environment.
- Temps injects the connection string as an environment variable automatically.
Access in your app
// Automatically available as environment variable after linking
const databaseUrl = process.env.DATABASE_URL;
const db = new Database(databaseUrl);
Database Migrations
Run migrations during deployment by configuring build hooks:
temps.json
{
"build": {
"command": "npm run build",
"beforeDeploy": [
"npm run db:migrate"
]
}
}
PostgreSQL WAL Health
Standalone managed PostgreSQL services run a read-only WAL/archive health probe on every background health-poll cycle. The probe never writes to your database — it only reads PostgreSQL system views and inspects the pg_wal/ directory — and the result is stored on the service so the dashboard can surface problems before they fill your disk.
Why this exists: in earlier versions, PostgreSQL containers were started with archive_mode=on regardless of whether an archive destination was configured. A service whose archive_command was never set would accumulate WAL forever — we observed a pg_wal directory grow to 191 GB in production. The WAL health probe detects this condition (and several others) and flags it in the UI.
What the probe checks
The probe computes warnings for the following conditions. Each warning carries a severity of either warning or critical:
| Condition | Triggers when | Severity |
|---|---|---|
| WAL bloat | pg_wal size is at least 3× max_wal_size | critical at 10×, otherwise warning |
| Stale replication slot | An inactive/stale replication slot is retaining a large amount of WAL | critical |
| Archive backlog | At least 100 .ready segments in pg_wal/archive_status | warning |
archive_mode without command | archive_mode is on/always but archive_command is empty or a no-op (/bin/true, true) | warning |
| WAL not recycled | The oldest WAL segment is at least 24h old and pg_wal is bloated (≥ 3× max_wal_size) | warning |
When any warning is present, the background monitor downgrades an Operational service to Degraded. It never escalates a service that is already Down.
Reading the snapshot
The latest snapshot is exposed at:
GET /api/external-services/{id}/wal-health
This is a plugin route served under the /api prefix. It requires the ExternalServicesRead permission and returns:
| Status | Meaning |
|---|---|
200 | A PostgresWalHealth snapshot (pg_wal size, max_wal_size, archive_mode, and any warnings) |
404 | Service not found, not a PostgreSQL service, or no snapshot has been recorded yet |
The web console renders the snapshot on the service detail page as a warning panel showing pg_wal size, max_wal_size, archive_mode, and one row per warning. Two of the warnings include a copy-to-clipboard SQL snippet:
| Warning | Remediation snippet |
|---|---|
| Stale replication slot | SELECT pg_drop_replication_slot('<slot>'); |
| WAL not recycled | CHECKPOINT; |
The remaining warnings (WAL bloat, archive backlog, and archive_mode without command) show plain-language guidance instead — for example, the archive_mode-without-command warning tells you to Stop/Start the service so it self-repairs (see below).
Safe archive_mode defaults
New PostgreSQL services now start with archive_mode=off. WAL archiving is only turned on once a destination is actually configured:
- When you enable WAL archiving, Temps writes the WAL-G environment file, sets
archive_command, and then recreates the container witharchive_mode=onbaked into its startup command. - On every start, the service reconciles: it probes the service volume for the WAL-G config and compares the intended
archive_modeagainst the running container's command. If they disagree, it recreates the container with the correct setting. This means a simple Stop → Start auto-repairs any existing service stuck in the old "archive on, no destination" state.
After such a reconciling recreate, Temps refreshes the WAL health snapshot inline, so the dashboard reflects the new state within about a second instead of waiting for the next poll cycle.
Redis Caching
Creating a Redis Instance
Provision a high-performance Redis instance for caching, sessions, or pub/sub:
Via Dashboard:
- Navigate to Services → Create Service
- Select Redis
- Choose version (6 or 7)
- Configure memory allocation
- Click Create Cache
Configuration Options:
| Option | Description | Default |
|---|---|---|
version | Redis version (6-7) | 7 |
memory | RAM allocation | 512MB |
persistence | Enable RDB/AOF | Disabled |
eviction | Eviction policy | allkeys-lru |
maxclients | Max connections | 10000 |
Connecting to Redis
Node.js with ioredis
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
// Set a value
await redis.set('session:123', JSON.stringify(sessionData), 'EX', 3600);
// Get a value
const data = await redis.get('session:123');
Python with redis-py
import redis
import os
r = redis.from_url(os.environ['REDIS_URL'])
# Set with expiration
r.setex('session:123', 3600, json.dumps(session_data))
# Get value
data = r.get('session:123')
Connection String Format: redis://username:password@host:6379/0
When linked to a project, Redis connection details are available as REDIS_URL, REDIS_HOST, REDIS_PORT, and REDIS_PASSWORD.
Common Use Cases
- Name
Session Storage- Description
Store user sessions with automatic expiration:
await redis.set(`session:${userId}`, sessionData, 'EX', 86400); // 24 hours
- Name
Rate Limiting- Description
Implement rate limiting with Redis counters:
const count = await redis.incr(`rate:${ipAddress}`); if (count === 1) await redis.expire(`rate:${ipAddress}`, 60); // 60 seconds if (count > 100) throw new Error('Rate limit exceeded');
- Name
Caching- Description
Cache expensive queries or API responses:
const cached = await redis.get(`cache:${key}`); if (cached) return JSON.parse(cached); const data = await fetchExpensiveData(); await redis.set(`cache:${key}`, JSON.stringify(data), 'EX', 300); // 5 minutes
- Name
Pub/Sub- Description
Real-time messaging between services:
// Publisher await redis.publish('notifications', JSON.stringify(message)); // Subscriber redis.subscribe('notifications', (message) => { console.log('Received:', JSON.parse(message)); });
S3 Object Storage
Creating S3 Storage
Provision MinIO-compatible object storage for files, backups, and assets:
Via Dashboard:
- Navigate to Services → Create Service
- Select S3 Storage
- Configure storage size
- Set bucket policy (private/public)
- Click Create Storage
Configuration Options:
| Option | Description | Default |
|---|---|---|
size | Storage allocation | 50GB |
public-read | Public read access | false |
versioning | Object versioning | Disabled |
lifecycle | Auto-delete rules | None |
Connecting to S3
Temps S3 is compatible with AWS S3 SDKs:
Node.js with AWS SDK v3
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({
endpoint: process.env.S3_ENDPOINT,
region: 'us-east-1',
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY,
secretAccessKey: process.env.S3_SECRET_KEY,
},
});
// Upload a file
await s3.send(new PutObjectCommand({
Bucket: 'media-storage',
Key: 'uploads/image.jpg',
Body: fileBuffer,
ContentType: 'image/jpeg',
}));
// Download a file
const response = await s3.send(new GetObjectCommand({
Bucket: 'media-storage',
Key: 'uploads/image.jpg',
}));
Python with boto3
import boto3
import os
s3 = boto3.client(
's3',
endpoint_url=os.environ['S3_ENDPOINT'],
aws_access_key_id=os.environ['S3_ACCESS_KEY'],
aws_secret_access_key=os.environ['S3_SECRET_KEY'],
)
# Upload file
s3.upload_file('local-file.jpg', 'media-storage', 'uploads/image.jpg')
# Download file
s3.download_file('media-storage', 'uploads/image.jpg', 'downloaded.jpg')
Public URLs
Generate pre-signed URLs for temporary access:
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const command = new GetObjectCommand({
Bucket: 'media-storage',
Key: 'uploads/private-document.pdf',
});
// URL expires in 1 hour
const url = await getSignedUrl(s3, command, { expiresIn: 3600 });
Service Management
Monitoring Service Health
View service metrics and health status in the dashboard:
- CPU & Memory Usage: Real-time resource consumption
- Connection Count: Active database/cache connections
- Storage Usage: Disk space utilization
- Uptime: Service availability percentage
- Query Performance: Slow query detection (PostgreSQL)
All monitoring is available in the Services section of the dashboard. Service-level monitoring is not yet exposed through the CLI.
Backups
All services include automatic daily backups retained for 7 days (configurable up to 90 days):
Manual backups and restore are managed through the dashboard. You can also use the temps backup CLI commands, which read from and write to your configured S3 bucket:
List and restore backups via CLI
# List available backups stored in S3
temps backup list \
--bucket-name my-temps-backups \
--endpoint $S3_ENDPOINT \
--access-key-id $S3_ACCESS_KEY_ID \
--secret-access-key $S3_SECRET_ACCESS_KEY
# Restore a specific managed service from a backup
temps backup restore-service \
--bucket-name my-temps-backups \
--endpoint $S3_ENDPOINT \
--access-key-id $S3_ACCESS_KEY_ID \
--secret-access-key $S3_SECRET_ACCESS_KEY \
--database-url $TEMPS_DATABASE_URL \
--backup-id <backup-id-from-list> \
--service-name production-db \
--encryption-key $TEMPS_ENCRYPTION_KEY
Scaling Services
Scale your services as your needs grow through the dashboard (Services → select service → Scale). Adjust storage, memory, and replica count from the service settings panel. To cap how much memory and CPU a service's container may consume, see Per-Container Resource Limits below.
Scaling operations are performed with zero downtime. Your applications remain connected during the scaling process.
Per-Container Resource Limits
You can cap how much memory, swap, and CPU each managed service's container is allowed to use, and inspect what it's actually consuming. This applies to all managed/external service types — PostgreSQL (standalone and cluster), Redis, MongoDB, RustFS, and S3. Services created before this feature, or any service with no limits set, run unconstrained (the default).
The service detail page in the dashboard shows a Resources card that polls runtime status every 30 seconds and live usage every 5 seconds, plus an Edit limits dialog. Behind the dashboard are three REST endpoints.
Editing limits
Open Services → select the service → Resources card → Edit limits. The dialog has independent Memory and CPU toggles:
| Field | Meaning | Empty / off |
|---|---|---|
| Memory | Hard memory cap, in MiB | Unlimited |
| Swap | Extra swap above the memory cap, in MiB (not Docker's raw total) | No extra swap |
| CPU | CPU cap, in cores | Unlimited |
The Swap input means swap in addition to the memory limit. For example, Memory = 512 and Swap = 256 lets the container use 512 MiB of RAM plus 256 MiB of swap. Leaving Swap empty (0) means no swap beyond the memory cap. Internally Temps submits memory + swap to Docker, so the API contract is unchanged while the input matches its label.
When you enable a memory cap, the dialog shows an explicit OOM warning: if the container's working set exceeds a hard memory limit, the kernel OOM killer terminates it — and that kill never reaches the application's own logs, so an oom_killed flag on the runtime endpoint is often the only signal that a memory limit was the cause.
Leaving every field unset removes all limits and returns the service to unconstrained operation.
Endpoints
All three are served under the /api prefix.
GET /api/external-services/{id}/runtime
GET /api/external-services/{id}/stats
PATCH /api/external-services/{id}/resources
GET /api/external-services/{id}/runtime — requires ExternalServicesRead. Returns a per-container snapshot from docker inspect: container status, restart_count (a steady climb signals a crash loop), the oom_killed flag, last exit_code, image, and the resource limits actually applied to the running container (so you can detect drift between configured and live caps). Standalone services return one member; clusters return one entry per member.
GET /api/external-services/{id}/stats — requires ExternalServicesRead. A one-shot CPU/memory usage sample per container (no streaming), cheap enough to poll on a 5–10s interval. cpu_percent uses Docker's standard formula; memory_percent is measured against the memory limit when one is set, or against host RAM when no limit is set (so an unlimited service's "5%" means 5% of host RAM, not of an allocation).
PATCH /api/external-services/{id}/resources — requires ExternalServicesWrite. Persists the new caps to the service's encrypted config and live-applies them via Docker's update API. Memory and CPU can be changed on a running container without a restart; a stopped container also accepts the update and picks up the caps on next start. The request body is the limits object — pass null (or omit) any field to leave it unlimited; a request where every field is null removes all limits.
The response includes a per-member applied[] list so you can tell which containers got the change. Each entry reports an outcome:
| Outcome | Meaning |
|---|---|
applied | Docker accepted the update; the caps are live now |
stopped | Container exists but isn't running; caps stored and applied on next start |
missing | Container does not exist yet; caps stored and applied on first start |
failed | docker update returned an error (e.g. the new memory cap is below current usage) |
The dashboard surfaces this as an apply-summary toast and rebases its CPU meter against the cap when one is set, so the bar reads as a fraction of the cap instead of a fraction of host cores.
Security Best Practices
- Name
Connection Security- Description
- Always use SSL/TLS connections (
sslmode=requirefor PostgreSQL) - Store credentials in environment variables, never in code
- Use the Temps-provided connection strings (automatically secured)
- Always use SSL/TLS connections (
- Name
Network Access- Description
- Services are only accessible from projects within your Temps instance
- Configure IP allowlists for external access if needed
- Use private networking for service-to-service communication
- Name
Credential Rotation- Description
Rotate credentials from the service settings page in the dashboard (Services → select service → Rotate Credentials). Connection strings are automatically updated in all linked projects after rotation.
- Name
Audit Logging- Description
All service access is logged for security auditing:
- Connection attempts (successful/failed)
- Query execution (optional for PostgreSQL)
- Object access (S3 operations)
- Configuration changes
Data Safety
Because managed services run on your own infrastructure, you own and control all data at every layer. This section covers how Temps protects your data against loss, corruption, and unauthorized access.
Encryption
Credentials (passwords, access keys) are encrypted with AES-256-GCM before being written to the Temps database. The key lives at ~/.temps/encryption_key — protect this file and back it up separately from the database.
Database contents are not encrypted at the application layer by default. If you need encryption at rest for the data itself, enable it at the infrastructure level — your cloud provider's encrypted volumes or LUKS on bare metal. Temps does not manage disk encryption.
In transit, all connections require SSL/TLS (sslmode=require for PostgreSQL). Temps never exposes service ports directly to the public internet.
Data Durability
- Name
PostgreSQL- Description
New services start with WAL (Write-Ahead Log) archiving off — enable it (which configures a WAL-G destination) to get point-in-time recovery (PITR) back to the oldest retained WAL segment. Automatic daily backups are retained for 7 days (configurable up to 90 days). A confirmed write to PostgreSQL is durable — data is fsynced before the client receives acknowledgment. Read replicas, when configured, maintain additional copies. A built-in WAL health probe watches for WAL bloat and misconfigured archiving so a stalled archive destination can't silently fill your disk.
- Name
Redis- Description
By default Redis is in-memory only — data does not survive a restart. For caching workloads that can tolerate data loss this is fine. For anything that must survive restarts (sessions, queues), enable RDB snapshots (periodic point-in-time dumps) or AOF persistence (append-only log of every write). If data loss is not acceptable under any circumstance, use PostgreSQL instead.
- Name
S3 Object Storage- Description
Objects are stored on the underlying server disk. Enable versioning to protect against accidental overwrites and deletes. For disaster recovery, replicate buckets to a separate off-server S3-compatible destination using
rcloneor a lifecycle replication rule — data on a single disk is not protected against hardware failure.
Data Isolation
Each managed service runs in its own container with a dedicated volume:
- No cross-service filesystem access — services cannot read each other's data at the OS level
- Per-project database users — when you link a service to a project, Temps creates a scoped database user with the minimum required privileges for that project; it does not reuse the superuser credential
- Network isolation — services are only reachable from containers on the same Temps internal network and are not exposed to the public internet unless you explicitly open a port
Deleting a Service
Deleting a managed service permanently destroys all data and backups associated with it. This cannot be undone.
Before deleting, always download a final backup and unlink all projects first:
- Download a backup — go to Services → select the service → Backups → download the latest backup.
- Unlink all projects — in the service settings, remove all project links so no running app still holds a reference to the credentials.
- Delete the service — from the service settings page, click Delete Service and confirm.
Data Ownership
Your managed service data never leaves your server. Temps (the company) has no access to your database contents, Redis data, or S3 objects. See Data Ownership & Privacy for the complete policy including GDPR considerations.
Pricing & Resource Limits
Self-Hosted Advantage: Since Temps is self-hosted, managed services run on your infrastructure. There are no per-GB storage charges, no connection limits, and no metered bandwidth fees like other platforms.
Recommended Resource Allocation:
| Use Case | PostgreSQL | Redis | S3 Storage |
|---|---|---|---|
| Small Project | 1GB RAM, 10GB storage | 256MB | 10GB |
| Medium Project | 2GB RAM, 50GB storage | 1GB | 100GB |
| Large Project | 4GB+ RAM, 200GB+ storage | 4GB+ | 500GB+ |
Comparison with Other Platforms
| Feature | Temps | Vercel | Netlify | Railway |
|---|---|---|---|---|
| PostgreSQL | ✅ Built-in | ⚠️ Vercel Postgres (extra) | ❌ External only | ✅ Built-in |
| Redis | ✅ Built-in | ⚠️ Vercel KV (extra) | ❌ External only | ✅ Built-in |
| S3 Storage | ✅ MinIO-compatible | ⚠️ Vercel Blob (extra) | ❌ External only | ❌ External only |
| Connection Limits | ❌ No limits | ✅ Metered/capped | ✅ Metered/capped | ⚠️ Plan-based |
| Storage Cost | ❌ Your infrastructure | ✅ $0.15/GB/month | ✅ $0.10/GB/month | ✅ Pay per GB |
| Backups | ✅ Automatic | ⚠️ Enterprise only | ⚠️ Add-on | ✅ Automatic |
Next Steps
- Configure Environment Variables to inject service credentials
- Set up Resource Limits to allocate CPU and memory
- Enable Monitoring to track service health
- Configure Backups to customize backup retention