Security Basics

8 min

Production API Hardening Guide for Live SaaS

Production API Hardening Guide for Live SaaS

The routes that worked in testing may now expose another user's invoice or trust a price sent from the browser. A route-by-route approach to hardening a production API: authorisation, Supabase RLS, payment trust boundaries, and damage limiting controls.

The routes that worked in testing may now expose another user's invoice or trust a price sent from the browser. A route-by-route approach to hardening a production API: authorisation, Supabase RLS, payment trust boundaries, and damage limiting controls.

A production API hardening guide matters most after launch, when real customers, real records and real money are moving through your application. The API routes that worked in testing may now expose another user’s invoice, accept an over-privileged token, trust a price sent from the browser or reveal a service credential in a deployed JavaScript bundle.

For startup teams, the objective is not an academic security exercise. It is to identify the few failures that can create account compromise, unauthorised data access, fraudulent transactions or a difficult incident response week. Start with what is exposed, who can call it and what each caller is permitted to do.

Start with the production attack surface

Write down every production entry point, rather than assuming the frontend is the only path users can take. Your API is callable directly. A user can modify request bodies, replay a request, replace an object ID, remove a UI restriction or send requests at a volume your interface never allows.

Include public API routes, authenticated routes, webhooks, admin functions, server actions, Supabase Edge Functions, file uploads and third-party integrations. Also account for staging or old deployments that still point to production services. A forgotten endpoint can be just as useful to an attacker as the route your team shipped yesterday.

For each route, record the method, intended caller, required authentication, expected authorisation rule, data touched and whether it triggers a sensitive action. Sensitive actions include changing an email address, issuing refunds, updating subscription status, exporting records, sending invitations and modifying roles.

This route inventory exposes vague assumptions quickly. “Authenticated users can call this” is not an authorisation rule. “The client hides the button” is not access control. The useful question is: which exact user is allowed to act on which exact record, under which conditions?

Authenticate carefully, authorise on every request

Authentication establishes that a request has a valid identity. Authorisation establishes what that identity may access. Many production incidents occur because an application does the first part correctly and treats it as enough.

Do not accept a JWT simply because it decodes. Verify its signature using the expected algorithm and key, check expiry, issuer and audience where applicable, and reject tokens intended for another environment or service. Avoid custom token handling unless there is a specific reason for it. Hosted authentication providers can reduce implementation work, but their configuration still needs review.

Then enforce authorisation at the API and data layer. If a route is GET /api/invoices/:invoiceId, the server must verify that the signed-in user belongs to the account that owns that invoice. Never rely on an account ID, role or organisation ID provided by the browser without checking it against trusted server-side context.

Test for IDOR, not just failed log-ins

Insecure direct object reference, or IDOR, is one of the fastest ways a multi-tenant SaaS product leaks data. An attacker signs in to their own account, changes invoiceId=123 to invoiceId=124, and receives another customer’s invoice because the API checked only that they were logged in.

Test this systematically with two ordinary user accounts from different organisations. Create records in both. Attempt to read, update and delete the other account’s records by changing IDs in URLs, JSON bodies, query parameters, download endpoints and GraphQL variables. Repeat the test for staff-only operations such as role changes and user exports.

Predictable IDs do not cause IDOR on their own. UUIDs are not a substitute for access control either. The issue is whether the server applies ownership or membership checks every time it retrieves or changes an object.

Treat Supabase RLS as a production control

Supabase can move quickly, but its flexibility creates a common failure mode: a table is available through the API while row-level security is disabled, incomplete or bypassed by an overly powerful key.

RLS should be enabled on every table in an exposed schema unless there is a deliberate and documented exception. Policies should cover SELECT, INSERT, UPDATE and DELETE separately. A policy that protects reads but allows an authenticated user to update any row is still a serious finding.

Review policies with realistic tenant scenarios. Can User A read User B’s rows? Can a user insert a row with another organisation’s org_id? Can they change ownership after creation? If a policy relies on a client-supplied column, the user may be able to choose the value that grants access.

Service-role keys require special care. They bypass RLS by design and belong only in trusted server-side environments. A service-role key in a frontend bundle, mobile build, public repository, error log or client-accessible environment variable should be treated as exposed. Rotate it, remove it from the exposed location and review what it could have accessed while public.

