Replit Agent can build and deploy a full-stack app from a description, with a database, auth and hosting included. That's a lot of moving parts, and the agent is focused on getting them working, not on locking them down.
This checklist covers what to verify in a Replit-built app before you share it. Most Replit apps have a real backend (often Node/Express, Python/Flask or FastAPI, or a full-stack framework), so the focus is on server-side checks.
☐ 1. Keep secrets in Replit Secrets, not in code
Replit has a built-in Secrets tool that stores values as environment variables. Your code reads them with process.env.NAME in Node or os.environ["NAME"] in Python.
Search your project for keys pasted directly into files:
grep -rnE "sk_live|sk_test|sk-[A-Za-z0-9]{10,}|service_role|sb_secret_|whsec_|postgres(ql)?://[^ ]*:[^ ]*@" \
--include=*.{js,ts,py,json,env} . 2>/dev/null | grep -v node_modules
Move anything you find into Secrets, then rotate the key, because it may already have been seen. See how to rotate leaked keys.
Also check your deployment. Depending on how your app is deployed, deployment secrets may be configured separately from workspace secrets, so make sure production has what it needs and nothing is hardcoded as a fallback, like this:
// Red flag: the fallback is a real key sitting in your code
const key = process.env.OPENAI_API_KEY || "sk-proj-abc123...";
☐ 2. Know who can see your code
Check whether your Repl is public or private in its settings. If it's public, anyone can view the source, and anything hardcoded in it is effectively published. If your project is also connected to GitHub, check that repo's visibility and history. Our guide on checking whether API keys leaked on GitHub explains how to scan it.
☐ 3. Keep secrets out of the frontend
If your app has a React or Vite frontend, any variable that's bundled into the client (for example anything prefixed with VITE_) is public. LLM keys, Stripe secret keys and database credentials must only be used by the backend. The frontend should call your own API, and your API calls the third-party service. See exposed keys in NEXT_PUBLIC_ and VITE_.
☐ 4. Require auth on every API endpoint
The agent tends to protect pages in the UI, but the backend endpoints behind them are directly callable. In an Express app, use middleware on the routes that need it, and don't forget any:
// auth middleware
function requireUser(req, res, next) {
if (!req.user) return res.status(401).json({ error: "Unauthorized" });
next();
}
app.get("/api/projects", requireUser, async (req, res) => {
const projects = await db.query(
"select id, name from projects where owner_id = $1",
[req.user.id]
);
res.json(projects.rows);
});
How req.user gets populated depends on your auth setup (Replit Auth, Passport sessions, Supabase, Clerk or similar). The important part is that it comes from a verified session or token, not from the request body.
In Flask, a decorator does the same job:
from functools import wraps
from flask import session, jsonify
def require_user(f):
@wraps(f)
def wrapper(*args, **kwargs):
if "user_id" not in session:
return jsonify(error="Unauthorized"), 401
return f(*args, **kwargs)
return wrapper
If you use Flask's cookie sessions, set a long random SECRET_KEY from Secrets. A hardcoded or default secret key lets attackers forge sessions.
If your app uses Next.js, see securing Next.js API routes with auth.
☐ 5. Scope every query to the current user
Auth checks that someone is logged in. You also need to check they own the record. Look for endpoints like GET /api/invoices/:id that fetch by ID alone:
// Red flag: any logged-in user can read any invoice
const r = await db.query("select * from invoices where id = $1", [req.params.id]);
// Better: only the owner's invoice
const r = await db.query(
"select * from invoices where id = $1 and owner_id = $2",
[req.params.id, req.user.id]
);
if (r.rowCount === 0) return res.status(404).json({ error: "Not found" });
Never read userId or role from the request body or query string for authorization.
☐ 6. Use parameterized queries
Generated code sometimes builds SQL with string templates. That's SQL injection waiting to happen:
# Red flag
cur.execute(f"select * from users where email = '{email}'")
# Safe
cur.execute("select * from users where email = %s", (email,))
The same goes for ORMs: use their query builders rather than raw string concatenation.
☐ 7. Protect your database
If your app uses a managed Postgres database, the connection string includes a password, so it belongs in Secrets and only in backend code. If you use Supabase or Firebase as the database instead, the security boundary is different: the client talks to them directly, so RLS or security rules are what matter. See the Supabase RLS checklist or Firebase security rules for AI-built apps.
Also make sure your development and production data are separate, so testing with the agent doesn't modify real users' data.
☐ 8. Verify payment webhooks
If you use Stripe, paid access must be granted by a webhook whose signature you verify using the raw request body. In Express, that means the webhook route needs express.raw({ type: "application/json" }) instead of express.json():
app.post("/api/stripe/webhook", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
req.headers["stripe-signature"],
process.env.STRIPE_WEBHOOK_SECRET
);
} catch {
return res.status(400).send("Invalid signature");
}
// handle event.type ...
res.json({ received: true });
});
Register this route before any global app.use(express.json()), or the body will already be parsed and verification will fail. For a Next.js version, see Stripe webhook signature verification in Next.js.
☐ 9. Rate limit expensive endpoints
Login, signup, password reset, email sending and anything that calls an LLM should be rate limited. In Express, express-rate-limit is a simple start:
import rateLimit from "express-rate-limit";
app.use("/api/generate", rateLimit({ windowMs: 60_000, limit: 10 }));
If you run more than one instance, use a shared store such as Redis so limits apply across all of them. Set a spending limit at your LLM provider too. See rate limiting OpenAI API routes.
☐ 10. Turn off debug mode and verbose errors
Make sure production doesn't run with debug=True in Flask or send stack traces to the browser. Return generic error messages and log details on the server.
☐ 11. Update dependencies
Run npm audit or pip list --outdated (and a tool like pip-audit) and update anything with known vulnerabilities.
☐ 12. Test like an outsider
Log out and call your API directly with curl. Then log in as one test user and try to fetch another user's records by changing IDs in the URL. Anything that works for you here works for an attacker.
Before you launch
For a broader list that also covers monitoring and backups, see the pre-launch security checklist for AI-built apps.
If your Replit project is connected to GitHub, PreflightPR can do a first pass for you. Run a free scan on a public repo to check for hardcoded secrets, unprotected routes and webhook problems. It's an automated best-effort review, not a pen test, so keep the manual tests above in your routine.