Creating a secure login system requires more than just a form and a database. In this post, I’ll show how I combined Next.js, NestJS, JavaScript, SQL, and JWT to build a full login flow with subscription and role validation. The entire stack works seamlessly to prevent unauthorized access and ensure proper user onboarding.
Project Setup and Goals
Firstly, the goal was to create a login form in Next.js that communicates with a NestJS backend. On the backend, user accounts are stored in PostgreSQL, and access is managed using SQL-based role assignments and JWT authentication.
Next.js Login Page with JWT Authentication
The Next.js frontend uses react-hook-form for form validation and sends a login request to the backend using fetch. If the login succeeds, a JWT token is returned and stored in localStorage:
const onSubmit: SubmitHandler = async (data) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) {
const result = await response.json();
setApiError(result.message);
return;
}
const { access_token } = await response.json();
localStorage.setItem('access_token', access_token);
router.push('/dashboard');
};
Once the user lands on the dashboard, we decode the token to extract user information. This is done using simple JavaScript code:
const payloadBase64 = token.split('.')[1];
const decodedPayload = JSON.parse(atob(payloadBase64));
setUser(decodedPayload);
This approach ensures that basic user context is always available on the frontend.
Email Verification and Role Assignment with SQL
After a new user registers, they receive a verification link. When it’s clicked, the backend:
- marks the account as active,
- sets the user type to
"Manager", - assigns the system role
"supervisor".
This logic is implemented directly in the verifyEmail() method on the NestJS backend. Role assignment is done with an SQL query, not ORM, to avoid unintended behavior:
const adminUser = await this.userRepo.query(
`SELECT psr.user_id
FROM users.user_system_roles psr
JOIN users.system_roles sr ON sr.id = psr.system_role_id
WHERE sr.name = 'admin'
LIMIT 1`
);
await this.userRepo.query(
`INSERT INTO users.user_system_roles (
user_id, system_role_id, assigned_by, created_by, updated_by
) VALUES ($1, $2, $3, $3, $3)
ON CONFLICT DO NOTHING`,
[user.id, systemRole.id, adminUser[0].user_id],
);
As a result, every new verified user starts with the correct role and access level.
Backend: Validating Login Credentials with NestJS and SQL
The login flow uses NestJS to validate credentials against PostgreSQL. However, it also checks two critical conditions:
- The account must be activated.
- The user must have at least one active subscription.
if (!user.active) {
throw new UnauthorizedException(
this.translationService.t('pl', 'errors.account_not_active'),
);
}
const hasActiveSubscription = await this.subscriptionRepo.count({
where: {
userId: user.id,
status: SubscriptionStatus.ACTIVE,
},
});
if (hasActiveSubscription === 0) {
throw new UnauthorizedException(
this.translationService.t('pl', 'errors.no_active_subscription'),
);
}
Moreover, this ensures that no inactive or expired users can access the system, even if they have valid credentials.
Summary
To sum up, combining Next.js, JavaScript, JWT, and raw SQL allowed me to build a secure and structured login flow. By validating subscriptions and roles server-side, I enforced access policies early in the authentication pipeline. This full-stack solution provides a great foundation for scaling future features like usage quotas, team-based roles, and more granular access control.
Want to know how I handle scoped access and permission enforcement using SQL views and role-based logic? Stay tuned for the next post.
Today I completed a critical milestone in building my SaaS platform — implementing a secure user login flow using NestJS on the backend and Next.js on the frontend. This full-stack authentication workflow includes:
- a responsive login page,
- backend login validation,
- email verification with automatic role assignment,
- and subscription enforcement based on active plans.
This post walks through the complete implementation and offers examples of how to structure secure login logic in a modern SaaS application.
Building a Secure Login Flow with JWT, Role Assignment, and Subscription Enforcement
Today’s development focus was on refining the full-stack authentication flow in my SaaS platform. I enhanced both the backend and frontend to ensure a smooth login experience, secure role assignment, and subscription-based access control.
Backend: Validating Login Credentials with NestJS
The backend is built with NestJS and PostgreSQL using TypeORM. During the login process, I use a custom validateUser method. It not only verifies the user’s email and password but also ensures two key conditions:
- the account must be activated,
- the user must have at least one active subscription.
Here’s the simplified version of the logic:
if (!user.active) {
throw new UnauthorizedException(
this.translationService.t('pl', 'errors.account_not_active'),
);
}
const hasActiveSubscription = await this.subscriptionRepo.count({
where: {
userId: user.id,
status: SubscriptionStatus.ACTIVE,
},
});
if (hasActiveSubscription === 0) {
throw new UnauthorizedException(
this.translationService.t('pl', 'errors.no_active_subscription'),
);
}
Therefore, even if a user has valid credentials, they still need a confirmed email and a valid subscription to gain access. This model is particularly useful in any subscription-driven system.
Email Verification and Role-Based Access Control
After registration, users receive a verification link via email. Once they click it, the backend handles several actions:
- marks the account as active,
- assigns the user type
Manager, - grants the system role
supervisor, - and creates a trial subscription.
This happens automatically within the verifyEmail method, enabling seamless onboarding and consistent role-based access control.
The assignment of roles uses a dynamic query to retrieve the ID of the existing admin user:
const adminUser = await this.userRepo.query(`
SELECT psr.user_id
FROM users.user_system_roles psr
JOIN users.system_roles sr ON sr.id = psr.system_role_id
WHERE sr.name = 'admin'
LIMIT 1
`);
await this.userRepo.query(`
INSERT INTO users.user_system_roles (
user_id, system_role_id, assigned_by, created_by, updated_by
) VALUES ($1, $2, $3, $3, $3)
ON CONFLICT DO NOTHING
`, [user.id, systemRole.id, adminUser[0].user_id]);
Consequently, every verified user gets appropriate default permissions right away.
Decoding the JWT Payload for Session Context
After logging in, the backend issues a JWT token, which contains key data about the authenticated user. Rather than making additional API calls to fetch this data, the frontend decodes the token directly in the browser:
const payloadBase64 = token.split('.')[1];
const decodedPayload = JSON.parse(atob(payloadBase64));
setUser(decodedPayload);
This allows me to render user information on the dashboard immediately after login, without fetching it again from the server. As a result, the UI feels much faster and more responsive.
Next.js Login Page with JWT Authentication
On the frontend side, the login form is built with Next.js (App Router) and react-hook-form. Validation is handled client-side, and the login API call is made to the backend:
# Building a Next.js Login Page with JWT, SQL, and Role Enforcement
Creating a secure login system requires more than just a form and a database. In this post, I’ll show how I combined **Next.js**, **NestJS**, **JavaScript**, **SQL**, and **JWT** to build a full login flow with subscription and role validation. The entire stack works seamlessly to prevent unauthorized access and ensure proper user onboarding.
## Project Setup and Goals
Firstly, the goal was to create a login form in **Next.js** that communicates with a **NestJS** backend. On the backend, user accounts are stored in **PostgreSQL**, and access is managed using **SQL-based role assignments** and **JWT authentication**.
## Next.js Login Page with JWT Authentication
The **Next.js** frontend uses `react-hook-form` for form validation and sends a login request to the backend using `fetch`. If the login succeeds, a **JWT token** is returned and stored in `localStorage`:
```tsx
const onSubmit: SubmitHandler = async (data) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) {
const result = await response.json();
setApiError(result.message);
return;
}
const { access_token } = await response.json();
localStorage.setItem('access_token', access_token);
router.push('/dashboard');
};
```
Once the user lands on the dashboard, we decode the token to extract user information. This is done using simple **JavaScript** code:
```tsx
const payloadBase64 = token.split('.')[1];
const decodedPayload = JSON.parse(atob(payloadBase64));
setUser(decodedPayload);
```
This approach ensures that basic user context is always available on the frontend.
## Email Verification and Role Assignment with SQL
After a new user registers, they receive a verification link. When it's clicked, the backend:
- marks the account as active,
- sets the user type to `"Manager"`,
- assigns the system role `"supervisor"`.
This logic is implemented directly in the `verifyEmail()` method on the **NestJS** backend. Role assignment is done with an **SQL query**, not ORM, to avoid unintended behavior:
```ts
const adminUser = await this.userRepo.query(
`SELECT psr.user_id
FROM users.user_system_roles psr
JOIN users.system_roles sr ON sr.id = psr.system_role_id
WHERE sr.name = 'admin'
LIMIT 1`
);
await this.userRepo.query(
`INSERT INTO users.user_system_roles (
user_id, system_role_id, assigned_by, created_by, updated_by
) VALUES ($1, $2, $3, $3, $3)
ON CONFLICT DO NOTHING`,
[user.id, systemRole.id, adminUser[0].user_id],
);
```
As a result, every new verified user starts with the correct role and access level.
## Backend: Validating Login Credentials with NestJS and SQL
The login flow uses **NestJS** to validate credentials against **PostgreSQL**. However, it also checks two critical conditions:
1. The account must be activated.
2. The user must have at least one active subscription.
```ts
if (!user.active) {
throw new UnauthorizedException(
this.translationService.t('pl', 'errors.account_not_active'),
);
}
const hasActiveSubscription = await this.subscriptionRepo.count({
where: {
userId: user.id,
status: SubscriptionStatus.ACTIVE,
},
});
if (hasActiveSubscription === 0) {
throw new UnauthorizedException(
this.translationService.t('pl', 'errors.no_active_subscription'),
);
}
```
Moreover, this ensures that no inactive or expired users can access the system, even if they have valid credentials.
## Summary
To sum up, combining **Next.js**, **JavaScript**, **JWT**, and raw **SQL** allowed me to build a secure and structured login flow. By validating subscriptions and roles server-side, I enforced access policies early in the authentication pipeline. This full-stack solution provides a great foundation for scaling future features like usage quotas, team-based roles, and more granular access control.
---
Want to know how I handle scoped access and permission enforcement using SQL views and role-based logic? Stay tuned for the next post.
This ensures that the access token is stored securely and can be used for further authenticated requests.
Dashboard with Logout and User Info
After login, the user is redirected to a simple dashboard. The page is protected using server-side logic that checks the presence of a valid JWT token. The decoded payload is used to display user data such as their name and email, and a logout button is included:
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
export default function DashboardPage() {
const [user, setUser] = useState<any>(null);
const router = useRouter();
useEffect(() => {
const token = localStorage.getItem('access_token');
if (!token) {
router.push('/login');
return;
}
const payloadBase64 = token.split('.')[1];
const decodedPayload = JSON.parse(atob(payloadBase64));
setUser(decodedPayload);
}, []);
if (!user) return null;
return (
<main className="p-8">
<h1 className="text-2xl font-bold mb-4">Dashboard</h1>
<p className="mb-2">Hello, {user.firstName} {user.lastName}</p>
<p className="mb-4">Email: {user.email}</p>
<button
onClick={() => {
localStorage.removeItem('access_token');
router.push('/login');
}}
className="text-red-600 hover:underline"
>
Logout
</button>
</main>
);
}
Thus, with just a few lines of client logic, we achieve a session-aware frontend without compromising performance.
In conclusion, today’s work was a full-stack effort that connected backend authorization policies with frontend session handling. By combining JWT authentication, email verification, subscription enforcement, and role assignment, I’m creating a solid foundation for scalable, secure, and user-aware access control.
I’ll continue refining these mechanics, including adding subscription tier checks and permission scopes. But for now, this feels like a good checkpoint.
On the backend, I use NestJS with PostgreSQL and TypeORM. The login process starts with the validateUser method, which not only checks the email and password but also enforces access requirements such as:
- account must be activated,
- user must have at least one active subscription.
„`ts
if (!partner.active) {
throw new UnauthorizedException(
this.translationService.t(’pl’, 'errors.account_not_active’),
);
}
const hasActiveSubscription = await this.subscriptionRepo.count({
where: {
partnerId: partner.id,
status: SubscriptionStatus.ACTIVE,
},
});
if (hasActiveSubscription === 0) {
throw new UnauthorizedException(
this.translationService.t(’pl’, 'errors.no_active_subscription’),
);
}
„`
This ensures that only verified users with valid plans can log in.
✉️ Email Verification and Role-Based Access Control
After registration, users receive a verification link. Once confirmed, they are:
- marked as active,
- assigned the partner type
"Manager", - and granted the system role
"supervisor".
This happens automatically during email verification and ensures proper role-based access control from the beginning.
``ts const adminPartner = await this.partnerRepo.query( \SELECT psr.partner_id
FROM partners.partner_system_roles psr
JOIN partners.system_roles sr ON sr.id = psr.system_role_id
WHERE sr.name = 'admin’
LIMIT 1`
);
await this.partnerRepo.query(
`INSERT INTO partners.partner_system_roles (
partner_id, system_role_id, assigned_by, created_by, updated_by
) VALUES ($1, $2, $3, $3, $3)
ON CONFLICT DO NOTHING`,
[partner.id, systemRole.id, adminPartner[0].partner_id],
);
„`
💻 Next.js Login Page with JWT Authentication
The Next.js frontend uses react-hook-form for validation and a simple API call to authenticate the user. Upon successful login, a JWT token is returned and stored in localStorage:
„`tsx
const onSubmit: SubmitHandler = async (data) => {
const response = await fetch(’/api/auth/login’, {
method: 'POST’,
headers: { 'Content-Type’: 'application/json’ },
body: JSON.stringify(data),
});
if (!response.ok) {
const result = await response.json();
setApiError(result.message);
return;
}
const { access_token } = await response.json();
localStorage.setItem(’access_token’, access_token);
router.push(’/dashboard’);
};
„`
This completes the frontend part of the JWT authentication cycle.
📊 Dashboard: Decoding the Token and Logging Out
After logging in, the user is redirected to a simple /dashboard page, which decodes the token and displays user metadata including email, ID, and permissions.
„`tsx
const payloadBase64 = token.split(’.’)[1];
const decodedPayload = JSON.parse(atob(payloadBase64));
setUser(decodedPayload);
„`
The dashboard also includes a logout button that clears the token and returns the user to the login page.
🧠 Summary
This release completes the core authentication and onboarding flow for my SaaS system:
- ✅ Backend login logic with full subscription and account validation
- ✅ Frontend login form with real-time error feedback
- ✅ Email verification triggers automatic role and type assignment
- ✅ Token-based authentication using JWT and localStorage
- ✅ Protected dashboard view with logout
The full-stack architecture ensures that authentication is both secure and user-friendly, while subscription enforcement allows for clear separation between free and paid access.
More to come soon, including subscription metadata in the dashboard and token refresh logic!

Dodaj komentarz