Stop Losing Data on Replit: Moving Prototype State to Managed PostgreSQL
A Replit prototype that stores counters or uploads on the local filesystem works until the first restart. Here is how to move durable state into managed PostgreSQL and inject configuration through deployment secrets.
02 Jul 2026, 00:17 UTC

Why the Editor Filesystem Betrays Prototypes
You build a small service in a Repl—a visit counter, a to-do list, an upload log. While you are coding, everything works. You restart the run to test a change. The counter resets to zero. You deploy the Repl. The database is empty again. The problem is not your code; it is the assumption that the Replit workspace filesystem is a durable disk. It is not. Treat it as scratch space that happens to persist between some editing sessions, not as a storage layer for production state.
Making PostgreSQL the System of Record
Replit offers several places to put data: the editor filesystem, Replit's key-value store, and managed PostgreSQL. For relational state—user rows, counters, audit logs—managed PostgreSQL is usually the right default because it gives you transactions, constraints, and SQL. The key-value store is fine for caches or session blobs, but it should not be your primary source of truth unless you have tested its consistency and limits for your plan. The filesystem is for scratch files and build artifacts. The decision rule is simple: if a write must survive a restart, it belongs in the database.
Worked Example: A Visit Counter That Survives Restarts
Below is a minimal FastAPI service that records every request to /visits in a managed PostgreSQL table. It reads the connection string from an environment variable, creates the table if needed, inserts a row, and returns the count.
import os
import psycopg2
from fastapi import FastAPI
app = FastAPI()
DSN = os.environ.get("DATABASE_URL")
def get_conn():
if not DSN:
raise RuntimeError("DATABASE_URL is not set")
return psycopg2.connect(DSN)
@app.get("/healthz")
def healthz():
return {"status": "ok"}
@app.get("/visits")
def visits():
conn = get_conn()
try:
with conn.cursor() as cur:
cur.execute(
"CREATE TABLE IF NOT EXISTS visits("
"id serial primary key, "
"seen_at timestamptz default now())"
)
cur.execute("INSERT INTO visits DEFAULT VALUES")
cur.execute("SELECT count(*) FROM visits")
count = cur.fetchone()[0]
conn.commit()
return {"visits": count}
finally:
conn.close()
Install the dependencies in the Replit shell with pip install fastapi uvicorn psycopg2-binary, then start the service with uvicorn main:app --host 0.0.0.0 --port 8000. The DATABASE_URL variable is typically injected when you attach a managed PostgreSQL database, but verify the exact name for your plan—workspace variables and deployment variables are not always the same. The database user created by Replit typically has the privileges needed for CREATE, INSERT, and SELECT; if you use a restricted user, grant those on the target schema. This example assumes a recent Python 3 runtime and current stable releases of FastAPI and psycopg2; verify package names against your Repl's Python version.
Immediate Verification
Call /healthz to confirm the process is alive. Call /visits twice and confirm the count increases. Then restart the run from the Replit interface. If the count continues from where it left off, your state is durable. If it resets, the connection string is missing or pointing at the wrong database.
Deployments, Secrets, and the Environment Gap
A workspace run and a deployed service are different environments. A secret set only in the editor may not appear in the deployment runtime. Before you deploy, add the same database connection string to the deployment's secret store. Do not paste it into code or commit it to the Repl.
Give the deployment a health endpoint. /healthz above is trivial, but it gives the platform a cheap way to distinguish a crashed process from a slow cold start. Make sure your build and run commands do not depend on untracked files—list every dependency in requirements.txt so the deployed container can rebuild them.
Trade-offs and Limitations
Managed PostgreSQL removes server administration, but it introduces new constraints. Connection limits matter: if your deployment autoscales or restarts frequently, each instance may open a new database connection. Without pooling, you can exhaust the plan's connection allowance. Use a connection pool or an external pooler if you expect concurrent traffic.
Cold starts are another difference. A workspace run may feel instant, while a deployed service can take longer to become ready after an idle period. Your health check should tolerate that. Backups and retention are plan-dependent; check Replit's current documentation for what is included before you treat the database as your only backup.
Actionable Close
Before your next deploy, do this:
- List every durable write in your Repl. If it must survive a restart, it belongs in PostgreSQL, not on the filesystem.
- Move those writes to a managed database and read the connection string from an environment variable.
- Set the secret in the deployment environment, not just the workspace.
- Add a health endpoint and a smoke test that restarts the service and confirms a previously inserted row still exists.
- Check the deployment logs for connection errors, missing variables, and cold-start delays.
If you need to undo the example, drop the visits table from a SQL session connected to the same database: DROP TABLE visits;. That removes the demo data but leaves the schema pattern intact for real tables.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.