Skip to content
8 min read

How to take a Lovable app to production

Take a Lovable app to production with practical checks for authentication, data access, secrets, performance, recovery, and technical ownership.

How to take a Lovable app to production
Table of Contents

A Lovable app is ready for production only when you can explain who may do what, where privileged code runs, how failures surface, and who can recover the system. A polished preview proves that the main path works under friendly conditions. Real users bring expired sessions, duplicate clicks, strange files, abandoned tabs, traffic bursts, password resets, refunds, deletion requests, and deliberate abuse.

The hardening work is less glamorous than generating the first version, but it is where a prototype becomes an accountable system. Treat every trust boundary as a claim that needs evidence. If you cannot demonstrate the control with a denied request, a restored backup, a measured response time, or a named owner, it is still an assumption.

This checklist assumes a common Lovable setup with a React client and either Lovable Cloud or Supabase behind it. Adapt the exact commands to your stack, but keep the tests. The interface may change quickly; authentication, authorization, data integrity, secret handling, observability, and ownership do not become optional because the interface was quick to build.

How to separate a convincing preview from production

A production decision needs explicit acceptance criteria, not a final visual review. Before inviting users, write down the system boundary: the browser, authentication provider, database, object storage, server functions, email provider, payment system if present, analytics, error reporting, DNS, and source repository. For each component, name an owner and record which identity controls it. This catches a common failure before any security test starts: a founder's personal account owns the domain, another contractor owns the database, and nobody has recovery codes.

Create three environments whenever the app holds real user data or money: local development, a staging environment with synthetic data, and production. They need separate database projects, storage buckets, API credentials, OAuth callback URLs, and webhook secrets. A staging label on the same database is not isolation. One mistaken delete or permissive policy can still touch production rows. If three environments feel expensive, compare that cost with discovering a destructive migration on the live database.

Define a launch contract in plain language. It should answer which users may register, which roles exist, which data each role may read or change, how long sessions last, what happens when dependencies fail, how support identifies a request, and how quickly the team can restore service. Include regulatory or contractual duties that actually apply, such as consent, retention, export, and deletion. Do not copy a compliance checklist for appearances. Map each obligation to a screen, query, log, or operational procedure.

A useful release record has five columns: risk, expected control, evidence, owner, and rollback. Evidence should point to a test result or an observed configuration, not a note saying "checked." A screenshot can prove that a setting existed on one day, but an automated test or versioned migration is stronger because you can run it again. Keep screenshots for provider controls that cannot be represented as code.

The distinction that teams often blur is functional acceptance versus operational acceptance. Functional acceptance says a user can create a project. Operational acceptance says a user cannot read another tenant's project, retries do not create two projects, a failed upload leaves no orphaned record, the action appears in useful telemetry, and support can recover it. The second standard is what production needs.

How to make authentication survive hostile use

Authentication must cover the entire account lifecycle and every privileged route, not only the sign-in screen. Test registration, email verification, sign-in, sign-out, password reset, invite acceptance, account deletion, provider linking, session refresh, session expiry, and a user who gets disabled while a tab remains open. If your app supports organizations, test removal from an organization while the former member still has an active session.

Keep authentication and authorization separate in your design. Authentication establishes an identity. Authorization decides whether that identity may perform this action on this object. A React route guard improves the interface, but it does not enforce access because a user can call the API directly. Every database query and server endpoint must make its own decision using a verified session and current permissions. Never accept a user_id, owner_id, price, role, or organization identifier from the browser as proof of authority.

Write an access matrix before adding more conditions to components. Use rows for actions such as view, create, edit, delete, invite, export, and administer. Use columns for anonymous users, members, managers, and administrators, or the roles your product actually has. For every cell, state allow or deny and the scope, such as "own records" or "current organization." That matrix becomes the source for database policies and negative tests. If the team cannot agree on a cell, the application code cannot resolve the ambiguity safely.

Account endpoints also need abuse controls. Rate-limit sign-in, registration, password reset, email change, invitation, and any endpoint that sends a message or performs expensive work. Use responses that do not reveal whether an email address exists. Add bot defenses where abuse appears, but do not mistake a challenge widget for authorization. Set cookie flags and redirect allowlists according to the deployment model, and remove localhost or preview callbacks from the production provider configuration.

Reserve stronger controls for roles that can view all users, change billing, export data, or alter permissions. Require multifactor authentication for administrators if the identity provider supports it, keep the administrator population small, and use a separate admin interface or server path when practical. Record sensitive administrative actions with actor, target, action, request identifier, and timestamp. Do not put tokens, reset links, passwords, or full personal records in that audit trail.

