Replit Data Handling
Overview
Manage application data securely across Replit's three storage systems: PostgreSQL (relational), Key-Value Database (simple cache/state), and Object Storage (files/blobs). Covers connection patterns, security, data validation, and choosing the right storage for each use case.
Prerequisites
- Replit account with Workspace access
- PostgreSQL provisioned in Database pane (for SQL use cases)
- Understanding of Replit Secrets for credentials
Storage Decision Matrix
| Need | Storage | API | Limits |
|---|
| Structured data, queries | PostgreSQL | pg npm / psycopg2 | Plan-dependent |
| Simple key-value, cache | Replit KV Database | @replit/database / replit.db | 50 MiB, 5K keys |
| Files, images, backups | Object Storage | @replit/object-storage | Plan-dependent |
Instructions
Step 1: PostgreSQL — Secure Connection
import { Pool, PoolConfig } from 'pg';
function createPool(): Pool {
if (!process.env.DATABASE_URL) {
throw new Error('DATABASE_URL not set. Create a database in the Database pane.');
}
const config: PoolConfig = {
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: false },
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
};
const pool = new Pool(config);
pool.on('error', (err) => {
console.error('Database pool error:', err.message);
});
return pool;
}
export const pool = createPool();
export async function findUser(userId: string) {
const result = await pool.query(
'SELECT id, username, created_at FROM users WHERE id = $1',
[userId]
);
return result.rows[0];
}
Dev vs Production databases:
Replit auto-provisions separate databases:
- Development: used when running in Workspace ("Run" button)
- Production: used when accessed via deployment URL
View in Database pane:
- Development tab: test data, iterate freely
- Production tab: live customer data, handle with care
Both use the same DATABASE_URL — Replit routes automatically.
Step 2: Key-Value Database — Session & Cache
Node.js:
import Database from '@replit/database';
const db = new Database();
export async function cacheGet<T>(key: string): Promise<T | null> {
const entry = await db.get(key) as { value: T; expiresAt: number } | null;
if (!entry) return null;
if (Date.now() > entry.expiresAt) {
await db.delete(key);
return null;
}
return entry.value;
}
export async function cacheSet<T>(key: string, value: T, ttlMs: number): Promise<void> {
await db.set(key, { value, expiresAt: Date.now() + ttlMs });
}
(): <> {
db.(, {
...data,
: .(),
});
}
(): <> {
db.();
}
(): <> {
keys = db.();
cleaned = ;
oneDay = * * * ;
( key keys) {
session = db.(key) ;
(session && .() - session. > oneDay) {
db.(key);
cleaned++;
}
}
cleaned;
}
Python:
from replit import db
import json, time
db["settings"] = {"theme": "dark", "lang": "en"}
settings = db["settings"]
user_keys = db.prefix("user:")
del db["old_key"]
def cache_set(key: str, value, ttl_seconds: int):
db[f"cache:{key}"] = {
"value": value,
"expires_at": time.time() + ttl_seconds
}
def cache_get(key: str):
entry = db.get(f"cache:{key}")
if not entry or time.time() > entry["expires_at"]:
return None
return entry["value"]
Step 3: Object Storage — File Uploads
Node.js:
import { Client } from '@replit/object-storage';
import express from 'express';
const storage = new Client();
const router = express.Router();
router.post('/upload', express.raw({ limit: '10mb', type: '*/*' }), async (req, res) => {
const userId = req.headers['x-replit-user-id'] as string;
if (!userId) return res.status(401).json({ error: 'Login required' });
const filename = req.headers['x-filename'] as string || `file-${Date.now()}`;
const path = `uploads/${userId}/${filename}`;
await storage.uploadFromBytes(path, req.body);
res.json({ path, size: req.body. });
});
router.(, (req, res) => {
path = ;
{
{ value } = storage.(path);
res.(.(value));
} {
res.().({ : });
}
});
router.(, (req, res) => {
objects = storage.({ : });
res.(objects.( ({ : o. })));
});
router;
Python:
from replit.object_storage import Client
storage = Client()
storage.upload_from_text("reports/daily.json", json.dumps(report))
storage.upload_from_filename("backups/db.sql", "/tmp/dump.sql")
content = storage.download_as_text("reports/daily.json")
storage.download_to_filename("backups/db.sql", "/tmp/restore.sql")
if storage.exists("reports/daily.json"):
storage.delete("reports/daily.json")
Step 4: Data Sanitization
import { z } from 'zod';
const UserInputSchema = z.object({
name: z.string().min(1).max(100).trim(),
email: z.string().email().toLowerCase(),
message: z.string().max(5000).trim(),
});
export function validateInput<T>(schema: z.ZodType<T>, data: unknown) {
const result = schema.safeParse(data);
if (!result.success) {
return { valid: false as const, errors: result.error.flatten().fieldErrors };
}
return { valid: true as const, data: result.data };
}
export () {
{ password_hash, email, phone, ...safe } = user;
safe;
}
() {
(!data) .(message);
redacted = .(.(data, {
([, , , , ].(key.())) {
;
}
value;
}));
.(message, redacted);
}
Step 5: Error Response Safety
app.use((err: Error, req: any, res: any, next: any) => {
safeLog('Error:', { message: err.message, path: req.path });
const isProduction = process.env.NODE_ENV === 'production';
res.status(500).json({
error: isProduction ? 'Internal server error' : err.message,
...(isProduction ? {} : { stack: err.stack }),
});
});
Error Handling
| Issue | Cause | Solution |
|---|
| DATABASE_URL undefined | PostgreSQL not created | Provision in Database pane |
KV Max storage exceeded | Over 50 MiB | Migrate to PostgreSQL or Object Storage |
| Object Storage 403 | Bucket not provisioned | Create in Object Storage pane |
| SQL injection | String concatenation | Use parameterized queries ($1, $2) |
| PII in logs | Full object logging | Use safeLog() with field redaction |
Resources
Next Steps
For team access control, see replit-enterprise-rbac.