Launchprep launchprep.
readiness scan for AI-built apps

Is your Replit app secure? What to check before launch

Replit Agent scaffolds fast and leaves a few doors open. What to look at before real people arrive.

If you are asking “is my Replit app secure” before you launch, the honest answer is that Replit gives you a real backend and then leaves the authorization decisions to you, so your app is about as safe as the hand-written checks inside server/routes.ts. There is no dashboard that shows you whether those checks exist. Five things go wrong on Replit apps often enough that you should check all five today: anyone with a Replit account can log into your app unless you stopped them, individual API routes forget to check who owns the thing they are returning, a secret gets renamed with a VITE_ prefix and lands in the public browser code, the string that signs your login cookies is invented in your source instead of set, and your data may be sitting in memory instead of the database. All five are checkable in an afternoon, and none require you to read much code.

One note first. If you described your app to Replit Agent without naming a framework, it very likely built a TypeScript project with client/, server/ and shared/ folders: React on the front, an Express server on the back, Drizzle talking to Replit’s managed Postgres database. If your folders look like that, everything below applies directly.

1. Anyone with a Replit account may already be a user

If you added login by asking for Replit Auth, your app has a file called server/replitAuth.ts. Your users sign in with their Replit account, and the generated middleware named isAuthenticated checks two things: that somebody is logged in, and that their Replit token has not expired yet, refreshing it if it has. Neither is a check that they are allowed to be there. Signing up for a free Replit account takes half a minute, so unless you wrote a list of permitted emails and check it after login, every stranger on the internet is on the inside of your login wall. Replit’s auth dashboard lets you ban a user once you have seen them, but it documents no allowlist, so the restriction is code you write. Open server/replitAuth.ts and look for anything that limits who may sign in. If there is nothing, add it before launch.

While you are in that file, look for serializeUser. In the code Agent generates it is handed the whole user object, and that object carries the access and refresh tokens Replit issued alongside the user’s claims. They end up as plain readable JSON in a sessions table in your database. Run SELECT sess FROM sessions LIMIT 1; and see whether the words access_token appear. If they do, anyone who gets a copy of your database gets tokens for your users’ Replit identities — the scopes requested are openid, email, profile and offline access — good until they expire, with the refresh token able to buy new ones. Narrow serializeUser to the fields you actually use. Be aware that the generated isAuthenticated reads refresh_token and expires_at back off the session to renew an expired login, so if you drop those, sessions will simply end rather than renew.

2. The check that has to be in every single route

This is the one that leaks customer data. Open server/routes.ts and find every line starting with app.get, app.post, app.patch or app.delete that has an :id in the path. For each one, the lookup has to be tied to the logged-in user — either the query filters on the owner, or the handler fetches the record and then compares, something like if (note.userId !== req.user.claims.sub) return res.sendStatus(403). Either is fine. A route that has isAuthenticated and neither of those will happily hand user A the invoice belonging to user B.

You can test it without reading code. Log in as yourself, open a page for one of your own records, and change the number or id in the address bar to a neighbouring one. If someone else’s data appears, that route is broken. If your ids are random UUIDs you will need one from a second account rather than a neighbouring number, but the test is the same. Agent writes these checks well in some handlers and misses them in others, so checking two routes proves nothing. Check all of them. If your app handles file uploads, also open server/objectAcl.ts. The generated check there is real, not a stub: it refuses a file that has no access policy attached, lets the owner through, and lets anyone read a file recorded as public. The gap is on the other side of it — a file only gets an owner if your upload handler sets that policy, and anything it marks public is readable by whoever knows the address. Read your upload route and check which of the two it is doing.

3. The four letters that make a secret public

Replit tells you to put keys in the Secrets pane, and that is right. But your front end is built with Vite, and Vite exposes any variable whose name starts with VITE_ to your client code, inlining it into the JavaScript that gets sent to every visitor’s browser. The Secrets pane does not stop this and Replit’s secrets documentation, as of this writing, does not mention it. The trap has a shape: you put a key in Secrets, the front end cannot see it, you ask Agent to fix it, and the fix is to rename it with the prefix. It works, and now it is public. Search your project for VITE_ and read every match out loud. Payment keys, database URLs, admin tokens and anything with the word secret or private in it must not carry that prefix.