Run denial tests with two ordinary accounts and one administrative account. Sign in as user A, capture a request for A's object, replace the object identifier with user B's identifier, and send it again. Repeat for reads, updates, deletes, downloads, search, exports, and nested resources. A disabled button proves nothing. The server must return a denial or behave as if the resource does not exist, and B's data must remain unchanged.

How to enforce data ownership in the database

The database must enforce tenant and record ownership even when the browser sends a handcrafted request. Supabase documentation says row-level security should be enabled on tables in exposed schemas, normally public. It also describes a policy as an implicit WHERE clause applied to queries. That model is useful, but only if you write restrictive policies for every operation and test the roles that actually reach the Data API.

For a table whose records belong to individual users, a minimal starting point looks like this:

alter table public.projects enable row level security;

create policy projects_select_own
on public.projects for select
to authenticated
using ((select auth.uid()) = owner_id);

create policy projects_insert_own
on public.projects for insert
to authenticated
with check ((select auth.uid()) = owner_id);

create policy projects_update_own
on public.projects for update
to authenticated
using ((select auth.uid()) = owner_id)
with check ((select auth.uid()) = owner_id);

create policy projects_delete_own
on public.projects for delete
to authenticated
using ((select auth.uid()) = owner_id);

create index projects_owner_id_idx on public.projects(owner_id);

The using expression limits existing rows that an operation may see. The with check expression limits the row state a user may create or leave behind. Omitting with check from an update policy can let a user move a row to another owner, depending on the other policies in force. Do not rely on a default generated policy without reading the SQL. Store policy definitions in migrations so review and disaster recovery do not depend on dashboard memory.

Organization access needs membership checks rather than a client-supplied organization ID. Keep memberships in a protected table, decide whether a user can belong to several organizations, and define what happens when membership ends. Be careful with helper functions used inside policies: control their search path, permissions, and execution context. A function that runs with elevated privileges can quietly punch through the boundary you meant to create.

Test RLS through the same client path the application uses. Cover anonymous and authenticated roles, empty results, bulk operations, filters, RPC calls, views, and storage objects. Check every new table created through raw SQL because Supabase notes that tables created with the SQL editor do not necessarily receive the same automatic RLS behavior as tables created through the dashboard's table editor. A table with RLS enabled and no applicable policy usually denies access, which is safer than a broad policy, but it can still break production if nobody tests it.

Protect integrity as well as confidentiality. Add not null, foreign keys, unique constraints, check constraints, and sensible delete behavior to the database. Client validation gives quick feedback; database constraints cover imports, scripts, functions, concurrent requests, and future clients. Use transactions for changes that must succeed together. If creating an order also reserves inventory and writes a payment reference, three unrelated browser calls can leave partial state after a network failure.

Plan deletion before users request it. Decide which records disappear, which must remain for a legitimate obligation, which files live in storage, and which external processors received a copy. A foreign key cascade may help, but do not enable it without tracing the full relationship graph. Test deletion on staging with a realistic account containing comments, uploads, memberships, and background jobs, then verify that search indexes, cached documents, and storage objects follow the intended rule.

How to keep secrets out of public code

A browser bundle cannot hold a secret because every user can download and inspect it. Build-time environment variables that reach client code are configuration, regardless of the word SECRET in their names. Browser developer tools and the network panel reveal requests, headers, endpoints, source maps, and shipped JavaScript. Hiding a value in an obfuscated module or asking an AI to move it into another frontend file changes nothing.

Classify every credential by where it may run. Publishable Supabase keys may be used in the client when RLS and database grants enforce access. Supabase documentation explicitly warns that secret and legacy service_role keys bypass RLS and must stay on the backend. Payment secrets, private AI provider keys, email provider keys, database connection strings, webhook signing secrets, and administrator credentials also belong in server-side secret storage, never in React source or a browser-visible environment variable.

Route privileged work through a server function. The browser sends the minimum input plus its user session. The function verifies the token, loads current authorization from a trusted source, validates the request, calls the provider with the server-side secret, and returns only the data the browser needs. A server function is not automatically safe: it still needs authorization, input limits, timeouts, careful error messages, and rate limits. Do not let it become a generic proxy that accepts arbitrary URLs or provider methods.

Scan the repository and built output before launch. In a typical local checkout, these commands provide a useful first pass:

git grep -n -I -E '(service_role|sb_secret_|BEGIN (RSA|OPENSSH|EC) PRIVATE KEY|sk-[A-Za-z0-9])'
git log -p | grep -n -E '(service_role|sb_secret_|BEGIN (RSA|OPENSSH|EC) PRIVATE KEY)'
npm run build
grep -R -n -E '(sb_secret_|service_role|PRIVATE KEY)' dist

