Skip to content
LockShip

Guide · Replit · Updated

Is my Replit app secure? A plain check before you launch

You told Replit Agent what you wanted, watched it build a working app with a login and a database, and now there is a .replit.app address you’re about to post somewhere. Before strangers start clicking around, look at it the way one of them would. This is the list we would work through on our own app, in plain words, with yours open in the next tab. Nobody can promise you an app is secure, us included. What you can do is check the handful of places where the trouble usually is.

The honest short answer

Replit gives you more security tooling than most builders. It has a Security Center, a package firewall that is on by default, automatic dependency checks, and a security scan that Replit says it always runs before you publish. It also published a shared responsibility model, a table of what Replit looks after and what you do. It is worth ten minutes of your time, because it is unusually blunt. “You own the code Agent generates and you’re responsible for it,” it says. In the rows for published apps, user management, access control, and “penetration testing of your app” all say You.

That isn’t Replit dodging the question. It’s the truth for every builder. A scanner can spot a key in the wrong place or a package with a known bug. It can’t know that customers should only see their own orders, or that only you should be able to change prices. So the useful question isn’t “is Replit secure?” It’s “does every part of my app check who is asking, and is everything I published meant to be public?” You can find out, and most of it takes an evening.

What your Replit app is made of

  • Its own server. Apps Agent builds often come with a backend that runs on Replit, next to the pages. Replit’s own example project notes describe a typical one as a React front end, an Express.js backend, and PostgreSQL with Drizzle ORM. Yours may differ, but the shape is common, and it is what this guide assumes.
  • Two databases. Replit’s database docs say every app works with a development database you and Agent build with, and a production database that is created when you publish and serves your live app.
  • Secrets. Keys and passwords go in the Secrets tool, which encrypts them and hands them to your code as environment variables.
  • Sign-in. Agent can add Replit Auth or Clerk Auth. Both handle the sign-in screens and sessions; your code still decides what a signed-in person may see.
  • Two addresses. While you build, there is a development address ending in replit.dev, which Replit’s docs say is private by default. When you publish, you get a .replit.app address, or your own domain.

That first point is the one that changes everything compared with a Lovable or Bolt app. In those, the browser usually talks to the database directly, so database rules are the lock. In a typical Replit app, the browser talks to your server, and your server talks to the database. Your server’s routes are the lock. If a route hands out data without asking who you are, no database setting will save you.

The eight checks

Do them in order. The first two are settings you can fix in minutes. Checks 4 and 5 take longer, and they’re the ones most likely to matter. Only test an app you own, and write down anything you change.

  1. Run Replit’s own scan, and know what it can’t see
  2. Decide who the app is for before you publish
  3. Keep keys in Secrets, and out of anything named VITE_
  4. Knock on your API routes without signing in
  5. Check that every record belongs to the person asking
  6. Know which database your live app uses, and who can touch it
  7. Make sure your server only hands out what you built
  8. Add the headers and cookie settings Express leaves out

01

Run Replit’s own scan, and know what it can’t see

Start with what Replit already gives you. In your project, open Tools → Security Center and select Run scan. Replit’s scan levels page explains the three choices:

  • Level 1 is free and checks the packages your app depends on for known vulnerabilities. The docs are careful to add that “a clean dependency scan does not mean your own code has no vulnerabilities.”
  • Level 2 needs a paid plan and adds an Agent review of your source code, using static analysis tools such as Semgrep alongside the model.
  • Level 3 adds a black-box test of your running Preview from the outside. For apps on Clerk Auth, it also signs in as a synthetic user, and creates a second one when it needs to check whether one account can reach another’s data.

Then open Publishing → Adjust settings and make sure Block publishing of critical vulnerabilities is on. Replit’s publishing page says it always runs a security scan before publishing; this setting decides whether a critical finding actually stops the release. In your account’s Settings → Advanced, consider turning on Auto-Protect, which the Security Center docs say is off by default. It has Agent prepare patches when a new vulnerability turns up in one of your packages. A patch only reaches your live app after you apply it and publish again.

Now the part that matters for the rest of this guide. Replit’s docs say plainly that “a scan is not a complete security review.” Its own white paper on securing AI-generated code found that AI-only scans are nondeterministic: “functionally equivalent code can receive different security assessments depending on syntactic form or prompt phrasing.” So treat a clean scan as a starting point. Rerun it after big changes, read each finding before you accept or dismiss it, and keep going.

