Skip to main content

πŸ” 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:

  1. Authorization: Bearer <token>: Proves who you are. Required for almost every endpoint.
  2. Idempotency-Key: <uuid>: Proves unique intent. Used on critical POST actions 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​

MethodEndpointProtectionPayloadDescription
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=...@PublicNone (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/meSecuredNoneReturns 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.

MethodEndpointRequired StatusIdempotencyPayloadDescription
POST/auth/business-detailsONBOARDING_BUSINESS_PENDINGYes{ businessName, businessType, monthlyMessageVolume }Submits business logic. Advances status to ONBOARDING_META_PENDING.
POST/auth/meta-onboarding-completeONBOARDING_META_PENDINGYes{ wabaId, phoneNumberId }Finalizes the Meta Embedded Signup flow. Advances status to ACTIVE.
POST/auth/retry-meta-onboardingSecuredNoNoneRestarts 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"
}