The expected output for the grep commands is empty. Treat matches as leads, not proof, because documentation and test fixtures can contain harmless patterns. Also inspect deployment settings, CI logs, source maps, copied .env files, chat transcripts, screenshots, and issue attachments. Secret scanners help, but they cannot identify a credential that has an unfamiliar shape or a private endpoint protected only by obscurity.

If a real secret entered Git history or a deployed bundle, remove it from current code and rotate it immediately. Rewriting history alone does not revoke a value that someone already copied. Find every environment and integration that uses the old credential, deploy the replacement, verify traffic, revoke the old value, and document the event. Use separate credentials per environment and, where providers allow it, narrow permissions and set spending or usage limits. Rotation becomes far less frightening when the team rehearses it before an incident.

How to prevent partial writes and duplicate actions

Add visibility before launch
Startup advisory includes production infrastructure with Sentry and Grafana for eligible startup clients.

Production endpoints must assume that requests arrive twice, arrive late, or stop halfway through. Mobile connections drop, users double-click, browsers retry, queues redeliver, and providers send the same webhook again when acknowledgment fails. Disabling a button after the first click improves the interface, but the backend still needs idempotency and atomic state changes.

Assign an idempotency key to operations such as checkout, invitation, report generation, or any action with an external side effect. Store the key with the authenticated actor, operation type, request fingerprint, status, and result reference under a unique constraint. When the same request returns, give back the recorded result. If the payload differs under the same key, reject it. Choose a retention period based on how long clients and providers may retry.

Validate input at the server boundary with explicit schemas. Set length, type, format, and range limits; reject unknown fields where they could alter behavior. Validate uploaded content by size and detected type, not only the filename extension. Generate storage paths on the server, and keep private objects in non-public buckets with access checks or short-lived signed access. If users can render rich text or HTML, sanitize it with a maintained policy instead of a homegrown regular expression.

Use a transaction for related database changes, but do not hold a transaction open while waiting on a slow external API. For mixed database and provider work, model states such as pending, confirmed, and failed, then reconcile using a webhook or background job. Store the provider's event identifier under a unique constraint. Verify webhook signatures against the raw request body, enforce a timestamp tolerance if the provider supports one, and make the handler safe to repeat. Return an acknowledgment only after durable work or durable queuing succeeds.

Failures must produce useful, bounded responses. Set timeouts on outgoing calls and cap retries with delay and jitter. Retry only operations that are safe to repeat, and distinguish temporary failures from invalid requests. Do not pass raw database or provider errors to the browser; map them to stable error codes and a request identifier. The detailed cause belongs in protected logs with secrets and personal data removed.

Rate limits should reflect cost and harm, not one global number. Search, AI generation, exports, file conversion, password reset, and invitation endpoints have different abuse profiles. Apply limits by authenticated account and, where appropriate, by organization and network source. Define what happens when a provider reaches its quota: queue work, degrade a feature, or return a clear temporary error. An unlimited expensive endpoint can turn one leaked account into a large invoice.

How to prove the app performs under real use

Performance needs measured budgets for the user journeys that earn trust or money. Pick a small set such as first page load on a mid-range phone, sign-in, dashboard load, search, save, and the largest ordinary file upload. Record browser timing, server duration, database query time, payload size, and error rate for each. A fast preview on the developer's laptop says little about a user on a slower device and network.

Start with the production build, not development mode. Inspect the generated bundle and browser network waterfall. Remove large libraries that support one minor interaction, lazy-load routes that most users never open, compress appropriately sized images, declare image dimensions, and avoid fetching the same record from several components. Cache only data whose staleness and privacy rules you understand. A shared cache key that omits the tenant or authorization scope can become a data leak.

Most slow dashboards come from request shape or database work, not React rendering. Look for sequential requests that can run together, repeated per-row queries, unbounded lists, broad select * calls, missing indexes, and filters that cannot use an index. Paginate on the server with a deterministic order. Select only the columns needed for the current view. Examine representative query plans in staging with data volume that resembles the next several months, not ten hand-entered rows.

RLS affects query planning, so policy expressions deserve performance testing too. Index columns used for ownership and membership checks. Keep policy logic understandable, and measure complex helper functions rather than assuming the database will simplify them. Test a user with one record and a user with many records. Authorization that is correct but takes seconds under a large tenant still blocks launch.