02

Decide who the app is for before you publish

This sounds too simple to matter. It is the one that got thousands of apps into the news.

In May 2026, WIRED reported on research by the security firm RedAccess. It found more than 5,000 apps built with Lovable, Replit, Base44 and Netlify that had “virtually no security or authentication of any kind.” Around 40 percent of them seemed to expose sensitive data, though WIRED notes it couldn’t confirm the data it reviewed was real. The researchers didn’t break anything to find them. They searched Google and Bing for the builders’ hosting domains. Replit’s CEO, Amjad Masad, replied that “Replit allows users to choose whether apps are public or private. Public apps being accessible on the internet is expected behavior.” Both things are true. The setting exists, and plenty of people never touched it.

Replit’s access docs list four options under Sharing in the Publishing tool:

  • Public: anyone on the internet with the address. Apps in a personal workspace start here.
  • Password protected: anyone with one shared password, no Replit account needed. Fine for a beta, but it is one password for everybody, not a login.
  • Workspace only and Invite only: Replit puts a sign-in screen in front of the app and only lets in people you’ve allowed. Use one of these for anything internal.

So ask the plain question: who is this app for? If the answer is “my team” or “me and a few clients,” it shouldn’t be Public. Changing it on a live app means unpublishing, choosing the new option, and publishing again. If the answer is “anyone who signs up,” Public is right, and your app’s own login is the lock. That is what checks 4 and 5 test.

Two smaller things while you’re here. Your replit.dev preview is private by default. If you ever switched off Private preview under Networking to show someone, switch it back on. For webhooks, Replit’s docs recommend an external access token instead. And look at who you’ve invited to the project. Replit’s Secrets page says a collaborator can see secret values, and even organization members who can’t view them “can access their values by printing the environment variables.” People who remix a public app are different: they get your secret names with empty values and a fresh database.

03

Keep keys in Secrets, and out of anything named VITE_

Every API key, token and password belongs in the Secrets tool (Tools → Secrets), not in your code. Replit encrypts secrets and passes them to your server as environment variables. Its Configurations section is for harmless settings like log levels, and the docs are firm: “Do not store API keys, passwords, or authentication tokens as configurations.” Replit’s publishing page adds that secrets sync to your published app each time you publish, and you can review them under Production app secrets in Adjust settings.

One rule decides whether a secret stays secret: its name. If your front end is built with Vite (look for a vite.config file in your project), any variable whose name starts with VITE_ is copied into the JavaScript that every visitor downloads. Vite’s docs say these variables “should not contain sensitive information such as API keys,” because “the values of these variables are bundled into your source code at build time.” It doesn’t matter that the value sits encrypted in Replit Secrets. Once it’s named VITE_, it’s public the moment you publish.

The quickest check is in Replit’s Shell. These print names only, never values:

Replit Shell
# In Replit's Shell: the names (not the values) of every VITE_ variable
printenv | cut -d= -f1 | grep '^VITE_'

# And every place the front end reads one
grep -rn "import.meta.env.VITE_" --include=*.ts --include=*.tsx --include=*.js --include=*.jsx \
  --exclude-dir=node_modules --exclude-dir=dist .

A VITE_ name holding a site address or a publishable key is fine. One holding an OpenAI, Anthropic, Stripe secret, or database value is not. Then check what your live app really ships. This script downloads the scripts your homepage loads, plus the files for screens that load later, and searches them for key-shaped strings. Run it in a terminal on your computer:

Terminal
APP=https://YOUR-APP.replit.app

# 1. The scripts your homepage loads
curl -s "$APP/" | grep -oE '(src|href)="/[^"]+\.js"' | cut -d'"' -f2 | sort -u \
  | while read -r f; do curl -s "$APP$f"; done > app.js

# 2. Code for screens that load later (Vite keeps it next to the rest, in /assets/)
grep -oE '"\./[A-Za-z0-9_.-]+\.js"' app.js | tr -d '"' | sed 's#^\./#/assets/#' | sort -u \
  | while read -r f; do curl -s "$APP$f"; done >> app.js