Keep secrets and privileged operations off the client

A browser is an untrusted environment. Anything sent to it can be inspected, copied and reused. That includes variables with reassuring names such as NEXT_PUBLIC_ADMIN_KEY or values hidden behind a minified bundle.

Client-side code may use deliberately public configuration such as a Supabase project URL or an anonymous key when RLS is correctly configured. It must not contain database passwords, payment provider secret keys, service-role keys, signing secrets, private API tokens or credentials for internal services.

Move privileged work to a server-side endpoint, then make that endpoint narrow. A route called adminExecute that accepts an arbitrary action name and payload is difficult to secure. Prefer explicit operations with fixed inputs, strict validation and an authorisation check tied to the caller’s current role.

Scan repositories, deployment settings and historical commits for secrets. Removing a key from the latest commit is not enough if it remains in Git history or was included in a deployed asset. Rotation is the corrective action, not merely deletion.

Make payment and webhook flows distrustful by default

Payment amounts, product IDs, discount eligibility and subscription status should not be accepted as truth because they came from your frontend. The server should retrieve the price from its own catalogue or the payment provider’s trusted record, create the checkout session server-side and associate the result with the correct internal account.

Webhook endpoints need signature verification before processing. Verify against the raw request body where the provider requires it, use the provider’s current signing secret and reject missing, malformed or stale signatures. Do not mark an order paid because the browser returns from a checkout page. Use a verified provider event instead.

Webhook retries are normal, so make fulfilment idempotent. Store a unique event ID before performing irreversible work. Without this check, a retry can create duplicate credits, duplicate orders or repeated entitlement changes.

Add the controls that limit damage

Hardening is not only about preventing the first bad request. It is also about reducing what happens when a credential is stolen, an endpoint is abused or a new code change creates an unexpected path.

Apply rate limits to sign-in, password reset, invitation, export, checkout and expensive AI-backed endpoints. Rate limiting is not a replacement for authorisation, but it slows account enumeration, credential stuffing and cost abuse. Choose limits based on expected customer behaviour, and make sure your team can distinguish a limit from an application outage.

Validate request schemas on the server. Reject unknown fields where practical, constrain types and lengths, and enforce business rules such as permitted status transitions. Parameterised database queries remain essential where raw SQL is used.

Set security headers deliberately. HSTS helps keep repeat visitors on HTTPS. A content security policy can reduce the impact of script injection, although it may require careful tuning for analytics tools, third-party widgets and modern frontend frameworks. Configure CORS for known origins rather than reflecting any Origin header, particularly when cookies or credentials are involved.

Log security-relevant events without logging secrets. Record authentication failures, permission denials, role changes, payment state transitions, webhook verification failures and unusual exports. Useful logs identify the account, action, timestamp and request context while keeping tokens, passwords and full payment details out of the record.

Production API hardening needs repeatable verification

A one-off review before launch is better than none, but production changes constantly. New AI-generated routes, copied snippets, dependency upgrades and last-minute integrations can all change the risk profile.

Build a release check around the routes that handle identity, tenant data, money and administrative power. Automated scans can catch exposed credentials, vulnerable packages and known insecure patterns. Manual testing is still necessary for IDOR, RLS policy logic, role boundaries and payment assumptions because these failures are specific to how your product works.

When you find an issue, record the route or file path, severity, realistic impact, reproduction steps and exact corrective change. “Improve API security” is not actionable. “POST /api/team/invite accepts any authenticated user; require organisation-admin membership before creating an invitation” is.

Your app does not need endless process to be safer. It needs a clear view of its exposed routes, strict server-side permission checks and a habit of testing the assumptions that the UI currently hides. If your team needs an independent check before a customer, investor or attacker finds the gap, HollowByte can review the live application and repository, then give you the fixes worth making first.

Back to blog

(function() { function applyMainRole() { var hero = document.getElementById('hero'); if (!hero) return false; var node = hero; while (node.parentElement && node.parentElement !== document.body) { node = node.parentElement; } if (node && node.parentElement === document.body) { node.setAttribute('role', 'main'); return true; } return false; } if (applyMainRole()) return; var attempts = 0; var interval = setInterval(function() { attempts++; if (applyMainRole() || attempts > 20) { clearInterval(interval); } }, 250); })();