React Node.js SaaS Authentication, Billing, and Roles

#saas #authentication #stripe #billing #rbac #nodejs #react

Almost every B2B SaaS application shares the same foundational requirements: users need to log in securely (Authentication), they need to pay for access (Billing), and different users within a company need different permission levels (Roles). If these three pillars are designed poorly, the application will suffer from data breaches, lost revenue, and an unmaintainable codebase.

This article details how to weave authentication, billing, and role-based access control (RBAC) together securely in a React and Node.js stack.

1. Authentication: The Foundation

Authentication proves who the user is. In a stateless Node.js API, JSON Web Tokens (JWT) are the standard.

The Refresh Token Architecture:
Never store a long-lived JWT in localStorage; it is vulnerable to XSS.

  • Login: User provides credentials. Node.js verifies them.
  • Issuance: Node.js generates an Access Token (expires in 15 minutes) and a Refresh Token (expires in 7 days).
  • Delivery: The Access Token is sent in the JSON response payload. The Refresh Token is sent as an httpOnly, secure cookie.
  • React Storage: React stores the Access Token in memory (e.g., Zustand or a React Context variable).
  • Refresh Flow: When an API call fails with 401 Unauthorized, an Axios interceptor in React catches it, pings the /api/refresh endpoint (which reads the httpOnly cookie automatically), gets a new Access Token, and retries the failed request seamlessly.

The JWT Payload: Keep the JWT payload small. It should contain the userId, tenantId (workspace ID), and the user's role within that tenant. Do not put sensitive data or massive arrays of specific permissions in the JWT.

2. Billing: The Stripe Integration

Billing logic dictates what the tenant has paid for. Stripe is the industry standard. The biggest mistake developers make is trusting the frontend to update the billing status.

The Source of Truth:
Your database must mirror Stripe's state, but Stripe is the ultimate source of truth. Add a stripeCustomerId, stripeSubscriptionId, and subscriptionStatus (e.g., active, past_due, canceled) to your Tenant/Workspace database table.

The Webhook Flow:

  1. The user clicks "Upgrade" in React.
  2. React calls the Node.js API. The API creates a Stripe Checkout Session and returns the URL. React redirects the user to Stripe.
  3. The user pays on Stripe. Stripe redirects them back to a success page in React. React does nothing to update the database.
  4. Asynchronously, Stripe sends an HTTP POST request (a Webhook) to your Node.js server (e.g., to /api/webhooks/stripe) with an invoice.paid event.
  5. Your Node.js route verifies the Stripe cryptographic signature, extracts the stripeCustomerId, finds the corresponding Workspace in your database, and updates the subscriptionStatus to 'active'.

By relying exclusively on webhooks, you guarantee that users cannot manipulate frontend code to grant themselves premium access without actually paying.

3. Roles and Permissions (RBAC)

Roles dictate what the authenticated user is allowed to do. In B2B SaaS, roles apply to the Workspace, not globally (User A might be an Admin in Workspace 1, but a Viewer in Workspace 2).

Backend Enforcement (The Ultimate Gatekeeper):
Every protected Node.js route must run through middleware.

router.delete('/projects/:id', 
  requireAuth, 
  requireRole(['admin', 'manager']), 
  deleteProjectController
);
The requireRole middleware inspects the user's role (extracted from the validated JWT) against the required roles for that route. If it fails, return a 403 Forbidden.

4. Tying It All Together: The Authorization Middleware

The true complexity of SaaS architecture is that a request must pass multiple checks before business logic executes. A robust Node.js middleware chain looks like this:

  1. Authentication Check: Is the JWT valid? (Who are they?)
  2. Tenant Isolation Check: Does the requested resource (e.g., Project ID 123) belong to the tenantId stored in the user's JWT? (Prevent BOLA/IDOR vulnerabilities).
  3. Billing Check: Is the workspace's subscriptionStatus active? If they are trying to create a 4th project, does their pricing plan allow 4 projects?
  4. Role Check: Does this specific user have the 'Admin' or 'Editor' role required to create a project?

Only if all four checks pass does the controller execute the database insert.

Frontend Reflection

The React frontend mirrors this logic to provide a good UX. It reads the role and subscriptionStatus from the user context. If the subscription is past due, it renders a persistent banner: "Your payment failed. Update billing to continue." It uses a <Can> component to hide the "Delete Project" button entirely if the user is only a 'Viewer', preventing them from clicking a button that will inevitably result in a 403 error.

Mastering these three pillars is what separates a prototype from a production-ready SaaS business. If you are a founder looking to build a secure, monetizable SaaS product, visit my hire MERN stack developer India page to discuss your technical requirements.


Prakash Tank

Prakash Tank

Full-Stack Architect & Tech Enthusiast. Passionate about building scalable applications and sharing knowledge with the community.