# 3. Anything shaped like a secret key or a database password
grep -ohE "sk-(proj|ant)-[A-Za-z0-9_-]{20,}|sk_live_[A-Za-z0-9]+|rk_live_[A-Za-z0-9]+|whsec_[A-Za-z0-9]+|AKIA[0-9A-Z]{16}|postgres(ql)?://[^\"' ]+|mongodb(\+srv)?://[^\"' ]+" app.js | sort -u

We tested it on a small Vite and Express app with two fake keys passed to the build as VITE_ variables. One was an OpenAI key used on the homepage, the other a Stripe secret key used only on a lazily loaded admin screen. The script found both. When we moved the keys to the server and rebuilt, it found nothing. Nothing printed is what you want. A real key in the output means you should treat it as already public. Rotate it at the provider first, put the new one in Secrets under a name without VITE_, and have Agent move the call to the server:

Prompt for Replit Agent
Find every place the front end reads a variable that starts with VITE_. For each one that holds an API key, token or password, move the code that uses it into a server route, read the key there with process.env from Secrets, and delete the VITE_ variable. List what you moved. Don't change anything else.

Then publish again, and run the script once more. Our guide to leaked API keys in your frontend bundle has a longer script that follows every file name, which keys are public by design, and the order to rotate in. One more habit, straight from Replit’s shared responsibility page: “Don’t paste API keys, tokens, or credentials into the chat.” Agent can use a secret without ever seeing it.

04

Knock on your API routes without signing in

Your server answers requests at addresses like /api/notes or /api/orders. The page uses them to fetch data, but they aren’t part of the page. Anyone can call them directly, with no browser and no login, and the only thing between a stranger and your data is the check at the top of each route. Replit’s security checklist says to “add authentication to sensitive endpoints.” Its Replit Auth page says to “always validate user authentication server-side before performing sensitive operations.” Hiding a button for signed-out visitors doesn’t count.

First, list your routes. In Replit’s Shell:

Replit Shell
# In Replit's Shell: every API route your server defines
grep -rnE "\.(get|post|put|patch|delete)\(\s*[\"'\`]/api" \
  --include=*.ts --include=*.js --exclude-dir=node_modules --exclude-dir=dist .

Each line shows one route. If your app uses a sign-in guard, its name usually sits on that same line, between the address and the handler, so routes without one stand out (unless the app guards a whole group of routes in one place, which Agent can tell you). If you ran the script from check 3, this lists every API address your front end mentions, which is what a stranger sees:

Terminal
# Every API address your front end mentions (run after the script in check 3)
grep -oE "[\"'\`]/api/[A-Za-z0-9_/-]+" app.js | cut -c2- | sort -u

For each route that returns data, ask for it the way a stranger would, with no cookie at all:

Terminal
curl -i https://YOUR-APP.replit.app/api/notes

On our test app, the route written carelessly answered 200 OK with every user’s notes and email address. The one that checked for a session answered 401 Unauthorized. A route meant for signed-in users should give you the second. If you get data back, ask Agent to fix it, then knock again:

Prompt for Replit Agent
Go through every route in the server under /api. For each one that reads or changes user data, make sure it checks that someone is signed in before doing anything, and returns 401 if not. List every route, whether it needed the check, and what it checks now. Don't change anything else.

Stick to plain GET requests like the one above. Don’t send POST or DELETE requests at your live app to see what happens, because they really run.

05

Check that every record belongs to the person asking

Check 4 asks “is anyone signed in?” This one asks the harder question: “is this theirs?” It’s the mistake we’d bet on finding in a new app. A route checks for a login, then fetches note number 42 for whoever asked, without checking that note 42 belongs to them. Change the number in the address and you’re reading someone else’s note. Replit’s shared responsibility model puts “usage policy / access control” squarely with you, and asks you to “tell Replit Agent who should have access to which functionality of your app.” Agent can only build the rule you describe.

In code, the fix is to put the user in the query itself, so the database only ever returns or changes their rows. Here is what that looks like in Express with Drizzle, the stack from Replit’s example. Your app will name things differently. With Replit Auth or Clerk, the signed-in user comes from their library rather than req.session.userId. The shape is the same:

