How to secure a Replit app
Replit apps fail in a predictable order: agent reach into production, split secret stores, missing authorization, open endpoints, stale deps. Fixes with code.
- security
- vibe coding
- replit
Replit gives you an agent, a database, secrets, auth, and hosting in one place, which is why apps get built there fast and why the security gaps are concentrated rather than scattered. Five fixes cover most of what an attacker or an over-confident agent would reach.
Do the first one before you do anything else, because it is the only one that protects you from your own tooling rather than from a stranger.
1. Confirm the agent cannot reach production data
In July 2025 Replit’s agent deleted a live database during an explicit code freeze, wiping records for more than 1,200 executives. Replit CEO Amjad Masad called it “unacceptable and should never be possible” and shipped automatic database dev and prod separation in response, so the agent now works against a development database by default.
That fix landed in the defaults for new apps. It does not retroactively split a database that was already provisioned as one, and plenty of projects predate it. Open your database pane and answer two questions: do you have two databases, and which one is your deployment pointed at?
The connection string is the ground truth, not the pane. Print it from the deployment and from the workspace and confirm they differ:
# In the workspace shell
echo "$DATABASE_URL" | sed -E 's#(//[^:]+):[^@]+@#\1:***@#'
Mask the password before you paste that anywhere, which is what the sed does.
If the workspace and the deployment resolve to the same host and database name,
your agent is iterating against your users’ data and no prompt instruction
changes that. Provision a separate development database and repoint the
workspace at it.
2. Get keys into Secrets, and into both of them
Replit’s Secrets tool encrypts values with AES-256 at rest and TLS in transit, keeps them out of your source and version history, and shows only the names (not the values) to someone who remixes your app. It is the right place for every credential, and using it is one panel click.
The failure mode is not that people ignore it. It is that Replit Deployments
read from a secrets store separate from the development environment, so a key
that works all afternoon in the workspace is undefined the moment you deploy.
The natural debugging move when a variable is undefined in production is to
inline the value “just to test,” and that line survives to the next commit.
So set the secret in both places, and make the code refuse to run instead of falling back:
// Fail loudly at boot rather than silently degrading at request time.
const required = ["DATABASE_URL", "SESSION_SECRET", "OPENAI_API_KEY"];
const missing = required.filter((k) => !process.env[k]);
if (missing.length) {
throw new Error(`missing required secrets: ${missing.join(", ")}`);
}
Then confirm nothing already leaked, because the working tree looking clean says nothing about the history:
git log -p --all | grep -nEo "sk-[A-Za-z0-9]{20,}|eyJ[A-Za-z0-9_-]{20,}|AKIA[0-9A-Z]{16}"
Any hit is compromised and needs rotating, not deleting. A commit removed from
HEAD is still in the object store and still in every clone and fork.
Why a leaked key stays leaked covers the
order to do this in.
3. Check ownership on every route, not just identity
This is the fix that matters most and gets skipped most, and Replit says so in its own vibe coding security checklist: “Always verify permissions before performing actions.”
Ask the agent for a feature and you get authentication, because login is a thing you can describe in a prompt. Authorization is a property every endpoint needs at once, and its absence is invisible: the app behaves identically whether or not the check is there, right up until someone changes an id.
The generated route usually looks like this:
// Authenticated, and still wrong: any signed-in user can pass any id.
app.get("/api/documents/:id", requireAuth, async (req, res) => {
const doc = await db.query(
"select * from documents where id = $1",
[req.params.id],
);
res.json(doc.rows[0]);
});
The fix is a predicate, and it belongs in the query rather than in an if after
it, so there is no window where the row was fetched and the check was forgotten:
app.get("/api/documents/:id", requireAuth, async (req, res) => {
const doc = await db.query(
"select * from documents where id = $1 and user_id = $2",
[req.params.id, req.user.id],
);
if (doc.rowCount === 0) return res.sendStatus(404);
res.json(doc.rows[0]);
});
Return 404 rather than 403. A 403 confirms the record exists, which hands an attacker a way to enumerate your ids before they find one they can reach.
Audit every route with a path parameter the same way. If :id comes from the
URL and no clause ties it to req.user, that endpoint is
an IDOR.
4. Close the endpoints the agent left open
Generated Express apps ship with a set of omissions that are individually small and collectively the difference between an app and a target: no rate limiting, no body size cap, no security headers, and CORS set to a wildcard because that made development work.
import helmet from "helmet";
import rateLimit from "express-rate-limit";
import cors from "cors";
app.use(helmet());
app.use(express.json({ limit: "100kb" }));
app.use(cors({
origin: ["https://your-app.replit.app", "https://yourdomain.com"],
credentials: true,
}));
// Tight budget on the endpoints that cost money or send mail.
app.use("/api/auth", rateLimit({ windowMs: 15 * 60 * 1000, limit: 20 }));
app.use("/api/generate", rateLimit({ windowMs: 60 * 1000, limit: 10 }));
The origin allowlist is the one to sit with. cors({ origin: "*" }) lets any
site on the internet call your API, and combined with credentials: true it is
worse than it looks. Replit gives you HTTPS by default, which is genuinely one
less thing to configure, and its checklist is explicit that DDoS protection
beyond that is your problem: “use a CDN or cloud service with built-in DDoS
mitigation capabilities.”
5. Update what shipped vulnerable
Generated manifests reach for the versions the model remembers rather than the current patched release, so a new project can start life with known CVEs already installed, and nothing bumps the lockfile afterwards.
npm audit --omit=dev
npm update
Re-run your tests, then confirm the audit is genuinely clean rather than merely
shorter. If your app is Python, the same applies with pip-audit.
What this does not cover
These five close the gaps that get Replit apps breached, and they are not a security review. Nothing above inspects your file upload handling, your webhook signature verification, the SQL your agent built by string concatenation in a route you have not read, or the secret in a commit from week one. Those belong to the common vulnerability classes that need something reading the whole repository.
That is the limit of a checklist honestly stated: it tells you what to look for and cannot tell you what is in your code. VibeZero scans a repo across five layers, with Gitleaks and TruffleHog over the full git history for step 2, OpenGrep over the routes for step 3, and OSV-Scanner and Grype over the lockfile for step 5. Either way, a fix counts once something other than the tool that wrote the code confirms it, which is a problem worth understanding on its own.
Reach, then surface
The order here is not arbitrary. Step 1 bounds what your own tooling can destroy and steps 2 through 5 bound what a stranger can reach, and those are genuinely different problems that get conflated whenever someone asks whether Replit is safe.
Replit fixed the first one at the platform level after it went wrong in public, which is more than most vendors do. The second one is sixteen items on their own checklist, and it is still yours.