4. A cookie password that is written in your code

Search the whole project for SESSION_SECRET. The generated Replit Auth file reads it straight from the environment with no fallback, which is the behaviour you want: no secret, no boot. What you are looking for is a fallback somebody added afterwards to stop a crash, a line like process.env.SESSION_SECRET || "some-string-here". That invented string then signs your login cookies. With the Postgres session store Replit Auth sets up, the cookie carries only a session id and the account lives in the database, so knowing the string does not by itself mint a session for an arbitrary account — but it lets anyone re-sign and replay a session id that leaks out of a log or a URL, and it removes the only thing standing between a visitor and a tampered cookie. If your app keeps session contents in the cookie itself instead, then yes, anyone with that string can mint a cookie for any account. Add a long random SESSION_SECRET in the Secrets pane and delete the fallback. If the fallback is randomUUID() rather than a fixed string, the same fix applies for a different reason: the value is reinvented every time the process starts, and Autoscale restarts and replaces instances as traffic moves, so your users get logged out at random.

5. Your data may not be in a database at all

Open server/storage.ts. If the thing being exported is new MemStorage(), your app is keeping everything in the server’s memory. Sign-ups and orders look like they save because the process stays awake while you build, and they disappear when the deployment restarts or scales down. The same applies if your sessions use memorystore. Switch both to the Postgres-backed versions.

Related: Replit keeps your building database and your published database separate, which is a genuinely good thing, and it means a first deployment gets your schema but none of your development data. When that happens, do not paste your development database URL into production secrets to get the data back. That reconnects the tool that writes your code to your live customer data, which is the exact arrangement the split was built to prevent.

One more, and nobody put it there on purpose

Open server/index.ts and search for capturedJsonResponse. Express projects built from Replit’s template usually carry a small logging block that appends the JSON body of every /api response to a log line, then cuts the whole line to 79 characters. The method, path, status and duration eat the front of that, so what reaches your deployment logs is more like the first forty characters of the response body — still enough to catch a token, a reset code or an email address when one sits near the start. Delete the part that adds JSON.stringify to the log line. Keep the rest.

Worth knowing how this differs from tools like Lovable, which put the browser in direct contact with a Supabase database. There, security lives in database rules you can see listed in a dashboard. On Replit the browser never touches the database, which removes a whole category of risk, but it also means your protection is a set of if statements spread across every route in the file, with nothing anywhere counting them. If you came from a Lovable project, our Lovable pre-launch guide covers the other shape. Either way, the full pre-launch list lives on our launch checklist.

npx launchprep checks some of these for you, the public-prefix secret among them, and takes a static pass at the ownership one. The free tier is unlimited, read-only and runs on your machine.

Questions people ask

Can people see my app while I am still building it? By default, yes. Replit’s development URLs ending in .replit.dev are reachable by anyone who has the link, and published apps in a personal workspace default to public. There is a private development URL toggle in the workspace, and published apps can be set to password protected, workspace only or invite only. If you have been testing with real data, turn one of those on now.

If someone remixes my app, do they get my keys? Replit’s documentation says someone who is not the owner and remixes your app gets the names of your secrets, not the values. Separately, it warns that people who can run code in a workspace they do not own can read secret values by printing the environment variables. So the safe habit is to treat any project you have handed to a collaborator as a project whose keys should be rotated.

I asked Agent to make my app secure. Isn’t that enough? Agent will fix what you point it at, and it is good at that. What it will not do is tell you which of your routes it forgot an owner check in, because it does not know which ones matter to you. Do the address-bar test yourself on your three most sensitive pages. It takes two minutes and it is the single most useful thing you can do before launch.

More writing · The pre-launch checklist

Run every check that applies to your project, on your machine, free and unlimited:

npx launchprep