server/routes.ts
import type { Express, NextFunction, Request, Response } from "express";
import { and, eq } from "drizzle-orm";
import { db } from "./db";
import { notes } from "./schema";

// 1. No signed-in user, no data.
function requireUser(req: Request, res: Response, next: NextFunction) {
  if (!req.session.userId) return res.status(401).json({ message: "Unauthorized" });
  next();
}

export function registerNoteRoutes(app: Express) {
  // 2. Only the caller's own notes, never everyone's.
  app.get("/api/notes", requireUser, async (req, res) => {
    res.json(await db.select().from(notes).where(eq(notes.userId, req.session.userId!)));
  });

  // 3. Only delete the note if it belongs to the person asking.
  app.delete("/api/notes/:id", requireUser, async (req, res) => {
    const deleted = await db
      .delete(notes)
      .where(and(eq(notes.id, Number(req.params.id)), eq(notes.userId, req.session.userId!)))
      .returning({ id: notes.id });
    if (deleted.length === 0) return res.sendStatus(404);
    res.sendStatus(204);
  });
}

The important part is and(eq(notes.id, …), eq(notes.userId, …)). Drizzle’s filter operators combine the two conditions, so a note that isn’t yours simply isn’t found. We ran this against a test database with two users. Signed in as the second user, deleting the first user’s note returned 404 and the note was still there. The careless version, which filtered on the note’s ID alone, returned 204 and the note was gone. To get Agent to apply the rule everywhere, describe who should see what in your own words:

Prompt for Replit Agent
Here is who should see what in this app: [describe it, e.g. "each customer sees and edits only their own bookings; staff can see all bookings; only I can change prices"]. Go through every server route and every database query. Make sure each one only returns or changes records the signed-in user is allowed to touch, by filtering on their user id or checking their role in the query itself. List each route you changed and the rule it now enforces.

Then do the test no code reading replaces. Make two accounts. Sign in as the second, and try to open something that belongs to the first, for example by pasting the address of the first account’s booking or profile, or changing the ID in it. You should get nothing. If you have an admin screen, sign in as an ordinary user and try its address too. A page that only hides the admin menu isn’t a lock. The route behind it has to check the role. Do all of this in the browser, on your own test accounts.

06

Know which database your live app uses, and who can touch it

Replit’s database setup is different from most builders’, and it changed after a very public bad day. In July 2025, the SaaStr founder Jason Lemkin was building an app with Replit Agent. During what he had declared a code freeze, the agent wiped his database. Masad posted that the “agent in development deleted data from the production database. Unacceptable and should never be possible.” He said Replit was rolling out automatic separation of development and production databases “to prevent this categorically.” As The Register reported, Replit’s announcement admitted that until then, Replit apps “used a single database for both development and live customer data.”

Today, Replit’s database docs say “Agent is not able to modify the production database.” That is a real improvement. Here is what is still worth checking:

  • What gets copied when you publish. Agent can still change the structure of your development database, and the docs say those changes, like added or deleted columns and tables, “are applied to your production database” when you publish. Before publishing a change that removes or renames a column, ask Agent what it will do to existing data.
  • Test data. Older Neon-based setups offer to copy your development data into production. Replit warns to do that only if the data is safe for the live app, because it “can include test accounts.” A leftover test admin with password test123 is a classic way in.
  • Which connection string you have. Open the Database tool’s Settings tab. If your development DATABASE_URL contains neon.tech, you’re on the legacy setup, which Replit says “used a full connection string. If leaked, anyone could access and modify your database.” A helium one is the current setup, which is “only accessible from within your app.” Your production database is different. Its connection string works from any PostgreSQL client, so treat it as a password. Never put it in code, in a prompt, or behind a VITE_ name. If it leaks, the connection details page explains how to regenerate the credentials.
  • Backups. A checkpoint rollback can restore your development database, but Replit notes it doesn’t restore production. Production has point-in-time restore, seven days by default, and you can turn on daily backups in the Database tool, per the data recovery docs. Check it’s on before you have customers.

If your app keeps its data in Supabase or Firebase instead, the browser may talk to the database directly, and the rules on that database become a second lock. Our Supabase RLS checklist and Firebase rules checklist cover those.

07

Make sure your server only hands out what you built

