Add login, teams and paid plans to a Replit Agent app, and keep Replit's database for your data
A Replit project is a real Linux workspace with a shell, so the SDK installs like anywhere else. The three sign-in routes go on the Express server Agent already wrote; we ran them on 27 October 2026.
In short
To add login, teams and paid plans to a Replit Agent app, run npx buildbase init, mount the three sign-in handlers on the Express server through a 26-line adapter tested on 27 October 2026, put the secret in Secrets and in the published app, and keep your data in Replit Database. Teams and plans are SDK calls.
You are adding real sign-in, a workspace per customer, paid plans and the email that goes out when a card fails to an app Replit's Agent built for you, without moving your data out of Replit Database. When it works, a stranger opens your published address in a private window, signs up with Google or a magic link, lands in their own workspace, invites a colleague who gets an email, picks a plan on a test card, and the feature they paid for appears without a reload. The client secret lives in Secrets and never reaches the browser.
Replit is the easiest of the builders to do this on, for one reason: a
Replit project is a real Linux workspace with a shell. npm install
installs, npx runs, and the server Agent wrote is a Node process you can
read. There is no "where does the server live" question, which is most of
the Bolt and Lovable guides. The question here is different: Replit does
not document which stack Agent generates, so step 1 is finding out, and
the rest of the guide is written for the layout Replit's own pages
describe indirectly, a React front end on an Express server with
PostgreSQL behind it.
What we ran on 27 October 2026: the three sign-in handlers from
BuildBase on Vite, unchanged, mounted on
Express 4.22.3 through a 26-line adapter, with @buildbase/sdk 0.0.90
installed from npm, Node 22, the API routes ahead of a stand-in for the
Vite middleware, on port 5000 as Replit's template expects. Each route
answered as the guide says it should: the session route returned
{"sessionId":null} with no cookie and {"sessionId":"abc123"} with one,
the verify route returned 400 with no code and 401 with a bogus one (our
platform rejected it, as it should), and sign-out set the cookie with
Max-Age=0. The same results held with express.json() applied globally
before the routes, which is how Agent's generated server does it, and
tsc reported no errors. What we have not run: anything inside Replit
itself (the Shell, Secrets, Publishing), npx buildbase init in a Replit
project, the ESM layout Replit's template uses, or the hosted sign-in
round trip, which needs a registered address. Where a Replit behaviour
decides whether a step works, this guide quotes Replit's page and says so.
Before you start
A Replit project built by Agent, on any plan (Starter publishes one app, which
Replit's page says goes down after 30 days). A BuildBase organization, and an
auth client created in the console under User Management → Authentication,
with its client ID and secret. Your published address, registered on that
client as a redirect URL ending in /sign-in. A Stripe account, only when you
reach plans.
Tip
The handlers in this guide are the Vite guide's, byte for byte, and the server-side pattern is the Express guide's. This page is the Replit wrapping around them: the Shell, Secrets, the adapter, and the prompts that keep Agent on the rails.
1. Find out what Agent built
Open package.json from the file tree and read the dependencies. Then
open replit.md, which Agent writes at the root of every project and
reads before every request; its first lines usually name the stack.
Replit's docs do not state what Agent generates, but three of its pages
say it sideways. The deployment types page says "Agent builds full-stack
apps that need a backend server, so use Autoscale or Reserved VM for
those". The Clerk Auth page says Agent protects "your API routes" with
"Clerk's Express middleware". And the replit.md examples describe "React
frontend, Node.js backend, PostgreSQL database" and an app "built with
React and Express.js". So the layout to expect is one package.json with
react, vite, express and a Postgres client, a client/ folder the
Vite dev server serves, a server/ folder Express runs, and a shared/
folder between them.
Three cases, and what each means for the rest of this page:
expressandviteboth listed. This guide applies as written. The three routes go on the Express server, and the provider goes in the React app.nextlisted. You have a Next.js app, andnpx buildbase initin the Shell writes all ten files, including the three route handlers and the webhook. Follow the v0 guide from its step 2 and come back here for Secrets and Publishing.expresswith novite, or a server with HTML templates. The Express guide is the right one: the server drives sign-in itself andreq.userkeeps working.
What you should see: a sentence you can say out loud, "my app is React on Express with Postgres", before you prompt anything.
2. Register where people come back to
The hosted sign-in page sends people back to one exact address per
environment, so register them before you prompt: your published
.replit.app address with /sign-in on the end, your custom domain with
/sign-in if you have one, and the preview's replit.dev address if you
want to test sign-in before publishing. An address it was not told about
is refused.
The preview address is the one to watch. It is a replit.dev address
Replit assigns to the running project, and it is not the published one.
Register it if you want to sign in during building; otherwise register the
published address and test sign-in there. Replit's own sign-in keeps a
list of your app's domains in the REPLIT_DOMAINS environment variable
and asks you to republish to refresh it; that list governs Replit Auth,
not this one, and the addresses on the BuildBase client are the only ones
that matter here. Login that works after you
publish is the troubleshooting
page for either.
What you should see: the auth client in the console lists each address,
and the client secret has been shown once. Do not paste it into the Agent
chat or into replit.md.
3. Install the skill and run init in the Shell
Open the Shell (Tools, then Shell) and run three commands. The first installs the BuildBase skill, which is the set of rules that keeps Agent from building its own sign-in next to this one; the second installs the SDK; the third writes the provider and the environment file.
npx skills add buildbase-app/claude-skill
npm i @buildbase/sdk
npx buildbase init --org-id <your 24-character org id>Replit's Agent Skills page says installed skills live under
/.agents/skills/, that "only a brief description loads until the skill
is invoked", and that skills "installed via CLI or copied from the internet
have not" been audited, so read the skill's SKILL.md once yourself; it
is short. If the first command put the skill somewhere else, move the
folder under .agents/skills/.
init reads package.json to tell Next.js, Vite and Express apart. When
it finds both vite and express, it treats the project as Vite, because
that is the order it checks in, so on this layout it writes the provider
under src/ (or client/src/) and .env.example with the variable
names, and prints the Vite guide's address for the three handlers, which
step 4 covers. It never overwrites a file. It leaves every secret empty
and refuses a secret passed on the command line, which is the behaviour
you want in a shared workspace. We have not run it inside a Replit
project; if it reports a path you did not expect, the provider is one file
and step 4's prompt names where it goes.
Then two lines in replit.md, so every future request starts from the
same rules:
## Sign-in, teams and billing
BuildBase (@buildbase/sdk) is the only sign-in in this app. Do not add Replit Auth, Clerk or a users table. Workspaces, roles, plans and credits live in BuildBase; app data stays in Replit Database and every query scopes by the workspace id from the BuildBase session. Secrets stay in the Secrets tool, never in code, never with a VITE_ prefix.What you should see: @buildbase/sdk in package.json, a provider file,
.env.example, and the two lines in replit.md.
4. Mount the three routes on Express
The handlers are the Vite guide's server/auth.ts, plain Request to
Response functions. Express 4 speaks its own request and response
objects, so a small adapter turns one into the other. This is the file we
ran today, as written:
// server/buildbase-routes.ts
import express, {
Router,
type Request as Req,
type Response as Res,
} from 'express';
import { session, signout, verify } from './auth.js';
type WebHandler = (request: Request) => Response | Promise<Response>;
/** Run a web-standard `Request -> Response` handler as an Express route. */
const adapt = (handler: WebHandler) => async (req: Req, res: Res) => {
const headers = new Headers();
for (const [name, value] of Object.entries(req.headers))
if (value !== undefined)
headers.set(name, Array.isArray(value) ? value.join(', ') : value);
const hasBody =
req.method !== 'GET' && req.method !== 'HEAD' && req.body !== undefined;
const request = new Request(
`${req.protocol}://${req.get('host')}${req.originalUrl}`,
{
method: req.method,
headers,
body: hasBody ? JSON.stringify(req.body) : undefined,
}
);
const response = await handler(request);
res.status(response.status);
response.headers.forEach((value, name) => res.append(name, value));
res.send(Buffer.from(await response.arrayBuffer()));
};
export const authRouter = Router();
authRouter.post('/api/auth/verify', express.json(), adapt(verify));
authRouter.get('/api/auth/session', adapt(session));
authRouter.post('/api/auth/signout', adapt(signout));Two choices in it are deliberate. The verify route parses JSON with
express.json() and re-serialises the body, rather than streaming the raw
request, because Agent's generated server/index.ts applies
express.json() to every route before any of yours, which consumes the
stream; this adapter works either way, and we checked both. And
Set-Cookie is copied with res.append, so the session cookie arrives as
its own header rather than folded into another.
Then the router goes on the app before the Vite middleware, so the API is never swallowed by the front-end fallback. Paste this prompt:
Use the @buildbase/sdk npm package, already installed, to add BuildBase sign-in to this app. Read https://www.buildbase.app/llms.txt first and do not invent endpoints.
- Create server/auth.ts with the verify, session and signout handlers from https://www.buildbase.app/guides/buildbase-on-vite, byte for byte.
- Create server/buildbase-routes.ts with the adapter from the BuildBase Replit guide and mount authRouter in server/index.ts before the Vite middleware and before any catch-all route.
- Keep the BuildBaseProvider that npx buildbase init wrote and wrap the React app in it in client/src/main.tsx, with import '@buildbase/sdk/css'.
- Read VITE_BUILDBASE_ORG_ID, VITE_BUILDBASE_CLIENT_ID and VITE_BUILDBASE_REDIRECT_URL from the environment; I will add BUILDBASE_CLIENT_SECRET in Secrets myself. Never read it in client/ and never give it a VITE_ prefix.
- Do not add Replit Auth, Clerk, Passport or a users table. If sign-in already exists in this app, stop and ask me before changing it.What you should see: three new files and one changed one, and nothing
under client/ that mentions the secret. The provider init wrote is the
Vite guide's: getSession fetches /api/auth/session once per page load,
handleAuthentication posts the one-time code to /api/auth/verify, and
onSignOut posts to /api/auth/signout, so the three routes are the whole
contract between the two halves.
Correcting potential issues. Read the change before you accept it. The ways Agent tends to drift, and the line to paste back:
- It mounted the router after the catch-all, so
/api/auth/sessionreturns the app's HTML. Paste: "Mount authRouter in server/index.ts before the Vite middleware and before any route that serves index.html." - It rewrote the handlers as Express middleware with
req.bodyandres.cookie. Paste: "Keep server/auth.ts byte for byte as the guide has it and keep the adapter; do not rewrite the handlers." - It added Replit Auth "as a fallback", or kept a
userstable for its own login. Paste: "BuildBase is the only sign-in in this app. Remove the other sign-in and its users table, and keep every other table." - It read the secret in a client file, or prefixed it
VITE_. Paste: "BUILDBASECLIENT_SECRET is read only inside server/auth.ts on the server. Remove every other reference and the VITE prefix."
5. Secrets, twice
Open Tools, then Secrets under Setup, and add BUILDBASE_CLIENT_SECRET
with the value the console showed you once. Replit's secrets page says the
tool "automatically encrypts the data and makes it available to your
Replit project as an environment variable", which is exactly how
server/auth.ts reads it. The three public values can go in the same tool
as configurations (its "separate sections for secrets and
configurations") or in .env; either way they reach Vite as VITE_
variables, which Vite inlines into the browser bundle, which is fine for
an organization id and a client id and fatal for a secret.
# Secrets tool (the one bare value) and configurations (the three public ones)
BUILDBASE_CLIENT_SECRET=<the secret, server only>
VITE_BUILDBASE_ORG_ID=<your 24-character org id>
VITE_BUILDBASE_CLIENT_ID=<the client id>
VITE_BUILDBASE_REDIRECT_URL=https://your-app.replit.app/sign-in
# VITE_BUILDBASE_SERVER_URL defaults to https://api.console.buildbase.appThen the second time, and this is the step people miss. Replit's
troubleshooting page says "Secrets you set in the Project Editor do not
automatically carry over to your published app. Add all production Secrets
and environment variables in the Publishing pane", under Adjust settings,
Production app secrets. One value differs between the two: the redirect,
which carries the replit.dev address in development and the published
one in production, and Replit's secrets page adds that the tool has a
"New published app configuration" option for exactly this.
What you should see: printenv | grep BUILDBASE in the Shell lists the
four names with the secret present (Replit's page says printenv is how
to see every variable; organization members without the Owner role cannot
read secret values in the tool but can print them, which is worth knowing
before you share the workspace).
6. Run, then publish, then sign in from a private window
Press Run and open the preview. Before any button exists, open the
preview's address with /api/auth/session on the end. You should see
{"sessionId":null}: the server half is alive. That is the response we
got today from the same router on the same handlers, and it is the first
thing to check whenever sign-in misbehaves later, because the app's HTML
there means the route is not mounted ahead of the fallback.
Publish with the default deployment type. Replit's page says Autoscale "is
the default and recommended choice" for web apps and APIs, that Static
"is not compatible with Replit Apps created using Agent", and that Secrets
"are available for all deployment types except Static Deployments".
REPLIT_DEPLOYMENT is set to 1 in the published app if you ever need to
tell the two apart in code.
Then sign in. Open the published address in a private window, click the
button that calls signIn() from useSaaSAuth(), and you are sent to the
hosted sign-in page, which offers whichever methods you switched on in the
console. Sign up with a second email. You come back to /sign-in on your
app, signed in.
Then the two checks a demo skips. Close the browser, reopen the site: still
signed in, because the cookie is good for 30 days, which is the
default session length. Sign out, then
reload: asked to sign in again, because sign-out ended the session on the
server rather than clearing a value in the browser; today's test showed
the sign-out response carrying bb-session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0.
The methods are switches, not code: email and password, a magic link, an email code, a phone sign-in code sent through your own Twilio account, passkeys, Google, LinkedIn, GitHub, Microsoft, Apple, your own OIDC provider, and Ethereum or Solana wallets. Turning on Google means creating the client in Google's console and pasting two values; nothing in the Replit project changes.
Correcting potential issues. "The redirect address is not allowed" on
the hosted page: the address in VITE_BUILDBASE_REDIRECT_URL and the one
registered on the client differ, usually by a trailing slash or the
/sign-in path, or production is still carrying the replit.dev value.
"Sign-in failed" after a correct code: the secret did not reach the
published app; Production app secrets, not the editor's Secrets, is where
it has to be. The preview works and the published app does not: Replit's
troubleshooting page puts production Secrets and the port configuration
first, and so do we.
7. A workspace per customer, and the colleague they invite
Every new user lands in their own workspace on first sign-in, with no setup step. That is on by default for new organizations. In your app it is one component for the switcher plus the hooks the SDK already has, so prompt Agent for the screens; the data model is already there.
Add a workspace switcher to the app header using useSaaSWorkspaces() from @buildbase/sdk/react, and a /settings page that opens the SDK's built-in workspace, members and billing screens.
Members with the admin role may invite teammates by email; everyone else sees the member list read-only.
Do not create workspace, membership or role tables in Replit Database; BuildBase holds them. Add a workspace_id column to every table that holds customer data, filled from the BuildBase session on the server, and filter every query by it.What you should see: a switcher listing the workspaces the signed-in person belongs to, with Add team creating another if you allow it. On the members screen, inviting an address that has no account yet sends an email with a link; the person signs up and lands in the workspace with the role you chose. That flow has been in the SDK since version 0.0.71, and the reasons an invite can be refused (a seat limit, an outsider policy) come back as named errors rather than silence.
Why not let Agent write the tables? Because Replit's own options do not have this layer either, and the page says so. Replit Auth means "users sign in with their existing Replit accounts" on a Replit-branded page. Clerk Auth gives your app its own tenant and branded sign-in, and under "What's not supported" the same page lists "Organization tenants, Clerk's organization and team-membership features". So whatever Agent builds for teams, it builds from scratch. Every audit of a generated app finds the same thing: a role column checked on one page and nowhere else. The SDK's roles are enforced by the API, so a member who types the address of an admin page is refused by the server, not just by a missing menu item.
Correcting potential issues. If a member can see another workspace's rows by changing an id in the address bar, Agent wrote a query that filters by the row id and not by the workspace. That is your data and your query, and the next section is about it.
8. Your data stays in Replit Database, and it is simpler here
Your tables stay in Replit Database. The rule we give every agent is "do not use BuildBase as the primary database", and it applies here. On Supabase-shaped backends there is a wrinkle, because row-level security policies keyed to the old sign-in cannot read a BuildBase session; on Replit there is not, because the database is PostgreSQL that your Express server reads with a server-side connection, and the permission check lives in the route, where it cannot be forged.
So the pattern is one helper and one habit. The helper binds the server SDK to the request's cookie:
// server/buildbase.ts
import BuildBase from '@buildbase/sdk';
import type { Request } from 'express';
export function bbFor(req: Request) {
const match = (req.headers.cookie ?? '').match(
/(?:^|;\s*)bb-session=([^;]+)/
);
return BuildBase({
serverUrl:
process.env.VITE_BUILDBASE_SERVER_URL ||
'https://api.console.buildbase.app',
orgId: process.env.VITE_BUILDBASE_ORG_ID!,
getSessionId: async () => (match ? decodeURIComponent(match[1]) : null),
});
}The habit is that every route that touches customer data asks the SDK who
is asking and which workspace they are in, and passes that workspace id
into the query. A route with no session gets an error from the SDK rather
than a guess, and a query without a workspace_id filter is the bug to
look for in every change Agent makes from now on. The pattern is spelled
out, with the audit that found "any logged-in user could grant themselves
admin", in teams, invites and admin versus
member.
One Replit-specific comfort. After the July 2025 incident in which the agent deleted a customer's production database during a code freeze, Replit rolled out automatic separation of development and production databases, so the test data you create in the preview does not sit next to real customers' rows. Jason Lemkin's line from that week, "there is no way to enforce a code freeze in vibe coding apps like Replit", is the reason to keep the permission check in a route you can read rather than in a table rule you cannot.
9. Plans people can pay for, and the email when a card fails
Plans, prices and trials live in the BuildBase console and sync to your own Stripe account at Stripe's rates, with a 0% platform fee, and no card data touches your app. The part that generated apps get wrong is what happens after the payment, so this step is mostly about the webhook route and the feature flag.
In the console, create two plans, Launch and Pro, say, each with a 7-day
trial and a monthly and a yearly price. Then the webhook. On Express it
needs the raw body, because the signature is computed over the bytes, and
Agent's global express.json() would parse them first:
Add server/buildbase-webhook.ts and mount it in server/index.ts as app.post('/api/webhooks/buildbase', express.raw({ type: 'application/json' }), buildbaseWebhook), before express.json(). Verify the signature with parseWebhookEvent from @buildbase/sdk on the raw body, using BUILDBASE_WEBHOOK_SECRET from Secrets. Handle payment.failed with our own side effects only (flag the workspace, post to Slack); the platform sends the customer's email itself. Ignore other events.
Gate the export feature behind a BuildBase feature flag tied to the Pro plan, checked in a server route. Do not store the plan in Replit Database.Point an endpoint in the console at
https://your-app.replit.app/api/webhooks/buildbase, add its signing
secret as BUILDBASE_WEBHOOK_SECRET in Secrets and again in Production app
secrets, and republish.
What you should see: subscribe on a test card and the Pro feature appears
without a reload, because the flag follows the workspace's plan. Let the
test card fail on renewal and the email goes out from your sending domain,
because payment.failed is one of the 129 events in the catalog and the
console's notification or a workflow sends it; the route in your app is
for whatever you want to happen besides. The console's delivery log shows
each attempt, and a delivery your server was down for is retried up to
five times over about two and a half minutes. Autoscale scales to zero
when idle, which is fine: the first retry wakes it.
Compare that with Replit's own routes to payments, read today: the Stripe connector, on paid plans, where Agent sets up a Stripe sandbox and you connect the live account from the Publish pane, or your own keys in Secrets with the Checkout page and webhook endpoint Agent writes. Its Stripe page walks through deposits, balance charges and refunds for a booking app and mentions a monthly subscription with Stripe Billing as a customisation. It says nothing about a plan that belongs to a team, seats, limits, trials that end without a card, or credits, so if a plan has to change what the app does for a whole workspace, entitlements rather than flags is the ten-minute read before you prompt.
Where Replit's built-in wins
Keep Replit Auth when your app is a prototype or an internal tool and Replit branding is acceptable, which is Replit's own description of where it fits; it is one sentence to Agent, Replit sends the password reset emails for you, and there is nothing to configure. Keep Clerk Auth when you want a branded sign-in with separate development and production user stores and you need nothing after login; Replit provisions the tenant, stores the keys and switches them at publish time, and its migration from Replit Auth to Clerk moves the accounts for you. Keep Replit's Stripe connector when you sell one thing to one person and want the sandbox and the live switch handled for you.
Choose BuildBase when the question is "what may this team do now that they paid": a workspace per customer, invites and roles enforced by the API, plans with versions, feature flags tied to the plan, credits with a hard stop, and the lifecycle email, from one SDK, with your data staying in Replit Database. That is the layer Replit's pages list as not there today, and the launch checklist is the twenty-item version of why it matters before strangers arrive.
Key takeaway
On Replit there is a shell and a server, so nothing here is a workaround: the three routes mount on Express through 26 lines we ran today. The two things to get right are the secret, twice, and a workspace id on every query.
What this costs in credits
Replit prices Agent work by effort. Its AI billing page says one checkpoint is created per request, that complex builds are "bundled into one checkpoint that may cost more than a simple request", and that "Replit asks for confirmation before a paid action". On 27 October 2026 the pricing page lists Core at $20 a month ($18 billed annually) with $20 towards models, Pro at $100 ($90 annually) with $100 and rollover for two months, and Enterprise as custom; the Starter plan has daily Agent credits up to a monthly cap and one published app that goes down after 30 days. Published apps, the database and storage are billed by usage from the same credits. Replit changes these; check the page.
The prompts in steps 4, 7 and 9 are three checkpoints, with perhaps two
fix-ups. The expensive thing on Replit is not a feature, it is a loop, and
the loop here has a name: a one-star Trustpilot review from 17 October 2026
reads, in full, "Users can't log in. AI gets into a failure loop." Another,
from 20 September, warns that "especially if you set the limit 50 dollar
it turns out take 300 dollar". Naming the exact files, the exact routes
and the exact guide is how you avoid the loop, and replit.md is how you
avoid it next month too. A non-developer who built a SaaS on Replit in
three hours in July 2026 chose it because "I wanted authentication,
payments, and a database without configuring anything myself", and called
the lock-in "the uncomfortable trade-off for the convenience"; this guide
is the version of that trade where the accounts, the teams and the plans
are yours to take with you.
BuildBase's side: a Free plan at $0, then Launch at $19 a month, with a 7-day trial and no card on the paid plans.
What breaks, and how you can tell
/api/auth/session returns the app's HTML. Step 4. The router is
mounted after the Vite middleware or the catch-all. Move it up.
"Sign-in failed" on a correct code. The secret is not in Production app secrets, only in the editor's Secrets. Step 5, the second half.
Sign-in works in the preview and loops after publishing. Production is
still carrying the replit.dev redirect value, or the published address
is not registered with /sign-in on the end. Step 2 and step 5.
The verify route returns 400 with a stack trace in the log. The body was not JSON. The adapter hands malformed JSON to Express's parser, which answers 400 on its own; the provider always sends JSON, so this is a hand-made request, not the app.
The webhook returns 401 on every delivery. express.json() parsed the
body before the raw handler saw it. Mount the webhook route with
express.raw ahead of express.json().
A member can see another workspace's rows. The query filters by the row
id and not by workspace_id. Section 8, the habit.
npx buildbase init says "could not tell the framework". Neither
next, vite nor express is in package.json. Pass
--framework vite for the provider, and mount the routes by hand as in
step 4.
Install
In the Shell:
npm i @buildbase/sdk
npx buildbase init --org-id <your 24-character org id>