π Frontend Integration Guide: Authentication & Security
This guide outlines exactly how to integrate the frontend with the Kloyst Backend Auth APIs, adhering to the strict security patterns, guards, and idempotency logic we have implemented.
ποΈ 1. Environment & Base URLβ
The backend runs locally and all routes are prefixed under the api/v1 namespace.
Local Environment Configuration:
NEXT_PUBLIC_API_BASE_URL=http://localhost:3000/api/v1
π‘οΈ 2. Core Security & Authenticationβ
Kloyst uses a highly secure, stateless JWT architecture protected by Global Guards and Interceptors.
The Two Critical Headersβ
To successfully interact with secured endpoints, your fetch wrapper must eventually handle two headers:
Authorization: Bearer <token>: Proves who you are. Required for almost every endpoint.Idempotency-Key: <uuid>: Proves unique intent. Used on criticalPOSTactions to prevent accidental double-billing or double-submitting if the user clicks a button twice.
How the Idempotency Interceptor Worksβ
We have a backend interceptor (IdempotencyInterceptor) backed by Redis.
- If you pass an
Idempotency-Key(e.g., a random UUID generated by the frontend), the backend hashes your request body and saves the response in Redis for 10 minutes. - If the user double-clicks "Submit" and sends the exact same payload with the exact same
Idempotency-Key, the backend will not execute the logic twice. It will instantly return the cached 200 OK response. - Rule: Generate a fresh UUID via
crypto.randomUUID()in the browser when a user initiates a critical action (like password reset or login) and pass it in the headers.
π 3. Native Fetch Integration Wrapperβ
Do not use Axios. Use this native fetch wrapper designed to automatically handle our JWT logic, Idempotency headers, and the strict 401/400 validation error structures.
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;
export const fetchClient = async (
endpoint: string,
options: RequestInit = {},
useIdempotency = false
) => {
const token = localStorage.getItem('kloyst_token');
const headers = new Headers(options.headers || {});
headers.set('Content-Type', 'application/json');
// 1. Attach JWT globally
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
// 2. Attach Idempotency-Key for protected mutations
if (useIdempotency && options.method && ['POST', 'PUT', 'PATCH'].includes(options.method)) {
headers.set('Idempotency-Key', crypto.randomUUID());
}
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
headers,
});
// 3. Global 401 Guard Interception
// If the backend JwtAuthGuard rejects the token, immediately flush frontend state
if (response.status === 401) {
localStorage.removeItem('kloyst_token');
window.location.href = '/login';
throw new Error('Session expired');
}
// 4. Map DTO Validation Errors seamlessly
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw { response: { data: errorData } };
}
return response.json();
};
πΊοΈ 4. Comprehensive Auth API Referenceβ
Here is the exact mapping of every Auth endpoint currently implemented, including the expected request payloads and explicit State Machine / Guard logic.
4.1. Core Authentication & Identityβ
| Method | Endpoint | Protection | Payload | Description |
|---|---|---|---|---|
POST | /auth/signup | @Public | { email, password, firstName, lastName } | Creates user in PENDING_VERIFICATION status and fires an email with a token. Password must be 8+ chars, 1 uppercase, 1 number, 1 special char. |
GET | /auth/verify-email?token=... | @Public | None (uses Query Param) | Validates the email token. Advances status to ONBOARDING_BUSINESS_PENDING. |
POST | /auth/login | @Public | { email, password } | Authenticates user. Returns { access_token, refresh_token, user }. |
POST | /auth/refresh | @Public | { refreshToken } | Trades a valid refresh token for a fresh JWT. |
POST | /auth/logout | @Public | { refreshToken } | Invalidates the refresh token. |
GET | /auth/me | Secured | None | Returns current user profile based on JWT. |
4.2. The Onboarding State Machine (Protected Routes)β
The following APIs are protected by the StatusGuard and IdempotencyInterceptor. If the user calls them when their database status does not match the required state, the API will return a 403 Forbidden error.
| Method | Endpoint | Required Status | Idempotency | Payload | Description |
|---|---|---|---|---|---|
POST | /auth/business-details | ONBOARDING_BUSINESS_PENDING | Yes | { businessName, businessType, monthlyMessageVolume } | Submits business logic. Advances status to ONBOARDING_META_PENDING. |
POST | /auth/meta-onboarding-complete | ONBOARDING_META_PENDING | Yes | { wabaId, phoneNumberId } | Finalizes the Meta Embedded Signup flow. Advances status to ACTIVE. |
POST | /auth/retry-meta-onboarding | Secured | No | None | Restarts the Meta onboarding if the user got stuck. |
(Note on Enums: businessType must map to Prisma Enums like "ECOMMERCE", "SAAS", etc. Check backend schema or api.d.ts for exact values).
[ ] ## π 5. Handling Advanced Errors (Guards & Throttlers)
You already know about the 400 Bad Request structure for DTO validation. However, you must also be prepared to catch these specific infrastructure errors:
1. StatusGuard Rejection (403 Forbidden)
If a user tries to submit /auth/business-details but they are already ACTIVE, the API returns:
{
"statusCode": 403,
"message": "You do not have the required status to perform this action",
"error": "Forbidden"
}
2. Rate Limiting / Throttling (429 Too Many Requests)
We use @nestjs/throttler. For example, /auth/login limits you to 5 attempts per minute.
{
"statusCode": 429,
"message": "ThrottlerException: Too Many Requests"
}
3. Idempotency Conflict (409 Conflict)
If you pass the same Idempotency-Key header with a different JSON body within a 10-minute window:
{
"statusCode": 409,
"message": "Idempotency key already used with a different payload",
"error": "Conflict"
}