Your server sends the built front end to visitors from a folder. If it’s the wrong folder, or the build publishes more than it should, strangers can download things you never meant to share.

Source maps turn your compressed JavaScript back into readable source, comments and all. Vite turns them off for production builds by default, but one line in its config turns them back on. Project files leak when a server serves its whole project folder instead of the built one. Express helps a little. Its static file middleware ignores files whose names start with a dot by default, but its docs add that this default “will not ignore the files within a directory that begins with a dot.” We tried it. A test server pointed at its project folder answered /.env with a 404 but handed out /.git/config and package.json.

Terminal
APP=https://YOUR-APP.replit.app
f=$(curl -s "$APP/" | grep -oE 'src="/[^"]+\.js"' | cut -d'"' -f2 | head -1)
for p in "$f.map" /.env /.git/config /.replit /package.json; do
  curl -s -o /dev/null -w "$p  %{http_code}  %{content_type}\n" "$APP$p"
done

Read the two columns after each path. For the .map line, 404 is what you want. A 200 with application/json means your source is public. For the rest, many apps send the homepage for any address they don’t know, so a 200 with text/html is just that, and fine. A JSON or octet-stream answer for /package.json or /.git/config means the server is handing out your project. If either turns up, ask Agent:

Prompt for Replit Agent
Make sure that in production the server only serves the built front-end folder, never the project folder, and that the production build does not publish source maps. Show me the lines you changed.

If your app takes uploads, the same idea applies. Keep files in Replit’s App Storage, as its checklist suggests, and serve them through a route that runs the checks from 4 and 5. Don’t use a public folder.

08

Add the headers and cookie settings Express leaves out

Headers matter less than everything above, and a missing one won’t leak data on its own. But they tell browsers how to protect your visitors, and a plain Express server sends almost none:

Terminal
curl -sI -H "Origin: https://example.com" https://YOUR-APP.replit.app \
  | grep -iE "^(content-security-policy|strict-transport-security|x-frame-options|x-content-type-options|referrer-policy|x-powered-by|access-control-allow-origin|set-cookie)"

On our careless test app, three lines came back. X-Powered-By: Express, which Express’s security guide says it sends by default. Access-Control-Allow-Origin: *, from the cors package with no options, which its docs say means any origin. And a session cookie with no Secure or SameSite flag, because express-session’s defaults are httpOnly: true, secure: false. The fix is a few lines, and it is what Express’s own guide recommends: Helmet for headers, and explicit cookie settings.

server/index.ts
import express from "express";
import helmet from "helmet";
import session from "express-session";

const app = express();

app.set("trust proxy", 1);
app.use(helmet());
app.use(
  session({
    name: "sid",
    secret: process.env.SESSION_SECRET!,
    resave: false,
    saveUninitialized: false,
    cookie: { httpOnly: true, secure: true, sameSite: "lax" },
  }),
);

With that in place, our test app sent a content security policy, Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options and Referrer-Policy. X-Powered-By was gone, and the cookie came back as HttpOnly; Secure; SameSite=Lax. Ask Agent to merge it into your server rather than paste it over the top, because your app probably already has a session store you need to keep. Three notes:

  • Helmet’s default policy is strict. It only allows scripts from your own site, which suited our test app. If yours loads Stripe, analytics or fonts from elsewhere, those need adding. MDN explains the directives. Try it in Preview and watch the browser Console for blocked requests.
  • trust proxy is there because express-session won’t send a Secure cookie when it thinks the connection isn’t HTTPS, which is what it sees behind a proxy. Its docs show exactly this setting. If sign-in stops working after the change, check this line first.
  • CORS is not a lock. The cors docs put it plainly: “CORS is not access control. Any HTTP client (curl, Postman, another server) can call your API regardless of CORS settings.” If your pages and API share one address, as they usually do on Replit, you may not need the package at all.

To see the cookie your app sets after sign-in, open DevTools → Application → Cookies and check the HttpOnly, Secure and SameSite columns. MDN’s cookie guide explains what each one stops.

What a clean result means

If you got through all eight and found nothing, that’s genuinely good news. It means nothing obvious is open today, not that the app is safe forever. A new route Agent writes next week can skip its checks, and a new package can bring a new bug. Repeat checks 4 and 5 whenever you add something that saves or shows data, and check 1 before each publish. Replit’s checklist covers things we didn’t, like input validation and rate limiting on sign-in.

