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,securecookie. - 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/refreshendpoint (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:
- The user clicks "Upgrade" in React.
- React calls the Node.js API. The API creates a Stripe Checkout Session and returns the URL. React redirects the user to Stripe.
- The user pays on Stripe. Stripe redirects them back to a success page in React. React does nothing to update the database.
- Asynchronously, Stripe sends an HTTP POST request (a Webhook) to your Node.js server (e.g., to
/api/webhooks/stripe) with aninvoice.paidevent. - Your Node.js route verifies the Stripe cryptographic signature, extracts the
stripeCustomerId, finds the corresponding Workspace in your database, and updates thesubscriptionStatusto '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:
- Authentication Check: Is the JWT valid? (Who are they?)
- Tenant Isolation Check: Does the requested resource (e.g., Project ID 123) belong to the
tenantIdstored in the user's JWT? (Prevent BOLA/IDOR vulnerabilities). - Billing Check: Is the workspace's
subscriptionStatusactive? If they are trying to create a 4th project, does their pricing plan allow 4 projects? - 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.