Run a modest load test against staging with synthetic accounts. Increase concurrency gradually, watch response times and errors, and stop before you damage a shared provider. Exercise reads and writes, session refresh, file handling, and the slowest server function. Confirm database connection limits, server function timeouts, provider quotas, and queue behavior. The objective is not a theatrical traffic number; it is finding the first bottleneck and knowing what the system does when it reaches that limit.

Set a budget the team can enforce in future releases, such as maximum compressed JavaScript for the initial route, maximum ordinary API response size, and a percentile latency target for the main actions. Choose values from product needs and measurements rather than copying another company's targets. Put repeatable build and smoke checks in CI. Performance that exists only after a founder manually opens developer tools will regress.

How to operate failures instead of discovering them from users

Put one owner on production
Fractional CTO leadership assigns decisions for auth, data, secrets, recovery, and launch risk.

A production app needs enough telemetry and recovery practice to turn an error report into an answer. Capture client errors, server function failures, rejected webhooks, database health, job backlog, authentication anomalies, and dependency failures. Attach environment, release identifier, route or operation, and a request identifier so the team can connect events across components. Define alerts around user harm, not every log line.

Design logging with a data policy. Record identifiers only when they help investigation, prefer internal object IDs over email addresses, and redact authorization headers, cookies, tokens, passwords, reset links, payment data, and request bodies that may contain personal content. Control who can search logs and how long the provider retains them. A debugging system becomes another sensitive database as soon as it stores production events.

Health checks should test what the platform can act on. A process that returns 200 while it cannot reach the database is alive but not ready. Keep lightweight liveness checks separate from readiness checks that cover essential dependencies. Do not make every health probe call every paid external service. For third-party outages, define a visible degraded mode and preserve work where possible. A clear retry state is better than a spinner that never ends.

Backups count only after a restore test succeeds. Confirm what the database provider backs up, the retention window, whether object storage and authentication data are included, and how encryption keys or secrets are recovered. Restore into an isolated environment, run integrity checks, and time the exercise. Record the recovery point and recovery time the business can accept. Those values determine backup frequency and architecture; they should not appear for the first time during an outage.

Write a short incident procedure with roles and access details. It needs an incident lead, a technical operator, a communication owner, the place where decisions are recorded, provider support paths, and criteria for rollback or shutdown. Keep emergency access protected with multifactor authentication and recovery codes under company control. Run a tabletop exercise: assume a secret leaked, a migration corrupted rows, or a provider stopped accepting writes, then walk through detection, containment, recovery, and user communication.

Support needs a safe diagnostic path. Give staff a request identifier and narrowly scoped tools instead of direct production database access. If impersonation exists, require a reason, log its use, limit who can invoke it, and make the state obvious. Never ask users to send passwords, session tokens, or full payment details. Good operations reduce both downtime and the temptation to bypass controls under pressure.

How to secure ownership and an exit path

Close the ownership gaps
A fractional CTO puts company control around repositories, providers, recovery access, and release decisions.

The company must control the code, data, domains, provider accounts, billing, and recovery methods before launch. Lovable's documentation says projects can sync to GitHub and describes the generated application as a standard Vite and React project that can be deployed elsewhere. That is meaningful portability, but a repository copy alone does not transfer databases, storage objects, authentication configuration, secrets, DNS, email templates, or operational knowledge.

Connect the project to a repository in a company-controlled organization, protect the default branch, require review for production changes, and make CI build every accepted commit. Lovable documents two-way synchronization on the default branch and warns that renaming, moving, or deleting the connected repository breaks the link. Decide which workflow is authoritative, test changes made from both sides, and document how the team resolves conflicts. Exportability is useful only when the exported commit builds and deploys without a forgotten manual step.

Create an ownership register for every service. Record the legal account owner, workspace administrators, billing contact, recovery email, multifactor method, data region if relevant, renewal date, and shutdown procedure. Use role-based company addresses where providers allow them. Remove former contractors promptly, review OAuth and GitHub app permissions, and keep at least two trained administrators for services that can stop the business. Shared passwords are not a substitute for managed access.

Prove the exit path once. Clone the repository into a clean environment, install pinned dependencies, run tests, build the app, and deploy it to an isolated target. Export a small database copy using the provider's supported tools and verify that schema, functions, policies, and storage references are represented. List anything that still requires a console click. You do not need to migrate providers before launch, but you should know the work, credentials, and downtime a migration would require.

Ownership also covers licenses and user promises. Review generated dependencies and assets for licenses you can comply with. Confirm that privacy terms describe the processors and data uses you actually have. Establish a deletion and export process you can execute. If the application makes decisions with an AI service, state what data is sent, prevent sensitive prompts from entering logs where possible, and give users an honest account of limitations. A generated interface does not generate legal permission to collect data.