Where LockShip fits

We built LockShip because the outside-in parts of this list are easy to forget and dull to repeat. You paste your app’s address and prove you own it. On a .replit.app address you can’t add DNS records, so use the meta tag or the /.well-known/lockship.txt file. Then the scan reads your app the way a stranger would. It recognises a Replit app from its replit.app or replit.dev address, and writes its fix prompts for Replit Agent. It downloads your homepage and a few pages linked from it (GET only, skipping links that look like logout, delete or checkout), follows the scripts they load, including screens that load later, reads any public source maps, and searches all of it for secret keys such as OpenAI, Anthropic and Stripe keys, and database connection strings with a password in them. On our test app it caught both fake keys and named the file each one was in. It tries /.env and /.git. It calls up to eight /api/ addresses it finds named in your JavaScript, once each, with a plain GET and no credentials. On our test app it flagged the careless /api/notes as high and left the route that answered 401 alone. It also reads your headers, the cookie your homepage sets, your CORS answer to a made-up origin, and your certificate. If your app uses Supabase or Firebase, it checks what their public keys can read.

It matters just as much to say what it doesn’t do. It never signs in, so it can’t do the two-account test in check 5 or see routes that only matter after login. It only calls API addresses your JavaScript mentions, only with GET, and never sends POST, PUT or DELETE, so a route that changes data goes untested. It never sees your source code or your Replit settings: not your Sharing choice, your Secrets, your production database, your backups or your Security Center results. It doesn’t test rate limiting or send attack payloads, and on a private or password-protected app it only sees the screen in front of it. It is a point-in-time, best-effort check, not a penetration test. A clean result means “nothing obvious from the outside right now,” not “safe.”

The summary is free: a score and the single worst thing we found. If you want everything, the $39 Launch Safe certificate includes every finding with a fix prompt you can paste into Replit Agent, a PDF, and seven days of re-scans while you fix things. A passing re-scan gives you a badge you can put on your site.

Want a second pair of eyes from the outside?

Paste your Replit app’s address, prove you own it, and see what a stranger can read. The summary is free.

Run a free scan

Before you rely on a result, read the disclaimer.

Questions people ask

Replit already scans my app before I publish. Do I still need to do this?

Keep the scan, and keep it blocking critical findings. But Replit’s own docs say a scan is not a complete security review, and its shared responsibility model puts access control in your column: only you know who should see which records. Checks 4 and 5 are about exactly that, and they take an evening.

Is my Replit app public?

If you published it from a personal workspace and didn’t change the setting, probably yes. Replit’s docs say apps in a personal workspace default to Public, which means anyone with the address can open it, while organization workspaces default to a private option. You choose Public, Password protected, Workspace only or Invite only under Sharing in the Publishing tool, and changing it on a live app means unpublishing first.

Can Replit Agent delete my production database?

Not directly, according to Replit’s current docs: Agent can create and change tables in your development database but can’t modify the production one. That rule came after a July 2025 incident in which, in Replit CEO Amjad Masad’s words, the agent “deleted data from the production database.” Changes Agent makes to your table structure are still copied to production when you publish, so read what you’re publishing and turn on production backups.

If someone remixes my app, do they get my API keys?

No. Replit’s remix docs say a remix gets your secret names with empty values, and a fresh database that never connects to yours. Values only come along when you remix your own app. People you invite to work on the project are different: a collaborator can see secret values, so be careful who you invite.

My database connection string ended up somewhere public. What do I do?

Look at what it points to. Replit says a current development database’s DATABASE_URL only works from inside your app, so a leak of that one can’t be used by anyone else. A legacy Neon connection string, or your production database’s, is a real password. For production, regenerate the credentials in the Database tool, which changes the password and redeploys your app, then replace the string anywhere you had copied it.

Keep reading

Sources

We checked each of these on 18 October 2026. We ran every command and code sample above against a small Vite and Express test app with fake keys, built and served on our own machine, using the libraries Replit’s example names (Vite, Express, PostgreSQL through Drizzle) plus express-session, cors and Helmet. Replit changes quickly, so if something here disagrees with its docs, trust the docs and tell us at hi@lockship.dev.