I use the Team & AI Audit at oleg.is to put an owner, evidence, and an operating cost against gaps like these before a company adds more engineers or more generated code. The useful outcome is not a longer checklist; it is a smaller set of risks that someone is funded and authorized to close.

How to run a release gate that can say no

A release gate should block launch when evidence is missing, even if the demo looks good and a campaign date is near. Schedule the review early enough to fix findings. Include the founder or product owner, the engineer responsible for production, and whoever owns privacy, payments, or customer support. Each person should understand which risks they accept; silence is not acceptance.

Use this final gate as a compact decision record:

  1. Prove access controls with two-user denial tests, administrator tests, RLS inspection, private storage checks, and lifecycle tests for reset, expiry, removal, and deletion.
  2. Prove change safety with a clean build, automated tests, reviewed migrations, a staging rehearsal, a rollback method, and a version identifier visible in telemetry.
  3. Prove failure handling with duplicate requests, provider timeouts, invalid webhooks, quota exhaustion, useful error codes, alerts, a backup restore, and an incident exercise.
  4. Prove ownership with company-controlled accounts, two administrators, protected recovery methods, a repository clone and build, a service register, and a tested offboarding procedure.
  5. Record remaining risk with an owner, deadline, temporary control, user impact, and explicit acceptance by the person who can bear the business consequence.

Do not turn every minor defect into a launch blocker. Classify findings by plausible harm and reversibility. Cross-tenant reads, exposed privileged secrets, unrecoverable data, unauthenticated paid actions, and accounts nobody at the company controls deserve a stop. A clipped label or a slow secondary animation usually does not. Document the threshold before the meeting so schedule pressure does not rewrite it.

Run a small release first when the product permits it. Limit the audience, watch the main journeys, stay available to respond, and verify that telemetry, support, and rollback work with real traffic. A limited release does not excuse weak authorization or secret handling because one invited user can still be curious or compromised. It reduces operational uncertainty, not the need for hard boundaries.

After approval, save the evidence with the release identifier and convert manual checks that caught real defects into automated tests or monitored controls. Review access, dependencies, backups, costs, and incident contacts on a schedule suited to the rate of change. Production readiness expires: a new table, provider, role, or generated function can reopen a boundary that passed last month.

The launch decision becomes defensible when the team can show denied access, repeatable builds, bounded failures, restored data, and company control of every critical account. Until then, the application is still a preview with users waiting outside.

Frequently Asked Questions

Is a published Lovable app ready for production?

Publishing proves that the app can be reached, not that it can safely handle real users. Production readiness also requires enforced authorization, protected secrets, failure handling, monitoring, recovery, and company ownership of the services it depends on.

Does Lovable own the code it generates?

Lovable's documentation says you own the code and can sync it to GitHub, clone it, modify it, or deploy it elsewhere. Still test a clean build and inventory the database, storage, secrets, and provider configuration because code portability does not move the whole operating system around the app.

Can I put a Supabase key in a Lovable frontend?

A Supabase publishable key can appear in client code when RLS and grants restrict what the client may do. A secret or legacy service_role key must stay on the backend because it has elevated access and bypasses RLS.

Do I need row-level security on every Supabase table?

Enable RLS on every table exposed through the Data API, then add only the policies the application needs. Check tables created through SQL, views, RPC functions, and storage separately rather than assuming one setting protects every path.

Are React route guards enough to protect admin pages?

No. A route guard hides interface elements but cannot stop someone from calling the API directly. The database or server endpoint must verify the session and current authorization for each privileged action.

How many environments should a production Lovable app have?

Use separate local, staging, and production environments when the app stores real data or performs consequential actions. Separate credentials, databases, buckets, callbacks, and webhooks prevent a staging test from becoming a production incident.

What should I test before inviting the first users?

Prioritize two-user authorization tests, secret scans, duplicate submissions, dependency failures, account recovery, backup restore, and a clean deployment from the company repository. Those checks expose harder failures than another visual pass through the happy path.

How do I stop duplicate payments or records?

Use server-enforced idempotency keys, unique constraints, and transactions for changes that belong together. For external providers, store event identifiers and make webhook handlers safe to repeat.

What monitoring does a small app actually need?

Capture client and server errors, failed jobs or webhooks, dependency health, release identifiers, and request identifiers. Alert on user harm, redact sensitive data, and prove that someone receives and can act on each alert.

When should a production review block launch?

Block launch for cross-user data access, exposed privileged credentials, unrecoverable data, unauthenticated costly actions, or critical accounts outside company control. Record lower-risk defects with an owner and deadline instead of pretending every issue has the same consequence.

Related Posts